RAG 答错数字,先查数据语义契约
文章用虚构的保险数据迁移案例说明:字段类型不变但业务含义改变,也可能让检索助手输出错误金额。排查应先核对检索记录和字段语义,不能仅靠更换提示词或模型。
AI 深度解读
RAG 助手报出一个离谱的数字,先别急着换模型、改提示词。作者提醒:模型可能只是忠实复述了检索结果,真正出错的是上游字段含义,而类型检查和数据任务仍然全部通过。
这篇短文讨论的是如何用数据契约拦住这类错误。它没有提供实测效果,但给出了一套可执行的排查顺序:先查检索记录,再查数据链路,最后决定该修哪里。
数字没有编造,答案为什么还是错的
作者用了一个虚构的保险理赔案例。
为了支持新的移动应用,一次数据库迁移把 max_coverage_limit_usd 改名为 per_incident_deductible_usd。前者是最高保额,后者是每次事故的免赔额。两者都用美元计价,但业务含义完全不同。
迁移后,单元测试通过,ETL 任务正常退出,也没有告警。因为字段仍然是数字,类型检查识别不出含义已经变了。
几天后,理赔员询问保单的赔付上限。检索片段里没有保额字段,只有标着免赔额的金额,助手最终回答了 $12,500,而真实保额是 $1,250,000。
类型正确,不代表业务含义正确。
作者认为,传统 BI 中的异常记录还有机会通过图表被人发现;RAG 则会直接把管道产物交给助手。一条状态正常、却把字段含义写错的数据任务,可能把问题一路送到用户面前。不过,BI 能否及时发现异常,原文并没有给出对照数据。
数据契约应该拦在哪几层
数据契约可以理解为生产方与消费方共同遵守的约定:字段叫什么、表达什么、必须满足哪些条件,以及变更时如何兼容。
作者借用了 Kleppmann 在 DDIA 中的视角:模式演进需要同时考虑新代码读取旧数据,以及旧代码读取新数据。
字段改名能否安全完成,还取决于格式。Avro 按字段名匹配,安全改名需要保留旧名称作为 alias;Protobuf 按 tag number 匹配,名称可以变化,tag 不能随意改变。直接导出原始 JSON,则不会自动替你检查这些兼容性。
作者建议在数据源与模型之间加三道约束:
- 在 CI 中检查兼容性,登记 AI 系统消费的每个 schema。文中指出 Confluent 默认模式为
BACKWARD,建议为 AI 消费方显式设置FULL或FULL_TRANSITIVE;被消费字段的改名和删除需要经过弃用窗口。 - 在生成 embedding 前校验业务条件。例如用 Pydantic 的
mode="after"验证器检查「保额 >= 免赔额」。校验失败要用raise ValueError,不要依赖assert,因为-O会移除断言。被拒绝的记录进入死信队列,并明确负责人。 - 为每个检索片段保留来源信息,包括 schema 版本、数据源和截至时间,方便过滤过期内容,并把错误追溯到具体记录。
这里要区分两种检查:兼容性检查关心新旧数据能否继续被读取,业务校验关心读出来的值是否符合规则。只做前一种,仍然可能放过「数字能读,含义错了」的问题。
我的判断
我赞同作者提出的修复原则:错误如果来自源数据,修复就应该进入契约或管道。只在 system prompt 里补一句「请检查保额」,无法补回检索结果里已经缺失的字段。
但契约也有边界。
「保额 >= 免赔额」能拦住部分异常,却不能证明某个数字确实是保额。如果契约一开始就把「赔付上限」映射到了错误字段,整条链路仍然可以校验通过,并留下完整的错误记录。
FULL 或 FULL_TRANSITIVE 也不是设置完就万事大吉。作者没有展开具体 schema、变更历史和消费关系,不能据此认定所有团队都应该直接采用同一种模式。
这是一套治理建议,原文没有提供降低错误率的实测数据。
它的代价也很具体:后端团队需要配合变更流程,隔离记录需要有人处理,老系统接入契约需要投入。作者建议优先覆盖答错会造成经济损失或合规风险的场景,这个取舍比一开始全量铺开更合理。
能拿来做什么
- 排查 RAG 数字错误时,先保存问题、实际检索片段和对应源记录,确认数字在哪一步出现偏差,再决定修改模型调用还是数据管道。
- 如果用 Node 搭建入库任务,可以把业务条件校验放在生成 embedding 之前,并为失败记录指定处理人,避免错误数据进入索引后继续参与检索。
- 为检索片段保存 schema 版本、来源和截至时间,让错误回答能够追溯到具体记录,也让过期数据有依据被过滤。
- 把 AI 应用读取的字段纳入后端变更流程,尤其关注金额、额度等字段的改名和删除,并安排明确的弃用窗口。