欢迎光临
我们一直在努力

AI 辅助 Rust 项目重构:让模型理解上下文后做安全的批量修改的方法

AI 辅助 Rust 项目重构:让模型理解上下文后做安全的批量修改的方法

一、重构恐惧症与 AI 的第一缕阳光

我第一次面对"把 120 个文件里的 anyhow::Result 统一改成自定义 DayuanResult"这个任务时,整整拖了三天。不是不会,是怕。怕改漏、怕改错、怕编译器不报但运行时崩。

后来是 AI 救了我——但不是那种"一键重构"的魔法按钮,而是一套我自己摸索出来的上下文注水 + 分批验证工作流。作为自学编程的程序员,我对 AI 的态度很务实:它不是一个能单独干活的高级工程师,而是一个理解力 80 分、记忆力 20 分的实习生。你的任务是帮它补齐那 60 分。

二、核心方法论:上下文是唯一的货币

AI 辅助重构最大的坑不是"AI 写得不好",而是"你给了它不完整的上下文"。LLM 的窗口再大也是有边界的,你不能一次性把整个项目塞给它。

三、实战:批量迁移错误处理类型

3.1 上下文提取

在把任务交给 AI 之前,我写了一个小工具来自动化上下文提取。它能分析 Cargo.toml 的依赖图,找到所有受影响的模块。

use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::fs;

/// 模块依赖图分析器
/// 用于在重构前确定"影响范围"
struct ModuleDependencyGraph {
/// 模块名 -> 它依赖的模块列表
deps: HashMap<String, Vec<String>>,
}

impl ModuleDependencyGraph {
/// 从项目源码中构建依赖图
fn from_project(root: &Path) -> Self {
let mut graph = HashMap::new();

// 遍历所有 .rs 文件
for entry in walkdir::WalkDir::new(root)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "rs"))
{
let content = fs::read_to_string(entry.path()).unwrap();
let module = Self::extract_module_name(entry.path());
let deps = Self::extract_use_statements(&content);
graph.insert(module, deps);
}

ModuleDependencyGraph { deps: graph }
}

/// 解析 use 语句,提取模块依赖
fn extract_use_statements(content: &str) -> Vec<String> {
content
.lines()
.filter(|line| line.trim().starts_with("use "))
.filter_map(|line| {
// 提取 crate 内部引用
let trimmed = line.trim().strip_prefix("use ")?;
// 只关心本项目内部的模块
if trimmed.starts_with("crate::") {
Some(trimmed.to_string())
} else {
None
}
})
.collect()
}

fn extract_module_name(path: &Path) -> String {
path.with_extension("")
.to_string_lossy()
.replace('/', "::")
}
}

3.2 喂给 AI 的 Prompt 设计

有了依赖图,我就知道"影响面有多大"。接下来是 prompt 设计的核心——给 AI 看什么:

# 我在给 AI 的 prompt 里固定的三段式

## 第一段:项目背景(一次性,不必重复)
你是一个 Rust 专家。我们正在维护一个 AI CLI 工具项目 "dayuan",使用
tokio + clap + anyhow。现在要把所有 `anyhow::Result<T>` 替换为自定义的
`DayuanResult<T>`,定义如下:

[粘贴 DayuanResult 的定义代码]

## 第二段:当前文件上下文(每次不同)
请修改以下文件。该文件在项目中的位置是 `src/handler/completion.rs`,
它被以下模块依赖:`src/main.rs`, `src/router.rs`。
所以你的修改不能改变公开 API 的签名。

[粘贴文件内容]

## 第三段:约束条件
1. 不要修改 pub fn 的返回类型
2. 内部函数可以用 ? 继续传播错误
3. 如果某个函数需要从 anyhow::Error 转为 DayuanError,显式写转换代码
4. 修改完成后,在注释中标记所有变更点

3.3 分批验证流水线

我没有一次性把所有文件丢给 AI,而是把 120 个文件分成 12 批,每批 10 个文件。处理流程如下:

/// 重构批次管理器
/// 将大重构拆解为多个小批次,每批独立验证
struct RefactoringBatch<'a> {
/// 本批次包含的文件路径
files: Vec<&'a Path>,
/// 批次编号
batch_id: u32,
}

