用 AI 给 Rust 项目做代码评审:先跑 clippy,再让模型提建议
一、自己写的代码,真的很难看出毛病
我有个习惯——写完一段代码后放一两个小时,再回来看。这个方法有时候管用,但更多时候是"看来看去都觉得写得挺好"。
人的认知盲区很难靠自己突破。你写代码时的思维定势,回看时依然在。如果有同事帮忙 review 当然最好,但很多时候是 solo 开发,或者时间上凑不到一起。
后来我开始试一个方法:把代码交给 AI 模型 review。但不是直接把代码扔给模型那么简单——我发现先用 Rust 自己的clippy跑一遍,把静态分析的结果也附上,模型给出的建议会具体得多,少了很多泛泛而谈的东西。
这篇文章就聊聊这个组合拳的实操方法。
二、两阶段代码评审流程
把 clippy 和 AI 模型组合起来,评审流程是这样的:
flowchart TD A[提交代码] --> B[运行 cargo clippy] B --> C{clippy 有警告?} C -->|有| D[记录 clippy 警告信息] C -->|无| E[标记: 静态检查通过] D --> F[组织评审上下文] E --> F F --> G[将代码 + clippy 结果 + 评审要求发给 AI 模型] G --> H[AI 模型分析并提出建议] H --> I[解析 AI 返回的建议] I --> J[人工逐条审核] J --> K{建议是否采纳?} K -->|采纳| L[修改代码] K -->|拒绝| M[记录原因] L --> N[再次运行 clippy 验证] M --> N N --> O[评审完成]这个流程里 clippy 负责"规则性的硬问题"——比如不必要的 clone、可以简化的模式匹配、可能的溢出。AI 模型负责"需要上下文的软问题"——比如命名是否清晰、逻辑是否合理、是否有更好的设计模式。
三、Prompt 模板与实战
3.1 评审用的 Prompt 模板
我整理了一个效果比较稳定的 Prompt:
你是一位资深的 Rust 代码审查员。请从以下几个方面评审以下代码: 1. **正确性**:逻辑是否有 bug?边界条件是否处理到位? 2. **安全性**:是否有潜在的 panic、unsafe 使用不当、数据竞争? 3. **性能**:是否有不必要的分配、克隆、或低效的数据结构? 4. **可读性**:命名是否清晰?是否需要增加注释说明? 5. **Rust 惯用法**:是否可以用更 idiomatic 的写法替代? 以下是 clippy 的静态分析结果: --- {clippy_output} --- 以下是需要评审的代码: ```rust {code}请逐条给出建议,每条建议标注:
- 严重程度:🔴 必须修复 / 🟡 建议改进 / 🟢 可选优化
- 涉及的行数范围
- 具体修改建议(如果需要,给出修改后的代码片段)
不要给出与 clippy 结果重复的建议。
### 3.2 实战示例:评审一段有问题的代码 假设我们有这样一段待评审的代码(处理用户配置的读取和缓存): ```rust use std::collections::HashMap; use std::fs; /// 配置管理器,负责读取和管理用户配置 struct ConfigManager { configs: HashMap<String, String>, loaded: bool, } impl ConfigManager { fn new() -> Self { ConfigManager { configs: HashMap::new(), loaded: false, } } fn load_config(&mut self, path: &str) { let content = fs::read_to_string(path).unwrap(); for line in content.lines() { let parts: Vec<&str> = line.split('=').collect(); if parts.len() == 2 { self.configs.insert( parts[0].to_string(), parts[1].to_string(), ); } } self.loaded = true; } fn get_value(&self, key: &str) -> String { if !self.loaded { panic!("配置尚未加载!"); } self.configs.get(key).unwrap().clone() } fn has_key(&self, key: &str) -> bool { self.configs.contains_key(key) } } fn main() { let mut manager = ConfigManager::new(); manager.load_config("app.conf"); println!("{}", manager.get_value("username")); }先运行 clippy:
$ cargo clippy warning: called `unwrap()` on a `Result` value --> src/main.rs:15:45 | 15 | let content = fs::read_to_string(path).unwrap(); | ^^^^^^ help: handle the error: `?` warning: called `unwrap()` on an `Option` value --> src/main.rs:32:43 | 32 | self.configs.get(key).unwrap().clone() | ^^^^^^ help: use `?`: `self.configs.get(key)?` warning: field `loaded` is never read directly --> src/main.rs:6:5 | 6 | loaded: bool, | ^^^^^^^^^^^^然后把代码和 clippy 结果一起发给 AI 模型,模型可能会返回类似这样的建议:
🔴 必须修复(正确性): - 第15行:fs::read_to_string 可能失败(文件不存在、权限不足等), 使用 unwrap() 会导致 panic。建议将 load_config 的返回类型改为 Result。 - 第32行:配置中不存在的 key 不应该 panic。建议返回 Option<String>。 🟡 建议改进(Rust 惯用法): - 第3行:HashMap<String, String> 作为配置存储不够灵活。 建议封装一个 ConfigValue 枚举来支持不同类型的配置值。 - 第19-23行:split('=').collect() 在 lines 循环中可能产生大量临时分配。 建议使用 split_once('=') 来避免 Vec 分配和 lines() 迭代中的额外开销。 🟢 可选优化(可读性): - loaded 字段的逻辑可以整合到 configs 的状态中: 如果 configs 为空且来自文件则为未加载,不需要单独的布尔标记。3.3 评审意见的筛选与合并
AI 的建议不能盲从,需要人工过一遍。我通常按这个优先级处理:
// 修改后的 ConfigManager,融合了 clippy 和 AI 的建议 use std::collections::HashMap; use std::fs; /// 配置管理器 /// 使用 Result 类型传递错误,避免 panic struct ConfigManager { configs: HashMap<String, String>, } #[derive(Debug)] enum ConfigError { /// 配置文件读取失败 IoError(std::io::Error), /// 配置格式错误(某行缺少 = 分隔符) ParseError(String), } impl From<std::io::Error> for ConfigError { fn from(e: std::io::Error) -> Self { ConfigError::IoError(e) } } impl ConfigManager { fn new() -> Self { ConfigManager { configs: HashMap::new(), } } /// 从文件加载配置,返回 Result 而不是 panic fn load_config(&mut self, path: &str) -> Result<(), ConfigError> { let content = fs::read_to_string(path)?; // ✅ 用 ? 传播错误 for line in content.lines() { // ✅ 跳过空行和注释行 let trimmed = line.trim(); if trimmed.is_empty() || trimmed.starts_with('#') { continue; } // ✅ 使用 split_once 避免 Vec 分配 if let Some((key, value)) = trimmed.split_once('=') { self.configs.insert( key.trim().to_string(), value.trim().to_string(), ); } else { return Err(ConfigError::ParseError( format!("无效的配置行: '{}'", trimmed) )); } } Ok(()) } /// 获取配置值(返回 Option,不 panic) fn get_value(&self, key: &str) -> Option<&str> { self.configs.get(key).map(|s| s.as_str()) } }可以看到,修改后的代码:
- 用
?操作符替代了unwrap(),错误传播更安全 - 用
split_once替代了split + collect,减少了不必要的内存分配 get_value返回Option<&str>而不是 panic- 增加了对空行和注释行的处理
- 去掉了冗余的
loaded字段 - 增加了
ConfigError自定义错误类型
四、边界与注意事项
AI 可能给出错误建议。模型不了解你的完整业务上下文,有时会建议把本来正确的逻辑改错。比如它可能建议把某个unwrap()换成?,但你看代码上下文后发现那个unwrap()前面有逻辑保证了它不会失败。
安全相关的修改必须人工复审。涉及 unsafe 代码、加密算法、权限检查的修改建议,AI 的分析可能不够深入。这类代码的评审还是要以自己的判断为准。
不要过度依赖。AI 评审是一个辅助手段,不能替代理解自己代码的过程。我一般是写完一段逻辑比较复杂的代码后用它扫一遍,看看有没有自己遗漏的改进点。
Prompt 的针对性很重要。如果代码涉及特定领域(比如嵌入式、GPU 计算),需要在 Prompt 中补充领域上下文,否则模型可能给出不切实际的建议。
五、总结
"clippy + AI"的组合思路本质上是分工:clippy 负责硬规则(编译器和静态分析工具的强项),AI 模型负责软判断(需要经验和语感的弱项)。两者加在一起,覆盖的面比单独使用任何一种都要广。
实际用下来,这套流程帮我发现了不少自己写代码时的盲区——特别是一些"虽然能跑但不够 idiomatic"的写法。clippy 虽然也能覆盖一部分,但 AI 给出的解释更详细,修改建议也更贴合上下文。
如果你也是 solo 开发的场景,不妨试试这个组合。不用每次都跑,复杂逻辑或者重构的时候跑一遍就行。成本很低(一次 API 调用几毛钱),但能帮到你发现的改进点可能价值远不止这些。