五年前,从发票中提取结构化数据意味着采用一个深度学习流程:OCR 阶段、在数千份人工标注文档上微调的布局感知模型(LayoutLM、Donut),以及每当供应商更改模板时都要承担的维护负担。2026 年,整个流程压缩为一次视觉 LLM API 调用:输入图像,输出经过 schema 验证的 JSON — 不需要训练数据,不需要托管模型,新布局也能零样本处理。本指南来自 Kunavo — 一个面向该模式所使用视觉模型的独立、兼容 OpenAI 的网关 — 将展示完整的工作模式、根据我们公布的费率计算的每张发票成本,以及如何将准确率保持在生产级别。
发票提取方法对比:规则、机器学习、深度学习、LLM
过去曾使用四代技术从发票中获取结构化数据。将它们并列比较很有价值,因为二十年来决定技术选择的取舍 — 通过标注数据换取准确率 — 已不再适用。
| 方法 | 工作方式 | 设置成本 | 处理新供应商布局 |
|---|---|---|---|
| 模板 / 规则(1990 年代至今) | OCR,然后为每个供应商设置坐标区域和正则表达式(“总额是 ‘TOTAL’ 右侧的数字”) | 每个供应商数小时,永久持续 | 否 — 需要新模板 |
| 经典机器学习(2010 年代) | OCR,然后将手工设计的特征(位置、字体大小、相邻词)输入 CRF 或 SVM,由其为每个 token 打标签 | 特征工程 + 数千个标注 | 部分可以 — 在未见布局上性能下降 |
| 深度学习(2020 年代至今) | 对布局感知 Transformer(LayoutLM、Donut、DocTR)进行微调,使其联合读取文本、位置和像素 | 3,000–50,000 张标注发票 + GPU 服务 | 通常不能 — 通常需要重新标注和重新训练 |
| 视觉 LLM(2024 年至今) | 一次 API 调用:发票图像加 JSON schema,结构化输出受该 schema 约束 | 无 — 一个 schema 和一个提示;每张发票 $0.00248–$0.00497 | 可以,零样本 |
前三种方法都有一个共同属性:准确率需要用标注数据购买。每个供应商模板、每个 CRF 特征、每次 LayoutLM 微调,都是一个标注项目。视觉 LLM 已在预训练期间承担了这项成本,因此新布局的边际成本为零。这就是整个转变 — 不是旧方法失效了,而是它们曾经花费预算购买的东西变得免费。
微调深度学习与视觉 LLM 的详细对比
| 微调布局模型(2021) | 视觉 LLM(2026) | |
|---|---|---|
| 训练数据 | 3,000–50,000 张标注发票 | 无(零样本)— 少量示例有助于处理边缘情况 |
| 新供应商布局 | 通常需要重新标注 + 重新训练 | 零样本处理 |
| 基础设施 | GPU 服务 + OCR 阶段 | 一次 HTTPS 调用 |
| 输出格式 | Token 标签 → 自定义解码 | 受 schema 约束的 JSON |
| 每张发票的成本 | 工程时间占主导 | $0.00248–$0.00497 |
在单一固定的高流量布局上,微调提取器仍然可能胜出;但对于现实发票的长尾场景 — 不同供应商、语言和扫描质量 — 零样本视觉 LLM 在实践中更准确,拥有成本也低得多。更多适用于此类工作负载的模式见数据提取用例。
完整模式:图像 → 经过 schema 验证的 JSON
以下所有内容都通过 Kunavo 的 OpenAI 兼容端点运行 — 将 base_url 指向 https://api.kunavo.com/v1,同一份代码只需更改一个字符串即可调用 Claude 或 GPT:
from openai import OpenAI
import base64, json
client = OpenAI(
base_url="https://api.kunavo.com/v1",
api_key="sk-kn-...",
)
SCHEMA = {
"name": "invoice",
"schema": {
"type": "object",
"properties": {
"vendor_name": {"type": "string"},
"vendor_tax_id": {"type": ["string", "null"]},
"invoice_number": {"type": "string"},
"invoice_date": {"type": "string", "description": "ISO 8601"},
"due_date": {"type": ["string", "null"]},
"currency": {"type": "string", "description": "ISO 4217"},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"unit_price": {"type": "number"},
"amount": {"type": "number"},
},
"required": ["description", "amount"],
},
},
"subtotal": {"type": ["number", "null"]},
"tax": {"type": ["number", "null"]},
"total": {"type": "number"},
},
"required": ["vendor_name", "invoice_number", "invoice_date",
"currency", "line_items", "total"],
},
}
def extract(path: str) -> dict:
image_b64 = base64.b64encode(open(path, "rb").read()).decode()
resp = client.chat.completions.create(
model="claude-haiku-4-5", # the cheapest capable model for clean invoices
response_format={"type": "json_schema", "json_schema": SCHEMA},
messages=[
{"role": "system", "content":
"Extract the invoice into the schema. Copy values exactly as "
"printed; use null when a field is absent. Never invent data."},
{"role": "user", "content": [
{"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{image_b64}"}},
]},
],
)
return json.loads(resp.choices[0].message.content)
inv = extract("invoice_0231.png")
# Cheap arithmetic guardrail: reject when the line items don't add up.
delta = abs(sum(li["amount"] for li in inv["line_items"])
+ (inv.get("tax") or 0) - inv["total"])
if delta > 0.02:
raise ValueError(f"line items disagree with total by {delta:.2f}")
print(inv["vendor_name"], inv["total"], inv["currency"])三个细节承担了大部分工作:response_format 中的 JSON schema(在 Claude 上,模型无法输出其他内容 — 参见下方链接参考文档中关于 GPT 的说明)、系统提示规则 “准确复制值;缺失时使用 null;绝不编造”(消除虚构税号),以及算术防护 — 行项目加税额必须等于总额,否则将文档发送给更强模型或人工处理。参阅聊天补全参考文档,了解 response_format 对每个模型系列的约束。
相同 schema,作为类型化的 Pydantic 模型
对于一个端点,原始 JSON schema 字典完全足够;对于十个端点,它就很痛苦。在生产环境中,大多数团队会将 schema 一次定义为 Pydantic 模型,并从中派生约束和解析后的对象,这样提取器与代码库的其他部分就永远不会对数据形状产生分歧:
from pydantic import BaseModel, Field
from typing import Literal
from openai import OpenAI
client = OpenAI(base_url="https://api.kunavo.com/v1", api_key=KEY)
class LineItem(BaseModel):
description: str
quantity: float | None = None
unit_price: float | None = None
amount: float
class Invoice(BaseModel):
vendor_name: str
vendor_tax_id: str | None = None
invoice_number: str
invoice_date: str = Field(description="ISO 8601")
currency: str = Field(description="ISO 4217")
line_items: list[LineItem]
tax: float | None = None
total: float
# A confidence field the model fills in itself is a cheap, surprisingly
# reliable router: low self-reported confidence correlates well with the
# documents a human should see.
confidence: Literal["high", "medium", "low"]
resp = client.chat.completions.create(
model="claude-haiku-4-5",
response_format={
"type": "json_schema",
"json_schema": {"name": "invoice", "schema": Invoice.model_json_schema()},
},
messages=[...],
)
invoice = Invoice.model_validate_json(resp.choices[0].message.content)这里有两点值得借鉴。将字段类型设为 float | None 而不是 float,可以让模型拥有合法方式来表达“缺失” — 没有它,缺失的税费行会强烈诱导模型编造一个零值。而自行报告的 confidence 字段只需三个输出 token,却具有出人意料的路由效果;它未经校准,但“低”是一个可靠信号,表示应该让人工查看。
两层路由:生产环境实际运行的方式
对每份文档使用同一个模型是错误的模式。清晰的打印发票 — 绝大多数发票 — 由可用的最低价视觉模型处理;困难的长尾情况(手写内容、糟糕的扫描件、异常布局)才是更强模型值得其费率的地方。应根据验证失败进行升级,而不是根据猜测:
CHEAP, STRONG = "claude-haiku-4-5", "claude-sonnet-5"
def valid(inv: dict) -> bool:
"""Arithmetic is the guardrail no confidence score replaces."""
items = sum(li["amount"] for li in inv["line_items"])
return abs(items + (inv.get("tax") or 0) - inv["total"]) <= 0.02
def extract_routed(path: str) -> tuple[dict, str]:
inv = extract(path, model=CHEAP)
if valid(inv) and inv.get("confidence") != "low":
return inv, CHEAP
# Escalate: same prompt, same schema, stronger model. Only the failures
# pay the higher rate, which is what keeps the blended cost near the
# cheap tier's.
inv = extract(path, model=STRONG)
if not valid(inv):
raise NeedsHumanReview(path, inv)
return inv, STRONG
# Blended cost at a 90/10 split, per 10,000 invoices:
# 9,000 x $0.00248 + 1,000 x $0.00497
# = $27.29算术校验才是真正发挥作用的部分:它是唯一独立于模型自我评价的信号。行项目之和等于所述总额的文档,几乎不会在重要方面出错;校验失败的文档,无论置信度字段声称什么,都值得交给更强的模型或人工处理。
按模型计费的每张发票成本
一页发票 ≈ 1,800 个输入 token(作为图像发送)+ JSON 输出约 350 个 token:
| 模型 | 每张发票 | 每 10,000 张发票 | 适用场景 |
|---|---|---|---|
claude-haiku-4-5 | $0.00248 | $24.80 | 默认:干净的打印发票和最便宜的大批量运行 |
claude-sonnet-5 | $0.00497 | $49.70 | 升级层级:扫描件、手写内容、失败请求 |
费率实时取自定价页面(约比供应商的标价低 30%),失败的请求不收费。采用两级路由——全部通过 Haiku,算术校验失败的请求升级到 Sonnet——每月 10,000 张发票通常低于 $27.29。
准确性:让你达到生产环境的检查清单
- 先定义 Schema。 将真正必需的字段标记为
required,其他地方都允许使用null——强制填写一个值,就会强迫模型产生幻觉。 - 发送图像,而不是 OCR 文本。 布局承载含义(列、总额框);视觉模型可以直接读取。只有对于能够提取出干净文本层的原生数字 PDF,才退回使用文本。
- 在代码中校验算术。 行项目 + 税 = 总额,可以免费捕获大多数提取错误。
- 升级处理,不要盲目重试。 失败请求会转交 Sonnet,并将校验错误包含在提示词中;持续失败的请求会进入人工队列。
- 针对真正的边界情况使用少样本示例。 在系统提示词中加入两三个最糟糕布局的示例(贷项通知单、多币种),效果胜过任何程度的指令调优。
发票之外
同样的 Schema 约束模式也适用于收据、采购订单、送货单、海关表格和银行对账单——替换 Schema,保留防护措施。浏览更广泛的结构化数据提取用例,了解临近 RAG 的模式;或者查看成本优化指南,了解规模增长后的路由策略。一个 Kunavo 密钥即可覆盖这里使用的所有模型层级——免费注册并创建密钥,即可按原样运行代码片段。
常见问题
我需要深度学习模型从发票中提取数据吗?
不再需要。经典流程 — OCR,然后使用在数千张标注发票上微调的 LayoutLM 等布局模型 — 对大多数团队而言已被一次视觉 LLM 调用取代:发送发票图像和 JSON schema,获取经过验证的结构化数据。不需要标注训练集,不需要托管模型,曾经会破坏微调模型的布局变化也能通过零样本方式处理。
机器学习发票提取与使用 LLM 相比如何?
经典机器学习提取器(在 OCR 输出上使用 CRF 或 SVM,并结合手工设计的位置和字体特征)及其深度学习后继者(LayoutLM、Donut)都需要通过标注数据换取准确性 — 每次部署需要数千张标注发票,布局变化时还要重新标注。视觉 LLM 在预训练期间已经承担了这项成本,因此可以零样本读取未见过的供应商布局,并通过一次 API 调用返回受 schema 约束的 JSON。在单一固定且超高流量的布局中,推理成本占主导时,机器学习仍然胜出;但对于现实发票的长尾场景,LLM 在实践中更准确,拥有成本也低得多。
我仍然可以使用 LayoutLM 或 Donut 提取发票数据吗?
可以;当你处理数百万份采用同一稳定布局的文档,并且能够摊销标注和 GPU 服务成本时,它们仍然合理。变化在于默认选择:对于新项目,视觉 LLM 无需训练集,便可在一个下午达到生产级准确率,而且单文档成本低到微调经济性通常无法收回。常见的折中路径是先运行 LLM,之后仅在必要时微调,并将 LLM 自身被接受的输出作为标注语料。
每份发票的提取成本是多少?
一页发票大约包含 1,800 个输入 token(作为图像)和约 350 个 JSON 输出 token。在 Kunavo 上,使用 claude-haiku-4-5 每张发票约为 $0.00248,使用 claude-sonnet-5 为 $0.00497 — 也就是说,即使采用 Sonnet 质量,1,000 张发票也只需几美元。失败的请求不计费。
应使用哪个模型提取发票数据?
从 claude-haiku-4-5 开始:它以最低价格近乎完美地处理清晰的打印发票。仅将失败情况 — 手写内容、低分辨率扫描件、特殊布局或未通过算术检查的文档 — 路由到 claude-sonnet-5。这种两层路由通常能让 90% 以上的流量保持在低价层。
如何保证输出是有效 JSON?
使用结构化输出:传入带 JSON schema 的 response_format,Claude 模型就会被约束为只输出该形状 — 无需正则表达式后处理。在此基础上增加一个低成本的应用层防护:检查行项目加税额是否等于总额,并将不匹配的结果发送给更强模型或人工队列。
我可以从 PDF 发票而不只是图像中提取数据吗?
可以 — 将每个 PDF 页面渲染为 PNG(例如使用 pdf2image),然后按上述方式发送。对于原生数字 PDF,还可以提取文本层并将其作为纯文本发送,这样成本更低;扫描件则继续使用图像路径。
LLM 发票提取的准确率有多高?
对于清晰的打印发票,带 schema 约束并配合算术防护的视觉提取通常能达到 98% 以上的字段级准确率 — 与微调模型不同,即使面对从未见过的布局,准确率也能保持。请在自己的文档上进行测量:100 张已标注发票就能组成可靠的评估集。
我的发票数据会被用于训练吗?
不会 — 通过 Kunavo 发送的请求会根据 API 使用条款(而非消费者应用条款)传递给模型提供商,不会用于训练模型。有关保留细节,请参阅计费与数据文档。