struct RefactoringPipeline {
/// 剩余待处理的文件
remaining: Vec<RefactoringBatch<'static>>,
/// 已完成批次
completed: Vec<u32>,
}

impl RefactoringPipeline {
/// 处理下一批次
/// 返回 (成功处理的文件数, 失败的文件)
fn process_next<F>(&mut self, ai_modifier: F) -> Result<(usize, Vec<String>), String>
where
F: Fn(&str) -> String, // AI 修改函数:输入代码,输出修改后的代码
{
let batch = self.remaining.pop()
.ok_or("所有批次已完成")?;

let mut success_count = 0;
let mut failures = Vec::new();

for file_path in &batch.files {
let original = fs::read_to_string(file_path)
.map_err(|e| format!("读取文件失败: {}", e))?;

// 调用 AI 进行修改
let modified = ai_modifier(&original);

// 写回文件
if let Err(e) = fs::write(file_path, &modified) {
failures.push(format!("{}: {}", file_path.display(), e));
continue;
}
success_count += 1;
}

// 批次完成后立即运行编译验证
let build_result = std::process::Command::new("cargo")
.args(["check"])
.output()
.expect("cargo 命令执行失败");

if !build_result.status.success() {
// 编译失败,回滚本批次所有修改
return Err(format!(
"批次 {} 编译失败:\\n{}",
batch.batch_id,
String::from_utf8_lossy(&build_result.stderr)
));
}

self.completed.push(batch.batch_id);
Ok((success_count, failures))
}
}

教训:上面这段 process_next 里有个隐蔽 bug——cargo check 会对整个项目做检查,但如果之前批次引入了一个只影响其他 crate 的问题,cargo check 可能通过了,到运行时才发现。解决方法是在更新频率低的批次末尾加一次 cargo test 全量回归。

四、真实踩过的坑

坑 1:AI 会"忘记"远程依赖

当你让它改一个函数签名时,它可能不知道这个函数在 15 个其他文件中被调用。所以我的做法是:让依赖图工具把"调用方列表"作为上下文一并喂进去。

坑 2:AI 的懒人倾向

LLM 有时会为了省事,把所有 anyhow::Error 都改成 Box<dyn Error>。必须在 prompt 里明确说:"不要引入任何新的 trait 或类型"。

坑 3:风格漂移

每批独立处理的文件,代码风格可能不一样。我会在最后统一跑一次 cargo fmt 和 cargo clippy –fix。这步必须放到 AI 修改之后、人工 review 之前。

AI 重构的真实数据:120 个文件总共改了 487 处 anyhow::Result。AI 自动处理了 412 处(成功率 84.5%),剩余 75 处需要人工修正。失败主要集中在两处:涉及泛型约束的类型替换(36 处)和跨 crate 边界的 API 变更(39 处)。人工修正花了 4 个小时,而如果全手动改,预估需要 16 个小时。省下的 12 个小时就是 AI 辅助重构的价值。

后来我们还发现一个规律:把重构 prompt 的主语从"你"改成"我们"("我们需要把 anyhow::Result 替换成…"),AI 生成的内容会更多保留原有的注释风格。可能是训练数据里大量高质量的协作式重构都用了"我们"这个视角。

五、总结

AI 辅助重构的关键不是"让 AI 替你写代码",而是"让 AI 在你验证过的上下文中做机械性修改"。你的价值体现在三个地方:

  • 提取上下文的能力——知道该给 AI 看什么、不该看什么;
  • 设计验证流程的能力——编译通过 + 测试通过 + 人工 review,三层防线;
  • 评估 AI 输出质量的眼光——能判断 AI 的修改是否"安全"。
  • 作为程序员,我在学校里没上过编译原理课,但我有和 AI 一起 debug 到凌晨三点的经验。这套工作流不是书本上学的,是一百多次"cargo check 炸了又修"中磨出来的。它不完美,但它 work。


    下一篇预告:异步 Rust 在嵌入式中的应用——用 embassy 在 STM32 上跑并发任务。

    赞(0)
    未经允许不得转载:171主机测评 » AI 辅助 Rust 项目重构:让模型理解上下文后做安全的批量修改的方法
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址