1. 代码审查怎么就成了走流程
我做了十多年开发,带过团队,也做过那种跨部门的大型项目评审。说句不客气的话,现在很多团队的代码审查,已经彻底变味了。打开一个PR,满屏都是“LGTM”、“+1”、“看起来没问题”,或者就是机器人自动跑一遍格式检查,然后大家点个通过就合并了。你要是问某个变量为什么这么命名,某个边界条件为什么没处理,大概率没人说得上来。
这就是我标题里说的“形式化代码审查”。它看起来在运转,实际上已经死了。真正的问题在于,我们把审查当成了一道流程关卡,而不是一次技术对话。流程关卡只需要盖章,技术对话才需要动脑子。当审查变成盖章,代码质量的下滑就是必然的,只是时间早晚的问题。
我见过最离谱的一个案例:一个涉及资金结算的核心模块,PR里改动了金额计算的精度处理逻辑,从float换成了double,理由是“精度更高”。三个评审人全部点了通过。上线之后对账差了十几万。事后复盘,没有一个人真正去看那段diff,因为那个PR有四十多个文件变更,大家都默认“别人看过了”。
这件事让我彻底反思:代码审查的本质到底是什么?我的结论是,代码审查的本质是知识传递和风险发现,而不是流程合规。知识传递意味着评审者要理解作者的意图,作者要理解评审者的顾虑;风险发现意味着要主动去找那些测试覆盖不到、静态检查扫不出来的逻辑漏洞。这两件事,没有一件能靠“点通过”完成。
所以这篇文章,我想认真聊聊怎么把代码审查从形式主义的泥潭里拉出来。我会讲清楚为什么现在的审查会失效,怎么用不变量思维去审查代码,AI和智能体在这个过程里到底能帮什么忙、不能帮什么忙,以及一套我自己在团队里跑了大半年、确实有效的审查方法。不管你是刚入行的新人,还是带了多年团队的Tech Lead,应该都能从里面找到能直接用的东西。
2. 形式化审查的根子出在哪
2.1 把审查当门禁,而不是当对话
大部分团队对代码审查的定位从一开始就错了。他们把审查当成一道门禁:代码要合入主干,必须经过审查。这个定位本身没错,但问题在于,门禁的默认状态是“关闭”,要通过就得“开门”。于是评审者的心理模式变成了“找理由不放行”,而不是“找问题帮改进”。作者的心理模式变成了“想办法让评审者放行”,而不是“想办法让代码更好”。
这两种心理模式一旦形成,审查就变成了博弈。作者会把PR拆得很碎,让每个PR看起来都很小、很安全;评审者会快速扫一眼,只要没有明显语法错误就放行。双方都在追求“快速通过”,而不是“质量提升”。我见过有团队甚至把“PR平均评审时长”当成KPI来考核,结果就是评审时长确实短了,但线上事故率上去了。
正确的定位应该是:审查是一次结构化的技术对话。作者通过PR向团队解释自己的设计决策,评审者通过评论提出质疑和建议,双方通过讨论达成共识。这个过程的目标不是“通过”,而是“理解”和“改进”。通过只是自然结果,不是目标本身。
2.2 评审者根本没有足够的上下文
我做过一个统计,在我们团队过去的PR里,评审者平均花在每个PR上的时间是4分32秒。而PR的平均变更行数是287行。这意味着评审者每分钟要看60多行代码。这个速度,别说理解逻辑了,连看清楚变量名都费劲。
为什么会这样?因为评审者没有足够的上下文。他们不知道这个改动的背景是什么,不知道作者尝试过哪些方案,不知道哪些地方是刻意为之、哪些地方是疏忽。他们看到的只是一堆diff,然后凭直觉判断“看起来有没有问题”。
这个问题在大型项目里尤其严重。一个模块可能经过五六个人的手,每个人只了解自己写的那部分。当有人改动了一个跨模块的接口,评审者往往只能看到自己熟悉的那部分,对其他部分的影响完全靠猜。这种审查,与其说是审查,不如说是盲人摸象。
2.3 工具把审查简化成了“通过/拒绝”
现在的代码托管平台,把审查简化成了两个按钮:Approve和Request Changes。这个设计本身没问题,但它诱导了一种二元思维:要么通过,要么拒绝。可实际上,代码审查的结论远不止这两种。有时候代码有问题但可以合并后再修,有时候代码没问题但设计思路值得讨论,有时候需要作者补充说明才能判断。
更糟糕的是,很多团队把“Approve数量”当成评审者的贡献指标。于是评审者倾向于快速Approve,因为这样显得自己“高效”。我见过有评审者在一天之内Approve了二十多个PR,平均每个PR的评审时间不到两分钟。这种“高效”,实际上是对团队的不负责任。
2.4 作者和评审者的信息不对称
还有一个根本问题:作者和评审者之间的信息不对称。作者花了三天写这个功能,脑子里有完整的上下文;评审者只有十分钟,看到的只是最终结果。作者觉得“这个改动很明显啊”,评审者觉得“这到底在改什么”。
这种不对称导致两个后果。第一,作者会不自觉地省略关键信息,觉得“评审者应该能看懂”;第二,评审者会不自觉地跳过不熟悉的部分,只评审自己看得懂的地方。结果就是,真正有风险的地方反而没人看。
我后来在团队里推行了一个规矩:每个PR必须包含一段“背景说明”,讲清楚这个改动要解决什么问题、尝试过哪些方案、为什么选这个方案、有哪些已知的取舍。这段说明不写清楚,评审者有权直接打回。这个规矩推行之后,PR的平均评审时间反而缩短了,因为评审者有了上下文,不用再靠猜。
3. 回归本质:审查到底在审什么
3.1 审查的第一目标是发现逻辑漏洞
测试能发现的问题,不需要审查来发现。静态检查能发现的问题,也不需要审查来发现。审查要发现的,是那些测试覆盖不到、静态检查扫不出来的逻辑漏洞。
什么是逻辑漏洞?举个例子:一个订单状态机,测试覆盖了所有正常流转路径,但没覆盖“已取消订单再次取消”这种异常路径。静态检查也扫不出来,因为代码语法完全正确。只有人去看代码,才能发现“取消”操作没有校验当前状态。
再举个例子:一个并发场景,两个线程同时修改同一个计数器。测试在单线程环境下跑得好好的,静态检查也看不出问题。只有人去看代码,才能发现缺少锁保护。
这类问题,才是审查真正要抓的。而形式化审查,恰恰抓不到这类问题,因为评审者根本没有深入去看逻辑。
3.2 审查的第二目标是传递知识
代码审查是一个团队学习的过程。作者通过写代码表达自己的设计思路,评审者通过阅读代码学习新的模式和方法。这个过程是双向的:新人可以从评审者的评论里学到最佳实践,老人可以从新人的代码里看到新的思路。
我特别看重这一点。我们团队有个规矩:每个PR至少要有一个人留下一条“实质性评论”,不能只是“LGTM”。这条评论可以是提问、可以是建议、可以是质疑,但必须是实质性的。这个规矩逼着评审者真正去看代码,而不是走过场。
3.3 审查的第三目标是统一团队认知
同一个问题,不同的人可能有不同的解法。代码审查是一个统一认知的机会。通过讨论,团队可以达成共识:以后遇到类似问题,就用这个方案。这种共识,比任何文档都有效,因为它是通过实际代码讨论出来的。
我见过很多团队,代码风格五花八门,每个人都有自己的写法。问起来,都说“能跑就行”。这种团队,代码维护成本极高,因为每个人都要花大量时间去理解别人的代码。代码审查就是解决这个问题的抓手。
3.4 审查的第四目标是建立质量文化
这一点最虚,但也最重要。当一个团队认真对待代码审查,认真讨论每一个设计决策,认真寻找每一个逻辑漏洞,这个团队的质量文化就建立起来了。反过来,如果审查只是走过场,那质量文化就是一句空话。
我自己的体会是,质量文化不是靠制度建立的,而是靠一次次具体的审查行为积累起来的。当评审者认真提出一个逻辑漏洞,作者认真修改并感谢,这个行为本身就在强化质量文化。当评审者敷衍了事,作者也敷衍了事,质量文化就在一点点瓦解。
4. 用不变量思维重构审查方法
4.1 什么是不变量
不变量这个概念,在算法领域用得很多。比如选择排序的循环不变量是:在每轮迭代开始时,数组的前i个元素已经有序。这个性质在每轮迭代开始时都成立,在迭代过程中保持不变。
把这个概念引入代码审查,就是:在代码执行的任何时刻,都必须成立的性质。比如一个银行账户,不变量是“余额永远不能为负”。不管发生什么操作,这个性质都必须成立。
用不变量思维审查代码,就是去找那些“必须成立但代码没有保证”的性质。这比逐行看代码高效得多,也深入得多。
4.2 怎么从代码里提取不变量
提取不变量,有几个常用的切入点。
第一个切入点是状态机。任何有状态的对象,都有状态转移的不变量。比如订单状态机,不变量是“已完成的订单不能再次支付”。审查代码时,就要去看每个状态转移是否都校验了前置状态。
第二个切入点是并发。任何共享可变状态,都有并发不变量。比如计数器,不变量是“任何时刻的值都等于所有增量之和”。审查代码时,就要去看所有修改共享状态的地方是否都有正确的同步。
第三个切入点是资源。任何资源(内存、文件句柄、数据库连接),都有生命周期不变量。比如文件句柄,不变量是“打开后必须关闭”。审查代码时,就要去看所有打开资源的地方是否都有对应的关闭逻辑,包括异常路径。
第四个切入点是边界。任何涉及数值计算的地方,都有边界不变量。比如数组索引,不变量是“索引必须在合法范围内”。审查代码时,就要去看所有访问数组的地方是否都做了边界检查。
4.3 用不变量审查的实际案例
我拿一个真实的案例来说明。有一次,我们团队的一个PR改动了一个缓存模块。改动内容看起来很简单:把缓存的过期时间从固定值改成了可配置值。评审者一开始都点了通过,因为改动确实很小。
但我用不变量思维看了一遍,发现了一个问题。原来的代码里,缓存过期时间是一个常量,所以“缓存项一旦写入,过期时间就不会变”这个不变量是天然成立的。改成可配置值之后,这个不变量就不成立了:如果配置在运行时被修改,已经写入的缓存项的过期时间会怎么处理?
代码里没有处理这个情况。结果是,配置修改后,新写入的缓存项用新过期时间,老缓存项用老过期时间,导致缓存行为不一致。这个问题测试没覆盖,静态检查也扫不出来,只有用不变量思维才能发现。
4.4 把不变量写进PR描述
我后来在团队里推行了一个做法:每个PR的描述里,必须列出这个改动涉及的不变量。比如“本改动涉及缓存过期时间不变量,已确保配置修改后所有缓存项行为一致”。这个做法有两个好处:一是逼着作者自己想清楚不变量,二是让评审者有了明确的审查目标。
这个做法推行之后,我们团队的线上事故率下降了差不多四成。不是说所有事故都能靠这个避免,但至少那些“逻辑漏洞”类的事故,明显少了。
5. AI和智能体在审查里的正确用法
5.1 AI能做什么,不能做什么
现在大家都在聊AI辅助代码审查,好像AI一上,审查问题就解决了。我的看法是:AI能帮大忙,但别指望它解决所有问题。
AI擅长的是模式识别。比如识别常见的代码坏味道、识别潜在的空指针、识别不一致的命名风格。这些事,AI做得比人快,也比人稳。我们团队现在用AI做第一轮扫描,把那些明显的问题先过滤掉,评审者就不用花时间在这些低级问题上了。
但AI不擅长的是理解意图。它不知道这段代码要解决什么业务问题,不知道作者为什么选这个方案,不知道哪些地方是刻意为之。所以AI发现的问题,需要人来判断是不是真问题;AI没发现的问题,不代表没有问题。
5.2 智能体在审查流程里的定位
智能体(Agent)比单纯的AI模型更进一步。它可以主动执行一系列操作,比如拉取代码、运行测试、分析依赖、生成报告。在审查流程里,智能体可以承担很多重复性工作。
我们团队现在用智能体做几件事。第一,自动检查PR描述是否完整,如果不完整就自动打回并提示作者补充。第二,自动运行相关测试,如果测试失败就自动评论并附上失败日志。第三,自动分析改动的依赖影响,如果改动涉及跨模块接口,就自动通知相关模块的负责人。
这些事看起来简单,但确实省了评审者很多时间。评审者可以把精力集中在真正需要人判断的地方,而不是浪费在流程性事务上。
5.3 多AI协作审查的实践
我们最近在尝试多AI协作审查。具体做法是:用不同的AI模型分别审查同一个PR,每个模型关注不同的维度。比如一个模型专门看逻辑漏洞,一个模型专门看性能问题,一个模型专门看安全问题。然后把所有模型的发现汇总起来,由人来判断哪些是真问题。
这个做法效果不错。不同模型的视角不一样,能发现一些单一模型发现不了的问题。但缺点是成本高,而且汇总结果需要人来过滤,不然会有大量误报。
我的建议是:如果团队规模不大,先用单一AI做基础扫描就够了。等多AI协作的成本降下来,再考虑上多AI。
5.4 AI审查的边界和风险
AI审查有几个边界要注意。第一,AI不能替代人的判断。AI发现的问题,必须由人来确认是不是真问题。第二,AI不能理解业务上下文。它不知道这个业务场景下什么是对的、什么是错的。第三,AI可能产生误报。如果评审者盲目相信AI,可能会把好代码当成坏代码。
还有一个风险是:AI审查可能让评审者更懒。如果评审者觉得“AI都看过了,应该没问题”,就会跳过自己的审查。这个风险很现实,我见过不少团队踩这个坑。所以我的建议是:AI审查的结果,只能作为参考,不能作为结论。评审者该看的代码,还是得看。
6. 一套可落地的审查流程
6.1 PR提交前的自检清单
作者在提交PR之前,应该先做一轮自检。这个自检清单,我们团队用了大半年,效果不错。
- 改动是否只做了一件事?如果一个PR做了多件事,拆成多个PR。
- PR描述是否包含了背景、方案、取舍、不变量?缺一项就打回。
- 是否运行了相关测试?测试是否全部通过?
- 是否检查了改动的依赖影响?是否通知了相关模块负责人?
- 是否检查了边界条件和异常路径?
- 是否检查了并发场景?
- 是否检查了资源释放?
这个清单看起来长,但实际操作起来,熟练之后五分钟就能过一遍。关键是养成习惯。
6.2 评审者的审查步骤
评审者拿到PR之后,按以下步骤审查。
第一步,读PR描述。如果描述不完整,直接打回,不用看代码。这一步能过滤掉大量低质量PR。
第二步,看改动范围。如果改动涉及多个模块,先看接口改动,再看实现改动。接口改动的影响面大,需要优先审查。
第三步,用不变量思维审查核心逻辑。找出改动涉及的不变量,逐个检查代码是否保证这些不变量。
第四步,检查边界和异常。看所有涉及数值计算、数组访问、资源操作的地方,是否都处理了边界和异常。
第五步,检查并发。看所有共享可变状态,是否都有正确的同步。
第六步,留下实质性评论。至少一条,可以是提问、建议或质疑。
这个步骤看起来多,但熟练之后,一个中等规模的PR,二十分钟能审完。比那种四分钟扫一遍、然后线上出事故再回头查,效率高多了。
6.3 审查会议的召开方式
有些团队会开审查会议,大家一起过PR。这个做法有利有弊。利是能集思广益,弊是效率低,而且容易变成“作者讲、大家听”的单向输出。
我的建议是:只在两种情况下开审查会议。第一种是改动涉及核心模块,影响面大,需要多人确认。第二种是改动涉及有争议的设计决策,需要团队讨论达成共识。其他情况,用异步审查就够了。
开审查会议时,作者先讲背景和方案,然后评审者提问和讨论,最后达成共识。会议时间控制在半小时以内,超过半小时就说明PR太大,应该拆分。
6.4 审查结果的跟踪和闭环
审查不是终点,合入才是。审查发现的问题,必须跟踪到解决为止。我们团队用了一个简单的做法:每个审查评论都必须有一个明确的结论,要么“已修改”,要么“已讨论,不改”,要么“转成后续任务”。没有结论的评论,不允许合入。
这个做法逼着作者和评审者把每个问题都闭环。我见过太多团队,审查评论提了一堆,最后不了了之。这种审查,提了等于没提。
7. 常见问题与避坑指南
7.1 作者觉得评审者太苛刻怎么办
这个问题很常见。作者花了几天写的代码,评审者提了一堆问题,作者觉得被针对了。我的建议是:把审查当成学习机会,而不是评判。评审者提的问题,不管对不对,都先想一想为什么他会这么提。如果确实是自己疏忽,就改;如果是评审者误解,就解释。解释的过程,也是统一认知的过程。
7.2 评审者觉得作者不配合怎么办
反过来,评审者提了问题,作者不改,评审者觉得作者不配合。我的建议是:先确认问题是不是真问题。如果是真问题,坚持要求修改;如果是风格问题,可以讨论后决定。关键是,评审者要给出明确的理由,而不是“我觉得这样不好”。有理由的评论,作者更容易接受。
7.3 PR太大怎么审
PR太大,是审查失效的主要原因之一。我的建议是:强制拆分。一个PR的变更行数,控制在400行以内。超过400行,必须拆分。拆分的原则是:每个PR只做一件事。如果一件事确实需要改很多文件,那就分多个PR,每个PR改一部分。
7.4 审查时间不够怎么办
审查时间不够,本质上是优先级问题。如果团队真的重视质量,就应该给审查留出足够的时间。我的建议是:把审查时间算进开发时间。比如一个功能预计开发三天,那就预留半天做审查。这样就不会出现“没时间审查”的情况。
7.5 怎么让新人快速上手审查
新人审查代码,最大的问题是不知道看什么。我的建议是:给新人一个审查清单,让他按清单逐项检查。清单可以从简单的开始,比如“变量命名是否清晰”、“是否有明显的逻辑错误”,然后逐步增加难度,比如“是否处理了边界条件”、“是否有并发问题”。清单用熟了,新人自然就知道看什么了。
7.6 审查和测试的关系怎么处理
审查和测试不是替代关系,而是互补关系。测试覆盖正常路径,审查覆盖异常路径。测试覆盖已知场景,审查发现未知风险。我的建议是:测试必须通过才能进入审查,但测试通过不代表审查可以放松。审查要关注那些测试覆盖不到的地方。
8. 我自己的几点体会
最后说几点我自己的体会,不一定对,但都是踩过坑之后总结出来的。
第一,审查的质量,取决于评审者的投入程度。如果评审者只是走过场,再好的流程也没用。所以选评审者,比定流程更重要。我现在的做法是:每个PR指定一个“主评审人”,这个人必须认真审,其他人可以辅助。主评审人对审查质量负责。
第二,审查的文化,比审查的工具更重要。工具再好,如果团队不重视质量,审查还是走过场。所以建立质量文化,是审查改革的第一步。怎么建立?从认真对待每一个审查评论开始。
第三,审查的收益,是长期的。短期内,认真审查会拖慢开发速度。但长期看,认真审查能减少线上事故、降低维护成本、提升团队能力。这个账,要算长期账,不能算短期账。
第四,AI和智能体是帮手,不是替代。它们能帮我们过滤低级问题、节省时间,但不能替代人的判断。审查的核心,还是人对代码的理解和判断。这一点,短期内不会变。
第五,审查的最终目标,是让代码更好,而不是让流程更顺。如果流程和代码质量冲突,选代码质量。流程是为人服务的,不是人为流程服务。
我现在的团队,PR平均评审时间是二十五分钟,比之前的四分钟长了六倍。但线上事故率下降了六成,代码维护成本下降了四成。这个投入产出比,我觉得很值。