目录
5515 字
28 分钟
代码评审里的沟通:把事实、偏好与决策分开说

周五下午四点,你在 PR 里留了一条评论:

这里每次都 JSON.parse,性能太差了,建议用 useMemo。

十分钟后,作者回了一句:

数据量很小。

对话结束。代码没改,你有点不爽,作者也觉得你没事找事。

把同一件事换一种说法:

第 42 行在每次 render 时都会重新 JSON.parse(raw)。我担心的是 raw 较大、列表又频繁重渲染时,解析会落在主线程上;如果 raw 只在进入页面时变一次,这个担心就不成立。能否确认 raw 的来源和更新频率?如果它确实每次都变,我建议在数据层解析一次,而不是在组件里 memo。

还是同一个技术判断,还是可能被拒绝。但这一次,作者知道你在担心什么、需要回答什么,也知道拒绝你的条件是什么。

两条评论的差别不在语气,而在信息结构:第一条只交付了结论,第二条交付了一个可验证的判断和一个具体的请求。代码评审里的沟通问题,大多不是「怎么说才显得礼貌」,而是「一句话里到底装了什么」。

一、评审沟通难,不是因为人难相处#

先承认一个事实:大多数评审冲突里,双方都不是坏人。

reviewer 看到一处可能出问题的地方,花了三十秒写下一句提醒;作者花了两天写这个 PR,被打断后看到一句「这里有问题」,本能地进入防守。两个人都觉得自己在讲道理,但两个人拿到的信息根本不一样。

这背后有四个结构性失真,和性格关系不大:

失真具体表现作者接收到的东西
异步没有语气、表情和即时澄清,文字会被读成最坏的语气「他在否定我」
公开评论有观众:其他 reviewer、组长、路过的同事「我被当众指出问题了」
压缩一条评论常常只有一句,省掉了观察、证据和请求「他说我不行,但没说哪里不行」
只看见 diffdiff 显示改了什么,不显示为什么改、放弃了什么「他根本不知道我考虑过什么」

再加一层:代码是作者的作品。被指出问题时,人很难把「这段代码有问题」和「我这个人有问题」完全分开。这不是玻璃心,是投入之后的正常反应。评审沟通的设计目标,不是要求作者「别玻璃心」,而是让评论携带足够的信息,使作者不必靠猜来补全你的意思。

二、先分级:四级评论,四种语法#

很多来回其实只卡在一个问题上:这条评论到底要不要改?

reviewer 用「建议」的语气说了一件必须改的事,作者就按「可选」处理;reviewer 用「必须」的语气说了一件个人偏好,作者只好照做,心里不服。语气和后果错位,讨论就会从技术问题滑向态度问题。

一个实用的做法是给评论标级别。级别不是礼貌程度,而是「不采纳会发生什么」:

级别它回答的问题不采纳的后果必须携带示例
blocking这段代码能不能合入?正确性、安全、数据丢失、破坏公开契约触发条件、复现步骤或明确的影响面「并发写同一个 key 时,后到的请求会覆盖先到的结果;这里有复现脚本。」
question我需要先知道什么?无法判断,讨论暂停一个具体、能被回答的问题「这个字段在 SSR 之后还会变吗?」
suggestion有没有更合适的写法?可以合入,但维护成本更高收益、代价、适用条件「拆成两个函数后,校验逻辑可以单独测;代价是多一层调用。」
nit这是不是纯风格偏好?不影响合入明确标注 nit,并说明不阻塞「nit: 这个变量叫 data 有点泛,叫 orders 更清楚。」

四条规则让这张表真正起作用:

  1. blocking 必须能被证伪。 「这里以后会出问题」不是 blocking,因为它没有触发条件;「当 items 为空数组时这里返回 undefined,下游 .map 会抛错」才是。
  2. nit 不阻塞合入。 如果一条 nit 卡住了 PR,说明级别标错了,或者评审流程把「作者必须全部照做」当成了默认规则。
  3. question 不是二等评论。 很多 blocking 是在回答完一个 question 之后才被确认的。先问,再判断,比先下结论更省时间。
  4. 一条评论只装一件事。 把「命名」「空值」「性能」塞进同一段话,作者只能挑一个回,剩下的会被静默忽略。

