5年前、請求書から構造化データを抽出するには、ディープラーニングパイプラインが必要でした。OCR段階、数千件の人手ラベル付き文書でファインチューニングしたレイアウト対応モデル(LayoutLM、Donut)、そしてベンダーがテンプレートを変更するたびに発生する保守負担です。2026年には、パイプライン全体が1回のビジョンLLM API呼び出しに集約されます。入力は画像、出力はスキーマ検証済みJSONで、学習データも、ホスティングするモデルも不要です。新しいレイアウトもゼロショットで処理できます。Kunavoは、このパターンで使用するビジョンモデルへの独立したOpenAI互換ゲートウェイです。Kunavoによるこのガイドでは、完全な実装パターン、当社が公開している料金に基づく請求書1件あたりのコスト、そして本番品質の精度を維持する方法を説明します。
請求書抽出アプローチの比較:ルール、機械学習、ディープラーニング、LLM
請求書から構造化データを取り出すために、4世代の技術が使われてきました。これらを横並びで比較する価値があります。20年間にわたって手法の選択を左右してきたトレードオフ、つまりラベル付きデータによって精度を得るという関係が、もはや当てはまらなくなったためです。
| アプローチ | 仕組み | 導入コスト | 新しいベンダーレイアウトへの対応 |
|---|---|---|---|
| テンプレート/ルール(1990年代〜) | OCRを実行し、ベンダーごとの座標領域と正規表現を適用する(「TOTAL」の右側の数字が合計) | ベンダーごとに数時間、以後継続 | いいえ — 新しいテンプレートが必要 |
| 従来の機械学習(2010年代) | OCRを実行し、手作業で設計した特徴量(位置、フォントサイズ、隣接する単語)をCRFまたはSVMに入力して各トークンにタグを付ける | 特徴量設計と数千件のラベル | 部分的 — 未知のレイアウトでは精度が低下 |
| ディープラーニング(2020年〜) | レイアウト対応トランスフォーマー(LayoutLM、Donut、DocTR)を、テキスト、位置、画素を統合的に読み取るようファインチューニングする | ラベル付き請求書3,000〜50,000件とGPUサービング | 多くの場合は不可 — 再ラベル付けと再学習が必要 |
| ビジョンLLM(2024年〜) | 1回のAPI呼び出しで、請求書画像とJSONスキーマを送信し、構造化出力をそのスキーマに制約する | なし — スキーマとプロンプトのみ。請求書1件あたり$0.00248〜$0.00497 | はい、ゼロショット |
最初の3つに共通する性質は、ラベル付きデータによって精度を得ることです。すべてのベンダーテンプレート、すべてのCRF特徴量、すべてのLayoutLMファインチューニングが注釈プロジェクトになります。ビジョンLLMは事前学習中にそのコストをすでに負担しているため、新しいレイアウトの限界コストはゼロです。変化の本質はそこにあります。従来の手法が機能しなくなったのではなく、従来の手法が予算を使っていたものが無料になったのです。
ファインチューニング済みディープラーニングとビジョンLLMの詳細比較
| ファインチューニング済みレイアウトモデル(2021年) | ビジョンLLM(2026年) | |
|---|---|---|
| 学習データ | ラベル付き請求書3,000〜50,000件 | なし(ゼロショット) — エッジケースには少数の例が有効 |
| 新しいベンダーレイアウト | 多くの場合、再ラベル付けと再学習が必要 | ゼロショットで対応 |
| インフラストラクチャ | GPUサービングとOCR段階 | HTTPS呼び出し1回 |
| 出力形式 | トークンタグ → カスタムデコード | スキーマ制約付きJSON |
| 請求書1件あたりのコスト | エンジニアリング時間が支配的 | $0.00248–$0.00497 |
ファインチューニング済み抽出器は、固定された単一の大量レイアウトでは今でも勝てる可能性があります。しかし、現実世界の請求書の長い裾野、つまり異なるベンダー、言語、スキャン品質に対しては、ゼロショットのビジョンLLMの方が実運用で高精度であり、所有コストも劇的に安価です。この種のワークロード向けのパターンは、データ抽出のユースケースに詳しく掲載されています。
完全なパターン:画像 → スキーマ検証済みJSON
以下はすべてKunavoのOpenAI互換エンドポイントに対して実行されます。base_urlをhttps://api.kunavo.com/v1に向ければ、1つの文字列を変更するだけで同じコードから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"])大部分の処理を担う重要な点は3つです。response_format内のJSONスキーマ(Claudeではモデルはそれ以外を出力できません。下記のリファレンスにあるGPTに関する注記を参照)、システムプロンプトのルール「値は正確にコピーし、存在しない場合はnullにし、決して創作しない」(幻覚による納税者番号を防止)、そして算術ガードレールです。明細と税額の合計が総額と一致しない場合は、文書をより強力なモデルまたは人手に回します。各モデルファミリーでresponse_formatが適用する内容については、チャット補完リファレンスを参照してください。
型付きPydanticモデルとしての同じスキーマ
生のJSONスキーマ辞書は、1つのエンドポイントなら問題ありませんが、10個では扱いづらくなります。本番環境では、ほとんどのチームがスキーマを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)ここで取り入れる価値がある点は2つあります。floatではなくfloat | Noneとして型付けされたフィールドにより、モデルは「存在しない」ことを許容される形式で表現できます。これがないと、税の行が欠落した場合、ゼロを創作する方向へ強く引っ張られます。また、自己申告のconfidenceフィールドは出力トークンを3つ消費するだけで、驚くほど適切にルーティングできます。これは校正済みではありませんが、「low」は人が確認すべき信頼できるシグナルです。
2層ルーティング:本番環境で実際に動く構成
すべての文書に単一モデルを使う構成は適切ではありません。大半を占めるきれいに印刷された請求書は、利用可能な最安のビジョンモデルで処理します。手書き、低品質なスキャン、特殊なレイアウトといった難しい裾野でこそ、より強力なモデルの料金を支払う価値があります。推測ではなく、検証失敗を条件にエスカレーションしてください。
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件あたりのコスト
1ページの請求書 ≈ 1,800入力トークン(画像として送信) + JSONの出力トークン約350:
| モデル | 請求書1件あたり | 請求書10,000件あたり | 用途 |
|---|---|---|---|
claude-haiku-4-5 | $0.00248 | $24.80 | デフォルト:きれいに印刷された請求書と最安の大量処理 |
claude-sonnet-5 | $0.00497 | $49.70 | エスカレーション層:スキャン、手書き、失敗 |
料金は料金ページから取得した最新のものです(プロバイダーのリスト価格より約30%低い)。失敗したリクエストには課金されません。2層ルーティング、つまりすべてをHaikuで処理し、算術チェックに失敗したものをSonnetへエスカレーションする構成では、月10,000件の請求書が通常$27.29未満に収まります。
精度:本番環境に到達するためのチェックリスト
- まずスキーマを定義します。 本当に必須のフィールドには
requiredを指定し、それ以外ではnullを許可してください。値を強制すると、幻覚を強制することになります。 - OCRテキストではなく画像を送信します。 レイアウトには意味があります(列、合計欄)。ビジョンモデルはそれを直接読み取ります。抽出可能なテキストレイヤーがあるデジタル生成PDFの場合に限り、テキストへフォールバックしてください。
- コードで算術を検証します。 明細 + 税額 = 総額のチェックにより、抽出エラーの大半を無料で検出できます。
- 盲目的に再試行せず、エスカレーションします。 失敗は検証エラーをプロンプトに含めてSonnetへ送信し、繰り返し失敗するものは人手によるキューに送ります。
- 真のエッジケースにFew-shotを使います。 最も難しいレイアウト(クレジットノート、多通貨)の例を2〜3件システムプロンプトに含める方が、指示チューニングをどれだけ行っても得られないほどの効果があります。
請求書以外への応用
同じスキーマ制約付きパターンは、領収書、発注書、納品書、税関申告書、銀行取引明細書にも適用できます。スキーマを置き換え、ガードレールは維持してください。RAGに隣接するパターンについては、より広範な構造化データ抽出のユースケースを、ボリューム増加後のルーティング戦略についてはコスト最適化ガイドを参照してください。ここで使うすべてのモデル層を1つのKunavoキーで利用できます。無料で登録し、キーを作成すれば、スニペットをそのまま実行できます。
よくある質問
請求書からデータを抽出するには、ディープラーニングモデルが必要ですか?
もう必要ありません。従来のパイプライン、つまりOCRを実行し、数千件のラベル付き請求書でLayoutLMのようなレイアウトモデルをファインチューニングする方法は、ほとんどのチームで、単一のビジョンLLM呼び出しに置き換えられています。請求書画像とJSONスキーマを送信すれば、検証済みの構造化データが返ります。ラベル付けする学習セットも、ホスティングするモデルも不要で、ファインチューニング済みモデルを壊していたレイアウト変更もゼロショットで処理されます。
機械学習による請求書抽出は、LLMを使う場合と比べてどうですか?
従来の機械学習抽出器(OCR出力に対してCRFまたはSVMを適用し、位置やフォントなどの手作業で設計した特徴量を加える方式)と、そのディープラーニング後継(LayoutLM、Donut)は、どちらもラベル付きデータによって精度を向上させます。導入ごとに数千件の注釈付き請求書が必要で、レイアウトが変わるたびに再ラベル付けも必要です。ビジョンLLMは事前学習中にそのコストを負担済みであるため、未知のベンダーレイアウトもゼロショットで読み取り、1回のAPI呼び出しでスキーマ制約付きJSONを返します。推論コストが支配的になる、固定された単一の非常に大量なレイアウトでは、機械学習が今でも勝ちます。しかし、現実の請求書の長い裾野に対しては、LLMの方が実運用で高精度であり、所有コストもはるかに安価です。
請求書抽出にLayoutLMやDonutを引き続き使えますか?
はい。安定した単一レイアウトの文書を数百万件処理でき、ラベル付けとGPUサービングのコストを償却できる場合には、現在でも合理的な選択です。変わったのはデフォルトです。新規プロジェクトでは、ビジョンLLMなら学習セットなしで午後のうちに本番精度へ到達でき、文書単価も十分に小さいため、ファインチューニングの経済性が元を取れることはまれです。一般的な中間策は、まずLLMを実行し、その後必要になった場合だけファインチューニングする方法です。このとき、LLMが受け入れられた自身の出力をラベル付きコーパスとして使います。
請求書抽出の文書あたりのコストはいくらですか?
1ページの請求書は、画像として約1,800入力トークンに加えて、JSONの出力トークンが約350トークンです。Kunavoでは、claude-haiku-4-5で請求書1件あたり約$0.00248、claude-sonnet-5で$0.00497です。つまりSonnet品質でも、1,000件の請求書で数ドルです。失敗したリクエストには課金されません。
請求書データの抽出にはどのモデルを使うべきですか?
まずclaude-haiku-4-5から始めてください。きれいに印刷された請求書なら、最低価格でほぼ完璧に処理できます。処理に失敗したものだけをclaude-sonnet-5にルーティングしてください。これには、手書き、低解像度のスキャン、特殊なレイアウト、または算術チェックに失敗する文書などが含まれます。この2層ルーティングにより、通常は処理量の90%以上を安価な層に維持できます。
出力が有効なJSONになることをどのように保証できますか?
構造化出力を使ってください。response_formatにJSONスキーマを渡すと、Claudeモデルはその形状どおりの出力だけを生成するよう制約されるため、正規表現による後処理は不要です。さらに、アプリケーションレベルの安価なガードレールを追加します。明細の合計に税額を加えた値が総額と一致するか確認し、不一致はより強力なモデルまたは人手によるキューに送ります。
画像だけでなく、PDFの請求書からもデータを抽出できますか?
はい。各PDFページをPNG(例:pdf2image)にレンダリングし、上記の方法で送信します。デジタル生成されたPDFでは、テキストレイヤーを抽出してプレーンテキストとして送ることもできます。こちらの方が安価ですが、スキャン文書には画像経路を使ってください。
LLMによる請求書抽出の精度はどの程度ですか?
きれいに印刷された請求書では、算術ガードレールを備えたスキーマ制約付きビジョン抽出により、フィールド単位の精度が通常98%以上になります。ファインチューニング済みモデルとは異なり、これまで見たことのないレイアウトでも精度が維持されます。自社の文書で測定してください。ラベル付き請求書100件が、十分に堅実な評価セットになります。
請求書データは学習に使われますか?
いいえ。Kunavo経由のリクエストは、消費者向けアプリの規約ではなくAPI利用規約に基づいてモデルプロバイダーへ渡され、モデルの学習には使われません。保持期間の詳細は課金とデータのドキュメントを参照してください。