Nguyen Le Phong

友善而高效的工程文化第 1 篇,共 5 篇

代码审查的艺术:如何批评代码而不打击人

代码审查是团队文化成败的关键所在。这是一份实用指南,教你如何在让代码更好的同时,也让作者越来越强——具体的措辞、审查者清单,以及那些悄悄让审查变成毒药的习惯。

想象一下:周一早上你打开自己的 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 有清晰的上下文和意图描述,一目了然。

A 2x2 quadrant mapping review feedback quality. The ideal quadrant is Kind and Specific. 友善 不友善 模糊 具体 友善 + 模糊 感觉不错但没有 帮助你进步。 “总体上看起来不错!” 友善 + 具体 ✦ 目标 ✦ 清晰、可操作、相互尊重。 传授知识,加快交付。 “nit:提取这个辅助函数—— 更容易隔离测试。” 不友善 + 模糊 危害最大。 打击士气却不给方向。 “这是一团乱。” 不友善 + 具体 技术上有用但 会逐渐侵蚀信任。 “你应该用 X, 这显然是错的。”
审查反馈的 2×2 矩阵。友善 + 具体(右上角,高亮)是目标:既诚实又尊重的反馈。友善 + 模糊感觉安全但什么也教不了。不友善 + 具体短期奏效但侵蚀信任。不友善 + 模糊——最糟糕的象限——打击士气却不给方向。

审查速度与 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(善意工程)

常见问题

代码审查应该花多长时间?
对于范围清晰、200 行以内的 PR,计划 15–45 分钟的专注时间。在几小时内确认审查请求,并在一个工作日内完成,是健康的目标。超过 48 小时的审查会成为瓶颈,拖慢整个团队——及时性是审查质量的一部分。
代码审查应该关注什么?
重点关注正确性(这在生产环境中能正常运行吗,包括边界情况?)、设计(它是否整洁地融入了架构?)、可读性(六个月后队友能看懂这段代码吗?)、测试(测试是否有意义,覆盖了重要情况?)和安全性(用户输入是否经过验证,是否有注入风险?)。跳过你的 linter 已经能捕捉到的格式问题——这些注意力最好花在只有人类能判断的地方。
如何给出代码审查反馈而不显得苛刻?
最有效的技巧是用问题代替判决,并始终解释为什么某件事很重要。“你为什么在这里选择 X?”邀请对话;“你应该用 Y”关闭了对话。给评论严重程度打标签(nit / suggestion / blocking)也有帮助——它区分了“这是阻断”和“这是偏好”,这样作者就不必猜测了。
应该因为代码风格细节而阻止合并 pull request 吗?
不应该。阻止合并意味着正确性 bug、安全问题,或对已商定团队标准的明显违反——如果合并会造成真正问题的事情。风格偏好和细微的格式问题应该标记为 nit,由作者自行决定。一旦所有阻断评论都解决了,就带评论批准,让作者处理其余的事情。
pull request 应该有多大?
200 行变更以内是高质量审查的甜蜜点。在这个大小,审查者能在工作记忆中保持变更并发现细微问题。超过 500 行的 PR 审查质量急剧下降——审查者快速浏览,疲劳来袭,关键问题被遗漏。对于大型功能,使用堆叠 PR:一系列小的、自包含的变更,每个都讲一个清晰的故事。