五年前,從發票擷取結構化資料意味著建立一條深度學習流程:OCR 階段、在數千份人工標註文件上微調的版面感知模型(LayoutLM、Donut),以及每當供應商變更範本時就必須維護的負擔。到了 2026 年,整條流程濃縮為一次視覺 LLM API 呼叫:輸入影像、輸出經 schema 驗證的 JSON — 零訓練資料、零託管模型,新版面以 zero-shot 處理。這份由 Kunavo 提供的指南 — Kunavo 是連接此模式所使用視覺模型的獨立、OpenAI 相容閘道 — 展示完整的可運作模式、以我們公布的費率衡量每張發票的成本,以及如何維持生產環境等級的準確度。
發票擷取方法比較:規則、機器學習、深度學習、LLM
人們曾使用四代技術從發票取得結構化資料。值得將它們並列比較,因為二十年來決定它們之間取捨的因素 — 以標註資料換取準確度 — 正是如今不再適用的因素。
| 方法 | 運作方式 | 設定成本 | 處理新的供應商版面 |
|---|---|---|---|
| 範本/規則(1990 年代起) | OCR,接著為每個供應商設定座標區域和正則運算式(「total 是 ‘TOTAL’ 右側的數字」) | 每個供應商數小時,永久持續 | 否 — 需要新的範本 |
| 傳統機器學習(2010 年代) | OCR,接著將手工設計的特徵(位置、字型大小、相鄰文字)輸入 CRF 或 SVM,由其標記每個 token | 特徵工程 + 數千個標籤 | 部分可以 — 在未見過的版面上會退化 |
| 深度學習(2020 年代起) | 具備版面感知能力的轉換器(LayoutLM、Donut、DocTR),經微調後可共同讀取文字、位置與像素 | 3,000–50,000 份已標註發票 + GPU 服務 | 通常不行 — 一般需要重新標註與重新訓練 |
| 視覺 LLM(2024–) | 一次 API 呼叫:發票影像加上 JSON 結構描述,結構化輸出受該結構描述限制 | 無 — 只需要結構描述與提示;每份發票 $0.00248–$0.00497 | 是,零樣本 |
前三者都有一項共同特性:準確度是用標註資料買來的。每個供應商範本、每個 CRF 特徵、每次 LayoutLM 微調,都是一項標註專案。視覺 LLM 已在預訓練期間支付了這項成本,因此新增版面的邊際成本為零。這就是整體轉變 — 並不是舊方法停止運作,而是它們原本花費預算的部分變得免費了。
詳細比較:經微調的深度學習模型與視覺 LLM
| 經微調的版面模型(2021) | 視覺 LLM(2026) | |
|---|---|---|
| 訓練資料 | 3,000–50,000 份已標註發票 | 無(零樣本)— 少量範例有助於處理邊緣案例 |
| 新的供應商版面 | 通常需要重新標註 + 重新訓練 | 以零樣本方式處理 |
| 基礎設施 | GPU 服務 + OCR 階段 | 一次 HTTPS 呼叫 |
| 輸出格式 | Token 標籤 → 自訂解碼 | 受結構描述限制的 JSON |
| 每份發票成本 | 工程時間占主要成本 | $0.00248–$0.00497 |
經微調的擷取器在單一固定且高流量的版面上仍可能勝出,但對真實世界發票的長尾情況 — 不同供應商、語言、掃描品質 — 零樣本視覺 LLM 在實務上更準確,且持有成本大幅更低。這類工作負載的更多模式,請參閱資料擷取使用情境。
完整模式:影像 → 經結構描述驗證的 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 結構描述(在 Claude 上,模型無法輸出其他內容 — 請參閱下方連結參考資料中關於 GPT 的注意事項)、系統提示規則 「完全照抄值;缺少時使用 null;絕不自行捏造」(可消除虛構的稅籍編號),以及算術防護欄 — 明細項目加上稅額必須等於總額,否則文件會交由更強的模型或人工處理。請參閱聊天完成參考資料,了解 response_format 對各模型系列的強制規則。
相同結構描述,改用具型別的 Pydantic 模型
原始 JSON 結構描述字典用於一個端點尚可,但用於十個端點就很痛苦。在正式環境中,多數團隊會將結構描述定義為單一 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(以影像形式傳送)+ 約 350 個 JSON 輸出 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。
準確度:協助你進入正式環境的檢查清單
- 先建立結構描述。 將真正必填的欄位標記為
required,其他欄位一律允許null— 強迫填值就會強迫模型產生幻覺。 - 傳送影像,而不是 OCR 文字。 版面承載意義(欄位、總額框);視覺模型能直接讀取。只有對可以擷取出乾淨文字層的原生數位 PDF,才退回使用文字。
- 在程式碼中驗證算術。 明細項目 + 稅額 = 總額,可免費捕捉大多數擷取錯誤。
- 升級處理,不要盲目重試。 將失敗案例連同驗證錯誤放入提示後交給 Sonnet;持續失敗的案例則進入人工佇列。
- 對真正的邊緣案例使用少樣本提示。 在系統提示中加入兩到三個最糟糕版面的範例(貸項通知單、多幣別),其效果勝過任何程度的指令微調。
發票以外的情境
相同的受結構描述限制模式也適用於收據、採購單、送貨單、海關表單與銀行對帳單 — 替換結構描述,保留防護欄。若要查看與 RAG 相鄰模式的更廣泛結構化資料擷取使用情境,或在用量成長後查看分流策略,請參閱成本最佳化指南。一組 Kunavo 金鑰涵蓋此處使用的每個模型層級 — 免費註冊並建立金鑰,即可原樣執行程式碼片段。
常見問題
我需要深度學習模型來從發票擷取資料嗎?
不再需要。傳統流程 — OCR,接著使用 LayoutLM 等在數千張標註發票上微調的版面模型 — 對大多數團隊而言已由一次視覺 LLM 呼叫取代:傳送發票影像和 JSON schema,取得經驗證的結構化資料。無須標註訓練集、無須託管模型,而曾經破壞微調模型的版面變更也能以 zero-shot 處理。
機器學習發票擷取與使用 LLM 相比如何?
傳統機器學習擷取器(對 OCR 輸出使用 CRF 或 SVM,加上手工設計的位置和字型特徵)及其深度學習後繼者(LayoutLM、Donut),都以標註資料換取準確度 — 每次部署需要數千張註釋發票,版面變更時還要重新標註。視覺 LLM 在預訓練期間已支付這項成本,因此能以 zero-shot 讀取未見過的供應商版面,並透過一次 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 品質,一千張發票也只需幾美元。失敗的請求不會計費。
應使用哪個模型進行發票資料擷取?
先從 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 使用條款(而非消費者應用程式條款)傳送給模型供應商,不會用於訓練模型。保留詳細資訊請參閱計費與資料文件。