如果团队用 GitHub、GitLab 这类工具,级别可以直接写进评论开头:blocking:、question:、suggestion:、nit:。它不解决所有问题,但能立刻消掉「这条到底要不要改」的来回。Conventional Comments 这类约定做的就是这件事,规则很简单,贵在团队统一。

三、把断言改成可证伪的提问#

分级解决「要不要改」,接下来解决「凭什么改」。

一条低效的评论通常长这样:

这里性能太差了,应该用 useMemo。

它有结论,没有观察,没有触发条件,也没有给作者反驳的入口。作者只能回「数据量很小」或者默默照做,两种情况都没有产生可复用的判断。

把评论拆成四段,信息量会完全不同:

观察:我看到了什么(文件、行号、具体行为)
影响:在什么条件下,会导致什么后果
假设:我为什么认为会这样(明确标成假设,允许被推翻)
请求:我希望你确认或做什么(一个具体动作)

套进前面的例子:

观察:第 42 行在每次 render 时都会重新 JSON.parse(raw)。
影响:如果 raw 较大、列表又频繁重渲染,解析会落在主线程上,滚动时可能出现掉帧。
假设:我猜 raw 在父组件里每次都是新字符串,所以 memo 也救不了;如果 raw 只在进入页面时变一次,这个担心不成立。
请求:能否确认 raw 的来源和更新频率?如果确实每次都变,我建议在数据层解析一次。

四段里最容易被跳过的是「假设」。跳过它,评论就变成了判决;写出来,它才是一个可以被作者用事实推翻的模型。好的评审评论不是证明自己对了,而是把自己的判断变成可检验的。

3.1 事实用证据,偏好用让渡#

评审里最耗时的争论,往往是把偏好说成了事实。

  • 「这个函数太长了」是偏好,除非你能指出它导致了什么具体问题:难以测试、修改时容易漏改、两个职责被绑在一起。
  • 「应该用 Map 而不是对象」是偏好,除非你能给出键的类型、插入顺序或原型链污染的具体条件。
  • 「这个命名不好」是偏好,除非团队有命名规范,或者这个名字已经在别处指向了另一个概念。

对偏好类意见,一个有效的句式是「我倾向于 X,因为 Y;如果你选 Z 是因为 W,那也可以,能否在注释或 PR 描述里记一下原因」。它同时做了三件事:表明立场、给出理由、把决定权留给最了解上下文的人。

对事实类意见,反过来:不要用形容词,用数字、复现步骤和日志。把「这里会卡」换成「输入 5 万条时,这段同步解析在低端机上大约需要几百毫秒」,把「可能空指针」换成「当 user 为 null 时,第 18 行会抛 TypeError」。

3.2 改写对照表#

下面这些句子在评审里出现频率很高,问题也很稳定:

原评论问题改写
「这里性能太差了」没有触发条件,不可证伪「每次 render 都解析;如果 raw 大于 100KB 且频繁重渲染,主线程会有可感知的卡顿。能否确认更新频率?」
「这个函数太长了」没有指认哪一段多余「这个函数同时做了解析、校验和写库;如果校验要单独测试,建议把第 40–70 行抽成 validateOrder。」
「为什么不用 X?」反问句,容易被读成质问「我倾向于用 X,因为……;你选 Y 是因为……吗?如果是,能否记一下原因?」
「这样写不对」没有给失败场景「当 items 为空数组时,这里返回 undefined,下游 .map 会抛错。能否补一个空数组用例?」
「显然应该……」「显然」在否定对方的判断力「我的理解是……,可能漏了上下文;如果是这样,请纠正我。」
「你总是把逻辑写在组件里」针对人,且无法执行「这个组件里的解析逻辑能否下沉到数据层?这样测试时不必渲染组件。」

