post · 2026-06-08

让 AI 代码审查工具不能再瞎引证

最近读了一个叫 Clawpatch 的开源项目源码,写一点感想。 本来没打算细看。它是个自动审代码的命令行工具——扫一遍仓库、找 bug、跑测试、顺手给个修复方案。这类工具 GitHub 上一搜几十个,听起来没什么特别。

最近读了一个叫 Clawpatch 的开源项目源码,写一点感想。

本来没打算细看。它是个自动审代码的命令行工具——扫一遍仓库、找 bug、跑测试、顺手给个修复方案。这类工具 GitHub 上一搜几十个,听起来没什么特别。

读着读着我停下来了。让我停下的不是它能做什么——做的事确实很普通——是它怎么对待模型说的话。一句话讲清楚:它默认模型会胡说,所以从一开始就没准备相信它。

这个态度反主流到什么程度?我先说一下现在大家都怎么应付"AI 输出不可信"这个问题。

主流办法是堆模型。让三个不同的模型独立看同一段代码,只采用重叠的发现。我自己在团队里也跑过这条路,确实管用。但贵——一次审查变成三次推理。而且它只能压一种风险:大家都看错。对另一种更头疼的情况——模型为了让自己看起来专业,编出根本不存在的证据——它无能为力,三个模型可能编出三种不同的假证据,重叠不上反而漏了。

Clawpatch 的思路是反过来的:不要求模型不胡说,要求工程层面把胡说拦在外面。这个区别后面会越说越具体。

先看一个让人血压升高的场景

想象你让 AI 看一个 Go 项目。它返回一条发现,说:

auth.go:120-125 存在 SQL 注入漏洞,user_id 参数未经过滤直接拼接到查询里。

你心里咯噔一下,打开文件——auth.go 总共只有 60 行。根本没有第 120 行。

这不是个例。LLM 在做代码审查时,会顺手给一个文件名加行号,目的是让 finding 看起来"有图有真相"。它知道你大概率不会一条条去查。

行业现在主流的对策是什么呢?在 prompt 里加一句话:

"Don't fabricate evidence. Only report findings with verified line numbers."

写了和没写差不多。模型该编还是编。你也没办法在它返回 finding 的那一秒证伪——除非你真的拿着文件一行行去核对。

Clawpatch 的做法是根本不给它编造的机会。下面我把这个机制说清楚。

这套机制的核心:把"看过什么"记下来,回头反查

要讲明白这一招,先讲一个准备动作。

Clawpatch 在调用模型之前,会先把准备喂给模型的内容清单化。它把这次要看哪些文件、每个文件取哪些行段、是否被截断,全部登记在一个数据结构里,叫 manifest——你可以把它理解成一份"我给模型看了什么"的台账。这份台账长这样:

files: [{
  path: "src/auth.ts",
  includedLineRanges: [{start: 1, end: 80}, {start: 200, end: 250}],
  truncated: true,
}]

意思是:我这次只让模型看了 src/auth.ts 的第 1-80 行和第 200-250 行,中间被截断了。

这份台账有什么用?等模型返回 finding 的时候用。

模型返回结果后,Clawpatch 不是简单解析 JSON 就完事——它对每条 finding 里的"证据"做四关校验,全部都拿这份台账来反查

  1. 证据引用的文件路径,必须出现在台账的文件列表里
  2. 路径不能搞鬼——比如写个 ../../../etc/passwd 这种一眼假的,直接拦
  3. 证据给出的行号范围,必须落在台账里该文件真正被喂进去的那几行里——不是文件存在就行,得是模型确实看见过那几行
  4. 证据引用的代码片段(quote),必须能在源文件对应行段里搜得到(空格多一点少一点没关系,做了压缩匹配)

四关任意一关没过,这条发现直接扔进 droppedFindings 垃圾桶,连带丢弃理由一起记进日志。同一个 feature 里其它没问题的 finding 不受影响。

回到刚才的例子。模型给一条"auth.go:120 SQL 注入"——但台账里 auth.go 只到第 60 行,第三关行号校验直接判它出界,drop。模型聪明一点,编一段 quote 想骗过去——源文件搜不到这句话,第四关 quote 校验把它截下来,drop。

这套机制干的事,用大白话总结就是:模型胡说不要紧,你说的我能反查;查不到就当你没说

这件事为什么值得专门写一篇

读到这里你可能会想:不就是加个校验吗,有什么稀奇?我想了很久,觉得它真正改变的是几个思维方式

改变一:AI 可不可信,原来一直被当成"模型能力问题"——模型不够好,所以会胡说;那就换更贵的模型、堆更多模型。Clawpatch 把它扳成了"工程能力问题"——同一个模型,配上一套反查脚手架,可信度直接跨一个档。你省了模型钱,加了点工程量,换了一个数量级的可靠性。

改变二:以前我们一直在"恳求模型诚实"。在 prompt 里写"don't lie"、"verify before output"、"think step by step"。这其实是把诚实义务摊给了模型——一个统计语言模型——本来就承担不了。Clawpatch 把这个义务挪到了协议层:模型不需要诚实,它只需要在我给过的范围内说话。超出范围说什么都会被拦截。这个区别看起来小,工程上天差地别——你不用再写道德 prompt,你写校验代码就行了。

