LLM 结构化抽取:让输出可校验,而不是让模型更聪明
四道关卡拦下 12 次调用里的 9 次;其中「幻觉」这一类,靠一次字符串包含检查就全挡住了
这一节的对象不是模型,是管线。
理由很直接:模型半年一换,而校验管线不用重写。
四道关卡跑一遍,12 次调用拦下 9 次:
▸ 合格输出 3 篇 ✅ 全部通过
▸ 少了 evidence 字段 3 篇 ❌ 关卡一拦下
▸ 幻觉:引用了原文没有的句子 3 篇 ❌ 关卡二拦下
▸ 数值与引用对不上 3 篇 ❌ 关卡三拦下
其中最危险的那一类——模型编造了一个原文里根本没有的数字——靠的只是一次字符串包含检查。
把"编造数字"变成可自动拦截的错误,不需要更聪明的模型,只需要要求它附上原文依据。
报销时要求"附发票原件"。
不是因为财务比你更懂这笔钱花在哪,而是因为"有没有这张发票"是一个机械可查的事实。
evidence 字段就是那张发票。
一、四道关卡
| 关卡 | 检查什么 | 拦住什么 | 说明 |
|---|---|---|---|
| 零 | 是不是合法 JSON | 格式崩坏 | 结构化输出模式能基本消除这一关 |
| 一 | JSON Schema:字段名/类型/必填/枚举 | 字段缺失、类型错、单位越界 | 能挡结构问题,挡不住内容编造 |
| 二 | evidence 必须逐字出现在原文里 | 幻觉 | 挡幻觉最有效的一关,且完全机械 |
| 三 | value 能否在自己的 evidence 里找到 | 单位错、抄错位 | 数值一致性 |
| 四 | 字段间的逻辑关系 | 净利润 > 营业收入这类矛盾 | 挡"看起来都对但合不拢" |
关卡二的实现只有三行:
for f in obj.get("fields", []):
if f.get("evidence") and f["evidence"] not in text:
issues.append(f"「{f['name']}」的 evidence 不在原文中")
关卡二是这套管线的核心,而它常常被跳过。
原因是它"看起来太笨":只做字符串包含检查,不理解语义。
但正因为笨,它才可靠:
- 它不依赖模型的能力——再强的模型也躲不过"这句话原文里有没有"
- 它不会随模型升级而失效
- 它零成本——不需要第二次调用去"检查第一次"
用 LLM 检查 LLM,是一个很自然但很贵的想法:成本翻倍,而且第二个模型也会幻觉。
能用字符串比较解决的,不要用模型解决。
二、每道关卡各自拦住了什么
实测(每类故障 3 篇文档):
| 故障类型 | 被哪一关拦下 | 拦截率 |
|---|---|---|
| 少了 evidence 字段 | 关卡一 Schema | 3/3 |
| 引用了原文没有的句子 | 关卡二 原文引用 | 3/3 |
| 数值与引用对不上 | 关卡三 数值一致 | 3/3 |
| 合格输出 | — | 0/3(全部通过) |
这张表的用处不是"拦截率 100%",而是告诉你哪一关不能省。
去掉关卡一,少字段的会漏过去;去掉关卡二,幻觉会直接进入数据库。
🔧 动手做:让 LLM 的输出必须先过关,才算数(5 分钟)
跑 python extract_pipeline.py。它对 LLM 抽取的结果设了四道关卡(必填字段、引用必须能在原文找到、类型校验、范围校验),故意喂进 12 次问题输出。
你会看到(实测):四道关卡共拦下 12 次里的 9 次。其中最要命的“幻觉引用”(模型编了一段原文里没有的话),被关卡二一次字符串包含检查全挡住了(3/3)。
想明白:对付 LLM 出错,靠的不是"换个更聪明的模型",是让它的输出可被机械校验——每条结论必须带上能在原文里定位的证据,不能定位就打回。校验管线不用随模型升级重写,这才是能沉淀下来的资产。别让 AI 的产出直接进你的系统,先让它过关。
三、成本控制:拦截 ≠ 白跑
被拦截的输出要重试,而重试是要花钱的——这就是 p7.1 计算器里那 10%。
三条能显著降本的做法:
- 把 Schema 写进请求(结构化输出模式),而不是靠提示词描述 → 关卡一失败率大降
- 只重试失败的字段,而不是整篇重来 → 重试成本降一个量级
- 失败样本存档:同一类失败反复出现,说明是提示词问题,不是模型问题
没有存档,你会一直在为同一个提示词缺陷付重试的钱。
四、管线拦不住的那部分
四道关卡全过,仍然可能是错的:
- evidence 是原文的句子,但抽错了字段(把去年的数当成今年的)
- 数值对得上,但漏抽了同样重要的另一条
- 文档类型判错,导致下游用错口径
所以管线之外必须有固定比例的人工抽检,并且:
- 抽检结果要记录,用来估计真实错误率(而不是凭感觉)
- 抽到的错误要回灌成新的校验规则或测试用例——这就是 p4.10 的回归测试思路
能自动化的部分自动化,不能自动化的部分抽样——而不是假装它不存在。
五、换成真实 API 时要改什么
只改一个函数。
def fake_llm(doc_id, mode): # ← 换成真实调用
...
# 下面的四道关卡,一行都不用改
真实调用版本还要补三件事(都与模型无关):
| 做什么 | 为什么 | |
|---|---|---|
| 落盘存证 | 记录模型名、版本、温度、提示词哈希、原始返回 | p7.1 的"确定性"维度;p1.5 的血缘纪律 |
| 限频退避 | 429 时指数退避,而不是死循环重试 | p7.1 的"速率与并发"维度 |
| 提示词版本化 | 提示词进 git,改了就升版本 | 否则无法解释"为什么上个月的结果不一样" |
📌 免责:本课为技术教学,样本公告为虚构主体(甲/乙公司),不涉及任何真实上市公司;不构成投资建议。
四件事:
- 这一节的对象是管线,不是模型——模型半年一换,校验管线不用重写。换真实 API 时只改一个函数,四道关卡一行不动
- 四道关卡实测拦下 12 次调用里的 9 次:关卡一挡字段缺失、关卡二挡幻觉(3/3)、关卡三挡数值对不上;合格输出 3/3 全部通过
- 关卡二是核心,而它常被跳过——因为它"看起来太笨"。但正因为笨才可靠:不依赖模型能力、不随模型升级失效、零成本。能用字符串比较解决的,不要用模型解决
- 管线拦不住的部分要靠抽检:抽错字段、漏抽、类型判错都能过四关。抽检结果要记录用于估计真实错误率,抽到的错误要回灌成新规则或测试用例(p4.10 的回归测试思路)
成本上三条降本做法:Schema 写进请求 · 只重试失败字段 · 失败样本存档。没有存档,你会一直为同一个提示词缺陷付重试的钱。
下一节换一个方向:不用 LLM,用经典机器学习直接预测涨跌——并且诚实呈现结果。
🔎 来源与核验· 1 条,点开核对
