复核代理未执行,流水线却报审核通过
一个双阶段 AI 审核流水线将四篇草稿标记为通过,但第二阶段代理实际从未检查,草稿仍含无来源数字和错误引用。作者复盘审核失效经过,并介绍新增的两项检查。
AI 深度解读
4 篇 AI 草稿被日报助手宣布「审核通过」,负责第二阶段裁决的模型却一篇都没看过。作者在发布前拦下了问题:无来源数字、效果宣称,以及从蚊子章节搬到蟑螂身上的事实,全都藏在 compliance: passed 后面。
这次故障值得程序员看几分钟,因为它暴露了 AI 流水线里的两个不同问题:状态没有执行证据,事实有出处却归错了对象。作者用两个小检查补洞,也明确交代了它们还管不到哪里。
为什么第二阶段没运行,草稿也能通过
作者维护一个副项目,用 AI agents 生成日文家庭防虫短帖。草稿进入 outbox/,由作者阅读后手动发布,没有自动发帖。
审核分两级:便宜的小模型 scan 检查禁用和边界表达,包括保证性措辞、100%、没有来源的效果数字等,只报告、不修改。明确违规的内容退回写作端;边界项交给更大的模型 judge,决定放行或提出修改要求。没有边界项时,可以跳过 judge。
因此,问题并不是每篇都必须调用大模型,而是必须区分「无需调用」和「根本没调用」。
出事的 4 篇草稿来自首次搭建流水线的试运行。scan 执行了,judge 没执行,文件却写上了 compliance: passed。这些创建于 2026-07-10 的草稿,在正式待发目录里留了 7 天。
另一份流水线日志明明写着试运行没有完成审核,但日报助手只读取草稿里的状态。它照字段报告,没有机制要求它核对另一份日志。
状态只能表达结论,不能证明产生结论的步骤确实执行过。
作者随后要求草稿携带 compliance_log:有边界项时,记录 scan 2026-07-17 和 judge 2026-07-17;没有边界项时,明确记录 judge不要,即日文的「不需要 judge」。
下面是发布前门禁的核心函数,原样保留。外围代码用正则提取日期:SCAN 匹配带日期的 scan,JUDGE 匹配带日期的 judge 或精确标记 judge不要。
export function checkDraft(fm) {
if (fm.compliance !== "passed") return { ok: false, why: "not marked passed" };
const log = fm.compliance_log;
if (!log) return { ok: false, why: "passed, but no compliance_log" };
const scan = log.match(SCAN);
if (!scan) return { ok: false, why: "no dated scan record" };
const judge = log.match(JUDGE);
if (!judge) return { ok: false, why: "scan recorded, judge stage missing" };
if (judge[1] && judge[1] < scan[1]) return { ok: false, why: "judge dated before scan" };
return { ok: true, why: `scan ${scan[1]} / ${judge[1] ? "judge " + judge[1] : "judge not needed"}` };
}
要看的是拒绝缺失 JUDGE 记录的那一行:只有第一阶段记录,不能算完整通过。日期采用 YYYY-MM-DD 格式,因此可以按字符串比较先后。
实际运行时,当周重新审核的 6 篇通过,下一周的 3 篇被拦下,文件名分别是 2026-07-24-post-1.md、2026-07-24-post-2.md、2026-07-24-post-3.md。
后面这 3 篇其实审核过:scan 找到 5 个边界项,judge 放行 2 个、要求修改 3 个;写作 agent 拒绝了其中 2 条修改意见并记录理由,修改后再次扫描干净。问题是证据只在日报日志里,没有写回草稿。
作者仍然保留 HOLD。对这个小项目,要求状态和证据放在同一个文件里,可以让门禁只读一处,避免再次面对互相矛盾的记录。
事实能搜到,为什么仍然是错的
作者手动重审 4 篇,3 篇没有问题,第 3 篇集中暴露了事实审核的漏洞。
其中一个数字声称虫害投诉会在某个月份走高,但参考文档根本没有季节数据。另一个温度数字则确实存在,只是出现在蚊子章节,描述蚊子的繁殖条件,被模型挪到了蟑螂活动上。
全文搜索会找到这个数字,句子读起来也合理。它还套了「据说」一类措辞,绕过了只针对确定性陈述的无来源数字规则。
错误发生在事实与对象的连接处。
judge 的修复又犯了一次类似错误。它找到蟑螂产卵季节的信息,却附上「根据厂商数据」。原文确实引用过厂商,但支持的是上方另一条「每个卵鞘有多少卵」的事实,并不支持产卵季节。
作者增加的第二个检查,便是返回搜索命中所在的 Markdown 标题路径。辅助函数 sections(markdown) 解析 #{1,6} 标题层级,再由下面的函数定位命中位置:
export function locate(markdown, needle) {
const { lines, list } = sections(markdown);
const hits = [];
lines.forEach((line, i) => {
if (!line.includes(needle)) return;
const owners = list
.filter((s) => s.start <= i + 1 && i + 1 <= s.end)
.sort((a, b) => b.depth - a.depth);
hits.push({ line: i + 1, path: owners.reverse().map((s) => s.title).join(" > ") });
});
return hits;
}
要注意 line.includes(needle):它查找的是字面字符串,无法识别改写后的同义表达。
CLI 将标题路径与目标对象比较。以 cockroach 为对象,错误温度信息返回 MISATTRIBUTED L89,路径落在 Guide > Details > 3. Mosquitoes;重写后保留的卵鞘孵化时长返回 OK L66,位于 Guide > Details > 1. Cockroaches > Egg-laying sites。原文展示的是翻译后的标题,实际资料使用日文。
这个检查能发现跨章节挪用,抓不到同章节里的引用错配。摘要也可能重复事实,Caveats 中的命中还可能是在反驳某个说法,所以它适合提示人工查看,不能单凭标题位置判定真假。
为什么审核修辞,还会漏掉整篇立意
问题草稿的主钩子,是某种家居设施在夏季经常成为蟑螂入口。资料虽然列过这种可能性,下一条却引用专业人士观点,认为它不太可能成为入口;文末 Caveats 还把相关说法列为争议项。
scan 没发现禁词,judge 只看 scan 提交的边界项。于是,决定文章是否成立的冲突,没有进入审核视野。最后作者换了一个没有争议的事实,重写整篇。
这暴露了串联审核的盲区:后一级模型再强,也看不到前一级没有提交的问题。
作者因此补了三条规则:所有数字和事实必须给出参考文档行号或一手来源 URL,「据说」不能豁免;每次检查 Caveats,主钩子依赖争议事实就重建文章;规划日志只能提供选题线索,不能充当证据。
允许 URL 是有实际原因的:季节性内容需要当前新闻,静态资料未必收录。若只接受内部行号,这类内容会被全部拒绝。
规划日志也差点制造下一次错误。里面曾建议围绕某个月份的虫害高峰选题,但后续研究没有找到支持统计,找到的数据反而指向另一个月份。计划写得再具体,也没有自动获得事实资格。
我的判断
我赞同作者把模糊要求变成可执行门禁。缺字段、缺阶段、日期倒置,适合交给确定性代码检查,没必要让模型每次重新理解一遍规则。
但这个脚本验证的是「记录形状符合要求」。它不能独立证明审核运行过,也没有把审核结果绑定到某一版草稿。若 agent 写错日志,或者审核后正文又被修改,现有检查未必能发现。作者没有给出这些情况的验证结果。
同文件存证适合这个文件驱动的小项目。一般团队已有 CI 或工作流系统时,也可以使用关联到具体产物版本的执行记录;关键是门禁能够确定地找到对应证据。分散保存不必然失效,缺少可靠关联才会制造盲区。
章节定位也是实用的低成本补丁,但距离事实核验还很远。作者没有给出与完整语义审核的对照数据,也没有报告整体漏检率,因此只能确认它能抓住这次跨章节错误,不能替它背书为普遍有效的审核方案。
更大的模型在修复时仍然错配引用,这个案例足以提醒我们复查修改结果,却不足以推出所有大模型审核都不可靠。
能拿来做什么
- 给日常 AI 编程流程里的
reviewed、approved、done补上检查记录,并让脚本拒绝缺失证据的结果。至少能回答执行了哪个检查、何时执行,以及哪些步骤被明确跳过。 - 用 Node 处理 Markdown 知识库时,让搜索结果同时返回行号和标题路径。审核 API 文档或技术内容里的数字时,先确认它属于当前对象,再检查它支持的具体说法。
- 将事实一致性检查与禁词扫描分别设计。如果后一级只接收扫描器的命中项,就需要给主张冲突、限制条件和争议说明安排独立检查入口。
- 检查正式待处理目录中首次搭建当天留下的产物,并复查审核 agent 修改后的事实和引用。试运行输出与修复结果,都可能带着看似可信的完成状态进入下一步。