用 LLM 做数据提取踩了 6 个坑,我加了 6 层防御——15000 张发票的实战总结

背景

我给一家公司做财务发票对账系统,核心任务是批量提取非结构化票据字段(金额、税号、开票日期等),然后做跨单据的语义比对。

系统上线后累计处理了 15000 张发票,消耗约 1 亿 token。

过程中踩了 6 个坑,每踩一个加一层防御。回头看,架构就是这样长出来的。

这篇文章是写给同样想用 LLM 做数据提取的人的------不是教你怎么设计,是告诉你会在哪里摔跤。


核心前提:把 LLM 当作不可信组件

LLM 本质上是一个根据上下文猜下一个 token 的机器,它不保证输出稳定。

这句话说起来简单,但真正把它内化为工程决策,需要亲自踩坑。

我踩完之后的结论是:用 LLM 做提取任务,你的代码要为它的每一种失败模式准备出路。

以下是 6 层防御,按我踩坑的顺序来的。


第 1 层:输出内容不可信 → 让代码算,别让 LLM 算

坑:我让 LLM 直接输出含税金额、不含税金额、税额,然后发现三个数字加起来经常对不上。

原因:LLM 在 JSON 里的几个 token 里做算术,精度和稳定性都不可靠。

防御:只让 LLM 提取原始字段(字符串形式),数值计算由我的代码完成。降低 LLM 的任务负担,把"计算"这件事还给确定性代码。

python 复制代码
# ❌ 让 LLM 直接算
{"含税金额": 113.0, "不含税金额": 100.0, "税额": 13.0}

# ✅ 只让 LLM 提取原始值,代码来算
{"含税金额_str": "113.00", "税率_str": "13%"}
tax_amount = calculate_tax(含税金额_str, 税率_str)  # 代码算

第 2 层:输出结构不可信 → 用 Pydantic 锁死 Schema

坑 :LLM 返回的 JSON 字段名会漂移------有时是 invoice_date,有时是 date,有时干脆没有这个字段。我写了一堆 if key in response 的校验代码,冗余且脆弱。

防御:全面换用 Pydantic。Schema 即契约,结构在定义时就锁死,不符合的直接报错,不给它漂移的空间。

python 复制代码
from pydantic import BaseModel
from datetime import date

class InvoiceExtraction(BaseModel):
    invoice_code: str
    invoice_date: date
    amount_str: str
    tax_rate_str: str

比手写 JSON 校验少写 80% 的代码,还更可靠。


第 3 层:输出值域不可信 → 在 Validator 里写断言

坑 :结构对了,但值不对。税率出现过 "130%",金额出现过负数,日期出现过 "2099-01-01"。

防御 :在 Pydantic 的 @validator 里直接写业务断言。通不过测试,实例就不生成。

python 复制代码
from pydantic import validator

class InvoiceExtraction(BaseModel):
    tax_rate_str: str
    amount_str: str

    @validator("tax_rate_str")
    def validate_tax_rate(cls, v):
        rate = float(v.strip("%")) / 100
        assert 0 < rate <= 0.3, f"税率异常: {v}"
        return v

    @validator("amount_str")
    def validate_amount(cls, v):
        amount = float(v.replace(",", ""))
        assert amount > 0, f"金额不能为负: {v}"
        return v

Validator 本质上是把业务规则写进类型系统,比写在外面的 if 判断更不容易被绕过。


第 4 层:输出错了 → 把错误喂回去,让它重试

坑:上面的 Validator 报错了,然后呢?直接丢掉这张发票损失太大。

防御:把报错信息提取出来,塞回 Prompt,让 LLM 再来一次。

python 复制代码
def extract_with_retry(llm_client, invoice_text: str, max_retries=3):
    error_context = ""
    for attempt in range(max_retries):
        prompt = build_prompt(invoice_text, error_context)
        raw = llm_client.extract(prompt)
        try:
            return InvoiceExtraction.parse_raw(raw)
        except Exception as e:
            error_context = f"上次输出有误,错误信息:{e},请修正后重新输出。"
    return None  # 进入第 5 层