改写不是把话说软,而是把可执行的部分补上。语气可以直,信息必须全。

四、作者侧:回应、拒绝与记录#

评审沟通的另一半在作者这边。作者最容易犯的两个错误,一个是把每条意见都当成必须执行的指令,另一个是把每条意见都当成对自己的否定。两者都会让讨论失焦。

一个更稳的处理顺序是:先分类,再用事实回应,最后记录决定。

4.1 先分类,再决定花多少力气#

收到一批评论后,先扫一遍级别,不要从第一条开始逐条回:

  • blocking:优先处理,因为它决定能不能合入;有疑问就要求对方给触发条件。
  • question:用事实回答,答完再判断要不要改。
  • suggestion:评估收益和代价,可以接受,也可以拒绝,但要说清理由。
  • nit:批量处理或明确说不改,不要让它占用讨论的主线程。

分类的另一个作用,是防止「评论数量」变成压力。一个 20 条 nit 的 PR 不一定比一个 2 条 blocking 的 PR 问题更多,但前者看起来更吓人。

4.2 回应模板:确认、事实、方案、决定#

确认:我理解你的担心是 ___。
事实:这里的实际情况是 ___(日志、代码路径、数据来源)。
方案:我倾向于 ___,因为 ___;另一个选项是 ___,代价是 ___。
决定:这条我 ___(改 / 不改 / 需要你确认 ___)。

一个完整的例子:

确认:你担心的是每次 render 都重新解析 raw。
事实:raw 来自 SSR,客户端不会重新赋值;组件只在挂载时渲染一次,列表滚动走的是虚拟列表,不会触发父组件重渲染。
方案:这种情况下 memo 的收益很小,我倾向于保持现状;如果你担心未来 raw 会变成动态的,我们可以加一条注释,说明这个假设。
决定:这条我先不改,等确认 raw 的更新路径后再定。

这段回应没有一句「你说得不对」,但把技术判断讲清楚了。作者不需要为了显得配合而改代码,reviewer 也能从事实里更新自己的模型。

4.3 拒绝的语法:给出「什么条件下我会改」#

拒绝不是对抗,含糊的拒绝才是。有效的拒绝长这样:

这条我不同意,因为 X。如果 Y 成立,我会改。

例如:

这条我倾向于不改:这个 catch 之所以吞掉错误,是因为上层会统一重试,重复上报会产生告警风暴。如果上层不再重试,这里就应该重新抛出。

它把「不」变成了一个有条件的判断。对方如果不同意,只需要攻击条件 Y,而不是猜测你的态度。

反过来,最消耗团队的拒绝是这两种:

  • 沉默式拒绝:不改,也不说为什么,等 PR 被搁置。
  • 服从式拒绝:照改,但在别处抱怨「评审就是走形式」。代码变了,判断没有变,下一次还会发生。

4.4 不要把「改了多少条」当成服从度#

评审的目标不是让作者把每条意见都执行一遍,而是让代码在合入前经过一次独立检查。作者保留判断权,reviewer 保留提出问题的责任,这两件事不冲突。

如果团队把「reviewer 提了多少条、作者改了多少条」当成绩效信号,很快就会出现两种反噬:reviewer 为了显得认真而凑评论,作者为了显得配合而机械照改。两种行为都会让评审失去信息量。

五、reviewer 侧:让意见被接受,而不是被服从#

站在 reviewer 的角度,有几件事比「说得客气」更能提高采纳率。

5.1 先读 PR 描述,再读 diff#

很多评论之所以跑偏,是因为 reviewer 不知道这个 PR 要解决什么问题。作者可能已经明确说了「这次只做重命名,行为不变」,reviewer 却在评论里要求顺手重构。先读描述和目标,再判断实现,能消掉大量无效评论。

