给 AI 输出加上分层事实核验
文章以邮件助手为例,结合确定性字段检查、带原文与消息 ID 的引用、来源核验及重复提取比对来发现幻觉。对高风险操作,再引入交叉验证 Agent 复核。
AI 深度解读
检索找对了邮件,AI 仍可能编出一个起飞时间。原文用虚构邮件助手 Mailmind 说明:降低幻觉不能只靠提示词,也不能让模型给自己打个高分就放行。事实要带出处,数字先交给代码核对,语义再交给模型,高风险操作追加独立复核。
这套方案值得程序员读,因为它把「别让 AI 胡说」拆成了可实现的校验流程。但它展示的是设计思路,没有提供生产实测结果,文中的成本对比和错误率例题也要分开看。
引用能查到,还不等于结论成立
Mailmind 会读取收件箱、分类邮件并起草回复。这里的幻觉,是看起来可信、却得不到用户数据支持的陈述。模型在续写可能出现的内容,并没有天然执行事实核对。
原文借用 Ji 等人 2022 年综述中的区分:内在幻觉与来源冲突,例如邮件写 $480,摘要写成 $540;外在幻觉则在来源中根本找不到依据,例如凭空出现一个退款金额。
第一道约束是让每个事实附带原文摘录和消息 ID。会议时间的结构化输出示例如下:
{"time": "3:15 PM", "quote": "Let's meet at 3:15 PM", "message_id": "msg_abc123"}
要留意 message_id:程序必须找到对应邮件,再检查 quote 是否存在。缺少引用的输出由解析器拒绝,伪造的引用则由文本校验拦截,不能只在提示词里要求「请提供依据」。
但引用匹配只证明这句话出现过。后续邮件可能已将会议从 3:15 PM 改到 4 PM;一句询问,也可能被误读成承诺。
引用存在,是事实核验的起点。
因此,语义还需要依据核验(grounding verification)。例如草稿声称供应商同意周五前退款,验证调用要对照原邮件,返回 supported、contradicted 或 not found,并摘录决定判断的句子。原文建议一次调用批量核验整封回复中的主张,减少逐条调用的开销。
能用代码核对的事实,为什么先别问模型
日期、金额、追踪编号适合先用确定性规则检查。线程里没有任何金额能对应 540,就直接拒绝 $540 的退款结论,无须再花一次模型调用。
难点是不能只搜原字符串。$540.00、USD 540 和 540 dollars 表达同一个金额;this Friday 则要结合邮件发送日期解析。先归一化,再比较,才能减少误报。
原文的 Python 草图同时演示金额提取和引用检查:
import re
from decimal import Decimal
MONEY = re.compile(r"(?:\$|USD\s?)(\d[\d,]*(?:\.\d+)?)|(\d[\d,]*(?:\.\d+)?)\s?(?:dollars|USD)", re.I)
def amounts_in(text):
"""'$540.00', 'USD 540' and '540 dollars' all become Decimal('540')."""
return {Decimal((a or b).replace(",", "")) for a, b in MONEY.findall(text)}
def verify_amount(claimed, thread_text):
return Decimal(str(claimed)) in amounts_in(thread_text)
def normalize(text):
return " ".join(text.lower().replace("’", "'").split())
def verify_citation(claim, messages):
msg = messages.get(claim["message_id"])
return msg is not None and normalize(claim["quote"]) in normalize(msg["body"])
要重点看 verify_amount 的返回行:它检查的是「这个金额是否出现过」,没有检查「这个金额属于哪笔退款」。如果线程同时讨论多个订单,金额真实存在,归属仍可能出错。verify_citation 同样只检查文本存在,不负责理解上下文。
这段代码适合作为校验入口,不能直接当成完整的业务事实验证器。日期解析也只是文中提出的要求,代码并未实现。
模型互相检查,究竟多可靠
作者安排了三种模型信号,但它们解决的问题不同。
第一种是置信度评分。模型可能给虚构的航班时间打出 0.95,所以低分可以触发复核,高分不能直接放行。原文前面说低置信度进入待审队列,后面的路由图则将其送往交叉验证;具体何时人工接手、阈值怎么定,没有说清。
第二种是一致性检查。同一线程使用略有不同的提示词提取两次退款金额:都返回 $540,认为结果稳定;一次 $540、一次 $520,则标记异常。作者将它关联到 Manakul 等人 2023 年的 SelfCheckGPT。
稳定依然可能出错。两次调用可以用相同方式误读邮件,因此一致性检查只能补充来源核验。
第三种是独立交叉验证。主 Agent 提取会议时间后,第二次调用只接收原始邮件和待确认时间,使用不同提示词,不读取主 Agent 的推理过程。两者有分歧,就阻止操作并交给用户处理。
这里的「独立」指调用和推理上下文分开,不代表使用不同模型,也不保证错误互不相关。原文还提到 Dhuliawala 等人 2023 年的 Chain-of-Verification:先规划核验问题,独立回答,再生成最终答案。它与前面的机制有关联,但不能直接视为同一套实现。
校验应该怎么分流,才能控制成本
作者没有要求每个事实依次经过所有检查。流程是先附带引用和消息 ID,再按事实类型分流:日期、金额、ID 走确定性检查,语义主张走模型依据核验;通过后,高风险操作或低置信度事实再追加交叉验证,失败则进入人工处理。
验证资源应按事实类型和出错代价分配。
原文的成本对照明确标注「示意,非实测」:模型核验按批次调用,延迟以秒计;确定性检查成本接近可忽略,延迟以毫秒计。它支持先做便宜检查的设计方向,却不是可直接套用的性能承诺。
最后的练习也不是 Mailmind 的上线报告:假设每周处理 10,000 个线程,抽查发现约 6% 至少包含一个错误事实,低置信度线程出错概率约为其他线程的三倍。
作者建议先对全部线程运行确定性检查,再用低置信度、较多 ReAct 迭代、长线程、新发件域名、多个相近数字等信号排序,把预算投入风险最高的一部分。例题只给低置信度提供了三倍风险的条件,其他信号并没有对应效果数据。
人工除了处理自动检查失败的线程,还要随机抽查已经通过的线程。否则系统只能统计自己拦住了多少,无法知道漏掉了多少。捕获的错误进入回归测试集,反复出现的模式再沉淀为规则。
我的判断
我认可这套方案把证据变成可执行约束:解析器检查结构,代码检查引用和数值,模型检查语义。相比单靠提示词要求「不要编造」,它更容易定位失败原因;相比所有事实都交给第二个模型,又能控制调用量。
但 Mailmind 是虚构案例,作者没有给出各层的检出率、误报率或组合后的剩余错误率。「多层防线能拦住多数幻觉」在这里仍是设计判断,缺少对照数据支持。
方案还依赖原始邮件可访问、消息 ID 可追踪、线程顺序可靠。它也没有完整解决事实归属和最新状态:数字出现过,不代表用于当前订单;引用真实,不代表仍然有效。没有来源时,原文的处理很明确:标记为不可验证,拒绝使用或请用户确认。
对一般团队,我会优先落实引用、业务校验和随机抽查,再根据真实错误决定是否增加双次提取。先知道自己错在哪里,才能判断额外模型调用是否划算。
能拿来做什么
- 为邮件提取或回复接口定义结构化输出,把事实值、
quote和message_id一起保存,并在 Node 服务端检查消息存在、引用匹配,失败后进入明确的待处理状态。 - 为金额和日期建立独立校验函数,同时保留原始文本与归一化结果;涉及退款或会议变更时,继续核对对应订单、上下文和最新邮件。
- 在回复审核界面展示待发送主张及其来源,优先突出被否定、找不到依据或复核有分歧的内容,让用户能据此决定是否发送。
- 对自动通过的线程保留随机人工抽查,并将确认的错误加入回归集,用漏检情况决定后续该补规则、改提示词,还是增加模型复核。