2026 年 9 月 9 日,我在用 AI 助手做电力市场业务分析。它汇报了一份”交易通知”的内容——时间、要求、口径,一应俱全。我问来源,它给了链接,还”印证”了一番。

问题是:这份通知不存在,那个链接也不存在。

这不是一眼假的胡说八道。恰恰相反,它格式规范、细节自洽、语气权威,还自带”佐证”。要不是按惯例把链接打开看了一眼,一份虚构的市场规则就可能进入真实业务流程——报价、签约、结算,每一步都是真金白银。

复盘:为什么提示词不管用

事故后我的第一反应是加提示词:”禁止编造,必须给来源。”没用。原因后来想明白了:

  • 幻觉不是”态度问题”,是模型的统计本性。你要求它”不编”,它编的时候并不觉得自己在编。
  • 同一个上下文里,模型无法自查自证——生成幻觉的机制和检查幻觉的机制是同一个。
  • 更麻烦的是”带来源的幻觉”:它连印证来源都能一起编。人工核对如果只看”有没有链接”,照样被骗。

结论:防杜撰不能靠模型自觉,要靠流程——而且关键环节必须是机械校验,根本不经过任何模型。

体系:规则层、工具层、强制层

我最终在自己日常使用的 AI 编码助手(ZCode)里落地了三层,所有项目通用:

规则层写在 AGENTS.md(助手每次会话自动加载的指令文件),六条硬规则:原文优先、观察与推断分离、禁止单点自证、电脑操作闭环(声称”已打开”必须先验证落点)、决策级数字双源、交付物出场检查。

工具层是一个两百行以内的 Python 脚本 verify_claims.py,做”三环核对”。

强制层是一个 Stop 钩子:我想结束回合时,它检查核对有没有跑,没跑就打回来。

三环核对:两道机械闸门,夹一道模型判定

交付任何含具体数字、日期、文件内容的产物之前,我先写一份 claims.json,把关键断言列出来,每条附上”原文摘录”和来源 URL(或证据快照)。然后跑脚本,三环依次过:

环 1:来源真实性(机械)。 脚本复抓每条 URL,把 HTML 剥成纯文本,检查我提供的”原文摘录”是否真实出现在页面上。匹配前做规范化(去空白、全角标点转半角、剥 markdown 装饰符),但本质是字符串包含——页面里没有这段字,就是不通过。需要登录的平台复抓不了,就改附快照文件路径,脚本比对快照内容。

环 2:断言支持(模型判定)。单条断言 + 原文摘录发给一个全新上下文的判定模型(temperature=0,要求 JSON 输出),提示词全文就这几行:

1
2
3
4
5
6
7
8
你是事实核对员。只依据【原文】判断【断言】,禁止使用你自己的知识。
规则:
1. verdict 只能是:支持 / 不支持 / 原文未提及
2. 判定为"支持"时,必须从【原文】中逐字复制一句支撑该断言的话填入 quote
3. 其他判定时 quote 填空字符串
4. 原文与断言语言可能不同(如中文断言、英文原文),按语义对照判断;
日期、数字、实体名称必须精确对应,不要求字面相同
输出 JSON:{"verdict": "...", "quote": "..."}

环 3:引文闭环(机械)。 模型判”支持”时交出的引文,脚本拿回原文做字符串回验。引文对不上原文,改判”存疑”。

设计逻辑:

  • 环 1 机械复抓,正是那次事故的形态杀手——真链接上编造原文、死链、空 source,全被拦;
  • 环 2 用 fresh context、只传单条断言,避免长对话里”顺着上文圆谎”;
  • 环 3 用机械手段约束模型判定——你说”支持”可以,但必须交出能在原文里逐字找到的证据,找不到就不算数。

一个意外的实测插曲

写这篇文章时,我做了一组现场测试:在真实可达的页面上编一段”原文”,看环 1 拦不拦。

第一条,编造的原文——拦下,符合预期。

第二条是阳性对照。我挑了全互联网最稳定的页面 example.com,凭记忆写下它那段著名的说明文字——也被拦了。愣了一下打开页面一看:人家真的换文案了,我记忆里的”经典原文”是旧版。

那一刻我反而踏实了:连”本人确信无疑的记忆”都过不了环 1。这套闸门拦的从来不只是模型的幻觉,还有人脑的过期缓存。fail-closed 的意义就在这里——宁可误拦,不可放过。

Stop 钩子:不跑核对,别想收工

最后一环是强制执行。指望模型”记得”规则是靠不住的,所以出场核对做成了硬闸门:

1
2
3
4
if [ -f claims.json ] && { [ ! -f verify_report.json ] || [ claims.json -nt verify_report.json ]; }; then
echo "claims.json 已更新但未跑出场核对" >&2
exit 2 # Stop 事件退出码 2 = 要求 agent 继续处理(最多 3 次)
fi

claims.json(要交付的断言清单)比 verify_report.json(核对报告)新,说明改了内容却没重新核对——回合结束被打回,最多续三次。核对报告里存在 REVIEW 项时脚本退出码为 1,必须修正断言、或显式标注〔存疑〕才能交付。

