前面的文章讲"怎么想",这一篇讲"我们真的把它做出来了"——一套可运行的 AI 审查评测系统,及它背后的技术取舍。
开场:方法论的终点,是一套能跑的系统
系列前面反复讲"可信要考证据、尺子要比被测对象稳"。这些话说起来容易,但真把它变成能跑的程序、能出数字、能反复复现的一套系统,才是这套方法真正被验证的时刻。
这篇不再打比方,直接讲我们做出的一件具体东西:一个只读、可评测、可复现的 AI 代码审查评测系统,以及为了让它"可信",我们在每个环节做的技术取舍。
一、系统由五块组成
整条流水线,拆成五个模块,每个都有明确的职责和验收标准:
① 考卷(Beach Fixture) 真实 PR + 人工标准答案,指纹封存
② 考生运行器(Runner) 调 AI 审查,注入工具,收卷
③ 工具与沙箱(Tools) 只读工具集 + 路径沙箱
④ 打分器(Scorer) 确定性判分(det-match)
⑤ 记录仪(Trace) 每一步留底 + 对账铁律
一句话:①提供"考什么",②③让 AI 去"考",④判"考几分",⑤保证"分数可信"。
二、考卷:真实数据,锁死指纹,断网可跑
用什么当题目? 拿了真实开源项目的真实 PR(合并请求),配资深工程师确认过的人工评审意见当标准答案(GT)。
为什么不用合成数据? 因为我们要测的是"真实世界里 AI 审代码行不行",合成题会测出"AI 在刻意设计的题上行不行"——不真实。
怎么保证考卷不变? 这是质量测试的核心,用了三重锁:
- git bundle 封存:每道题的代码打包成 git bundle,带完整历史,
git bundle verify可校验; - sha256 指纹:每个 bundle 算一个 64 位哈希,记进 manifest。加载时重算比对,改一个字节就报警(第 7 篇的"篡改必被抓"落地);
- 断网可跑:整套测试在无网络环境下跑全绿,证明考卷 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),到底有没有让它审得更好?
要做这个实验,先按下所有变量:
- 同一个模型 vs 同一个模型;同一张考卷;同一套打分规则;
- V1 的提示词由 V0 的提示词代码生成(只替换"工具清单"那一段),从机制上保证两版除了工具描述,其余逐字相同;
- 工具集、提示词、打分规则都编版本号,写进每次运行的 config_hash,让"这个数字是哪个版本跑出来的"永远说得清。
跑批是串行的(避免并发引入限流变量)、带断点续跑(中断后跳过已完成继续)。
七、结果:数字说话,也有反直觉
3 个模型 × 2 个工具版 × 重复多次,关键结果:
| 模型 | V0 召回率 | V1 召回率 | 变化 | 备注 |
|---|---|---|---|---|
| 模型 A | 0.24 | 0.36 | +50% | 误报却从 32→48(工具放大报告倾向) |
| 模型 B | 0.30 | 0.46 | +50% | 误报反而 36→32(收敛),还最快最省 |
| 模型 C | 很低 | 很低 | 无变化 | 有 13/15 次"探索很多但不交卷" |
三个有信息量的结论:
- 两个模型召回率同时 +50%——不是单点运气,是"加结构化工具确实有帮助"的一致信号;
- 但"有用"不是绝对的:同一个工具,模型 A 的误报大增、模型 B 的误报反而下降——工具像"性格放大器",收益取决于底层模型的性格(呼应第 12 篇);
- 模型 C 的病与工具无关:它"探索几十轮最后不交卷",是格式遵从性缺陷,换工具救不了。
八、这套系统"可信"靠的是方法论,不是运气
把前面 14 篇的方法论,落到这套系统的每一处:
| 方法论(前文) | 在系统里的落地 |
|---|---|
| 考卷要可信 | git bundle + sha256 + 断网可跑 |
| 尺子要稳 | det-match 纯函数、阈值 0.5 先注册 |
| 防线要验证 | 篡改考卷/改坏账目/坏答案——全部必须被抓住 |
| 对照要公平 | V1 提示词由 V0 代码生成,版本号入 config_hash |
| 数据要可信 | trace 五铁律对账,断点续跑 |
| 结论要诚实 | 公布 recall 下界、样本有限、误报问题 |
这就是把"方法论"变成"结论可信"的全过程。
九、诚实的技术边界(不藏着)
这篇讲了我们做的东西,也要讲我们没解决的:
- 样本仍有限:几十道题,相对真实世界是极小样本;结论只能说"在验证过的场景上如此表现";
- det-match 是下界:真实能力可能更高,但我们选择了"可复现"优先,牺牲了"绝对精确";
- 严重度有主观性:同一个问题两位专家可能判不同级,这块难以客观度量;
- 模型 C 表现得差,但那是格式遵从问题,不代表它没有审查能力——这是两类不同的问题。
一句话收束:这套系统证明的不是"我们的 AI 审查很强",而是**“我们能用一整套确定性、可复现、可被检验的工程手段,把『AI 审查到底行不行』这个原本含糊的问题,变成一个能给出可信答案的问题。” 工具会过时、模型会换代,但"怎么诚实地证明它行"这套工程方法论**,是能一直带走的资产。