智能体时代如何进行代码审查
在 2025 年,我们见证了代理编码的兴起(显然"氛围编码"这个词已经过时了)。在 AI 助手和代理工作流之间,功能以前所未有的速度涌现。公司夸耀其代码库中有多少百分比的代码完全由 AI 编写已经不足为奇。
这是好事还是坏事,我们还有待观察(我个人认为是好事),但这种编写速度的提升并非没有代价:审查海量的产出令人疲惫,代码审查正在迅速成为瓶颈。一些团队/开源项目甚至采取了极端选项——完全不接受 AI 生成的 Pull Request。
虽然禁止 AI 可以让人们喘口气,但我认为从长远来看这不是一个好选择。"抵抗是徒劳的",正如我最喜欢的虚构种族会说的那样。要在这个新的生产力水平下生存,我们必须停止做机器能做得更好的工作。用 AI 对抗 AI。但不仅仅是 AI,一套好的老式确定性工具也能创造奇迹:如果 Linter 能捕获一个问题,我就不应该去看它。如果格式化工具能自动修复它,我真的不在乎。
我的"不受欢迎"观点:我不太关心代码是人类还是代理编写的。在开源中,贡献是零信任的。无论代码是由经验丰富的 FAANG 工程师编写的,还是由斯里兰卡的高中生编写的,这不应该有什么区别。那么为什么我们应该关心它是否由 AI 编写呢?
理论上,人类生成的 PR 会更小,但在这个行业工作了 20 多年后,我见过不少巨型 PR,所以我可以自信地说,处理巨型和/或粗心的 PR 并不是一个新问题。
我从表面价值来评估代码。它能工作吗?安全吗?是否修复了已知问题?是否符合我们的路线图?是否符合我们的标准?
这就是为什么在今天的文章中,我想谈谈我是如何进行代码审查的,不仅是在处理外部贡献时,也包括处理我自己 AI 生成的代码时——因为事实上,使用 AI 编程意味着一直在对 AI 进行代码审查。
1、我真正关心的事情
如今当我审查代码时,我的关注点变得越来越高层。在某种意义上,我手动编写的代码越少,我就越不关心代码的个别方面。我在每一个我担任领导角色的团队中总是说:代码是可丢弃的。这在今天比以往任何时候都更真实。我再说一遍:代码是可丢弃的。不可丢弃的是你在开发某些代码时获得的系统知识。这种知识通常能很好地从一种实现迁移到另一种实现,或者例如,在从 API v1 迁移到 v2 时保留下来。
第二次编写同样的东西更容易,因为你已经经历了发现大量信息和减少许多模糊性的成长痛苦。你学到了什么有效、什么无效。什么是过度工程、什么是工程不足。这才是软件工程中重要的部分:收集知识、迭代、演进。这是将在 AI 时代存活下来的知识。代码只是一个实现细节。
基于这个理念,以下是我在代码审查时关心的(非穷尽)事项:
1.1 架构和系统设计
AI 模型在把握大局方面存在困难,并且有走很多捷径的倾向。我的审查过程会寻找这些信号,如硬编码的值和配置、问题空间的过度简化(AI 经常将编码请求视为原型或演示),以及矛盾的是,过度工程。AI 模型还有一个令人烦恼的特点,即假设生产就绪程度等于复杂性。换句话说,它们在平衡和实用主义方面存在困难——这些东西是我们通过经验学到的,通常很难用语言表达。
1.2 公共 API 和模块
我们构建的东西的人体工程学很重要。公共 API 需要让必须使用它的普通开发者"感觉对"。一个设计良好的接口是直观的、难以误用的,并且对代码库的其余部分隐藏了杂乱的内部实现。我检查接口是否健壮且正确地限定了范围,目标是尽可能小的表面积。如果 API 笨拙,底层代码再优雅也没用。代码是否易于使用且有良好的文档?公共 API 好的一个好迹象是测试质量高。糟糕的 API 设计本质上难以测试。
1.3 算法和模式
LLM 经常默认使用最天真、最暴力的方式来解决问题。代理尝试使用嵌套循环和每几行提交一次来运行大规模数据迁移,而批量插入策略才是正确的方法,这种情况很常见。或者在核心层面:当 map 或字典是正确的数据结构时使用列表。验证数据结构和算法是否真正适合问题空间可以防止巨大的性能下降。目标是可扩展的代码,而不仅仅是能通过测试的代码。不过,过早优化仍然是一个风险。如果一个更简单的方法稍微慢一点但更易读,并且我们处理的是一个小而有界的数据集,可读性通常会胜出。
1.4 依赖
每个新包都带来外部风险、潜在的安全缺陷和维护开销。保持应用小型化可以减少我们的攻击面。核心工具如我们的 GenAI SDK 或主要 Web 框架可以获得更快的通过,但其他一切都会受到严格审查。少量复制(或重新实现)比少量依赖好。代码生成和维护变得越容易,我就越不担心复用——特别是如果这意味着向我的代码库添加一个新的攻击向量。
1.5 反模式和质量问题
仅举几例:忽略或静默错误、副作用、全局状态、可变状态、资源泄漏、未使用的函数或变量等等。语言习惯也很重要。虽然我非常关心这些,但它们也是最容易通过使用静态分析(Linter)自动化的,如 golangci-lint(Go)和 ruff(Python)。
1.6 可测试性
难以测试的代码通常是设计不良的,将来会抗拒变更。清晰的关注点分离、干净的输入和纯函数是理想的。好的测试证明代码有效,并为我们的未来修改提供安全网。对于 UI 组件和复杂系统,我接受实用的测试策略而非严格的单元覆盖率,但核心逻辑必须被覆盖。
我不再试图为每个项目添加覆盖率目标,因为每种情况都是不同的,但我需要知道应该被测试的内容是否被测试了。理想情况下,正常路径 100% 覆盖,异常路径有良好的覆盖率,但我不会试图实现所有代码 100% 覆盖或接近那个数字。只要你有良好的可观测性策略和良好的错误消息,你就为成功做好了准备,因为新的错误模式可以稍后添加到测试套件中。
1.7 基准测试
对于关键路径,我们需要实际数字而不是对性能的猜测。对于任何影响高流量组件的变更,必须有清晰的基准测试,以阻止慢代码到达生产环境。
1.8 精简日志
日志必须是可操作的。不必要的日志会增加我们的云账单,并可能泄露私有信息。冗长的日志在开发期间是可以的,但在合并之前必须清理掉。
2、我不太关心的事情(大部分)
我让自动化工具处理细节,这样我就可以专注于难题。如果机器能做,人类就不应该做。
2.1 每一行代码
审查 LLM 生成的每一行代码是编译器或静态分析器的工作。我专注于逻辑和连接点。
2.2 格式化
自从我开始做 Go 以来,我从未就格式化风格进行过讨论,但我知道在某些领域这些讨论仍然存在。你能做的最好的事情就是设定一个标准,让 Linter 和格式化工具来处理。一旦标准确立,代码代理也能更好地遵守它。如果 CI 管道通过,那就没问题。
2.3 次要语法和代码细节
有很多方法可以解决一个问题,强制特定的语法选择限制了开发者的自由。我不在乎它是 for 循环还是列表推导式,只要逻辑是正确的。
2.4 调试
我几乎从不进行调试会话(在严格意义上使用调试器)。对我来说,调试是最后的手段,是添加一堆"我在这里"打印语句的同义词——而那些本来应该是日志行。
如果某些东西不工作,我会创建一个新测试来模拟问题。如果在重现问题后我仍然无法弄清楚发生了什么,这意味着我的可观测性和日志不够充分,所以我专注于改进那些方面。
2.5 未导出的名称
当一个名称是函数局部的或作用域有限时,我对它的关心程度远低于它在许多函数或文件中使用时。我会快速浏览代码,如果我瞥见任何荒谬的东西,我可能会停下来整理一下,否则我对模型决定使用的任何名称都可以接受。
2.6 次要依赖
那些不是你的主要框架或客户端库的依赖。如果它们满足我们的安全基线,这些就较少令人担忧。不过,检查安全漏洞和有问题的许可证仍然是强制性的。如果我只是为了一个"辅助"函数导入某些东西,我会 100% 在我的代码中重新实现那个函数并去掉依赖。
3、结束语
这不是一个一刀切的协议。关于你如何为代码库添加可观测性,也有很多可以说的。仅靠代码审查无法捕获所有潜在问题,这就是为什么我强烈提倡自动化,现在有了代理编码,比以往任何时候都更需要。
现代编码代理有许多扩展模式,允许你约束模型并获得更确定性的输出:Agent Skills、钩子、MCP 工具、策略、规则……使用这些工具为你的代理设定明确的范围限制,你的生活将变得更加轻松。
一辆汽车只能跑得和它的刹车允许的一样快。投资学习你喜欢的编码代理的护栏,用你宝贵的时间审查那些无法自动化的东西。
编码愉快!
原文链接:How to Do Code Reviews in the Agentic Era
汇智网翻译整理,转载请标明出处