返回指南
架構·2026年7月17日·更新於 2026年9月30日·閱讀約 13 分鐘

發票資料擷取:深度學習 vs 機器學習 vs LLM(2026 指南)

發票擷取歷經四代技術——範本、傳統機器學習、微調深度學習,以及現在的一次視覺 LLM 呼叫。以下比較它們,並提供完整可運作的 LLM 模式:程式碼、結構化輸出、防護措施與每張發票的經濟成本。

最後審核於 。

五年前,從發票擷取結構化資料意味著建立一條深度學習流程: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:

extract_invoice.py
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 模型,再從中衍生限制條件與解析後的物件,如此擷取器與其餘程式碼庫就不可能對資料形狀產生分歧:

schema_pydantic.py
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,卻有出乎意料的良好分流效果;它未經校準,但「低」是應由人工查看的可靠訊號。

雙層分流:正式環境實際執行的方式

對每份文件使用單一模型並不是正確的架構。乾淨的印刷發票 — 絕大多數文件 — 交由可用的最低成本視覺模型處理;困難的長尾案例(手寫、糟糕的掃描、不尋常的版面)才是更強模型值得其費率的地方。根據驗證失敗進行升級,而不是根據猜測:

two_tier_routing.py
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。

準確度:協助你進入正式環境的檢查清單

  1. 先建立結構描述。 將真正必填的欄位標記為 required,其他欄位一律允許 null — 強迫填值就會強迫模型產生幻覺。
  2. 傳送影像,而不是 OCR 文字。 版面承載意義(欄位、總額框);視覺模型能直接讀取。只有對可以擷取出乾淨文字層的原生數位 PDF,才退回使用文字。
  3. 在程式碼中驗證算術。 明細項目 + 稅額 = 總額,可免費捕捉大多數擷取錯誤。
  4. 升級處理,不要盲目重試。 將失敗案例連同驗證錯誤放入提示後交給 Sonnet;持續失敗的案例則進入人工佇列。
  5. 對真正的邊緣案例使用少樣本提示。 在系統提示中加入兩到三個最糟糕版面的範例(貸項通知單、多幣別),其效果勝過任何程度的指令微調。

發票以外的情境

相同的受結構描述限制模式也適用於收據、採購單、送貨單、海關表單與銀行對帳單 — 替換結構描述,保留防護欄。若要查看與 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 使用條款(而非消費者應用程式條款)傳送給模型供應商,不會用於訓練模型。保留詳細資訊請參閱計費與資料文件。