实测大部分结构错误在第 2 次重试时能自我修正,成本增加有限,准确率提升明显。


第 5 层:实在不行 → 不静默失败,打标记交人工

坑:有些发票版面复杂,重试 3 次还是过不了 Validator。如果静默丢弃,下游对账就会有漏洞,而且你不知道漏在哪。

防御 :最后一次失败,把 LLM 原始输出保留下来,打上 需人工复核 标记,进入人工队列。

css 复制代码
if result is None:
    fallback = {
        "status": "需人工复核",
        "raw_llm_output": last_raw_output,
        "invoice_id": invoice_id,
    }
    human_review_queue.append(fallback)

不输出比输出错更危险。 让系统知道自己不知道,比系统假装知道要安全得多。


第 6 层:不要设计太复杂的架构 → 架构是跑出来的

这是最后一条,也是我觉得最反直觉的。

我的性格是"想不清楚不动手"。但这个系统我是直接跑起来的:

复制代码
发现数字不对   →  加第 1 层(代码算)
发现字段漂移   →  加第 2 层(Pydantic)
发现值域异常   →  加第 3 层(Validator)
发现结构报错   →  加第 4 层(错误重试)
发现静默丢失   →  加第 5 层(打标记)

回头看,这 5 层防御覆盖了 LLM 输出失败的所有主要模式。但我在开始之前完全没有预见到这个架构------它是被坑"逼"出来的。

对于 LLM 工程,"先跑起来发现坑,再加防御"比"先设计再执行"更有效。 因为 LLM 的失败模式在真实数据上才会暴露,沙盘推演推不出来。


总结

层 防御目标 工具
1 内容不可信 代码算,LLM 只提取
2 结构不可信 Pydantic Schema
3 值域不可信 Pydantic Validator
4 偶发出错 错误回注重试
5 兜底失败 Fail-loud + 人工标记
6 架构过设计 跑起来再加层

如果你也在用 LLM 做数据提取,这套框架可以直接拿去用。有问题欢迎评论区讨论。


目前在做 AI Agent 工程方向,专注 LLM 落地与 Harness 设计,欢迎同方向的朋友交流。

相关推荐
云和数据.ChenGuang5 小时前
Python 重写(Override)深度解析:从 MRO 到 super() 的完整机制
java·开发语言·人工智能·python·langchain·1024程序员节
西西木科技丨Shopify开发机构6 小时前
CalDigit TS4案例:AI产品图的Shopify整站接入与验收
人工智能·shopify·独立站
2601_965448876 小时前
AI视频制作:做系列节气民俗视频音色不统一,AI 音色克隆省事的方法
人工智能·机器学习·音视频
美狐美颜SDK开放平台6 小时前
视频美颜SDK试用DEMO阶段应该测试什么?直播APP接入实操指南
开发语言·人工智能·深度学习·音视频·美颜sdk·直播美颜sdk·第三方美颜sdk
HRaitest6 小时前
秋招技术岗招聘提效:三类 AI 招聘方案的技术底层与选型逻辑
大数据·人工智能·求职招聘·ai-native
天远数科7 小时前
零信任架构实战:基于天远企业股权变更构建自动化商业银行对公授信存续期股权异动与资产抽逃预警网关
运维·人工智能·架构·自动化
车软学堂7 小时前
从测试转开发,到底要补什么
网络·人工智能·python·功能测试·单片机·车载系统·自动化
海盗12347 小时前
AI 新闻日报 10-28:Atria Dawn 完整版开源、Cursor Agent SDK 转正、Genie 4 实时世界模型
人工智能·机器人·开源·人工智能aigc
志尊宝8 小时前
AI原生前端开发(035):AI时代的工程化最佳实践总结
人工智能·ai-native
冬奇Lab8 小时前
一天一个开源项目(第251篇):Claude SEO —— 装进 Claude Code 的 SEO 审计团队,26 个子技能 + 19 个专家 Agent 并行体检网站
人工智能·开源