用 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 调用几毛钱),但能帮到你发现的改进点可能价值远不止这些。
290



被折叠的 条评论
为什么被折叠?



