post · 2026.09.27

把方法论落成实物:我们的 AI 代码审查评测系统是怎么搭的(技术篇)

系统#AI Code Review#研发效能#质量测试#产品设计#工程实践

前面的文章讲"怎么想",这一篇讲"我们真的把它做出来了"——一套可运行的 AI 审查评测系统,及它背后的技术取舍。


开场:方法论的终点,是一套能跑的系统

系列前面反复讲"可信要考证据、尺子要比被测对象稳"。这些话说起来容易,但真把它变成能跑的程序、能出数字、能反复复现的一套系统,才是这套方法真正被验证的时刻。

这篇不再打比方,直接讲我们做出的一件具体东西:一个只读、可评测、可复现的 AI 代码审查评测系统,以及为了让它"可信",我们在每个环节做的技术取舍。


一、系统由五块组成

整条流水线,拆成五个模块,每个都有明确的职责和验收标准:

① 考卷(Beach Fixture)  真实 PR + 人工标准答案,指纹封存
② 考生运行器(Runner)   调 AI 审查,注入工具,收卷
③ 工具与沙箱(Tools)    只读工具集 + 路径沙箱
④ 打分器(Scorer)       确定性判分(det-match)
⑤ 记录仪(Trace)        每一步留底 + 对账铁律

一句话:①提供"考什么",②③让 AI 去"考",④判"考几分",⑤保证"分数可信"。


二、考卷:真实数据,锁死指纹,断网可跑

用什么当题目? 拿了真实开源项目的真实 PR(合并请求),配资深工程师确认过的人工评审意见当标准答案(GT)。

为什么不用合成数据? 因为我们要测的是"真实世界里 AI 审代码行不行",合成题会测出"AI 在刻意设计的题上行不行"——不真实。

怎么保证考卷不变? 这是质量测试的核心,用了三重锁:

  1. git bundle 封存:每道题的代码打包成 git bundle,带完整历史,git bundle verify 可校验;
  2. sha256 指纹:每个 bundle 算一个 64 位哈希,记进 manifest。加载时重算比对,改一个字节就报警(第 7 篇的"篡改必被抓"落地);
  3. 断网可跑:整套测试在无网络环境下跑全绿,证明考卷 100% 自包含,不偷偷依赖外网。

GT 结构:每道题 = {issue_index, comment, severity}。这里有个坑后面讲——答案里没有文件名和行号,直接决定了打分器怎么设计。


三、工具与沙箱:AI 的"眼睛"怎么装、怎么锁

AI 模型本身看不到文件系统,是我们给的工具替它看。工具集有两版,正好做了后面要讲的对照实验:

V0(纯文本工具):

  • git_diff:对比改动前后(用 merge-base 找共同祖先,避免把别人的改动混进来);
  • read_file:读文件,带 offset/limit;
  • text_search:正则搜全仓。

V1(加结构化工具):

  • 在 V0 基础上加 code_search(按代码结构搜,不是纯文字)和 code_outline(给文件的"目录页"——列出函数/类/导入,不读全文)。

这两个工具用 ast-grep 实现。这里踩了个真实的坑值得说:官方 Node 库只内置 6 种语言(ts/js/tsx/jsx/css/html),没有 Python/Java——但我们的考卷里正好有 Python 和 Java 题。最后转用 CLI 子进程方案(它动态加载 tree-sitter,全语言),代价是每次调用有进程开销,换来语言全覆盖。

沙箱(最重要的一层):AI 的所有文件操作必须先过路径沙箱——把请求先规范化成绝对路径,再判断是否越界,越界一律硬错误、不静默降级。防止 AI “顺手"读到仓库外的文件(比如 ../../etc/passwd)。


四、打分器:为什么用"确定性匹配”,不用 LLM 当裁判

第 5 篇讲过理论,这里讲实现。

关键约束:GT 没有文件/行号,只有一段文字描述。所以原计划的"文件+行号窗口匹配"做不了。我们设计了 [email protected]:

① token 化:把 GT 描述和 AI 的 finding 都切成关键词集合
   (camelCase/路径/下划线都归一,去停用词)
② 覆盖度:coverage = |GT关键词 ∩ finding关键词| / |GT关键词数|
③ 判定:coverage ≥ 0.5 算候选命中
④ 一对一贪心:所有 (GT, finding) 按覆盖度降序配对,一个 GT 只认一个 finding
⑤ 纯函数:同输入必同输出,无随机

