工程 · Engineering
Claude Code 团队如何做代码审查
我们之前说过,Claude Code 自身约 90% 的代码是 Claude 写的。那么,在我们这样的产出速度下,代码是怎么被审查的?现在还有人在读代码吗?如果真的出了问题,又是谁发现的?
这篇文章讲的是 Claude Code 团队的代码审查流程,以及它是如何演进的。从 6 月到 9 月,合并进 Claude Code 代码库的 pull request 数量增长了一倍多,而其中不得不回滚的比例下降了超过 40%。
我们并没有取消审查。但我们把大部分「阅读」工作交给了机器,围绕人的时间和注意力做优化,并在「让错误能被快速、轻松地撤销」这件事上投入了大量精力。
重新思考代码审查的目的
当写代码的是 agent,变更数量会迅速上升。在 Anthropic 的 monorepo 中,每天落地的变更从今年 5 月的约 2,500 个增长到 9 月的 30,000 多个。
Claude Code 住在自己的仓库里,与 monorepo 分离,但它同样承受着巨大压力。以这样的速度交付时,逐行的人工代码审查即使并非不可能,也极其困难。
代码审查承担两项职责:
- 在代码上线前发现并修复问题
- 在团队中建立共享的上下文与对代码库的理解
本文主要聚焦第一项,但最后我们会回到第二项。
我们认为,「有没有人类读过这段代码?」只是「这次变更可能出什么问题,以及应该由哪道检查——自动的还是人工的——来抓住它?」这个问题的一个并不精确的替代指标。
问出这个问题,也改变了审查发生的时间。以前,审查是放在最后的一道关卡:写代码、开 PR、等同事来看。
现在,审查是持续进行的,更像是你写作时的拼写检查。大多数变更在被要求人来看之前,已经至少被 Claude 审过、修过一遍。这拦住了大部分常规错误,也把团队注意力留给了更高层的问题与更细致的审视。
稀缺资源:人的时间与注意力
我们希望把人的时间和注意力保留给只有人才能解决的问题,并确保其余部分由自动化检查兜住。以我们当前的变更速度,人类仍有大量工作要做,团队几乎每天都会遇到新的、前所未有的挑战。
人的时间涵盖审查成本的两端:人们花在审查上的时间,以及人们花在修复烂代码造成的破坏上的时间。如果削减了前者,时间却在后者身上冒出来,那就没有意义。
因此我们的目标是:Claude Code 各仓库(CLI、桌面端与 Web 应用、移动端应用)中的绝大多数 pull request,在最多只有一个人参与的情况下完成合并——这个人可以是作者本人——同时回滚率和事故率不上升。为了衡量人的时间,我们跟踪每个 PR 的人工批准次数(中位数与 95 分位)、人工评论数量,以及一个 PR 等待某个人所花的时间。回滚率和事故率则是护栏:如果它们上升,我们就并没有节省人的时间,只是把时间挪了个位置。
一次变更的一生
一次典型的变更,从工程师的笔记本电脑(或 Slack 里的一个 Claude 会话)出发,会经过多层代码审查。
左右滑动,查看完整图表 →
逐层拆解
左右滑动,查看完整图表 →
1在别人看之前,先验证工作本身
如今大多数审查发生在 PR 存在之前。我们会给 Claude 一些它可以运行的东西——比如测试、构建,或一张可与设计稿对比的截图——并要求它展示输出,而不是只说「做完了」。随后,一个 subagent 会以全新的上下文审查这份 diff。这两个习惯都来自我们的最佳实践指南。
在开 PR 之前,我们中很多人还会跑几个内部 skill:一个专门找 bug,一个负责简化 diff,还有一个端到端地检查行为。UI 变更还会对照我们的设计规范再检查一遍。
Claude Code 的创造者 Boris Cherny 曾说,给 Claude 一种验证自己工作的方式,大概是最重要的一件事。他估计这能让最终结果好上两到三倍,并写道:「验证在每个领域长得都不一样。它可能只是跑一条 bash 命令,或者跑一套测试,或者在浏览器或手机模拟器里把应用试一遍。」
当自动审查者反复指出同一个问题时,我们会从源头修掉它:改 skill、加一条 lint 规则,或者更新这类指导性文件:
CLAUDE.md告诉 Claude 这个项目是什么:技术栈、约定、目标与非目标。每次审查都会加载它,所以在大型代码库里我们保持它简短,并从那里指向其他文档。审查者也会标出违反其中约定的代码,以及那些让其中某句话过时的变更。REVIEW.md由代码所有者编写,告诉审查者在他们负责的那部分代码里什么才是重要的。我们会单独检查审查者是否真的遵循了它。写得好的都很短。它们会说明在那个仓库里失败长什么样(对文档来说是「它在说谎」,对配置来说是「它把流量路由错了」),限制吹毛求疵的数量,列出需要跳过的路径,并要求以文件和行号作为证据,而不是凭名字猜。它们还会要求审查结论以数字开头,并在没有需要修的东西时直说。
2每个 pull request 上的 AI 审查
在有人看之前,每个 pull request 都会先得到一次 Claude 审查。它把 diff 放在整个仓库和这个 PR 声明的意图之下来读,并追问:这个变更是解决了它自己的问题,还是契合系统,又或者破坏了别的东西?
在 bug 修复上,它会去找根因。公开版本 Code Review for Claude Code 现已在 Team 和 Enterprise 套餐上正式可用,而在本地跑 /code-review 会得到更轻量的版本。
在我们推出研究预览版时,获得实质性审查意见的内部 PR 占比从 16% 上升到了 54%。超过 1,000 行的 PR 大多能被发现问题,而大多数小 PR 会干净通过。管理员可以为每个仓库设置为 Quick、Standard 或 Deep。在「漏掉一个 bug 的代价高于多花的审查时间」的地方,Deep 是值得的。
在人来审之前,发现的问题会先由其他 agent 验证。多个 agent 同时找 bug。随后另一组 agent 对每条发现投票,只有足够多的 agent 认为它是真的,它才会被发布出来。只涉及风格、诊断信息或清理工作的发现会被丢弃。我们在自己的 PR 上对这些过滤器做过 A/B 测试,才决定保留它们。剩下的以按严重程度排序的行内评论呈现。每条都标注为 Important、Nit 或 Pre-existing,这样不是这个 PR 引入的 bug,看起来就不会像是作者的责任。
每条发现都必须被回应。要么修掉它,要么带着理由把讨论串标记为已解决。通常会有 agent 在夜里接手修复,于是作者第二天回来时,PR 已经又过了一轮。
3能够阻止合并的安全审查
安全有它自己的自动审查。只要 bug 审查或安全审查还有任何未关闭的发现,PR 就不能合并。你要么修掉它,要么用一句简短的理由关掉讨论串——这也是清除误报的方式。
如果后续的提交修复了问题,那条发现会自动清除。你也可以用开源的安全审查 GitHub Action 或 /security-review 命令搭建同样的机制。搭建方式见 Claude Code 中的自动化安全审查。
4能够阻止合并的自动化检查
就是那些常规项:lint、类型检查、各平台上的测试,以及构建。(我们写过一篇关于如何为这种体量扩展 CI 的文章,见这里。)一个典型的 CLI pull request 在有人看之前可能会跑几十项检查。PR 模板会问这个变更是怎么测的、怎么撤销。Claude 可以把它填好,但提出这个变更需求的人要对它签字确认。
5人工批准,按风险分级
一次文档修复和一次自动更新器的改动,风险并不相同,所以它们得到的审查也不同。
| 风险 | 示例 | 人工批准 |
|---|---|---|
| 低 | 文档、文案、仅内部使用的功能、开关后面的增量变更 | 自动审查与安全检查全绿后,无需人工批准 |
| 中 | 普通的产品变更,以及对敏感区域的变更,例如缓存、自动更新器或系统提示词 | 一人 |
| 高 | 风险最高的代码,以及一小批始终需要两人的路径 | 两人 |
分级由变更决定,而不是由作者决定。一个分类器读取变更内容,判定它的风险层级。第二个分类器则追问:即使这个层级本不需要,这次变更是否仍需另一个人参与。有些路径永远需要人,包括所有者列为敏感的任何东西,以及任何 CI workflow 文件。
只有受信任的作者才能走更轻的路径。只有在仓库受信任名单上的作者才能走更轻的路径,而名单上主要是拥有这个仓库的团队。其他人都需要第二个人来批准。agent 的批准也永远不能算作人的批准。自动审查可以放行一个低风险变更,但它不能填补一个需要人来的位置。
谁提出的变更,谁就要为它负责。在一个由 agent 写出的 PR 上,指挥这个 agent 的人算作必需的审查者之一,就如同这些代码是他亲手敲的一样。
一次批准只覆盖一个特定的 commit。如果之后落入了系统无法担保的新 commit——比如 agent 推送的,或某人手工解决的合并——批准就必须重新给出。
6合并队列、开关与灰度发布
合并队列会在应用了该变更的基础上,对 main 重新跑所有检查,所以两个各自通过的 PR 不可能组合出一个坏掉的 main。新行为通常会先放在开关后面,先发给 Anthropic 内部员工;我们自己的重度使用,往往能在用户之前暴露出回归。公开发布先走 latest 通道,stable 通道随后跟进。
7快速发现,快速回滚
总还会有一些错误漏过去,所以最后一层是尽快抓住它们。开关可以不用部署就能关掉。Claude Code 有每周轮值的 on-call,事故复盘会追问本应由哪一层抓住这个问题。
发现一次糟糕的合并,仍然依赖作者、on-call,以及回滚和事故看板。我们目前还没有能在这些指标变动时收紧审批的熔断机制;在更多代码转向「零人工合并」之前,工程师们希望先有它。
一位同事说过,慢下来确实会降低出错率,但那主要是因为你在所有事情上都做得更少。我们更愿意在「更快地发现并修复问题」上变得更强。
我们如何让审查者变得更好
我们的代码审查流程在一个循环中不断改进。
任何事故现在都是一份 eval。团队收集了过去引发事故的 PR,连同它们的 fix-forward 和回滚,并请熟悉那部分代码的工程师确认每一个 bug 以及它所在的位置。审查者每天都会在这个集合上跑一遍。
左右滑动,查看完整图表 →
我们通过观察人们如何对待评论来衡量噪声。如果你不在意误报很多,想多抓 bug 是很容易的,所以我们需要对这一点加以检查。我们会按 PR 上那个人的反应给审查者留下的每条评论分类:他修了它、他说它错了、他说这个行为是有意为之,或者他认为它无关紧要。我们还会把审查者跑在一些我们已知是干净的 PR 上,统计它报告出的、会让人做出有害改动的问题有多频繁。我们写下来的优先级是:先抓真 bug,其次减少审查轮次。
当作者是 agent 时,噪声的代价更高。一个人可以在几秒内把一个 nit 挥手带过。而 agent 会去修它,这意味着又一次提交和又一轮审查。
审查必须收敛。一个 agent 作者和一个 agent 审查者可以让彼此无限忙碌下去。有一个 CLI PR 在一夜之间经历了五十多轮审查与修复,最后是作者叫停了它。
当我们对审查者反复指出的内容跑 git blame 时,大约 59% 是审查者第一轮就已经存在的代码。审查者很少在发现新 bug,它只是在每一轮里多发现几个旧 bug。
这是我们自己搞出来的。我们已经习惯默认跑好几轮,因为每一轮都会抓到一点新东西。这在一次高风险变更上也许值得,但不该在每一个 PR 上都如此。团队现在把「过审轮次」和召回率放在一起跟踪,并修改了重审策略:
- 轮次上限。三轮之后,审查者会请作者停下来,重新考虑做法。
- 只审 diff 的重审,在前几轮之后启用。
- 标出 bug 的类型,让作者修掉所有同类实例,而不只是一行。
- 不重复已经发布过的发现。
审查 agent 还会每晚在既有代码上跑一遍,帮我们在旧 bug 冒到别处之前抓住它们。
左右滑动,查看完整图表 →
人类仍然留在环路中的地方
把一位资深工程师从深度工作中拉出来,只为抓一个常规错误,代价远高于一次自动审查。团队里的人仍然要对这些事情负责:
- 决定是否要做。方向、品味与范围。有一个团队在宣布自己的审查瓶颈已解决之后,仍然对来自团队之外的变更保留人工审查,好让有人问一句「我们真的该做这件事吗?」
- 审查高风险代码,例如计费、自动更新器和 CI workflow。
- 审查架构,即那些会为他人树立范式的设计。
- 阅读证据。测试输出、跑过的命令及其返回、一张截图、审查者的发现。如果你说不出这些检查会漏掉什么,那就去要一次真正的审查,而不是草草批准。
- 对结果负责。每一个上线的变更都有人负责,无论代码是谁敲的。
在团队中共享上下文
代码审查的第二项职责,一直是传播对代码库的理解。读一位同事的 diff,是你了解发生了什么变化、各部分如何拼在一起的方式。如果大部分阅读都由机器完成,人们又怎么学习代码?
我们没有为此设立正式流程。团队里没有人脑中保留着 Claude Code 的完整心智模型,我们也认为没这个必要。当有人想理解代码库的某一部分时,他们会请 Claude 针对那一个切片写一份讲解。几分钟就能出来,而且反映的是当下的代码。
我们中的一些人还会更进一步,让 Claude 做一个小产物,把那部分代码可视化,并按这个人喜欢的学习方式来讲解。
如果目标是把人的注意力花好,这比逐行阅读琐碎的 diff 强得多。一处一行的文案改动几乎什么都教不了你。为你即将改动的模块写一份讲解,能教你很多。我们「默认在公开 Slack 频道里工作」的文化也有帮助,因为别人在构建东西时,你能看见他们在构建什么。
「你合入,你负责」
如果你合入了一个小或中等规模的变更,而事情仍然出了问题,团队规范是通常由你来修。有几件事让它不至于变得糟糕:
- 变更很小,容易审查、调试和回滚。
- 一切都是可逆的。开关、回滚和分阶段发布能在几分钟内撤销大多数错误。
- 影响半径决定门槛。昂贵的代码会得到更多的人和更多的检查。
- 回归暴露得很快,通过遥测和内部使用。
- 这不是追责。拥有修复意味着你掌握最多上下文;事故复盘看的是系统。
责任也是共享的。团队大部分人都在公开频道里工作,任何人都能看到别人在构建什么、接手一个讨论串,或者把一个 Claude 指向某处空白;反馈也来自那些并不拥有这段代码的人。这种开放性是质量能保持在高位、而不必对每一行都设关卡的重要原因。
当我们改变方向时
agent 让开始一件事变得很便宜,所以有些工作不该落地。通常是频道里有人问一句为什么、指出一个更早的决定,或者提出一条更简单的路径,然后作者关闭或重塑这个 PR。
既然代码写得很快,关掉这个 PR 并不让人觉得是损失。诀窍在于尽早在这个讨论串里提出另一个方向,而不是把它作为一条阻塞性评论,留在一个已经完成的 PR 上。
你如何采用这套流程
和你的团队一起做这件事所需的大部分组件,今天都已经存在了。
▸在 pull request 之前
- 给你的 agent 一些它能跑的检查,写在简短的
CLAUDE.md里,再加上运行格式化和 linter 的 hook。 - 在本地跑
/code-review和/security-review,或者跑一次全新上下文的 subagent 审查。 - 加一个
REVIEW.md,把重复出现的审查意见搬进去。
▸在每个 pull request 上
- 要求 CI 通过,并在 PR 模板里加上「测试方式」「风险」「如何回滚」。
- 先在一个仓库上加 AI 审查者(Code Review for Claude Code,或 Claude Code GitHub Action),花几周时间调整
REVIEW.md,然后再铺开。 - 加上自动化安全审查,并要求合并前所有审查讨论串都已解决。
- 让审查投入与风险匹配:常规变更快速过一遍,漏掉 bug 代价高昂的地方做深度审查(Code Review 中的 Quick、Standard 或 Deep)。
- 给审查轮次设上限;前几轮之后,只重审新的 diff。
▸批准
- 把风险层级写下来,由变更而非作者决定;从严开始,再根据证据放宽。
- 把更轻的批准路径留给你信任的作者。
- 使用 CODEOWNERS 和分支保护,让认证、计费、CI 与发布 workflow、安装器始终需要人。
- 永远不要让 bot 的批准替代一个必需的人;有新 commit 时撤销已有批准。
▸合并与之后
- 使用合并队列、功能开关、内部先用、分阶段发布和一步回滚。
- 把事故和回滚变成一套 eval,每当审查者的提示词、工具或模型变化时就重新打分。
- 衡量人的时间,并以回滚率和事故率作为护栏。
这件事将走向何处
Code Review 的公告说,批准一个 pull request 仍然由人决定;产品发布的是发现,而不是批准。我们自己的仓库走得更远,让所有者决定哪些低风险变更可以仅凭自动化检查就合并,而这条线还在不断移动。
当一次审查漏掉了什么,我们会问:模型当时手上缺了什么?然后把修复写进 CLAUDE.md、一个 skill 或一项新检查。目标没有变:每个变更花更少的人类时间,同时回滚不比以前更多。
如果你的团队让 agent 开 pull request,我们很想听听你们自动化了哪些检查、哪些仍然需要人,以及你认为我们哪里搞错了。
感谢 Emma Webb、Michael Segner 和 Amie Rotherham 的审阅。