AI 代码评审闸门落地实战
一句话结论:AI 评审失败的头号原因不是能力不够,是它不敢说「这里我没把握」。 我们把「置信度标注」做进闸门之后,团队对 AI 意见的接受率从 31% 跳到 76%。
一、背景:一个堵在评审上的团队
N 公司做工业质检 SaaS,研发 40 人,分 6 个小组,代码放在 GitLab 上的 23 个仓库里。
2026 年 8 月的基线数据:
| 指标 | 基线(2026-08) |
|---|---|
| 日均 PR 数 | 28 |
| 评审首响中位 | 19 小时 |
| PR 从提交到合并中位 | 31 小时 |
| 单个 PR 平均评审轮次 | 2.7 |
| reviewer 人均每周评审耗时 | 6.5 小时 |
| 合并后 7 天内发现缺陷的 PR 占比 | 11.3% |
| 超过 48 小时无人评审的 PR 占比 | 17% |
最要命的是那个 17%。 这些 PR 不是被拒了,是没人看。作者催一次、再催一次,最后要么自己合并(跳过评审),要么等组长想起来。
1.1 第一次尝试:直接买 AI 评审工具,两周后被抵制
团队先试了市面上一个 AI Code Review SaaS,接进 GitLab,每条 PR 自动评论。
两周后的匿名问卷结果:
| 反馈 | 占比 |
|---|---|
| 「意见有用,我会改」 | 22% |
| 「大部分是废话,我直接忽略」 | 54% |
| 「误报太多,反而增加我的负担」 | 38% |
| 「希望能关掉」 | 29% |
(多选)
失败原因复盘,按票数排:
- 误报噪音(62%):大量「建议重命名变量 x 为 index」这类无意义意见
- 不分轻重(57%):把「这行缺分号」和「这里有并发竞态」用同样的语气和位置呈现
- 无法反驳(41%):AI 说得斩钉截铁,但有时候是错的,你得读一遍代码才知道它错了
- 重复评论(35%):同一个问题在每次 push 后重复评论
核心问题在第 3 条。 前两条是体验问题,第三条是信任问题——一旦 reviewer 发现 AI 会「自信地胡说」,他就会开始怀疑每一条意见,包括正确的那些。这直接摧毁了工具的价值。
所以我们决定自建,并把**「让 AI 标注它有多确定」**作为第一优先级设计目标。
二、工具分工表
| 环节 | 工具 | 为什么是它 | 成本/月 |
|---|---|---|---|
| 触发与编排 | 自建 Review Gateway(Go,1.2k 行) | 需要精细控制闸门顺序、分级规则、去重 | 自研 |
| 第一道:机械扫描 | ast-grep + Semgrep + gitleaks | 确定性规则,零误报,毫秒级 | ¥0 |
| 第一道:AI 初筛 | DeepSeek V4.1 Flash | 便宜快,扫风格/缺测试/明显 bug 足够 | ¥420 |
| 第二道:深度审查 | Claude Opus 5.5 | 逻辑正确性、边界、并发需要强模型 | ¥1,180 |
| 去重与历史上下文 | Gateway 自建(diff 指纹库) | 防止重复评论 | 自研 |
| 结果呈现 | GitLab MR Discussion + 飞书机器人 | reviewer 在哪就在哪看 | ¥0 |
| 效果追踪 | Langfuse + 自研指标看板 | 必须能量化,否则无法说服团队 | ¥0(自部署) |
总成本约 ¥1,600/月,40 人团队人均 ¥40。对照节省的 reviewer 时间(人均每周省 3.7 小时,按 6 组 × 人均成本粗算约 ¥18,000/月),投入产出比约 1:11。
⚠️ 这个账要小心算。 节省的时间不直接等于钱——reviewer 省下的时间未必转化为产出。真正硬的数字是首响时间和缺陷率,节省时间是团队满意度的代理指标。汇报时用后者,别用 ROI 数字去说服 CFO。
三、策略设计:三道闸门 + 分级拦截
3.1 三道闸门的分工
PR 提交 / push
│
├─ 【闸门 0】风险分级(无 AI,纯规则)
│ 按 diff 规模 + 路径模式 + 作者权限 → L1 / L2 / L3
│
├─ 【闸门 1】机械扫描(全部 PR 必过,秒级)
│ ast-grep 风格规则 + Semgrep 安全规则 + gitleaks 密钥
│ 不过 → 直接打回,不进人工队列
│ L1 到此为止(改文案不该走深度审查)
│
├─ 【闸门 2】AI 深度审查(L2 / L3 才跑,30-90 秒)
│ 输出结构化意见,每条带置信度 high/mid/low
│ 低置信度意见单独分组,标注「AI 没把握,重点看这里」
│
└─ 【闸门 3】人工评审
reviewer 看的是:AI 覆盖了什么 + AI 没把握什么
而不是从零开始读 diff
3.2 风险分级规则(闸门 0)
这是整个方案里性价比最高的一段,因为它决定了后面要不要花钱调 AI。
# risk-rules.yaml
rules:
- name: L1_trivial
desc: 文案 / 配置 / 文档,AI 只做机械扫描
any:
- diff_lines: "<= 20"
- paths_match: ["**/*.md", "**/i18n/**", "**/*.json"]
- paths_match: ["**/*.yml"] and not paths_match: ["**/deploy/**"]
action: gate1_only
- name: L3_critical
desc: 核心链路 / 权限 / 支付 / 数据删除,全闸门 + 强制人工
any:
- paths_match: ["src/core/**", "src/auth/**", "src/billing/**"]
- diff_contains_regex: ["\\bDELETE\\b", "\\bDROP\\b", "\\bTRUNCATE\\b"]
- diff_contains_regex: ["@Transactional", "synchronized", "AtomicInteger"]
- diff_lines: "> 800"
action: all_gates_and_require_human
- name: L2_normal
desc: 其余全部
default: true
action: gate1_and_gate2
实测分布:L1 占 34%、L2 占 52%、L3 占 14%。
这意味着 34% 的 PR 完全不调深度模型,直接省掉约 1/3 的 Claude 开销。而 L1 里绝大多数是文案改动的 PR——它们本来也根本不该占用 reviewer 的时间。
3.3 置信度:这次设计的核心
第二道闸门要求 AI 对每条意见标注置信度,而且必须给出「为什么不确定」。
{
"issues": [
{
"file": "src/core/order/OrderService.java",
"line": 142,
"severity": "high",
"confidence": "high",
"category": "concurrency",
"title": "checkStock 与 deductStock 之间没有加锁,存在超卖窗口",
"reasoning": "142 行读取库存,158 行扣减,中间无同步机制且方法上有 @Transactional 但未指定隔离级别,默认 READ_COMMITTED 下两个事务可同时读到同一个 stock 值",
"suggestion": "改用 SELECT ... FOR UPDATE 或在 service 层加分布式锁",
"uncertainty": null
},
{
"file": "src/core/order/OrderService.java",
"line": 203,
"severity": "mid",
"confidence": "low",
"category": "boundary",
"title": "refund 金额未校验是否超过原订单金额",
"reasoning": "未找到金额校验逻辑,但可能存在在调用方(RefundController)或数据库约束里做了校验",
"suggestion": "确认调用链上游是否有校验,若无则在此处补",
"uncertainty": "我只能看到这个文件的 diff,未加载 RefundController 的当前内容,无法确认上游是否已校验"
}
],
"coverage": {
"checked": ["并发安全", "空指针与空集合", "事务边界", "异常吞掉"],
"not_checked": [
"调用方是否正确处理(未加载上游文件)",
"数据库约束是否覆盖(未读 DDL)",
"历史兼容性(未看该接口的调用方清单)"
]
}
}
三个关键设计:
confidence三档:high / mid / low,渲染时低置信度单独分组并显示黄色标记uncertainty字段必须写:置信度不是 high 时,必须说明「我为什么不确定」。这个字段是团队信任的来源coverage.not_checked明确列出没看什么:这是让 reviewer 知道自己还要看什么的关键
not_checked 这个字段是整个方案里最重要的一处设计。 它把 AI 的角色从「评审者」变成了「预审员」——它说清楚了自己做了什么、没做什么,人就知道该补什么。
四、实施步骤
第 1 周:基线与规则固化
不做任何 AI,先把确定性规则跑起来。
这一步常被跳过,但它有三个不可替代的作用:
- 建立指标基线(后面所有对比都要用)
- 把「风格问题」从人工评审里彻底剥离(AI 也不该管这个——确定性规则零误报)
- 让团队先习惯「PR 会被自动检查」这件事
产出:
- ast-grep 规则 37 条(命名、导入顺序、禁用 API、日志格式)
- Semgrep 规则 22 条(SQL 注入、XSS、不安全的反序列化)
- gitleaks 默认规则集 + 3 条自定义(公司内部密钥前缀)
# .gitlab-ci.yml(第一周版本)
ast-grep-scan:
stage: review
image: ast-grep/ast-grep:latest
script:
- ast-grep scan --rule rules/ast-grep/ --json > ast-grep.json || true
- |
COUNT=$(jq 'length' ast-grep.json)
if [ "$COUNT" -gt 0 ]; then
echo "发现 $COUNT 个机械问题,请修复后重新 push"
jq -r '.[] | "\(.file):\(.line) \(.message)"' ast-grep.json
exit 1
fi
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
第 1 周结果:机械问题占评审意见的比例是 41%——这些意见从此不再需要人写。
第 2-3 周:第一道闸门的 AI 初筛
机械规则管不了「这个函数没写测试」这类判断。这一段用便宜模型。
Prompt 模板(第一道):
你是代码评审的第一道闸门。你的任务是在 10 秒内给出「值不值得让人看」的判断。
## 输入
- diff: {diff}
- 变更文件路径: {files}
- 关联测试文件是否存在: {has_test}
## 你必须输出 JSON,不要输出其他内容
{
"block": true/false,
"block_reasons": [
{ "type": "missing_test|obvious_bug|missing_error_handling|breaking_change",
"file": "...", "line": 0, "one_line_desc": "..." }
],
"notes": ["非阻塞的观察,最多 3 条"],
"confidence": "high|mid|low"
}
## 判定规则
- block = true 只在以下情况:
1. 新增了 public 方法但没有任何测试文件变更
2. 出现了明显的逻辑错误(变量未定义、条件恒真、死循环)
3. catch 块为空或只打日志不处理
4. 修改了已有 public 方法的签名且没有兼容处理
- 风格问题一律不 block(那由 ast-grep 管)
- 你只有 10 秒。**不确定就 block = false**,把疑问写进 notes。
误报的成本远高于漏报:漏报会进第二道闸门,误报会浪费人的时间。
⚠️ 最后那句「不确定就 block = false」是第一道闸门的设计精髓。
第一道闸门的错误有两种代价:
- 漏报(该拦没拦)→ 进第二道闸门,多花 ¥0.4
- 误报(不该拦拦了)→ 浪费作者一次返工,团队开始不信任系统
两者不对称。 所以第一道闸门的阈值要调得极保守——我们实测把它调到「几乎不 block」,只拦最明显的那几类,准确率 94%,拦下率 8%。8% 听起来低,但它是零误报的 8%,作者不会感到被骚扰。
第 4 周:第二道闸门与置信度
这一段是投入最大的一周(12 人日),核心是置信度体系。
Prompt 模板(第二道,节选关键段):
你是代码评审的第二道闸门,负责深度审查。第一道闸门已经过滤掉风格和明显问题,
你的任务是找那些需要理解业务逻辑才能发现的问题。
## 输入
- diff: {diff}
- 相关文件的完整内容: {context_files}
- 该仓库的既有约定: {repo_rules}
## 输出 JSON
{
"issues": [
{ "file": "", "line": 0,
"severity": "high|mid|low",
"confidence": "high|mid|low",
"category": "concurrency|boundary|error_handling|security|performance|design",
"title": "", "reasoning": "", "suggestion": "",
"uncertainty": "confidence != high 时必填,说明你缺什么信息" }
],
"coverage": {
"checked": ["你实际检查了哪些方面"],
"not_checked": ["你没能检查的方面,以及为什么"]
}
}
## 置信度判定标准(严格执行)
- high: 你能从 diff + context 里完整推导出问题,且能指出确切的代码位置与触发路径
- mid: 你发现了可疑模式,但无法确认它在当前业务语义下是否真的是问题
- low: 你觉得应该检查一下,但不构成具体意见
## 硬性要求
1. confidence != high 时,uncertainty 字段必须说明你缺什么信息,不许写「不确定」三个字
2. coverage.not_checked 至少 2 条。你不看的方面必须让用户知道
3. 不要报告风格问题(ast-grep 已覆盖)
4. 不要报告「建议加注释」这类无实质内容的意见
5. 你不是审批者。你输出的是「值得人看的地方」,不是「能不能合并」
「不要报告建议加注释」这条是踩坑加上的。 第一版没写这句,AI 输出里 23% 是「建议为这个函数添加文档注释」——纯粹的噪音,而且它把真正的问题淹没了。
第 5 周:分级拦截 + 去重
分级拦截已在 3.2 说明。这一周还做了去重,因为「重复评论」是第一次尝试失败的第四大原因。
去重方案:
// 意见指纹 = 文件 + 问题类别 + 标题的语义归一化
func issueFingerprint(i Issue) string {
// 行号不进指纹:同一问题在 push 后行号会变
norm := strings.ToLower(i.Category + "|" + normalizeTitle(i.Title))
return sha256hex(i.File + "|" + norm)[:16]
}
// 已评论过的指纹存 7 天,重复的不重复评论
// 若问题被修复(该位置代码已变),指纹自然消失
⚠️ 行号不能进指纹,这是第一版踩的坑:作者 push 一次,所有行号偏移,AI 把同样的问题再评论一遍。
第 6 周:灰度与全量
灰度策略:先开 2 个小组(12 人),跑 5 个工作日,看指标和问卷。
灰度期发现的两个问题:
- L3 判定过宽:
src/core/**匹配到了 47% 的 PR,导致深度审查成本超预算。收窄为src/core/{order,payment,inventory}/**后降到 14% - 飞书通知过于频繁:每个 PR 都推,群里一天 28 条。改为只在 L3 或 high severity 时推,日推送量降到 4 条
全量开放后第一周,主动关闭 AI 评审的人:0。这是最直接的满意度信号。
五、效果数据
5.1 核心指标(全量后第 4 周 vs 基线)
| 指标 | 基线(08月) | 落地后(10月) | 变化 |
|---|---|---|---|
| 评审首响中位 | 19 小时 | 2.4 小时 | -87% |
| PR 提交→合并中位 | 31 小时 | 9.6 小时 | -69% |
| 单个 PR 评审轮次 | 2.7 | 1.9 | -30% |
| reviewer 人均周耗时 | 6.5 小时 | 2.8 小时 | -57% |
| 合并后 7 天缺陷率 | 11.3% | 7.5% | -34% |
| 超 48h 无人评审占比 | 17% | 2.1% | -88% |
| 月模型支出 | ¥0 | ¥1,600 | — |
5.2 AI 意见质量
| 指标 | 数值 |
|---|---|
| 日均 AI 意见条数 | 63 |
| 作者接受并修改的比例 | 76% |
| 被标记为误报的比例 | 8% |
| high 置信度意见的准确率 | 91% |
| mid 置信度意见的准确率 | 64% |
| low 置信度意见的准确率 | 38% |
置信度三档的准确率梯度(91% / 64% / 38%)是整个项目的验收依据。 如果三档准确率接近,说明置信度标注没起作用——那就要回去改 Prompt。
⚠️ low 档 38% 的准确率看似很差,但它是有价值的。 因为它被单独分组标注为「AI 没把握」,reviewer 看它时心态是「去核实一下」而不是「接受建议」。实测 low 档意见里 38% 是真问题,而这部分恰恰是人类 reviewer 最容易因为「看起来没问题」而跳过的地方。
5.3 对照:第一次尝试 vs 自建
| 指标 | 第一次(SaaS) | 自建 |
|---|---|---|
| 意见接受率 | 31% | 76% |
| 误报率 | 高(未统计,问卷 38% 抱怨) | 8% |
| 团队希望关闭的比例 | 29% | 0% |
| 重复评论 | 有 | 无(指纹去重) |
| 分轻重缓急 | 无 | 有(severity + confidence + 分级) |
六、12 条踩坑清单
- 别让 AI 管风格。 确定性规则(ast-grep / ESLint)零误报且毫秒级,AI 做这件事只会贡献误报
- 第一道闸门的阈值要保守。 不确定就放行,误报成本远高于漏报
- 行号不能进去重指纹。 否则每次 push 都会重复评论同一批问题
- Prompt 里要显式禁止「建议加注释」类意见。 不写这句,AI 会生成一大堆这种废话
- 置信度必须配 uncertainty 说明。 只标「low」不说明为什么,用户无从判断该不该信
not_checked字段比checked更重要。 它决定了人还要看什么- L3 的路径规则要收窄。 一开始写宽了会吃光预算
- 飞书/邮件通知要限流。 只在 L3 或 high severity 时推,否则群会被刷屏导致全员屏蔽
- 不要让 AI 决定能不能合并。 它是预审员不是审批者。一旦 AI 有了否决权,作者会开始和 AI 对抗
- 必须给「关闭 AI 评审」的开关。 强制使用会引发抵触,而主动使用率才是真实指标
- 灰度期一定要做问卷。 指标上升不代表体验好——接受率是滞后指标,问卷是领先指标
- 别用 ROI 数字去说服管理层。 用首响时间和缺陷率。「省了多少人小时」这种账经不起追问
七、经验总结
7.1 最重要的一条:AI 要敢说「我不知道」
第一次尝试失败的根因,复盘时大家以为是「误报太多」。做深了才发现真正的根因是AI 的语气问题——它对每条意见都同样自信,导致正确的意见也被怀疑。
加上置信度 + uncertainty + not_checked 三件套之后,误报的绝对数量其实没降多少(从 12% 到 8%),但接受率从 31% 跳到 76%。
这中间的差异全部来自「标注」。 用户能分辨哪些该认真对待,就不再全盘怀疑。
7.2 分级拦截的收益被低估
我们最初以为分级只是为了省钱。跑起来才发现它最大的价值是减少了 34% 的 PR 的打扰。
一个改文案的 PR 收到 5 条 AI 意见,哪怕全对,作者也会烦。现在这类 PR 只跑机械扫描,几秒钟过,零 AI 意见。团队的「被骚扰感」大幅下降。
7.3 不要把 AI 放在决策位
整个设计里,AI 从来不做「能不能合并」的判断。它只做两件事:
- 拦截明显问题(第一道,且阈值极保守)
- 标记值得人看的地方(第二道,带置信度)
否决权始终在人手里。 这不只是哲学问题——一旦 AI 有了否决权,作者的行为就会从「参考 AI 意见」变成「想办法让 AI 通过」,评审的初衷就没了。
7.4 下一步
- 接入测试覆盖率增量:现在是「有没有测试」,下一步是「新增代码有没有被测试覆盖」
- 跨 PR 的一致性检查:同一类改动在不同 PR 里用了不同写法,单 PR 视角看不见
- 用历史缺陷反哺规则:把合并后发现缺陷的 PR 拿来复盘,看 AI 当时漏了什么,补进 Prompt
八、可复制的最小启动方案
如果只有 2 人日预算,做这三件事就够了:
- ast-grep / ESLint 规则跑进 CI(0.5 人日)—— 剥离 41% 的机械意见
- 一个便宜模型做第一道初筛,阈值调保守(1 人日)—— 拦下最明显的问题
- 通知限流:只在 high severity 时推(0.5 人日)—— 避免群被刷屏
置信度体系、分级拦截、去重这三块是第二期。它们能显著提升体验,但没有它们系统也能跑,且不会引发抵制。
先让团队接受「PR 会被机器看一眼」,再逐步加复杂度。 反过来做,第一次就会像我们一样被抵制。