如果 PR 描述写不清目标,那本身就是一条 blocking:不是代码有问题,而是这次改动无法被验证。

5.2 一次只提影响最大的几条#

一次评审给出 20 条评论,作者的注意力会被平均分配,真正重要的两条反而被淹没。一个实用的限制是:一次最多提 3 条 blocking,其余写成 suggestion 或留到后续 PR。

这不是放过问题,而是承认评审的带宽有限。把「必须现在改」和「可以以后改」分开,作者才知道先做什么。

5.3 明确说出「哪些地方不用改」#

只指出问题,作者会误以为整个 PR 都需要重写。花三十秒写下「数据层这部分我看过了,没问题;我重点担心的是组件里的解析和错误处理」,能显著降低作者的防御感,也能让讨论聚焦在真正的分歧上。

这不是鼓励表演式表扬。它传递的是评审的边界:我看了什么、跳过了什么、哪些判断我暂时没有依据。

5.4 对初级和高级,给的东西不一样#

  • 对刚入门的作者:给方向和一个具体例子,不要直接重写。直接重写会让作者失去自己动手建立判断的机会。
  • 对熟悉的作者:给判断依据,不要给指令。「这里可能有问题,因为……」比「改成 X」更有价值,因为对方有能力自己选方案。
  • 对跨模块的改动:先问「你希望我重点看哪一块」,再分配注意力。reviewer 不必假装自己懂所有上下文。

5.5 「我们」有边界#

把「你这里错了」换成「我们可以看看这里」,确实能降低对抗,但用过头会让责任变模糊:出了问题,没人知道是谁的决定。

一个简单的区分:讨论方案时用「我们」,确认决定时用「我」和「你」。 「我们一起看看这段并发逻辑」是邀请;「我同意这个方案,你来改」是决定。两者都需要。

六、异步评审的节奏:什么时候该停下来开会#

代码评审默认是异步的,但异步有成本:作者在等,reviewer 在切换上下文,讨论在文字里来回。

6.1 diff 越大,发现率越低#

业界常引用的评审研究建议,单次评审控制在 200–400 行左右,超过这个规模,缺陷发现率会下降。数字不必当成硬指标,但方向很清楚:大 PR 的评审质量会肉眼可见地变差。

对应的做法不是「让 reviewer 更努力」,而是拆 PR:

  • 纯重命名、格式化、依赖升级,单独一个 PR;
  • 行为改动和重构分开;
  • 一个 PR 只解决一个问题,描述里写清「不包含什么」。

拆 PR 的成本在作者侧,收益在 reviewer 侧和未来的 bisect 上。多数团队低估了后者的价值。

6.2 两种评审,不要混着用#

模式适合什么产出
同步走查架构调整、跨模块改动、方向不确定一个决定,以及决定背后的理由
异步评论局部实现、命名、测试补充、文档一组可执行的修改项

把架构问题塞进异步评论,通常会来回三轮以上;把命名问题拉去开会,则是浪费所有人的时间。先判断属于哪一种,再选媒介。

6.3 三个升级信号#

出现下面任一情况,就应该从文字切换到语音或当面,十五分钟往往能解决三轮评论:

  1. 同一个问题来回超过三轮,双方都在重复自己的话;
  2. 出现明显的误解,比如 reviewer 以为作者漏了某个分支,作者以为 reviewer 要求重写;
  3. 涉及跨团队或架构决策,需要更多人一起拍板。

升级之后,把结论写回 PR。不要让它只存在于会议里,否则下一个看 PR 的人还要重新问一遍。

6.4 把评论变成决策记录#

代码评审的一个隐藏产出是决策记录:为什么选这个方案、放弃了什么、谁同意了。没有它,三个月后同样的讨论会再来一次。

不需要长篇大论,PR 描述里加一小段就够:

