第2353篇:AI时代的代码Review——LLM辅助代码审查的工程实践和边界
第2353篇:AI时代的代码Review——LLM辅助代码审查的工程实践和边界
适读人群:团队技术负责人和高级工程师 | 阅读时长:约13分钟 | 核心价值:LLM辅助代码审查的实际方法,以及它能做什么、不能做什么的清醒认识
我们团队引入LLM辅助Code Review已经快一年了。
一开始我是有些抵触的。Code Review对我来说不只是"找bug",更是团队知识传递、架构对齐、互相了解代码思路的过程。我担心如果引入AI,大家会偷懒,不认真看代码,把决策权交给机器。
实际做下来,我的看法变了。
LLM辅助Review在某些方面确实能大幅减轻负担,但它的边界非常清晰,清晰到如果你越过那条边界,效果会急剧变差。
今天把这一年的经验整理出来,供你参考。
Code Review的真实成本
先说一个现实:大多数团队的Code Review都没做好。
原因不是工程师不负责,而是时间根本不够。你有自己的feature要做,突然来一个PR通知,代码量两百行,你扫了一眼,觉得没问题,approve了。
这种"扫一眼"的Review是没有效果的。真正有效的Review,你需要:
- 理解这段代码要解决什么问题
- 阅读每一行实现,理解设计选择
- 想清楚有没有遗漏的边界条件
- 评估这个实现对后续维护的影响
- 给出具体、有建设性的反馈
一个两百行的PR,认真Review需要30-60分钟。如果你一天有三四个PR要看,Review本身就会成为全职工作。
这正是LLM辅助Review能发挥作用的地方——它可以承担其中一些机械性、模式化的检查工作,让人类审查者集中精力在真正需要判断的地方。
LLM擅长做什么样的Review
我把Review工作拆解成几个层次:
代码Review层次(由低到高)
├── 语法与风格:命名、格式、注释
├── 安全模式:SQL注入、XSS、不安全的反序列化
├── 常见bug模式:空指针、资源泄漏、并发问题
├── 测试覆盖:有没有测试,测试是否覆盖关键路径
├── 设计质量:单一职责、接口设计、扩展性
├── 业务正确性:实现和需求是否一致
└── 架构影响:这个改动对整体架构的影响LLM在前四层(语法到测试覆盖)的表现相当不错,在第五层(设计质量)有一定能力但不稳定,在后两层(业务正确性、架构影响)几乎帮不上忙。
原因很直接:后两层需要的是对具体业务逻辑和系统全局架构的理解,而LLM没有这些上下文。
实际使用的提示词模板
我们团队目前用的是这套提示词框架,经过多次迭代:
基础检查提示词:
你是一个有丰富Java工程经验的资深工程师,请对以下代码变更进行Code Review。
代码改动的背景:[一两句话说明这个PR要解决什么问题]
请重点检查以下几个方面:
1. 潜在的空指针、数组越界等运行时异常
2. 资源管理(连接、流、锁是否正确关闭/释放)
3. 并发安全(是否有共享状态的并发问题)
4. 安全漏洞(SQL注入、敏感信息泄漏)
5. 错误处理(异常是否被正确捕获和处理)
对于每个发现的问题,请指出:
- 具体在哪一行
- 问题是什么
- 建议怎么修改
不需要评价代码风格,也不需要给出正面评价,只列问题。
代码:
[粘贴代码]深度分析提示词(用于较复杂的变更):
请对这段代码进行更深入的分析:
1. 这段代码在以下场景下会不会有问题:
- 输入为空或空集合
- 并发调用(100个线程同时调用)
- 依赖的下游服务超时或返回错误
2. 这段代码有没有性能问题:
- 有没有在循环里做了不必要的重复计算
- 有没有N+1查询问题
- 内存使用是否合理
3. 测试覆盖是否充分(根据提供的测试代码判断)
代码:[粘贴代码]
测试代码:[粘贴测试]一个真实的例子:LLM帮我找到了什么
前几周一个同事提了一个PR,是一个文档处理服务的改动,大概150行。我先让AI跑了一遍:
原始代码(简化):
public class DocumentProcessor {
private final EmbeddingClient embeddingClient;
private List<String> processedIds = new ArrayList<>(); // 问题在这里
public ProcessResult process(List<Document> documents) {
List<DocChunk> chunks = new ArrayList<>();
for (Document doc : documents) {
if (processedIds.contains(doc.getId())) {
continue;
}
List<DocChunk> docChunks = splitDocument(doc);
float[] embedding = embeddingClient.embed(
String.join(" ", docChunks.stream()
.map(DocChunk::getContent)
.collect(Collectors.toList()))
);
chunks.addAll(docChunks);
processedIds.add(doc.getId()); // 问题在这里
}
return new ProcessResult(chunks);
}
}AI的反馈:
发现两个问题:
1. [第3行] processedIds是实例变量,如果DocumentProcessor被作为Bean注入(Spring单例),
多个线程并发调用process()方法时,processedIds会有并发写入问题,
且数据会在多次调用间累积,可能导致内存泄漏。
建议:如果需要跨调用去重,使用ConcurrentHashMap;
如果只是方法内去重,改为方法级别的局部变量。
2. [第10-13行] 使用contains()检查processedIds的时间复杂度是O(n),
如果文档数量较多,整体复杂度会变成O(n²)。
建议:改用HashSet替代ArrayList。我检查了一下,这两个问题都是真实的。第一个并发问题我自己看代码可能也会发现,但可能要花更长时间。第二个性能问题,说实话,如果代码量大,我可能会直接漏掉。
LLM做不到的事:真实案例
同样是上面这个PR,AI没有发现的一个真实问题:
这个DocumentProcessor是一个Spring Bean,按照我们的架构设计,它不应该有任何实例状态(stateless design)。原来代码里的processedIds违反了我们的架构约定,但AI不知道这个约定,所以它只提示了并发问题,没有提示架构合规问题。
这个问题是我看代码时发现的,因为我知道我们团队有"Service层Bean必须无状态"的设计原则。
类似的情况还包括:
- AI不知道我们的业务规则(某些文档类型必须走不同的处理路径)
- AI不知道我们的性能约束(这个接口P99延迟必须在500ms以内)
- AI不知道某段代码和另一个模块有隐性的耦合关系
这些都是人类审查者必须承担的责任,不能交给AI。
我们团队的实际工作流
目前我们的流程是:
graph TD
A["提交PR"] --> B["CI/CD自动运行\n静态分析和测试"]
B --> C["自动触发AI Review\n输出评论到PR"]
C --> D["作者查看AI评论\n修复明显问题"]
D --> E["人工Reviewer\n进行深度Review"]
E --> F{"需要设计讨论?"}
F -->|是| G["同步会议或\n详细评论讨论"]
F -->|否| H["Approve或\n请求修改"]这个流程的好处是:当人工Reviewer拿到PR时,AI已经过滤掉了一批明显问题。Reviewer可以把时间集中在AI发现不了的东西上:业务逻辑正确性、架构决策、设计质量。
几个使用注意事项
1. 不要让AI做最终决策
有些团队引入AI Review之后,开始出现"AI说没问题就approve"的情况。这是危险的。AI的判断不能代替人的判断,尤其是对于业务核心逻辑。
2. 给AI足够的上下文
只贴代码片段效果会差很多。最好说明:这段代码要解决什么问题,它会被谁调用,系统的哪些约束需要注意。上下文越充分,AI的分析越准确。
3. 对AI的输出做二次判断
AI有时候会报false positive——把正确的代码标记为有问题。作者和Reviewer都应该对AI的评论做独立判断,而不是无条件信任。
4. 关注AI的"没发现问题"不代表真没问题
AI没报问题,不代表代码没问题。这是最容易犯的错误。AI的沉默不是保证。
结语:工具改变了什么,没有改变什么
引入LLM辅助Code Review之后,我们团队最明显的变化是:低级bug的漏过率降低了,特别是那些隐藏在某个角落里的空指针、资源泄漏、并发问题。
没有改变的是:真正难的Review工作还是需要人做。业务逻辑对不对,架构选择合不合理,这段代码三个月后好不好维护——这些判断,AI给不了。
Code Review的本质是经验传递和集体决策,AI能帮你做其中的体力活,但核心的智识工作还是人的事。
别把工具当作免责牌,也别因为用了工具就觉得不需要认真看代码了。