成本与权衡

判定模型走过一段折腾:最初用 DeepSeek,图的是”跨家族验证”——主模型是 GLM,核验换一家,独立性更强。但费用扛不住,切到智谱的 GLM flash 档;glm-5.3-flash 的按量付费额度跑批耗尽后,一度落到免费的 glm-4.5-flash;最后发现智谱有 Anthropic 协议兼容端点,消耗 GLM Coding Plan 套餐额度——就是我日常订阅用的那个套餐——默认模型这才定回 glm-5.3,判定调用不再另花钱。想换模型,一个环境变量的事。

拿这篇文章自己开刀:第一天修出来的稳健

文章交付前,我把全文 20 条关键断言喂进了这套核对体系——用刚切好的 GLM flash 当判定模型。第一天就炸出一串问题,每一个都变成了脚本的一处修复:

  • 空响应之谜。 判定模型间歇性返回空正文,时好时坏。起初以为是思考型模型偶尔把答案全留在思考里,加了重试兜底;后来抓到真凶——思考型模型的 max_tokens 连思考 token 一起计,2048 的上限被思考吃光,正文自然为空。上限调到 8192,问题绝迹。
  • 429 罗生门。 跑批到一半全线 429。起初一律当限流处理,读了响应体才发现两种病共用一个症状:code 1302 是账户级限流,等几分钟就自愈;code 1113 是余额不足——glm-5.3-flash 的额度已经被这几轮跑批耗光。错误体从此读进报错信息,一眼分诊;默认模型一度落到免费档 glm-4.5-flash。
  • 引文回验误伤。 判定模型交出的引文偶尔带 markdown 装饰符(星号、反引号、直角引号),机械回验认死理、一字不差才算数,于是误判”存疑”。解法不是放水,而是规范化时把装饰符从引文和原文两边一起剥掉——比的还是字,不是符号。引文仍未命中时先重判一次,再判存疑。
  • 缓存的设计缺陷。 为了省 token,给判定结果按”模型+断言+原文”加了缓存。第一版把”不支持”也缓存了——一条 flaky 的误判进了缓存就永远翻不了案,这恰恰违背 fail-closed 的本意:拦错的必须留复核的路。第二版只缓存”通过”项——通过的重跑零成本,没过的每次重判。
  • 终局:套餐端点。 免费档也逃不掉账户级限流,而且 4.5-flash 这类小模型判代码类断言偶尔接不住语义。最后的解法是换端点而不是换模型:智谱提供 Anthropic 协议兼容端点,消耗 GLM Coding Plan 套餐额度——判定调用直接吃订阅额度,按量付费的余额与限流从此与它无关,默认模型定回 glm-5.3。两个协议细节:该端点走 Anthropic Messages 协议,没有 response_format,JSON 输出靠提示词约束,解析容错用 raw_decode 取首个完整 JSON——大模型偶尔会在 JSON 后面再补一个对象,把”截取首尾花括号”的老容错直接干穿;响应是 content 块数组,正文只取 text 块,思考内容待在独立的 thinking 块里。(这个端点官方定位是给 Claude Code 这类指定工具用的,裸脚本调用属于条款灰度,自己掂量。)
  • 行数追着自己跑。 文章里有一句”脚本共 197 行”,但每修一个问题行数就变,这条断言前后追着刷新了九版,从 143 一路改到 197。这大概是 fail-closed 最幽默的自噬:闸门连作者自己都不放过。

回头看,这些坑没有一个出在”三环”的设计上,全出在工程接缝处——而每一个,都是机械校验环节自己暴露出来的。

代价是核验变回”同家族”:GLM 写的断言由 GLM 判定。我认为这个取舍可以接受,因为三环里真正硬的是环 1 和环 3——两道纯机械的字符串校验,跟用什么模型毫无关系。模型判定(环 2)反而是三环里最”软”的一环。

已知局限

  • 跨语言断言(中文断言 + 英文原文)偶尔被判”原文未提及”——误拦方向是安全的(fail-closed 逼人工复核),不会放行虚构;
  • 截图证据机器无法比对文字,规则要求交付时向用户出示原图;
  • 页面改版会导致旧摘录被拦——这是特性,不是 bug(见 example.com 插曲);
  • temperature=0 下判定仍有小波动(”不支持” vs “原文未提及”),方向都是收紧。

从”出场核对”到”源头治理”

出场核对是兜底,更进一步是让常用事实只核一次:一个事实库脚本,入库时强制复抓来源、把条文和页面快照一起存下,复抓找不到条文就拒绝入库;文档写作时用 {{事实ID.条文键}} 占位符引用,渲染时由代码从库里替换并自动附引注——库里没有这个 ID,硬失败,绝不静默跳过。数字从源头锁死,出场核对只兜新增断言。

结语

这套东西的核心思想就一句话:不要信任模型,要信任流程;而流程里最可信的环节,是根本不经过模型的那几步。

我作为人类,保留最后一个检查点——它也是整个体系的原型:

“把链接或截图给我。”