决策:在数据层解析 raw,组件只接收已解析的 orders。
理由:raw 只在进入页面时变化;放在组件里会让解析和渲染耦合,测试时需要渲染组件。
替代方案:useMemo(未采用,因为父组件不会重渲染,收益接近零)。
拍板:@alice(前端 owner),2026-11-06。

这段记录的价值不在于「留痕」,而在于它把一次口头讨论变成了可检索的资产。下次有人问「为什么不用 memo」,答案在 PR 里,而不是在某个人的记忆里。

七、把重复意见沉淀成规则#

如果同一条评论在不同 PR 里出现了三次,第四次就不该再写评论。

原因很简单:重复评论说明问题不在某一段代码,而在团队的默认约定或工具链。继续评论,只会让 reviewer 变成人肉 linter,让作者觉得自己总在同一个小坑里摔倒。

三类沉淀方式,按成本从低到高:

7.1 自动化:交给 lint 和类型检查#

能在提交前自动发现的问题,不要留到评审:

// .eslintrc.json(片段)
{
"rules": {
"no-floating-promises": "error",
"consistent-type-imports": "error",
"@typescript-eslint/no-misused-promises": "error"
}
}
// tsconfig.json(片段)
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}

规则一旦生效,评审里就不用再解释「为什么 await 不能漏」。reviewer 的注意力可以留给工具判断不了的东西:命名是否表达了意图、抽象是否多了一层、这段逻辑是否应该存在。

7.2 模板:把期待写进 PR 和 issue#

如果团队反复要求「PR 要写测试说明」「要写回滚方案」,把它写进 PR 模板,而不是每次口头提醒。模板不是官僚流程,它把隐性的期待变成显性的检查项。

7.3 文档:把约定写进 CONTRIBUTING#

评论级别的含义、命名规范、目录结构、错误处理约定,写进 CONTRIBUTING.md 或团队 wiki。评论里附一个链接,比重复解释十遍更省时间。

一个最小的约定可以只有四行:

blocking: 不改不能合入,必须给出触发条件。
question: 需要信息才能判断,请用事实回答。
suggestion: 可以合入,但建议考虑;作者可以拒绝并说明理由。
nit: 纯风格,不阻塞合入。

八、两份检查清单#

发评论之前#

  • 我知道这个 PR 要解决什么问题吗?
  • 这条是 blocking 还是 suggestion?我标注了吗?
  • 我给了触发条件、影响或复现步骤,而不只是形容词?
  • 我是在问问题,还是在发指令?
  • 这条意见是否已经出现过三次?该不该写进 lint 或文档?
  • 我有没有指出哪些地方不用改?

回应之前#

  • 我把意见按级别分类了吗?
  • 我用事实回应了吗,还是只在说「我觉得」?
  • 我拒绝时,有没有给出「什么条件下我会改」?
  • 这个决定需要记录吗?谁来拍板?
  • 我有没有把「被评审」当成对能力的评价?

九、结语:评审的产出不是一句「批准」#

代码评审最常见的误解,是把它当成一道门:reviewer 守着,作者想办法通过。门开了,流程结束,代码合入。

但一次好的评审留下的东西不止于此。它留下的是一段共同理解:这段代码为什么这样写、哪些取舍是有意的、哪些地方下次可以做得不同。作者关掉 PR 之后,遇到同类问题时能自己做出判断,而不是再问一次「这里要不要 memo」。

所以,评论的质量不看它多礼貌,而看它是否让下一次不再需要这条评论。把事实、偏好和决策分开说,是朝这个方向走的一小步:事实可以被检验,偏好可以被讨论,决策可以被记录。三者混在一起,才会变成「你错了」和「你不懂」的来回。

参考来源#

代码评审里的沟通:把事实、偏好与决策分开说
https://www.hehonglei.cn/posts/code-review-communication-facts-preferences-decisions/
作者
Honglei He
发布于
2026-11-06
许可协议
CC BY-NC-SA 4.0