过去一年里,你在 code review 时应该已经明显感觉到一种撕裂感:AI 生成的代码越来越流畅,函数命名整齐、注释齐全、结构分层合理,但评审者却越来越不敢点 Approve。团队里开始有人抱怨“既然 AI 都写出来了,为什么还要人审”,也有人因为连续在 AI 的漂亮代码里挑出隐蔽 bug,而变得对每一行都过度警觉。代码审查这件事,并没有因为 AI 的加入而变得轻松,反而出现了一种反直觉的现象:代码越好看了,越不敢合并了。
“AI Broke Code Review and It‘s Breaking Your Team”,这个标题在英文技术社区引起了很多人的共鸣。它想表达的不是“AI 工具不稳定”或“模型写不出好代码”,而是一个更结构性的问题:代码评审原本建立在一套默认假设上——人的错误是零散的、个体化的、可以通过经验和清单来发现。但 AI 生成代码的错误不是这样的,它们是统计学上自然的、流畅的、甚至看起来很有道理的臆测。当错误从“偶然手滑”变成“分布在代码库每个角落的合理幻觉”,团队的评审机制如果还在原地踏步,那就不是在 review,而是在给 AI 的错误批量盖章。
这篇文章要讲清楚三件事:第一,AI 究竟改变了代码审查的哪个底层环节;第二,为什么 AI 生成的“合理错误”那么难被发现;第三,团队应该如何调整评审流程、提示词方式和验收标准,把质量底线重新握回人手里。文章后半部分会给出可复制到团队里的提示词模板、diff 检查命令,以及一套双轨评审流程。
1. 代码审查没有被 AI 加速,而是被 AI 改写成了另一种风险
先做一个最直接的对比。在 AI 辅助编码普及之前,团队里坏代码通常来自哪里?理解偏差、手误、状态漏处理、命名混乱,以及极少数刻意绕过的逻辑。这些错误有个共同特征:写错的人通常知道“这里有风险”,只是没注意到。评审者要做的,是在这些自然稀疏的错误中,用经验和清单把遗漏找出来。
AI 时代完全变了。以常见的大语言模型辅助编程为例,模型输出的每一行都经过概率平滑处理,它会主动避开那些“看起来别扭”的写法,而是生成它在训练数据里见过大量次的常规写法。这带来一个隐蔽后果:AI 代码的错误不再是“某个局部写错了”,而是“整体都非常合理,但某一个假设错了”。比如它默认某个字段永远非空,默认某个接口不会抛异常,默认时间戳来自同一时区,默认用户输入不会超过某长度。这些假设不会被 lint 发现,也不会让 happy path 上的单元测试失败。
团队感受上的变化更明显。以前一个 PR 的缺陷密度是可预估的,评审者能按节奏慢慢看。现在 AI 辅助提交的 PR 体积更大、速度更快、风格几乎无瑕疵,想要从中挑出“看起来没错但语义上错误”的逻辑,需要的信息量远超 diff 本身。很多团队因此把合入速度提上来了,但发现问题的概率反而降下去了。这就是我认为“AI 不是修复了 code review,而是把 code review 从一个查错工作,变成了一个判断模型假设工作”的原因。
如果只看这些描述,可能会觉得是危言耸听。但这些现象背后有认知科学和机器学习研究的影子。下面两节把这个问题拆开讲。
2. 代码评审到底在审什么
代码评审表面上是在审 diff,实际上是在审两层内容。
第一层是“表达层”:命名是否清晰、结构是否合理、有没有重复代码、是否遵守团队规范。第二层是“契约层”:这段代码是不是真的实现了需求要求的行为,边界条件是否覆盖,出现异常时会不会导致数据不一致,线上会不会因为调用方的不同用法而翻车。
过去,表达层占了评审者大量精力。因为人写代码会出现命名随意、结构凌乱、风格不一的情况,所以 Git 提交里充满了手动调整的痕迹。而现在的 AI 生成代码,把表达层绝大多数问题都抹平了。命名工整、函数够短、类型看起来也对,注释还能对上。理论上,评审者可以腾出精力专注于契约层,这本来是好事。但问题在于:表达层变好之后,人类评审者会产生“代码质量高,出错概率低”的直觉,反而降低了在契约层上的投入。这就是心理学里的“认知流畅性效应”——看起来流畅的内容,更容易被我们采信。
用一个表格概括这三个阶段的差异:
| 阶段 | 错误主要来源 | 评审者要做的事 | 常见失败方式 |
|---|---|---|---|
| 纯人工编写 | 手误、理解偏差、风格不一 | 对照需求和常识逐行检查 | 漏看、没时间 |
| 静态工具 + 人工 | 逻辑错误、边界遗漏 | 工具过滤常规问题后聚焦逻辑 | 过度依赖工具,忽略语义 |
| AI 生成代码 | 模型凭空产生的合理假设 | 识别“看起来很对”但缺少依据的假设 | 认知流畅性导致过度信任 |
这也就解释了为什么标题会说 “breaking your team”:评审者不是不努力,而是在处理一类新型问题——模型假设。这类问题无法靠 Git diff 页面里的红绿行看到,必须把需求文档、调用链、异常路径、甚至模型在训练数据里的常见偏见一起带进来才有机会发现。
3. 为什么 AI 生成的“合理错误”这么多
深入看机器学习原理,大语言模型做的事情本质是“预测下一个词”。它在训练时被优化的是文本连贯性,不是业务正确性。所以它更倾向于输出“更常见、更像人话、和前文更一致”的代码,而不是“更符合某个私有仓库业务规则”的代码。你给它上游函数、类名、变量名,它会调用训练数据里相似模式的记忆来补全。如果仓库里的业务逻辑和公开代码非常相似,补全结果通常可用;一旦你的业务有独特约束,比如“金额必须四舍五入而不是截断”“告警时间按客户时区计算”“缓存过期时间必须是业务参数而不是固定值”,模型就很容易凭常规经验补一个假设上去。
这里要重点理解一个概念:分布偏置。训练语料里,开源项目、技术博客和教学代码占了大头。这些代码面向的通常是教育场景或演示场景,对生产级约束的考虑并不充分。教学代码里datetime.now()满天飞,但生产系统要考虑时区、夏令时、时间戳精度;教学代码里split("=", 1)很常见,但生产配置要处理注释、转义和编码。AI 学到的不是“你的代码库事实”,而是“整个互联网代码的平均形态”。因此,那些“在平均值里对、在你的场景里错”的代码,会以非常高的比例出现。
另一个因素与训练目标有关。生成式模型天然带有不确定性,但代码评审场景里的风险,不在于那句明显错误的 API 调用,而在于那些“旁边没有明显报错信号”的代码。假如你让 AI 写一个配置文件解析函数,训练数据中的常见版本几乎都长这样:跳过空行、忽略注释、按=切分。它不会基于你的配置里有# 注释就主动规避,它只会生成一个“看起来没问题,但读到注释行就崩”的版本。如果你在 review 时只跑一条正常配置的冒烟用例,这个 bug 会悄悄进入主分支。
最后还有一层认知因素。人类对流畅感的敏感度极高,看到函数里注释完整、命名规范、结构清晰,大脑会自动把它归类为“可信内容”,分配给它的注意力随之下降。这不是哪个人不认真,而是大脑在做资源分配时的自然倾向。要对抗这一点,只能把流程设计成“默认不信任”,而不是“默认信任然后找茬”。
4. 三张最典型的“AI 假完美”现场
理论说完,看几个具体场景。这三个场景是真实团队里反复出现过的模式,代码经过简化。
4.1 场景一:边界条件看着都处理了,细看缺少关键判断
需求很简单:在证件到期前 30 天内,系统需要给用户发送续期提醒,已经过期的也要提醒。
from datetime import datetime def should_remind(expiry_date): if not expiry_date: return False remaining_days = (expiry_date - datetime.now()).days return remaining_days <= 30这段代码第一眼很自然:有非空判断,有剩余天数计算,有阈值比较。但它隐藏了好几个假设:expiry_date和datetime.now()是不是同一时区?一个没有时区信息的datetime直接相减会不会抛异常?remaining_days是精确到秒的概念,还是日期概念?更微妙的是,它把“剩余不足 30 天”和“已过期很多年”合在同一个分支里。如果需求希望“过期超过一年只提示一次”,这段代码就不符合要求。
这类边界问题在人工代码里也会出现,但 AI 版本的特点是函数命名和注释都很好,足以让评审者看完后产生“挺完善”的感觉。如果评审者不把需求拆成“已过期”“剩余 0-30 天”“剩余大于 30 天”三条路径逐一对照,很容易漏掉。
4.2 场景二:重构优雅,但行为契约悄悄变了
团队有一段历史代码,功能是发送通知。原代码有一个重要约定:用户没填邮箱时,什么都不做。
public void sendNotification(User user) { if (user.getEmail() == null || user.getEmail().isBlank()) { return; } emailService.send(user.getEmail()); }AI 参与重构后,可能被改成:
public void sendNotification(User user) { try { emailService.send(user.getEmail()); } catch (IllegalArgumentException e) { log.warn("send email failed: {}", e.getMessage()); } }新代码看起来更健壮了:加了异常捕获,不会因为空邮箱直接抛错。但行为契约变了:旧代码是“空邮箱直接不发”,新代码是“空邮箱会走到emailService.send(null),由底层抛出异常后吞掉”。如果emailService.send在传入null时不抛IllegalArgumentException,而是把空对象写入数据库或放进 MQ 消息队列,那么错误会在这个函数之外更远的地方爆发。
这种重构在 AI 辅助开发里非常常见,因为模型从训练数据中学到“try-catch 是好的健壮性实践”,但它不理解这里的业务约定“空邮箱必须直接返回”。评审者如果只看到新代码里的 catch 和 log,很容易直接给出 LGTM。
4.3 场景三:模型凭记忆调用 API,忽略版本差异
第三种场景更偏“幻觉”。当模型需要调用某个不常见的 SDK 时,它会倾向于生成一个“最像真的”调用方式,而不是查询当前项目的实际依赖版本。例如:
def request_refund(order_id, reason): client = RefundClient() result = client.create( order_id=order_id, reason=reason, auto_approve=True, ) return result.refund_id问题可能出现在任何一层:RefundClient不存在、create方法签名不一致、auto_approve参数名错误或者在新版本中已被移除。更隐蔽的是,即使类名和参数名都对,模型也可能遗漏调用方要求的幂等参数request_no。这类代码若在编译期暴露还好,真正危险的是在测试环境被 mock 掩盖,直到订单系统在线上出现重复回调才发现。
这说明一个原则:AI 写的 API 调用代码,必须一律当作“可能基于过时或错误的 API 记忆”来处理。评审时的第一步不是看逻辑,而是去核对当前项目里的依赖版本、类定义和接口文档。
5. 把提示词从“帮我写代码”改成“帮我理清假设”
既然问题核心是模型在替你做假设,应对方式就是把假设权收回来。最有效的改变发生在提示词层:不要求 AI 直接写完整实现,而是要求它先列出实现需要满足的约束、边界和不确定项,再由你确认。
一个适合评审和开发场景的提示词模板如下:
你是一名严谨的 Python 工程师。请根据下面的需求描述,先输出“需求理解”和“实现假设”,再输出代码。 需求描述: 1. 根据证件到期日,判断是否需要在未来 30 天内提醒用户续期。 2. 已经过期的证件也需要提醒。 3. 提醒事件每天只能触发一次。 输出格式: - 需求拆解:列出每条需求的实现要点。 - 实现假设:列出你认为正确、但需求里没有明确写的假设,尤其是字段时区、空值、并发、幂等。 - 代码:只输出核心函数。 - 注意:没有把握的假设,用“需要人工确认”标注。这样的提示会逼着模型把“时区怎么处理”“每天一次怎么实现”等问题摆到台面上。即使模型仍然会列出一堆假设,评审者也有了可以直接对照核验的需求清单。经验是:当模型主动列出假设后,评审者发现问题的速度会显著快于直接读代码。
除了提示词,还可以把团队约定写进项目根目录的AGENTS.md,或者代码评审模板。例如:
## 评审时需要确认的 AI 假设清单 - 这个函数是否隐含了“输入非空”的假设? - 是否隐含了“时间都是 UTC”的假设? - 是否隐含了“并发只会有一个调用方”的假设? - 对第三方 API 的调用是否核对了当前依赖版本的实际签名? - 是否在异常处理中把不该吞掉的错误吞掉了?清单不复杂,但能显著提高评审者对模型假设的敏感度。
6. 新评审流程:双轨审查
把代码评审流程拆成两条轨道,是现阶段比较务实的做法。
轨道 A 是机器快速检查。凡是规则能判定的东西,不要让 AI 或人逐行去看。比如运行git diff --check检查空白错误和 conflict markers,跑一遍 lint,跑单测,跑依赖漏洞扫描,再执行秘密信息扫描。这些规则可以写进 CI,保证每个 PR 合入前自动执行。
git diff main...feature --check git diff main...feature --stat git diff main...feature -U20 | head -300-U20的目的是让审查时看到更多上下文。默认的三行上下文往往不够还原模型被打断的语义,尤其是在大模型生成的长函数里,上下文比行数重要得多。
轨道 B 是人工语义检查。这一步不接受“代码能编译”“单测过了”作为通过标准,而是要求评审者回答三个问题:这段代码是否兑现了需求里的每一条行为?它有哪些新增假设,这些假设是否经过确认?如果这个函数在线上异常退出,日志、数据和资金流会发生什么?建议采用“小步阅读”的方式,一个 PR 只审 200 行以内的实质性语义变更。超出部分要求提交者拆分,因为多模型生成的 800 行代码里,人类能维持有效注意力的区间非常有限。
在实践中,轨道 A 的自动化率越高,评审者越能保留体力给轨道 B。AI 在这里可以做一件事:把 diff 里“只改命名”“只改格式”的低风险代码筛选出来,让评审者重点看“逻辑实质变化”的部分。但要注意,AI 只能做摘要和风险标注,不能做最终裁判。
7. 完整示例:用双轨审查流程处理一个 AI 生成的 PR
假设团队收到一个 AI 辅助生成的 PR,功能是计算订单实付金额。需求如下:满 100 减 20,新人首单再打 9 折,两种优惠不叠加。实付金额保留两位小数,四舍五入。
AI 生成的代码可能是这样的:
def calculate_payable(amount, is_new_user, has_coupon): if amount >= 100 and has_coupon: amount -= 20 if is_new_user: amount *= 0.9 return round(amount, 2)第一轮轨道 A 检查:格式没问题,Lint 能过。单元测试如果只写了“输入 150、新人、有券 → 输出 117.0”这种 happy path,也能通过。但轨道 B 的语义检查会立刻发现几个问题:
- “满 100 减 20”的触发条件里,代码写成了
has_coupon,而需求并没有说必须有优惠券。这里多了一个假设。 - “折扣不叠加”没有体现。当前代码会先减 20,再打 9 折,相当于先减后折,属于叠加计算。如果需求是“两种优惠只能选一种”,这里就是确定的逻辑错误。
- 新人首单的“首单”没有判断。传入
is_new_user=True就会打 9 折,但用户可能是老用户复购。 - 金额单位没有说明。如果接口传入的是“分”,
round(amount, 2)会把单位差异掩盖掉。
评审者可以在 PR 里这样写评论:
1. 需要确认:满减是否要求必须有优惠券?需求写的是“满 100 减 20”,没有提到券。 2. 逻辑问题:需求和“折扣不叠加”矛盾,当前代码是减 20 后再打 9 折。 3. 首单未校验:is_new_user 不等于 first_order,需要增加订单表查询。 4. 金额单位:请确认传入参数是元还是分,否则 round 会掩盖单位错误。这个例子说明,真正需要人的地方不是“跑得通”,而是“需求契约与代码实现之间的一致性”。AI 很擅长生成一个“看起来完整真实”的实现,但验收标准从来不是代码像样,而是行为符合业务。
8. 团队层面的制度调整
前面几节是评审者个人能做的调整。但问题如果已经发展到“breaking your team”,就需要在制度层面补规则。
第一是明确 AI 生成代码的申报规则。团队可以不用禁止 AI,但可以在 PR 描述里声明哪些片段由 AI 生成,哪些经过人工仔细审阅。申报不是为了追责,而是帮评审者分配注意力:AI 生成的代码默认按“高风险”处理,人工写的常规业务代码按正常节奏审。
第二是设置单个 PR 的语义变更上限。当 PR 里实质性逻辑变更超过 300 行时,应要求拆分。原因不是行数本身,而是评审者的有效注意力有限。AI 让单人单日产出上千行代码变得很容易,但评审者能承受的语义审查总量并没有同比例增长。如果只追求生成速度、不控制合并速度,团队会在“表面上很成功”的情况下,积累大量未被真正审查的代码。
第三是重新定义“测试通过”的含义。对 AI 辅助生成的功能,建议把测试从“覆盖主要路径”升级为“契约测试 + 反例测试”。比如金额计算,除了验证金额减少,还要验证“不满足门槛时不减”“优惠不叠加时的分支”和“金额为负数时如何处理”。反例用例不需要很多,但对捕捉模型假设极有帮助。
第四是生产环境变更必须走完整安全流程。任何涉及线上行为的变更,都要能在测试环境验证,要有开关、监控和回滚方案。这笔原则并非 AI 时代才有,只是在 AI 生成代码更多、逻辑审查压力更大的时候,更加重要。CI 里可以增加一个强制检查,要求 PR 描述包含“影响范围”和“回滚方案”两栏,否则不允许合入。
9. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 代码风格统一但功能经常出错 | AI 按训练集平均风格生成,忽略业务私有约束 | 检查是否缺少需求拆解和假设确认环节 | 用“需求拆解 + 假设清单”提示词重新生成或修复 |
| 单测全过,上线后出现边界类故障 | 测试只覆盖 happy path,行为契约未被验证 | 查看测试用例是否包含反例、空值、并发场景 | 增加契约测试和反例测试用例 |
| 评审者表示“没什么可看的” | AI 代码过于流畅,触发认知流畅性效应 | 在 PR 模板里强制要求填写需求要点和假设清单 | 把“审假设”作为评审必读项 |
| 大 PR 合并后问题集中爆发 | 单 PR 语义变更过大,评审注意力不够 | 统计逻辑变更行数与缺陷密度的关系 | 拆分 PR,控制单次语义变更规模 |
| 第三方 API 调用在真实环境抛异常 | 模型使用了过时或错误的 API 签名 | 核对依赖版本、接口文档和调用参数 | 在评审入口增加“API 签名需核对”清单 |
10. 建议放在手边的行动清单
如果只想带走几条可落地的建议,我建议是这四条。
第一,下次拿到 AI 生成的代码,不要先看“写得对不对”,而是先问“它做了哪些需求里没有的假设”。把假设逐条列出来,和需求文档对照,你会发现比直接读代码更快地发现问题。
第二,把代码评审从“逐行通读”改成“小步阅读 + 契约核对”。一次最多审 200 行实质性逻辑变更,超出就要求拆分 PR。CI 里配置好git diff --check、Lint、单测和秘密扫描,机器能做的先做完,人工只保留真正需要语义判断的部分。
第三,写提示词时主动索要“需求拆解”和“实现假设”。让模型先把不确定的事项暴露出来,再让它在确认后的规则上写代码。这个习惯的价值,往往比挑选哪个模型更大。
第四,团队立一条简单规矩:AI 生成代码默认高风险,合入前必须有人明确验证行为契约。这条“默认不信任”的规则不会拖慢团队,反而会让 AI 带来的速度真正转化为可持续效率。
代码评审的本质,从来不是“看别人有没有犯错”,而是“我们如何以团队为单位,对一段将在生产环境长期运行的行为达成共识”。AI 把写代码的成本降低了,却没有降低判断代码的难度。越早接受这个现实,越早把评审流程改成“验证模型假设”的模式,团队就越能在 AI 编程的浪潮里站稳。