想象一下:周一早上你打开自己的 pull request,为这几个月来写得最整洁的代码感到骄傲。到了下午,一条评论让你对以后的每次部署都心生畏惧。不是因为反馈说错了——它没有——而是说的方式。“这没必要这么复杂。你为什么要这样做?”七个字,零可操作建议。整整一周从此都蒙上了一层阴影。
代码审查是任何工程团队中杠杆率最高的仪式。做好了,它能交付更好的软件,让工程师成长更快,并悄然建立信任文化。做差了,它会让人精疲力竭,拖慢交付速度,把你最优秀的工程师逼向那些不需要在每个 pull request 里自我辩护的团队。本文的目的就是把它做好——包括具体措辞、审查者清单、那些悄悄让审查变成毒药的习惯,以及当你是接受评论的一方时该怎么做。
代码审查是文化,不只是找 bug
关于代码审查是为了什么,有一个常见的误解。技术目标——在 bug 到达生产环境之前发现它——是真实而有价值的,但只是故事的一部分。
你留下的每一条评论都在传授某种东西。它向作者传授关于代码库的知识、关于权衡取舍、关于团队所在意的标准。经过数百次审查,你就是团队集体记忆的显性化。新工程师通过阅读你的评论来了解这里的“好”是什么标准。初级工程师通过内化审查者持续追问的问题成长为高级工程师。高级工程师则通过把直觉背后的“为什么”表达出来保持敏锐。
审查也在无形中塑造规范。一个持续留下善意、具体、充满好奇心评论的团队,会成为一个心理上安全的地方——人们可以提交不完美的东西,并从中学习。一个留下生硬、轻蔑或模糊评论的团队,则会让人们在沉默中过度设计以避免批评,拖延开 PR,或者不再提问。
当工程师开始害怕开 pull request,他们会把工作积压成越来越大的变更集,以减少审查次数。越大的 PR 越难审查好,于是审查质量下降,反馈越来越尖刻,循环不断加深。解药不是减少严格——而是在严格的同时增加善意。
这并不意味着代码审查应该软弱无力。最有效的审查者是非常诚实的。他们清楚地指出问题,在真正重要的事情上坚守底线。但他们的方式让作者感觉获得了一位合作者,而不是面对了一位法官。
真正该审查什么——以及什么可以放过
最常见的审查错误之一是对所有内容一视同仁。因为缺少一个末尾逗号就阻止合并的审查者,与发现 SQL 注入漏洞的审查者拥有同等的否决权。这种不对称会迅速侵蚀信任。首先在心里明确什么才是真正重要的:
- 正确性:这段代码做了它应该做的事吗?边界情况处理了吗?测试没有覆盖到的生产环境失败场景有哪些?
- 设计:这个变更能自然地融入现有架构吗?是否引入了不必要的耦合?有没有更简单的方法解决同一个问题?
- 可读性:六个月后,没有写过这段代码的队友能看懂它吗?命名清晰吗?流程容易跟踪吗?
- 测试:测试是否有意义?是否覆盖了正常路径、边界情况和失败模式?测试失败是否真的能告诉你某些有用的信息?
- 安全性:用户输入是否经过验证?是否可能引入数据泄露、权限提升或注入漏洞?(如果是,视为阻止合并的问题。)
不值得花审查精力的地方:
- 格式与代码风格——如果你的 linter 或 formatter 会自动处理,你就不需要管了。CI 能抓到的,你不必重复。人工审查的时间宝贵——不要花在机器能处理的事情上。
- 不符合团队标准的个人偏好。“我会用不同的名字”不是审查意见。“这个名字与领域中其他地方的通用语言不一致”有时是。
- 在 PR 评论里进行架构辩论。如果你对方向有根本性的分歧,那是一次设计对话,不是代码审查。在下一个 PR 开始之前,单独、协作地进行这个对话。
在写评论之前,问自己:“如果作者做了这个改动,代码会明显变好吗?”如果答案是“有一点”或“也许”,想想它是否值得他们和你的时间。如果答案是“是的,原因如下”,那就写——但要友善地写。
如何友善而诚实地措辞
代码审查中最大的杠杆就是用词。同一个顾虑,用两种不同的方式表达,会在读者心中产生截然不同的反应。这里有一套实用工具:
- 用问题代替判决。“你为什么在这里选择 X 而不是 Y?”邀请对话。“你应该用 Y”则关闭了对话。问题可能揭示你没有的上下文——即便没有,作者也能在不感到被指责的情况下得出同样的结论。
- 解释为什么,而不只是说什么。“我们能把这个逻辑移到 service 里吗?”比“我们能把这个逻辑移到 service 里吗?把它放在 controller 里更难做隔离测试,而且如果将来 cron job 需要同样的行为,我们就得复制一份”要弱。为什么能传授;只说什么只是在发号施令。
- 提供建议,而不只是批评。如果你发现了问题,展示你会怎么做——或者至少给出大概方向。“我不确定,但类似……这样的东西”远比“这好像不对”要有用得多。
- 明确称赞好代码。“这个错误处理非常整洁——我要借用这个模式”花十秒打出来,完全免费。它稳固了作者的自信,把这个模式确立为团队标准,也让后面的批评评论更容易落地。
- 在合适的地方用“我们”和“我们的”。“我们的惯例是把这个放在 repository 附近”比“你把这个放错地方了”更温和,但同样准确。
苛刻 vs 友善:同一个观点,两种表达
| 苛刻(但技术上正确) | 友善——同样诚实 |
|---|---|
| “这是错的。你需要处理 null 的情况。” | “如果 user 是 null 会抛出异常——我们能在这里加一个守卫条件,或者注释说明调用方保证非空吗?” |
| “你为什么要这样做?这没必要这么复杂。” | “我觉得这段有点难跟上——有没有更简单的表达方式?也许我漏掉了什么上下文,有需要的话可以聊一下。” |
| “这个函数做的事太多了。拆开。” | “这个函数同时处理验证和持久化——把这两个职责分开可能会让每个部分更容易单独测试。你怎么看?” |
| “你显然没测试这个。” | “我没看到空输入的测试用例——是有意为之,还是我来开一个跟进工单?” |
“这里用 const,不是 let。” |
“nit:这里用 const,因为它从未被重新赋值——意图更清晰。” |
| “这是个安全漏洞。” | “blocking:这里是用户输入,直接进了查询——有注入风险。合并前能参数化吗?” |
| “抽象错了。” | “我在想这个抽象是否方向正确——感觉下个功能可能会更难做。值得快速同步一下吗?” |
注意“友善”那一列不是:软弱、含糊或不诚实。安全问题仍然被标记为阻止合并。null 情况仍然需要修复。函数仍然需要拆分。这里的善意是指:具体、有解释,并作为一个人与另一个人对话来传达,而不是法官在宣判。
细节、建议与阻断——给评论打标签
对审查文化最有实际意义的一件事,就是给每条评论标注严重程度。没有标签,作者只能猜测每条评论是“合并被阻止”还是“随便想想”。这种猜测消耗精力,而且往往猜错。
一个被许多团队使用的简单约定(由 Conventional Comments 推广):
- nit:细微的风格或偏好问题——改或不改由作者决定。“nit:这里加个末尾逗号,与文件其他地方保持一致。”
- suggestion:会让代码变更好但不是阻断。“suggestion:可以提取成一个辅助函数——但这个 PR 不是必须的。”
- blocking:这个 PR 在解决这个问题之前不应该合并。保留给正确性 bug、安全问题,以及对团队标准的明显违反。
- question:你是真的在问,不是暗示需要改动。“question:这个在后台 worker 上也运行,还是只在 web 进程上?”
- praise:明确、真诚的正面反馈。有价值,别跳过。
当所有阻断问题都解决了,就带评论批准。这是一个重要的习惯。它意味着:“我相信你会处理剩余的细节;你不需要我再来一轮。”它解放了你的同事,传递了信任,让审查循环不至于成为瓶颈。另一种做法——等到每个小问题都解决了再批准——是审查文化中的慢性毒素之一。
一旦所有阻断问题都解决了,就批准,让作者自行决定其他的事。把一个 PR 扣押在细节上,会让人觉得审查是障碍而非协作。如果你确实很在意某个非阻断点,明确地说出来——“我确实在意这一点,但由你来决定”——这样作者就有了完整的信息。
作者的视角:接受反馈而不过度个人化
被审查是一项独立的技能,大多数工程文化写作只关注审查者。但作者的行为同样塑造着审查文化。
默认假设对方出于好意。一条生硬的评论几乎总是一个忙碌的人快速写下的,而不是有意伤害你。在你从一条评论里读出语气之前,先问问自己是否有一种中性或正面的解读。通常是有的。
回复每一条评论。即使你不同意。对已实施反馈打个 ✅ 或写“已完成”,对你想反驳的内容给出解释,关闭循环,向审查者表明他们的时间得到了重视。沉默,无论你有没有那个意思,都会被解读为消极对抗。
当你真的不同意时,清晰而冷静地反驳。“我理解这个顾虑,但这是我这样做的原因:[理由]。如果你仍然有强烈感觉,很乐意讨论。”这与条件反射式地为每个决定辩护不同。目标是进行真正的对话,而不是赢。有时候审查者是对的,有时候你是对的,有时候正确的答案是“让团队来决定”。
不要在 PR 描述里过度解释自己的代码。如果你的代码需要一大段文字来为每个决定辩护,这是改进代码、命名或内联注释的信号——不是写更长 PR 描述的信号。最好的 PR 有清晰的上下文和意图描述,一目了然。
审查速度与 PR 大小——两个影响深远的杠杆
代码审查的质量在很大程度上由两个与审查者技能无关的因素决定:审查的及时性和 PR 的大小。
及时审查。等待审查的 pull request 背后是一个等待继续前进的人。当审查拖了好几天,工作就会积压——工程师开始下一个任务以保持生产力,上下文切换增多,等审查回来时,作者已经忘了自己当时在想什么。一个好的经验法则:在被 @ 到的几小时内确认 PR(哪怕只是“我今天下班前会看”),并在可行的情况下在一个工作日内完成审查。
更小的 PR 获得的审查质量显著更高。这有充分的记录,实践中也显而易见。100 行的 PR 每一行都会得到仔细关注。1000 行的 PR 会被橡皮图章盖过、草草浏览,或者审查过程拖得很长以至于成为冲突的根源。保持 PR 小的纪律——“每个 PR 一个逻辑变更”是一个不错的启发——带来的回报超过几乎任何其他审查卫生改进。它也让每个 PR 更容易写,因为你必须清楚地思考一个变更意味着什么。
| PR 大小(变更行数) | 典型审查质量 | 审查时间 |
|---|---|---|
| < 200 行 | 高——审查者能在工作记忆中完整保持它 | 15–45 分钟 |
| 200–500 行 | 中——在细微问题上开始疲劳 | 45–90 分钟 |
| 500–1000 行 | 下降——审查者快速浏览或分几次审查 | 数小时到一天 |
| > 1000 行 | 基本走过场——关键问题经常被遗漏 | 数天(或永远没完成) |
如果一个功能确实需要数千行,考虑用堆叠 PR:一系列小的、自包含的变更,彼此叠加构建,每个都可以单独审查。多开几个 PR 的开销几乎总是值得的。
不同团队规模下的审查文化
代码审查的正确方式随着团队的成长而发生实质性变化。三个工程师适用的做法,在三十个人时往往会主动崩溃,企业级团队则需要完全不同的工具。
独立开发或早期创业(1–5 人)。在这个规模,可能根本没有审查,有时完全没问题,有时则是灾难,取决于风险有多高。当你确实在审查时,关系通常足够紧密,以至于生硬的反馈也能被接受——但这也是坏习惯在没人命名之前就被固化的阶段。尽早建立给评论严重程度打标签的习惯;没有成本,但以后会变得无价。
小型成长团队(5–20 人)。这是审查文化被塑造的阶段。团队足够大,“我们都认识彼此”的默契开始消退,但又足够小,规范仍然可以由几个有影响力的声音来设定。一个以善意、具体、带标签的审查为榜样的高级工程师,能在几个月内改变整个团队的文化。这也是团队审查指南——哪怕只有一页——开始产生价值的时候:什么是阻断,什么是细节,审查应该花多长时间。
中型团队(20–100 人)。现在审查跨越了可能几乎互不相识的小组。在小型团队中自然发生的建立信任的工作,现在必须有意为之。如果规范不共享,跨团队审查会成为主要的摩擦来源。代码所有者、轮流审查和轻量级审查清单会有帮助。在工程回顾中把审查质量作为话题——不只是“我们交付快吗”,还要“我们审查得好吗”。
大型或企业团队(100+ 人)。在这个规模,审查同样是治理和合规功能,不只是质量功能。工具非常重要:自动化检查、必要的审查者、审查 SLA、审查周期时间指标。人工审查时间宝贵,应该保留给只有人类能做的事情——设计判断、领域知识、跨团队影响。任何 linter 或静态分析器能捕捉到的,都应该在 PR 打开之前就捕捉到,而不是在评论里。
核心要点
- 代码审查是文化第一,找 bug 第二。每条评论都在传授某种东西,评论的模式会在数月间建立或侵蚀心理安全感。
- 把审查精力集中在重要的地方:正确性、设计、可读性、测试和安全性。让你的 linter 处理格式问题。
- 友善 + 具体是目标。解释为什么,用问题代替判决,明确称赞好代码。
- 给评论打标签:nit / suggestion / blocking / question / praise。它消除歧义,尊重作者的时间。
- 阻断问题解决后就批准。不要把 PR 扣押在细节问题上。
- 作为作者:默认假设对方出于好意,回复每一条评论,有分歧时冷静反驳,不要在 PR 描述里过度解释。
- 更小的 PR,更快的审查。200 行以下获得最好的关注。堆叠 PR 处理大型功能。
- 审查规范需要有意为之——尤其是当团队超过 10–20 人时。把它们写下来,在回顾中重新审视。
如果这篇文章引起了共鸣,本系列的下一篇文章视野更广——从你如何审查代码,到你每天作为工程师如何展现自己:Kind Engineering(善意工程)。