改变三:它给出的不可信信号是机器可读的。每条被丢弃的 finding 都会带一个失败原因——path-not-includedline-out-of-rangequote-mismatch——写进日志。这意味着你可以观察:哪个模型在哪种 prompt 形态下幻觉率最高?换 prompt 模板有没有改善?这是真正的 telemetry,多模型投票方案给不了这种粒度。

当然这条路不是没代价。你必须严格控制 prompt 的装配过程——每个进入 context 的文件、每段截断后的行号区间,都要登记进台账。这意味着你的 prompt 管线得是个"白盒",不能是一行字符串拼接搞定的事;得有一份能被代码读取的清单跟着 prompt 一起出门。但话说回来,多花的工程量也就一两天的事,思维方式一旦转过来,回不去。

还有一个配套招式:把 JSON Schema 当合同交给模型

光有 evidence 校验还不够。还有个更基础的问题:模型有时候返回的根本就不是合法 JSON——要么裸字符串,要么外面包了一层 markdown 围栏 ```json,要么字段名直接拼错。

Clawpatch 怎么解决这个?它没有在 prompt 里写"return strict JSON only"——那种祈祷式的指令大家都试过,效果有限。它的做法很巧:把期望的 JSON Schema 写到一个临时文件,通过命令行参数交给底层的 Codex CLI,让 Codex 在生成阶段就按 schema 约束输出。

codex exec --output-schema schema.json -

这个 --output-schema 参数干的事,是把约束从"prompt 里的话"挪到"模型解码层的硬规则"。前者是软建议,后者是硬限制。差别非常明显——后者基本上没法返回非法 JSON。

中间还有些工程妥协值得提一句。Clawpatch 用 Zod 这个 TS 库写好类型定义,再自动转成 JSON Schema。但 OpenAI 的 strict mode 不认识 schema 标准里的几个关键字($schemaexclusiveMaximummultipleOf 这些),所以转换时要先把它们剥掉;同时还得给每个 object 强制加 additionalProperties: falserequired: 全字段,否则模型会漏字段。这些都是用工程细节换可靠性,没什么浪漫,但管用。

万一模型还是返回了非法 JSON,Clawpatch 留了三级容错网:先 JSON.parse 直接试;失败就抓 ```json ``` 围栏里的内容再试;再失败就上状态机扫第一对配平的花括号。这三层都过不去才报错——退出码 8,错误归类 malformed-output,附 200 字符的输出预览方便排查。

把这一层和上面的 evidence 校验合起来看,模型输出到最终落库要过四道关卡:CLI 层的 schema 硬约束 → 解析层的三级容错 → schema 字段验证 → evidence 反向校验。每一层都把不可信信号往外拦一截。

这套思路其实和代码审查没多大关系

我说说为什么写这篇文章。Clawpatch 这个工具本身好不好用是另一回事,真正值钱的是这条思路——只要你在做"让 AI 给出结构化结果"的产品,就能直接用。

举几个我能马上想到的场景。

做 AI 客服。模型回答完用户问题后说"根据知识库第 3 条解答"——但你怎么知道知识库第 3 条真的进了 context?办法:调用前把检索出来的知识库片段清单(带 ID)登记好,模型只能引用清单里出现过的 ID;返回时校验,引错的 ID 直接拦。

做 RAG 应用。模型动不动就说"参考文档 doc_42 第二段",doc_42 你压根没塞进 context。同样的招——给每段检索结果分配一个临时 token,模型只能用这些 token 来引用,超出范围的引用整段丢弃。

做合规 / 审计 agent。模型说"违反条款 5.3"——你得确定条款 5.3 的原文真的进了它能看见的范围。还是同一招,prompt 装配产出清单,校验反查清单。

你会发现共同的工程动作就两件:让 prompt 的装配过程产出一份可读取的清单让结果校验拿这份清单去反查。剩下的事情就交给模型——它敢瞎引,工程就把它的瞎引扔进垃圾桶。

我的核心观察:别把模型当人,把它当不可靠的 IO

最后一段是我读完源码之后冒出来的一个想法,可能比上面所有具体技术更值得记下来。

很多团队在做 AI 工程的时候,潜意识里把模型当成"会出错的实习生"——会想各种办法去说服它:写更长的 prompt、给更多示例、让它先 reasoning 再回答、让它自我检查。这条路其实越走越累,因为你在试图改造一个统计语言模型的"动机",而它根本没有动机。

Clawpatch 的设计是反过来的——它把模型当成不可靠的 IO 通道,和磁盘损坏、网络丢包同一个等级。

你不会因为硬盘可能损坏,就买三块硬盘做投票。你会做 checksum。

你不会因为网络可能丢包,就把同一个包发三遍。你会做 CRC。

那为什么对待模型输出的时候,我们突然就开始"恳求它讲道理"了呢?应该和对待磁盘、网络一样——加校验位、加白名单、加范围检查。别和模型讲道理,讲不通

我看完源码当晚就把这个思路用到了自己手头一个 agent 项目上。原来模型给文档摘要时会引用 page=237 这种根本不存在的页码——我们之前一直在调 prompt 想让它别乱写。加了反向校验之后,编造的引用直接被丢,问题彻底没了。一周后回头看,团队对这个 agent 输出的信任度肉眼可见地提了一档。

如果你也在做依赖 AI 输出的工程产品,这一招值得加进你的工具箱。Clawpatch 源码 MIT 开源,核心实现就在 src/review-validation.tssrc/prompt.ts 两个文件里,半天可以读完。