原文:Anthropic,Solving code review at Anthropic。本文是按原文结构整理的要点笔记,用我自己的话转述,不是翻译;数字以原文为准。最后一节是我对照自己的 Skill 体系做的取舍,具体改动过程见《一次 Skill 迭代的全过程》。
这篇文章讲的是 Claude Code 团队在 AI 写出大部分代码之后,怎么重新设计代码审查。我读它的起因,是另一篇文章(我的笔记见《当智能体代劳时,如何保持批判性思维》)引用了它的一句经验法则:说不清这些检查会漏掉什么,就该请求一次真正的审查,而不是草草批准。
审查到底在解决什么
文章先把代码审查拆成两个作用:在发布之前发现问题,以及让团队对代码库保持共同的理解。
接着它换了一个问法。”有没有人读过这段代码”这个问题不够好,因为它没有回答真正要回答的事:这次改动可能出什么问题,该由人还是由自动化检查来拦。换了问法之后,审查也从”写完、提 PR、等同事反馈”的一道关卡,变成写代码过程中一直在跑的检查,像写作时的拼写检查。常见错误在早期就被拦下,人只需要处理更高层的问题。
稀缺的是人的时间和注意力
文章把人的成本算成两部分:花在审查上的时间,和花在修复漏网问题上的时间。只是把时间从前者挪到后者,不算节省。
团队的目标是:Claude Code 各个仓库里的大多数 PR,最多一个人参与就能合并,这个人可以是作者本人,前提是回滚和事故不增加。他们记录每个 PR 需要的人工批准次数、人工评论数量和等待时间;回滚率和事故率是反向指标,这两个数一涨,就说明省下的人工时间只是转移到了别处。
一个改动要经过的几层
每一层拦住一部分问题、漏掉另一部分,最后漏过去的靠回滚兜底。
- 提交前自验证。 跑测试、构建或截图对比,把输出给人看;让全新上下文的子 agent 审一遍 diff;反复出现的审查意见在源头修掉,比如改 skill、加 lint 规则或更新指令文件。
CLAUDE.md写项目背景、规范、目标和非目标;REVIEW.md由代码负责人写,告诉审查者这块代码里什么最重要。好的REVIEW.md很短:给出这个仓库里”失败长什么样”的例子,列出要跳过的路径,要求证据给到文件和行号,总结先给数量,并说明什么情况下不用修。 - 每个 PR 都有 AI 审查。 几个 agent 并行找问题,另一组 agent 对每条发现投票,足够多的 agent 认为是真问题才发出来;只关于风格或清理的发现直接丢掉。留下来的按严重程度标成 Important、Nit 或 Pre-existing,Pre-existing 表示问题在这次改动之前就有。每条发现都要回应:修掉,或写明理由后关闭。上线后,收到实质性评论的内部 PR 从 16% 升到 54%。每个仓库还能选审查深度,漏一个 bug 的代价高于多花的审查时间时才开最深一档。
- 安全审查。 单独一轮。两种审查里未解决的发现都会阻止合并。
- 阻断式自动检查。 lint、类型检查、各平台的测试和构建。PR 模板要求写清楚怎么测的、怎么回滚。
- 按风险的人工批准。 低风险改动(文档、文案、受开关保护的新增代码)自动检查通过即可;中风险一个人;高风险两个人。分级由分类器根据改动本身判定,不看作者。agent 的批准永远不算人的批准;批准只对应某一个 commit。
- 合并队列、功能开关和分阶段发布。 合并队列在最新的 main 上带着这次改动重跑全部检查,防止两个各自通过的 PR 合在一起弄坏 main。
- 快速发现、快速回滚。 事故复盘问的是”哪一层本该拦住它”,不是追究个人。
审查怎么停下来
这一节对我最有用。审查一度停不下来:一个 CLI 的 PR 一夜之间被审了 50 多轮。审查器反复标出的问题里,约 59% 是这次改动之前就存在的旧代码,每一轮只是又挖出几个旧 bug。
他们的修法有四条:设轮次上限,几轮之后作者要重新考虑做法;前几轮之后只审 diff;标出问题的类别,同类实例一次修完;同一个问题不重复报。修完以后,一轮审查加修复就通过的比例从 65% 升到 78%,需要五轮以上的从 8.9% 降到 1.5%。每次事故还会变成审查器的评测用例,审查器的提示词、工具或模型一改,就在全部用例上重新打分。
人仍然留在哪些环节
决定做不做(方向、品味、范围);高风险代码,比如计费、自动更新和 CI 配置;别人会照抄的架构;读证据,也就是测试输出、命令和结果、截图和审查结论;以及为结果负责,每个上线的改动都有一个人负责。
最后一节讲共享上下文:没有人掌握整个代码库的完整模型。需要理解某一块时,让 Claude 现场写一份解释,几分钟就好,而且反映的是当前的代码。
我们的取舍
读完之后,我对照自己的 Skill 体系逐条讨论,决定吸纳什么、不吸纳什么,当天全部实施。代码在 design-integrity-guardrails、reality-evidence-engineering 和 agent-skills-index。
吸纳了的
| 文章的做法 | 我们的版本 | 落在哪里 |
|---|---|---|
REVIEW.md 由代码负责人写 |
同样由负责人写,另加一节机器可读的 ## Acceptance surfaces(验收面 glob),scanner 只读这一节,其余内容给两个审查 Skill 读。新增 review-md 负责起草:只在我点名时运行,只问代码答不出的问题,我批准后才保存 |
guardrails 仓库的 review-md/ 和 design_integrity.py |
| 全新上下文的子 agent 审 diff | design-integrity-review 恢复干净上下文:只给目标、非目标和验收条件,不给实现理由;每条 FAIL 附一个能走到被标代码的输入 | design-integrity-review |
| Pre-existing 标签 | 标 [pre-existing],不阻断本次改动,单独开票 |
两个审查 Skill、evidence-first-testing |
| 标出问题类别、同类一次修完、不重复报 | 报告写类别并列出全部实例;修复时整类修完;复审只检查旧问题修好没有 | 同上 |
| 轮次上限 | 原来就有:一次审查,修完最多一次定向复审;同一条不变式再被打破时返回 needs model,改为写出不变式、系统地搜索 |
两个审查 Skill、全局 AGENTS.md |
| 总结先给数量 | 报告第二行是 SUMMARY:,没有问题就写 No findings. |
两个审查 Skill |
| 按改动本身分风险等级 | 低、中、高三级:文档、文案、配置值不跑审查 Skill,diff 就是证据;普通代码按路由表;敏感路径、验收面、schema、迁移、公共接口两个审查都跑,再由我看证据批准 | 全局 AGENTS.md 的 Review depth |
| PR 模板写怎么测、怎么回滚 | 完成报告:状态词,加 Works / No regression / Risk / undo 三行 | 全局 AGENTS.md |
| 合并队列在最新 main 上重跑全部检查 | 全量测试放在合入点;仓库的 CI 会跑全量时,本地提交前不重复跑 | AGENTS.md 的 Testing、evidence-first-testing、项目模板 |
| 反复出现的审查意见在源头修 | 同类问题再出现时,先给出能拦住它的检查及运行位置;机器判定不了才写文字规则;区域级要点写进 REVIEW.md |
两个审查 Skill |
| 说不清检查会漏掉什么,就要真正的审查 | LIMITATIONS 写没覆盖的范围;没复现的问题写 “may”,不写成事实 | 两个审查 Skill |
没有吸纳的
- 多 agent 并行找 bug、再投票验证。 成本高。我们用”问题必须先复现才算数”达到同样的目的:每条 FAIL 都要附一个能走到它的输入,只靠读代码推出来的问题只能写成 “may”。
- 风险分类器、CODEOWNERS、分支保护、功能开关和分阶段发布。 这是团队级基础设施,个人 Skill 用不上。
- 把事故做成审查器的评测集。 方向对,但需要数据量。以后可以拿多 agent 项目的审查报告当素材。
- 人工时间、回滚率这类指标。 个人使用,样本太少,统计不出东西。
文章没覆盖、我们补上的
- agent 改验收测试。 文章默认测试本身可信。可我实际遇到过 Codex 在功能没做完时改测试、宣布完成。我们把验收面写进
REVIEW.md,实现期间冻结;Stop hook 发现验收面被改就阻断。REVIEW.md本身也冻结,否则删掉一条 glob 就能绕过。 - 比较的起点。 SessionStart 时记下会话起点,scanner 从起点开始比,agent 中途 commit 的改动也看得见。
- Codex 上的 hook 会静默失效。 未被信任的 hook 在
codex exec里会被跳过,没有任何警告。所以检查装好后,要用一次真实运行确认它确实跑了。 - 测试的成本。 跑测试前先说这次要证明什么;全量通过只能证明没有回归,不能证明新功能是对的。
这篇文章写的是一个团队的系统:分类器、投票、合并队列、发布开关,个人很难照搬。但它背后的几个判断对个人一样成立:人的注意力是最贵的资源;审查要能停下来;问题要先能复现,才算问题。
读者怎么说
先看读者反馈,再直接在当前页面继续讨论。不用绑定社交账号,填个昵称、抓一只娃娃就能留言。
快捷身份
留下你的想法
选一个预设身份,或者直接填昵称;写完点发布,抓到指定的娃娃就能提交。若显示“需要配置 Waline”,说明站点还缺少可写评论后端。
当前未选择预设身份