为什么阈值定 0.5? 跑数据前就钉死(先注册后运行),不根据"哪个阈值好看"反推——那是改口径作弊。0.5 的含义是"AI 说出了答案一半关键词"。

诚实的妥协:这种匹配会漏掉"换一种说法说同一个问题"的情况,所以它是 recall 的下界(测出来的数字偏保守)。但对 V0 vs V1 这种相对比较完全够用——对所有版本一样严。


五、记录仪:五条对账铁律,保证数据没坏

跑批会产生海量数据,怎么保证它们没悄悄坏掉?用复式记账 + 对账。每条运行记录成一串 JSONL 事件,末尾记"总账",用五条铁律(I1-I5)校验:

  • I1 序列完整:首条必须是开始、末条必须是结束、序号连续(0,1,2…),断了一条就报警;
  • I2 时间单调:后一条时间不早于前一条,防时钟乱跳;
  • I3 usage 守恒:结尾总 token 必须等于所有单条 token 之和;
  • I4 工具调用守恒:总数 = 实际记录数;
  • I5 轮次守恒:总轮数 ≥ 出现过的最大轮号。

还用"故意破坏"来验证这些铁律真的灵敏:删结尾记录、改大总账、时间戳倒流——每一条都必须被抓住(第 7 篇)。


六、实验设计:V0 vs V1,一次诚实的对照

系统的核心价值,用一次对照实验体现:给 AI 加结构化工具(ast-grep),到底有没有让它审得更好?

要做这个实验,先按下所有变量:

  1. 同一个模型 vs 同一个模型;同一张考卷;同一套打分规则;
  2. V1 的提示词由 V0 的提示词代码生成(只替换"工具清单"那一段),从机制上保证两版除了工具描述,其余逐字相同;
  3. 工具集、提示词、打分规则都编版本号,写进每次运行的 config_hash,让"这个数字是哪个版本跑出来的"永远说得清。

跑批是串行的(避免并发引入限流变量)、带断点续跑(中断后跳过已完成继续)。


七、结果:数字说话,也有反直觉

3 个模型 × 2 个工具版 × 重复多次,关键结果:

模型V0 召回率V1 召回率变化备注
模型 A0.240.36+50%误报却从 32→48(工具放大报告倾向)
模型 B0.300.46+50%误报反而 36→32(收敛),还最快最省
模型 C很低很低无变化有 13/15 次"探索很多但不交卷"

三个有信息量的结论:

  1. 两个模型召回率同时 +50%——不是单点运气,是"加结构化工具确实有帮助"的一致信号;
  2. 但"有用"不是绝对的:同一个工具,模型 A 的误报大增、模型 B 的误报反而下降——工具像"性格放大器",收益取决于底层模型的性格(呼应第 12 篇);
  3. 模型 C 的病与工具无关:它"探索几十轮最后不交卷",是格式遵从性缺陷,换工具救不了。

八、这套系统"可信"靠的是方法论,不是运气

把前面 14 篇的方法论,落到这套系统的每一处:

方法论(前文)在系统里的落地
考卷要可信git bundle + sha256 + 断网可跑
尺子要稳det-match 纯函数、阈值 0.5 先注册
防线要验证篡改考卷/改坏账目/坏答案——全部必须被抓住
对照要公平V1 提示词由 V0 代码生成,版本号入 config_hash
数据要可信trace 五铁律对账,断点续跑
结论要诚实公布 recall 下界、样本有限、误报问题

这就是把"方法论"变成"结论可信"的全过程。


九、诚实的技术边界(不藏着)

这篇讲了我们做的东西,也要讲我们没解决的:

  • 样本仍有限:几十道题,相对真实世界是极小样本;结论只能说"在验证过的场景上如此表现";
  • det-match 是下界:真实能力可能更高,但我们选择了"可复现"优先,牺牲了"绝对精确";
  • 严重度有主观性:同一个问题两位专家可能判不同级,这块难以客观度量;
  • 模型 C 表现得差,但那是格式遵从问题,不代表它没有审查能力——这是两类不同的问题。

一句话收束:这套系统证明的不是"我们的 AI 审查很强",而是**“我们能用一整套确定性、可复现、可被检验的工程手段,把『AI 审查到底行不行』这个原本含糊的问题,变成一个能给出可信答案的问题。” 工具会过时、模型会换代,但"怎么诚实地证明它行"这套工程方法论**,是能一直带走的资产。