1. 一次评审翻车后,我重新思考了"代码评审到底在看什么"
先说一个让我印象很深的场景。前阵子我在某个订单系统的权限模块上提了一个合并请求,自认为把所有边界情况都想到了,改动也就一百多行,干净利落。结果评审会上,同组的A同学只问了一句:"如果下游服务三秒还没返回,你这边的重试逻辑会把库存扣两次吗?"我当场愣住——我的确加了重试,但重试时没有做幂等校验。
那次评审最终没有通过,却让我意识到一件事:我们对"代码评审"这四个字的理解,很多时候是错的。
大家习惯把代码评审当成一道"质量关卡",觉得是找毛病、挑刺、拦住有问题的代码。但实际上,代码评审更核心的价值是信息同步。写代码的人往往陷在自己的假设里,评审者因为带着"局外人视角",反而最容易发现"这里跟需求文档对不上""这里跟调用方的约定不一致""这里没考虑并发下的重复请求"这类问题。A同学能问出那句话,不是因为我写得不认真,而是因为她看到了我看不到的上下文。
这也是我把这套实践命名为open-code-review的原因。它不是某个现成的开源工具,而是一套我自己从多次评审中沉淀下来的开放评审方法:对代码保持开放,对意见保持开放,对流程本身也保持开放。你不需要成为专家才能用上它,刚接触评审的新手可以拿它当入门指南,带团队的负责人可以拿它当流程改版的参考。它要解决的核心问题就一个:让评审真正拦住该拦的缺陷,而不是变成走过场。
顺带说一句,很多人觉得代码评审浪费时间。这个账要算清楚:缺陷发现得越晚,修复成本越高。写代码时发现的是低固定成本,到了线上出故障,可能牵扯数据修复、客户投诉、紧急回滚。一次十到二十分钟的评审,如果能拦下一个潜在的线上事故,这笔投入就非常划算。问题是,大多数团队的评审并没有在拦事故,而是在比谁的英语语法好、谁的命名风格更符合某个人的偏好。
所以这篇内容我把整个 open-code-review 实践拆成六个方面:评审对象、评审节奏、工具辅助、意见表达、常见陷阱,以及我最终落地的检查清单。内容会比较多,但每一条都是踩过坑之后整理出来的,希望你不用再走一遍弯路。
2. 评审对象与流程设计:从"看代码"到"看设计意图"
2.1 评审的粒度:小型异步评审才是常态
很多人一提到代码评审,脑子里浮现的是一屋子人围在投影仪前。那是过去重型评审的典型画面,适合非常核心的模块,但对日常开发来说太笨重了。我们团队现在的主流方式是小型异步评审:开发者提交合并请求,评审者在自己的电脑上看diff,写行内评论,最后给出结论。不占用会议室,不用凑齐所有人,随时随地可以开始。
不过这里有个前提:合并请求的粒度必须足够小。我见过最夸张的一次,有人把一个迭代两周的功能、七八个文件、三千多行改动一次性塞进一个合并请求里,美其名曰"完整交付"。结果就是评审人根本看不过来,草草 approve,上线第二天就出了事故。
现在我对粒度的要求很简单:一个合并请求只干一件事。重构一个函数就只重构这个函数,修复一个bug就只修复这个bug,新增一个接口就只新增这个接口。改动行数最好控制在三百行以内,超过这个数就要反思是不是没拆干净。为什么是三百行?我记得有一次项目统计,改动行数超过五百行的合并请求,评审者平均只在三成区域留下了有效评论,而小于两百行的合并请求,评审覆盖率能到八成以上。人脑对连续信息的处理容量是有限的,收益随着代码量增加迅速递减。
小粒度的合并请求还有一个隐藏好处:回滚方便。万一上线出问题,一行git revert就能干净地撤销整件事,不用担心带上无关的改动。这对线上稳定性的价值,很多时候比评审本身还要大。
2.2 评审清单怎么设计:分层拦截,而不是逐行读代码
很多人评审的时候是闷头读代码,从第一个文件开始,一个循环看一屏,看到哪算哪。这种"流式评审"的效率非常低,因为你没有带着靶子去看。我现在的做法是先列清单,再按清单去扫描。
清单不是流水账,而是分层设计。第一层是正确性与安全性,这个问题最大,直接决定上线后会不会炸。重点关注数据校验是否完善、越权访问有没有拦截、并发场景下状态会不会乱、异常分支是否处理、敏感信息有没有打日志。
第二层是可维护性,这个问题不直接导致事故,但决定了下个月的你或者同事会不会想骂人。函数是否过长、命名是否名不副实、是否存在重复代码、模块之间的依赖是否合理、有没有留下让人困惑的"魔法数字"。
第三层才是代码风格与性能。风格问题尽量交给自动化工具,不值得人工花时间;性能问题只在有明显瓶颈的时候提,不要在业务代码里纠结一次遍历是 O(n) 还是 O(2n)。
这三层看起来简单,实际操作时有一个关键技巧:先看测试,再看实现,最后看上下文引入点。大多数评审者习惯拿过代码就开始读实现,读到一半才去想"这个函数应该是干什么的"。如果你先看测试用例,测试的名称和断言会直接告诉你作者的真实意图;然后带着这个意图去看实现,就能很快发现实现跟意图之间的偏差;最后再看这个改动被谁调用、影响哪些上游下游,把全局上下文补上。这个顺序我第一次用的时候,只花了过去一半的时间,就发现了两个真正的逻辑漏洞。
2.3 合并请求模板:把"该写的信息"前置
流程设计里最容易忽略的,是合并请求描述本身。我见过太多人只写一句"fix bug"或者"update",评审人被迫从代码里反推意图。信息不对称是评审低效的最大来源,而模板是解决这个问题的最便宜的方案。
一个实用的合并请求模板应该包含三块:这次改动解决什么问题(最好带上复现步骤或现象截图)、改动涉及哪些模块和文件(让评审人知道哪里是重点)、测试情况如何(单元测试、手动验证、影响范围)。最后一块特别重要,很多人提交之前根本没想自己测过什么,写着写着就发现自己还有一堆没验证的地方。
有了模板,评审人可以在一两分钟内建立起对改动的全局认知,然后把注意力集中在真正有风险的地方。这个前置信息的小改动,带来的效率提升远超你想象。
3. 评审节奏与时间管理:把评审控制在十五分钟以内
3.1 为什么评审会拖成"无效加班"
不知道你有没有过这种经历:一个合并请求挂在列表里好几天,催了两次都没人看,最后催急了才有人熬夜帮你点了通过。评审周期拖得越长,上下文丢失得越严重,作者甚至都忘了自己当时为什么这么写,评审人还得花大量时间重新理解。更糟的是,有些团队会抱怨"代码评审拖慢了迭代",其实拖慢迭代的不是评审本身,而是没有节奏的评审。
我总结过几个常见的节奏杀手:评审请求发在群里没人明确接单、评审人赶在下班前才开始看所以草草了事、一个改动同时等两三个人评论导致结论打架、作者提交代码后不主动跟进提醒。这些问题跟技术关系不大,但跟评审能不能跑起来关系很大。
3.2 设定明确的时间盒:十五分钟决策机制
我现在的经验是,每次评审最多给自己十五分钟。听起来很激进,但配合小粒度的合并请求完全可行。前两分钟看描述和测试,中间十分钟过 diff 和关键分支,最后三分钟写结论或者跟作者在线沟通。超过十五分钟还没看完,说明这个改动已经超出了它的安全评审范围,这时候就停下来,让作者拆下一次评审,而不是硬撑着读完。
时间盒的好处是逼迫你区分"必须看"和"可以跳过"。很多人觉得每行都得看,这是误解。关键逻辑看三遍都不嫌多,样板代码扫一眼就够。有了时间压力,你的注意力反而更集中,不会读着读着就跳到某个无关紧要的格式问题上。
实际操作中我会配合一个简单仪式:评审完成后,不管结果是通过还是打回,都在评审记录里写一句"已评审,耗时X分钟,主要关注点是ABC"。这句话帮作者快速定位风险区域,也帮团队统计评审效率。后来我们的合并请求平均合并速度从三天缩短到半天,靠的其实都是这种小节奏的改进。
3.3 评审会议出现在什么时候
异步评审是主流,但有一个场景我会特意安排同步讨论:跨模块的接口变更。比如你的改动涉及调用方,或者改了某个公共方法的入参,这时候异步评论来回一轮可能要半天,同步开个短会,大家直接把路由打通,把实现方的假设和调用方的需求放在同一张白板上,效率反而更高。
这类会议有个前提:你必须带着已经完成的异步评审结论来,而不是把会议当成评审起点。我参加过很多"一起看代码"的会,实际效果基本是几个人盯着投影仪发呆,谁都不好意思发言。正确姿势是每个人先自己看一遍,写好问题清单,会上只讨论清单里的差异点,十分钟内解决问题,十五分钟聊完收工。
4. 工具链怎么辅助 open-code-review:diff、行内评论与自动化检查
4.1 diff 阅读顺序与上下文切换
代码托管平台的合并请求界面,本质上是把 diff 呈现给你看。但这个界面有个陷阱:你看到的是变更后的代码,而评审真正需要对比的是"变更前"和"变更后"之间的语义差异。很多评审者看到一行删除、几行新增就急着评论,其实根本不知道删除的上下文在哪里。
我建议按"先新增、再删除、后调整"的顺序读 diff。新增代码说明这个改动带来了什么,删除代码说明它移除了什么,调整代码说明它改了原来的什么假设。读的时候注意平台提供的展开上下文功能,尤其是那些"看似没变"的周边代码,有时候 bug 就藏在那几行没变的调用里。
行内评论是异步评审的核心工具。它的优势是可以精确定位到某一行,作者一打开就明白你在说什么,完全可以替代那种"整体写得还不错,但逻辑有问题"的模糊评语。我个人的习惯是,每条评论尽量短,只说一件事,不搞长篇大论。如果确实需要解释大背景,我会先写一行核心结论,再用一个"背景"子段落展开,方便作者快速决定要不要看细节。
4.2 自动化检查该拦什么、不该拦什么
自动化工具是评审的好帮手,但用不好也会制造噪音。我的原则其实很简单:能让机器确定的事情,就不要浪费人的时间;机器不确定的事情,也不要让机器乱发言。
可以交给自动化工具的有:代码格式、明显的类型问题、未使用的变量、明显的空指针风险、日志中是否打印了敏感信息、依赖license问题、重复代码比例。这些检查建议全部接入提交前和合并前的流水线,拦截了就把这类型的评论从人工评审里剔出去。
不该交给自动化工具的有:业务逻辑是否正确、接口语义是否符合需求、并发处理是否合理、数据一致性是否保证。这些判断需要完整的业务上下文,机器现在远做不到。很多团队在流水线里塞了一堆"建议"级别的检查规则,结果开发者天天被逼着改无意义的告警,反而冲淡了真正有价值的人工评审。自动化检查应该是守门员,而不是评论员;该守的门守住,剩下的交给人去对话。
4.3 评审结论的三种表达:通过、评论、请求修改
工具上通常有三个按钮:同意合并、评论、请求修改。这三个按钮的语义一定要在团队里对齐,否则很快会出现"一群人点了评论,作者不知道到底能不能合并"的混乱状态。
我把它们的边界定义得很清楚。通过意味着"我认为这个改动可以上线,如果后续有问题我负责得起",这是一个强承诺,不是敷衍的点赞。评论意味着"当前代码没问题,但我有改进意见,你可以择机处理,不阻塞合并"。请求修改意味着"当前代码存在我无法接受的缺陷,合并前必须解决"。
使用"评论"这一档要尤其克制。我看到很多人明明发现了会影响线上正确性的 bug,却只写了一句"你可以考虑一下这里是否应该加个判断",然后点了通过。这是典型的躲避冲突。正确的做法是:如果有顾虑,就明确说请求修改;如果只是品味偏好,就放在评论里,别阻塞别人。这个边界不清晰,是评审失去效力的最大隐患之一。
5. 评审意见的表达:如何让"这段逻辑必须重写"不变成一场争执
5.1 意见分三档:必须修、建议修、风格偏好
评审者给出意见之前,先自己心里分个档。必须修是正确性问题,比如数据会被覆盖、权限会被绕过、异常会触发严重 bug。建议修是可维护性问题,比如命名不清、逻辑重复、缺少测试,不修也不至于上线出事,但留着会持续消耗后来的维护成本。风格偏好是纯个人倾向,比如变量名用哪个词、花括号怎么换行、注释中英文比例。
这三个档位在评论里要用明确的词标出来,比如"[必须修] 这里缺少对空值的判断""[建议修] 这个函数可以拆成两个小函数""[风格] 我更喜欢用 start 而不是 begin"。这样作者接到意见时不需要自己去猜严重程度,也避免了"你居然为了一个变量名就打回我的代码"这种事。
其实很多评审摩擦,本质就是档位没对齐。作者以为你在说风格,结果你暗中是把他往必须修的档位上算;评审者以为自己在提建议,结果用词又像是在下命令。把档位亮在明面上,冲突能少掉一大半。
5.2 用问题代替断言:评审者的"苏格拉底式"提问
我在评审中学到的最重要的一课是:结论自己会让人防御,问题会让人思考。"你这块代码有并发风险"和"如果两个请求同时在跑,这段逻辑会不会出现重复扣款?",后者的接受度要高得多。
原因很简单,代码是作者花费时间和心思写出来的,直接下结论相当于否定了他的一部分劳动。但只要换成提问,你们就从对立关系变成了一起探索问题的队友。我自己做评审时有个硬性规定:凡是"必须修"级的问题,全部写成问题形式,措辞也尽量具体,比如"这个 null 是从哪里传进来的?调用方有没有可能不传?"而不是"这里空指针了,修一下"。
提问式评审的额外产出是,它逼着你自己先想明白问题到底出在哪。比起脱口而出的断言,提问要经过更严谨的思考,这对评审质量本身也是一个正向加成。
5.3 作者侧的反向操作:主动标记"这里我看过,没把握"
评审意见的沟通压力不只是评审者的问题,作者也能做很多事。我的习惯是:在提交信息或者合并请求描述里,明确标出"我自己怀疑过但拿不准的区域"。这样评审者就能把精力放在你最心虚的地方,而不是均匀撒在全篇上。反过来,作者越坦诚地暴露自己的不确定,评审者越愿意给出真正有帮助的建议,这是一条正向反馈。
作者收到意见之后也要有个处理清单:必须修的问题逐一确认修改;建议修的问题能改就改,不能改要给出理由;风格偏好看情况,跟自己的想法一致就采纳,不一致可以保留。最差的做法是看到评论就情绪上头,逐条反驳,把评审变成辩论赛。代码评审的目标是共同把代码变得更好,不是证明谁更聪明。
6. 那些年踩过的评审坑:形式化评审、无休止讨论与漏网之鱼
6.1 全员通过却照样出事故:评审为什么失效了
最令人沮丧的情况莫过于:评审流程走完了,每个人都点了通过,结果上线还是出了事故。这不是评审系统失效,而是评审变成了形式化盖章。我可以列出几种常见病根。
一种是因为权力关系不对等,资深的人说"没问题",其他人就不好意思再说话。一种是因为评审者对业务背景完全不了解,看了代码只觉得"语法没毛病"就通过。还有一种是最普遍的:改动太大、太杂,评审者看了一部分就开始犯困,最后大家都想着"反正 CI 过了,先合并再说"。
针对这些病根,我做了两手调整。其一,评审意见和决定严格基于内容和数据,不基于等级资历;其二,合并请求描述必须包含测试情况,如果作者写不出自己验证过什么,就不进入评审环节。这两条规定一起执行之后,"全员通过但事故照出"的情况才真正开始变少。
6.2 评审拖太久导致的上下文丢失
评审周期过长,不只是流程效率问题,更会影响评审质量本身。几个真实体会:改动提交第一天,作者脑子里还有完整的上下文,可以立刻响应评论;到第三天,他自己都要重新看代码才能想起当时的决策,评论的回复质量明显下降;到一周,几乎所有人都对这个改动失去新鲜感,看到的只是"一堆代码"而不是"一个问题"。
所以我对评审超时的处理很干脆:超过两个工作日的评审,自动提醒作者和评审者,不解决就升级到同步会议。升级不是惩罚,而是承认异步沟通在这个场景下已经失效,强制换一种模式。这时候开一个十五分钟短会,通常比继续挂一天效率高得多。
我还有一个亲测有效的细节:作者提交合并请求之后,不要默默等待,主动在团队沟通频道里发一条简短说明,包含描述链接和需要谁来评审。很多合并请求挂在列表里被遗忘,不是大家不负责,而是真的没注意到。主动跟进这一下,能把整个评审周期缩短一大截。
6.3 低层次问题抢走了高层次注意力
最后一个坑,是评审者把精力花在了机器能解决的问题上。我看到过太多评审讨论在吵命名:这个变量叫 result 还是 outcome,这个函数放 utils 还是 helper。吵到天荒地老,却没有一个人关心核心业务逻辑有没有缺陷。
这不是说命名不重要,而是说解决这个问题有更便宜的方式。代码格式化工具、文档规范、高质量模板,都能处理绝大多数低层次问题。人工评审是团队成本最高、心智资源最稀缺的环节,应该把它留给"只有人才能发现的问题"。每当有人开始对着格式争议,我都会提醒一句:先看逻辑,格式上如果有洁癖,请先去改自动化规则,不要在这里消耗人。
这个观念的转变,也是 open-code-review 从"找错的流程"进化成"质量共建系统"的关键节点。评审不再是防御性的关卡,而是大家一起讨论设计意图、发现盲区、对齐认知的协作场合。如果能做到这一点,代码库的质量会以肉眼可见的速度提升。
7. 我现在的评审习惯与最终落地清单
从那次"订单系统重试扣库存"翻车到现在,我一直在迭代自己的评审方式,最终形成了三个固定的习惯。
第一个习惯是先问三个问题再看代码:这个改动要解决什么问题?它影响哪些模块和调用方?作者验证过什么?三个问题在合并请求描述里找不到答案的话,我会直接请作者补充,而不是自己猜着往下读。这让我避开了大多数"读了一大半才发现方向不对"的情况。
第二个习惯是必须在一个小本子里留一句评审备注。评审完之后,顺手记一句"这个改动哪个区域风险最高、哪条意见最值得后续跟进"。本来只是给自己留痕迹,没想到后来做线上回查时帮了大忙,能快速定位当初评审时担心的地方。
第三个习惯是评审别人之前先把自己的合并请求随手处理掉。一个团队里如果资深的人总把自己的评审压到最后一刻,新人很快就会觉得评审只是走个形式。先把别人交给我的代码处理好,再去推进自己的开发任务,这个次序本身就在传递一种信号:评审是值得被认真对待的事,它不是可有可无的尾巴工程。
如果你所在的团队还在被无效评审困扰,我的建议是不要指望一步到位。先从一件小事开始:把合并请求拆分到三百行以内,给评审设定十五分钟的时间盒,把意见分档标清楚。这三个动作不需要引入任何复杂平台或工具,只需要大家达成共识。我见过很多团队做到这一步之后,评审会议的争吵少了,上线故障也明显少了,因为大家终于把力气花在了真正值得花的地方。
open-code-review 说到底不是某种固定的形式。对别人写的代码保持开放,对自己的盲区保持开放,对流程的改进保持开放——能做到这三点,代码评审就再也不是每月吐槽清单里的负担,而会成为整个团队质量感觉最灵敏的一根神经。