NuExtract 3でPDF・請求書をローカルでJSON抽出する|クラウドAI(GPT-5.5)との使い分け・選び方

NuExtract3に関する記事のアイキャッチ画像 - NuExtract 3でPDF・請求書をローカルでJSON抽出する AI×ライティング

請求書のPDFを開いて、発行日と請求番号を目で追い、明細を1行ずつ表計算に打ち込む。月末に何十枚も処理していると、転記ミスと終わりの見えない単純作業で時間が溶けていきます。やりたいのは「決まった項目を、決まった形で、機械的に抜き出す」こと。それだけのはずなのに、汎用の文章生成AIに丸投げすると今度は別の問題が顔を出します。

NuExtract3とは、文書から指定したスキーマのJSONを抽出する、構造化出力に特化したローカル実行のVLMである。

ここで扱うのは、この「決まった形で抜き出す」を専門にしたモデルを、手元のPCで動かして請求書PDFを検証可能なJSONに落とすところまでの道筋です。OCRと正規表現を継ぎ足したパイプラインや、汎用LLMへのプロンプト依存でなぜ抽出が崩れるのか。その症状から逆算して、構造化出力専用のアプローチが何を解決するのかを順番に見ていきます。

この記事の要点

  • NuExtract3はテンプレート(欲しい項目の型)を渡すと、その形に沿ったJSONを返す構造化抽出向けのモデル
  • 画像も読むVLMなので、PDFをページ画像化すれば、テキスト抽出済みでなくレイアウトを保ったまま入力できる
  • ローカルで動かせるため、文書を外部の推論APIへ送らない構成を組める(ただし外部接続を使わない前提での話)

請求書・PDFの情報抽出でつまずく具体ポイント

帳票の自動化が一筋縄でいかないのは、「読み取り」と「構造化」が別の難しさを抱えているからです。スキャンした請求書は文字が読めても、どの数字が小計でどれが税額かをプログラムが取り違える。逆に項目の意味は分かっても、出力が毎回違う形で返ってきて後処理が安定しない。多くの現場が踏むのは、このどちらか、あるいは両方の地雷でしょう。

最初に詰まるのは、たいていフォーマットの揺れです。取引先が10社あれば請求書のレイアウトも10通り。合計欄が右下にある書式もあれば、明細の下にまとめて並ぶ書式もあります。人間なら一目で「これが合計」と分かる配置の手がかりが、テキストだけを順番に読むツールには伝わりません。

汎用LLMにPDFを丸投げすると起きること

文章生成が得意なLLMに「この請求書から合計金額を教えて」と頼むと、それらしい答えは返ってきます。問題は、その答えの形が制御しにくい点。あるときは「合計は110,000円です」と文章で、別のときは「¥110,000」と通貨記号つきで、さらに別のときは「税込110000」と返す。プログラムから使うには、この自由な出力を毎回パースし直す羽目になります。

もっと厄介なのが、空欄を勝手に埋めてしまう挙動です。請求書に振込先が書かれていないのに、文脈から「ありそうな」値を補ってしまう。生成モデルは「自然な文章を続ける」ことに最適化されているため、抽出タスクでは事実にない情報をもっともらしく差し込むリスクが残ります。金額や口座番号でこれが起きると、目視で気づくまで誤りが下流に流れ続けるという、見つけにくい不具合になりかねません。

JSONで返すよう指示しても安心はできません。波括弧の閉じ忘れ、文字列内の引用符のエスケープ漏れ、配列の途中で説明文が混ざる。構文として壊れたJSONはjson.loadsで例外になり、せっかくの抽出結果が丸ごと使えなくなります。プロンプトの工夫である程度は減らせても、ゼロにはなりにくいのが実情です。

OCR後の後処理が破綻するパターン

「ならばOCRで文字を起こしてから正規表現で抜けばいい」という発想は自然です。実際、定型フォーマットが1種類なら、この方式でもそれなりに回ります。ただ取引先が増えてレイアウトが多様化した途端、正規表現の例外パッチが雪だるま式に膨らみます。

OCRの読み取り精度そのものも変数になります。スキャンの傾き、かすれた印字、表の罫線が文字に重なった箇所。こうした入力では「0」と「O」、「1」と「l」、桁区切りのカンマと小数点の取り違えが起こります。後段の正規表現は「読めた文字列が正しい」前提で書かれているので、上流の誤読をそのまま受け取って誤った数値を確定させてしまう。

レイアウト情報が失われる点も無視できません。OCRは多くの場合、ページの文字を一次元のテキスト列に変換します。その過程で「この数値は明細テーブルの3行目の単価列」という二次元の位置関係が落ちてしまうと、どの数字がどの項目に属するのかを後から復元するのは難しくなります。表が複数ページにまたがる請求書だと、なおさら。読み取り工程と構造化工程を別々の道具で継ぎ接ぎしていると、こうした境界でほころびが出やすいわけです。

つまり、欲しいのは「画像のレイアウトを保ったまま読み、決めた形のJSONだけを返す」一貫した仕組み。ここに構造化出力専用のVLMが効いてきます。

NuExtract3の仕組みとテンプレート方式

NuExtract系は、NuMindが公開している情報抽出向けのモデル群です。汎用的な対話や長文生成ではなく、「与えた文書から、指定した項目を、指定した形で抜き出す」ことに用途を絞っています。バージョンの正確な世代名やモデルサイズ、ライセンスの詳細は更新されるため、導入前に公式のモデルカードで最新の表記を確認してください。本記事では、抽出に使ううえでの考え方を中心に扱います。

中核にあるのは、出力を自由作文にさせず「テンプレートに沿わせる」という方針です。あらかじめ欲しい項目の型を渡しておき、モデルはその型の空欄を埋める形で値を返す。文章を生成させるのではなく、決められた枠に値を流し込ませることで、出力の形が毎回そろいます。これがJSON構文の安定につながる土台になります。

抽出テンプレート(JSONスキーマ)の渡し方

テンプレートは、抜き出したい項目を並べた「空のJSON」のようなものだと捉えると分かりやすいでしょう。請求書なら、発行日・請求番号・請求元・合計金額といったキーを並べ、値は空にしておく。モデルはこのテンプレートと文書を受け取り、各キーに対応する値を文書中から見つけて埋めて返します。

考え方を概念的に書くと、入力と出力はこういう対応になります。

// 渡すテンプレート(各項目に「欲しい型」を指定)
{
  "issue_date": "date-time",
  "invoice_number": "verbatim-string",
  "biller": "verbatim-string",
  "total_amount": "number"
}

// 返ってくるJSON(型に沿って値が埋まる)
{
  "issue_date": "2026-06-01",
  "invoice_number": "INV-2026-0042",
  "biller": "サンプル商事株式会社",
  "total_amount": 110000
}

このやり方の利点は、出力の構造を呼び出し側がコントロールできる点にあります。各キーの値には、抜き出したい型を文字列で指定します。そのまま文字で欲しいならverbatim-string、日付ならdate-time、数値ならnumberといった具合です。プロンプトで「JSONで返して」とお願いして祈るのではなく、型を先に固定しておくぶん、出力の構造がそろいやすくなります。ただし常に壊れないJSONが返る保証ではないため、受け取った文字列は必ずjson.loadsでパースして検証します。指定できる型名の一覧や正確な記法は、公式モデルカードを参照してください。

テンプレートに無いものは返らない、という性質も押さえておきたいところ。これは欠点ではなく、抽出範囲を自分で決められる利点として働きます。請求書から税額だけが欲しければ税額のキーだけを置けばよく、余計な情報を拾わせずに済みます。

テキストOCRモデルとVLMの違い

NuExtract3がVLM、つまり画像も入力にできる点は、帳票抽出で実用上の差を生みます。テキストOCRを前提とするモデルは「すでに文字列に起こされた入力」を必要とするため、前段にOCR工程が要る。そこで読み取りミスが起きれば、抽出モデルがどれだけ優秀でも誤った文字列を処理することになります。

VLMはページの画像そのものを読めるので、文字の並びだけでなく配置やレイアウトの手がかりも手がかりとして使える可能性があります。合計欄が枠で囲まれている、明細が罫線で区切られている、といった視覚的な構造が、どの値がどの項目かの判断材料になり得る。OCRで一次元のテキストに潰す前の情報を保ったまま扱える、というのがVLM方式の勘所です。

ただし、ここで測れているのは「レイアウトを含めて入力にできる」という入出力の枠組みであって、「あらゆる文書で抽出が正確」という品質保証とは別の話です。スキャン画質が悪ければ読み取りは当然落ちますし、見慣れない様式の請求書では取りこぼしも起こります。何が構造として保証され、何が文書次第で変わるのか。この線引きを意識しておくと、後で精度を検証するときに評価がぶれません。

ローカル環境の構築と最小実行

ここからは実際に動かす段階です。NuExtract系はローカルで実行できる公開モデルとして提供されているため、手元のPCにPython環境と推論ライブラリを用意すれば、1ドキュメントの抽出を試せます。クラウドのAPIキー発行や従量課金の設定なしに、まず自分の請求書サンプルで挙動を確かめられるのが利点。

最初に全体の流れを掴んでおきましょう。やることは大きく3つです。まずPythonと推論ライブラリを入れる。次に公式のモデルカードからモデルを取得する。最後に、文書とテンプレートを渡して抽出を1件実行する。難所はモデルの取得と最初の1回が通るかどうかで、そこさえ越えれば後はテンプレートを差し替えるだけで応用が利きます。

初回はいきなり請求書の本番データではなく、項目が少ない簡単な文書サンプルで「JSONが返ってくる」状態をまず作るのがおすすめ。テンプレートのキーを2〜3個に絞って成功させてから、項目を足していくと切り分けが楽になります。

必要な環境とモデルの取得

前提になるのはPythonの実行環境と、Hugging Faceからモデルを読み込む推論ライブラリです。一般的には仮想環境を作り、必要なパッケージをインストールしてから始めます。

# 仮想環境を作って有効化(例)
python -m venv .venv
source .venv/bin/activate   # Windowsは .venv\Scripts\activate

# 推論に使うライブラリの導入(device_map="auto" を使うため accelerate も入れる)
pip install -U transformers accelerate pillow
# torch は環境依存。PyTorch公式セレクタ(https://pytorch.org/get-started/locally/)でOS/CUDA対応版を導入

導入するパッケージの正確な組み合わせや、GPUを使う場合のPyTorchのビルド指定は環境で変わります。CUDA対応版のインストール方法などは、公式の導入手順とモデルカードの記載に合わせてください。ここで挙げているのは「何を入れる工程があるか」を示す概念的な例です。

NuExtract3は4Bと小型なので、動かすためのハードルは高くありません。量子化版(GGUF)を llama.cpp や LM Studio などのランタイムで動かせばモデルの占有メモリは3〜4GBほどまで下がり、エントリークラスのGPUはもちろん、GPUを積んでいないPCでも、CPUと十分なメインメモリ(16GB程度が目安)があれば動作自体は可能です。ただし速度は大きく落ちるため、量をこなす実用処理ではGPUの利用が現実的です。Apple Silicon向けには、Metal上で動くMLX量子化版もNuMindから公開されています。ユニファイドメモリは、ディスクリートGPUのVRAM上限に縛られない点が利点です(速度や実用性はMacのメモリ容量・入力画像・量子化方式によるため、実環境での確認が必要です)。

数値の目安として、手元のGPU1枚(VRAM 16GBクラス)で実測したところ、フル精度(bf16)でモデルの占有は約9GB・1ドキュメントあたり数秒で抽出でき、4bit量子化なら約3.4GBまで下がって8GBクラスのGPUにも収まりました(いずれも筆者環境での単発の参考値で、再現性は環境に依存します)。最低スペックはモデルのバージョンや量子化で変わるため断定はしませんが、専用のハイエンドGPUがなければ動かない、という類のモデルではありません。まずは小さな文書で1件試すのが確実です。量子化方式ごとのVRAMと速度の関係は、姉妹サイトのローカルLLMの量子化はどれを選ぶか(Q4_K_M・Q8_0・FP16の実測)が参考になります。

モデルの取得は、推論ライブラリ経由でモデル名を指定して読み込む形が一般的です。初回はダウンロードが走るため、回線とストレージに余裕を見ておいてください。正確なモデル名(リポジトリ名)は公式モデルカードに記載されているものを使います。記憶や推測で似た名前を打ち込むと、別物を引いてくる原因になります。

最小コードで1件抽出するまで

環境が整ったら、文書とテンプレートを渡して抽出を1件実行します。下のコードは概念例ではなく、公式モデルカードのTransformers例に合わせた最小構成です。モデルの読み込みから、画像とテンプレートを渡してJSON文字列を受け取るまでを、この順番でそのまま動かせます。

# 公式モデルカードのTransformers例に合わせた最小構成
import json
import torch
from PIL import Image
from transformers import AutoModelForImageTextToText, AutoProcessor

model_id = "numind/NuExtract3"

processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
model = AutoModelForImageTextToText.from_pretrained(
    model_id,
    dtype=torch.bfloat16,
    device_map="auto",
    trust_remote_code=True,
).eval()

def run_nuextract(messages, **chat_template_kwargs):
    inputs = processor.apply_chat_template(
        messages,
        add_generation_prompt=True,
        tokenize=True,
        return_dict=True,
        return_tensors="pt",
        **chat_template_kwargs,
    ).to(model.device)
    with torch.inference_mode():
        generated_ids = model.generate(**inputs, max_new_tokens=4096, do_sample=False)
    generated_ids = generated_ids[:, inputs.input_ids.shape[1]:]
    return processor.batch_decode(
        generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False,
    )[0].strip()

# 抽出したい項目を「型」で宣言(verbatim-string / date-time / number など)
template = {
    "issue_date": "date-time",
    "invoice_number": "verbatim-string",
    "total_amount": "number",
}

# 文書ページを画像として読み込む(PDFは事前にページ画像化しておく)
page = Image.open("invoice_page1.png").convert("RGB")
messages = [{"role": "user", "content": [{"type": "image", "image": page}]}]

result_json = run_nuextract(
    messages,
    template=json.dumps(template, indent=4),
    enable_thinking=False,
)
print(result_json)

trust_remote_code=True は、モデルリポジトリ内のカスタムコードをローカルで実行する設定です。請求書のような機密文書を扱う前に提供元とコードを確認し、本番では revision を固定して検証済みの環境で実行してください。

ポイントは、テンプレートをapply_chat_templateの引数として渡すこと。欲しい項目の型を宣言しているので、返ってくるのはその型に沿ったJSON文字列です。受け取った文字列はjson.loadsでパースし、構文として読めるかをまず確認します。ここでパースが通れば、テンプレートで指定したキーに値が入っているはず。

PDFを入力にする場合は、ページを画像に変換する一手間が要ります。PDFを画像化するライブラリでページごとに画像へ起こし、その画像をモデルに渡す流れです。複数ページの請求書なら、ページごとに抽出して結果をまとめるか、関連するページを束ねて渡すかを後で設計します。まずは1ページ・1テンプレートで「JSONが返り、パースが通る」最小の成功を作るのが先決。

最初の1件が通ったら、テンプレートのキーを請求書向けに増やしていきます。発行日や請求番号といった単純な単一項目から始め、明細のような繰り返し構造はその後で足すと、どの項目で精度が落ちるかを切り分けやすくなります。この最小実行を土台に、次は請求書の本番スキーマ設計と抽出結果の検証へ広げていきます。

請求書PDFを構造化JSONに抽出する手順

最小実行で型に沿ったJSONが返るところまで来たら、次は請求書という実務の対象に合わせてテンプレートを作り込んでいく段階。ここがこの作業の山場です。請求書は発行元によってレイアウトがまるで違うのに、抜きたい項目はだいたい共通しています。発行日、請求番号、請求元、明細、小計、消費税、合計。この「共通して欲しい型」をテンプレートに落とし込めるかどうかで、抽出の安定度が大きく変わってきました。

汎用LLMにそのまま文章で「この請求書から項目を抜いて」と頼むと、戻り値の形が毎回ぶれます。構造化出力に特化したモデルにテンプレートを渡す方式なら、戻り値のキー構成が固定されるので、後段のプログラムが値を受け取りやすい。この差が、量産処理では効いてきます。

請求書向け抽出スキーマの設計

請求書スキーマで最初に決めるのは、単一項目と繰り返し項目の切り分けです。発行日や請求番号は文書に1つだけ出てくる単一項目。一方、明細行は商品名・数量・単価・金額が何行も並ぶ繰り返し構造です。これを配列として表現します。

invoice_template = {
    "issuer_name": "verbatim-string",      # 請求元の会社名
    "invoice_number": "verbatim-string",   # 請求番号
    "issue_date": "date-time",             # 発行日
    "due_date": "date-time",               # 支払期限
    "line_items": [                         # 明細行(繰り返し)
        {
            "description": "verbatim-string",
            "quantity": "number",
            "unit_price": "number",
            "amount": "number",
        }
    ],
    "subtotal": "number",                  # 小計
    "consumption_tax": "number",           # 消費税
    "total_amount": "number",              # 合計
}

設計のコツは、いきなり全項目を盛り込まないこと。まず単一項目だけのテンプレートで安定させ、明細の配列は後から足します。そうすると、精度が落ちたときに「単一項目で落ちたのか、繰り返し構造で落ちたのか」を切り分けやすい。最初から全部入りにすると、どこが原因なのか分からなくなります。

項目名も抽出精度に響きます。taxのような曖昧なキーより、consumption_taxのように何の税かを明示したほうが、モデルが該当箇所を見つけやすくなる傾向がありました。日本語の請求書を扱うなら、キー名は英語でも、会社名や品目などの文字列項目は日本語のまま返る前提でテンプレートを組みます(金額はnumber型なので数値で返ります)。

テンプレートに書いた項目しか返ってきません。逆に言えば、請求書に存在しない可能性がある項目もテンプレートに含めておけば、モデルが該当情報を見つけられない場合に null や空配列で返るため、その項目が「無い」または抽出に失敗した可能性を検出できます。必須項目の null・空チェックを設計に入れておくと、後段の品質管理に効いてきます。

抽出結果のバリデーションと信頼度の扱い

戻ってきたJSONをそのまま会計システムへ流すのは危険です。モデルの抽出結果は、必ず元の請求書と突合する前提で扱います。最低限のバリデーションとして、必須フィールドの欠落チェックと、金額の整合チェックを入れておくと事故が減りました。

import json

def to_number(v):
    # number 型なら数値で返る。文字列で返った場合もカンマ・円を除いて数値化
    if isinstance(v, (int, float)):
        return v
    if isinstance(v, str):
        return int(v.replace(",", "").replace("円", "") or 0)
    return 0

def validate_invoice(result_json: str) -> list[str]:
    errors = []
    data = json.loads(result_json)

    required = ["issuer_name", "invoice_number", "issue_date", "total_amount"]
    for key in required:
        if data.get(key) in (None, "", []):
            errors.append(f"必須項目が空です: {key}")

    try:
        subtotal = to_number(data.get("subtotal"))
        tax = to_number(data.get("consumption_tax"))
        total = to_number(data.get("total_amount"))
        if subtotal + tax != total:
            errors.append("小計+税と合計が一致しません")
    except (ValueError, TypeError):
        errors.append("金額の数値変換に失敗しました")

    return errors

金額の整合チェックが通れば、明細から合計までの読み取りがおおむね揃っている目安になります。逆に小計と税の合算が合計と合わなければ、どこかの行を読み落としたか、桁を取り違えた可能性が高い。ただし整合が取れても、抽出値が正しいとは限りません。請求元・請求番号・税区分の取り違えや、同じ誤読に基づくたまたまの一致は、この検査では検出できない。こうした項目は別途のルールチェックやサンプリング目視に委ね、整合チェックは怪しい1枚を拾い上げる一次フィルタと位置づけます。

抽出されたJSONを人の確認なしに会計処理へ自動確定させないでください。構造化出力モデルでもゼロミスではありません。特に手書き文字、かすれたスキャン、複雑な明細レイアウトでは読み取りが揺れます。整合チェックを通った請求書だけを「要確認リストから外す」運用にとどめ、最終判断は元帳との突合に委ねるのが安全です。

信頼度の扱いも実務では悩みどころ。モデルが返すのは値そのものであって、「どのくらい確信して抽出したか」のスコアが必ず付くわけではありません。そこで、整合チェックの通過・必須項目の充足・桁数の妥当性といった外形的な指標を自前で組み合わせ、擬似的な信頼度として運用する方法が現実的でした。完璧な確信度が得られない以上、機械チェックで足切りして人の目を要所に集中させる、という割り切りが要ります。

クラウドAI(GPT-5.5)とどう使い分けるか|コスト・データ送信・運用の観点

ローカルで動く構造化出力VLMと、クラウドのマルチモーダルモデル。どちらで請求書を処理すべきかは、精度の優劣だけでは決まりません。判断を分けるのは、文書を外部に送ってよいか、どれだけの量を回すか、初期構築にどこまで手をかけられるか。この3つです。

クラウド側の比較対象は、2026年時点の現行モデルであるGPT-5.5を置きます。GPT-5.5は画像や文書を直接読み取り、スキャンや帳票から構造化JSONを返す処理にも対応しています。そのため「クラウドの汎用モデルは構造化が苦手」という以前の図式は薄れ、抽出の精度そのものでローカルとクラウドを選び分ける段階ではなくなりました。差が出るのは、次に挙げる運用条件のほうです。

下の表は、両者を運用面で並べたものです。数値での精度比較は、同じ請求書サンプルに対して両方を走らせた実測がないと公平に語れないため、ここでは傾向と運用条件で整理しています。

比較軸 NuExtract3(ローカルVLM) クラウドVLM(GPT-5.5など)
実行場所 手元のGPU/サーバー 提供元のクラウド
文書の外部送信 外部APIへ送らない構成にできる APIへ画像を送信する
課金体系 自前ハード・電力コストのみ 従量課金(入力・出力トークン)
セットアップの手間 大きい(環境構築・モデル取得) 小さい(APIキーのみ)
カスタムスキーマ追従 テンプレート方式で型に沿いやすい プロンプト設計でJSON Schema指定
オフライン可否 可(モデル取得後・外部API不使用の構成。初回取得はネット要) 不可(常時接続が前提)

抽出精度とJSON構文の安定性

測れる次元と測れない次元を分けて考える必要があります。テンプレートを渡したときのJSON構文の安定性、つまり「壊れたJSONが返ってこないか」という点では、構造化出力に特化したモデルが型を保ちやすい設計になっています。一方、あらゆる業務帳票での抽出品質、たとえば崩れたレイアウトや特殊な様式での読み取り精度は、文書の種類に大きく左右されるため一律には語れません。

GPT-5.5のような最新のクラウドモデルは画像入力と構造化出力に対応し、幅広い文書を扱えます。ただし自社の請求書に対する抽出精度は本記事では比較実測していないため、機能対応と実精度は分けて捉えてください。専用VLMの優位は、読み取り精度そのものよりも、テンプレート方式で型を安定させやすい設計と、ローカルで完結できる運用面に移っています。どちらを採るかは、扱う文書の均一性と、データを手元に置く必要性の兼ね合いで変わってきます。

コストとデータ送信先の違い

コスト構造は根本的に違います。クラウドAPIは処理した文書の量に比例して費用が積み上がる従量課金。月に数千枚、数万枚の請求書を回すなら、この積み上がりは無視できません。ローカル実行なら、初期のハード投資と電力以外に処理ごとの課金は発生しないため、量が増えるほどローカルが有利になっていきます。

データの送信先も選定を分ける要素。取引先の請求書には、社名・金額・取引内容といった機密が詰まっています。これを外部のAPIへ送ることに制約がある組織では、手元で完結できるローカル実行の価値が大きい。ただし、この「外部に出ない」という性質は無条件ではありません。ローカルモデル単体で動かし、外部の推論APIやWeb検索機能を呼ばない構成にして初めて成り立つ条件付きの利点です。便利だからと外部サービスを組み込めば、その経路でデータは外へ出ます。

逆にクラウド側を使う場合は、データの取り扱いも確認しておきます。OpenAI APIは既定でAPI送信データをモデルの学習には使いませんが、不正利用の監視などで一定期間ログが保持される場合があります。機密文書を扱うなら、組織の契約内容やゼロデータ保持(ZDR)の可否、データの保存先を確認してから採用してください。

RAG前処理・OCRパイプラインへの組み込み

請求書の構造化は、それ単体でも価値がありますが、RAG(検索拡張生成)の前処理として組み込むと別の効きどころが出てきます。非構造のPDFをそのままベクトル化して検索する構成だと、レイアウト情報が失われ、どこが金額でどこが日付なのかをモデルが取り違えやすい。先にJSON化してからインデックスを作ると、フィールド単位で意味を持った検索ができるようになります。ローカルでのRAG構築そのものは、Ollama・ChromaDBで作るローカルRAGOllamaでローカルRAGパイプラインを構築する方法で扱っています。

抽出JSONをRAGのメタデータに使う

抽出したJSONは、検索のメタデータとして使えます。たとえば「2026年の○○社向け請求書で、合計が10万円を超えるもの」といった絞り込みは、本文をベクトル検索するより、構造化済みのフィールドでフィルタするほうが正確で速い。発行日・取引先・金額がメタデータとして付いていれば、検索の前段で候補を絞り込み、本文検索はその中だけに限定できます。資料を読み込んで扱うツールという観点では、NotebookLMのような文書活用サービスとも組み合わせられます。

役割分担を整理すると、既存のOCRが「画像から文字を読み取る」工程を担い、NuExtract3のような構造化出力モデルが「読み取った内容を決めた型に整える」工程を担う、という形が考えられます。ただしVLMは画像から直接構造化できるため、用途によってはOCR工程を挟まずに済む場合もある。どちらの構成にするかは、扱う文書の画質と、既存パイプラインとの兼ね合いで決めます。

大量文書のバッチ前処理では、1枚ずつ処理する素朴なループより、GPUのメモリに収まる範囲でまとめて流すほうがスループットが上がります。夜間にまとめて構造化し、翌朝には検索可能なメタデータ付きインデックスができている、という運用イメージ。前処理を分離しておけば、後段のRAGモデルを差し替えても前処理資産はそのまま使えます。なお抽出後のRAGで精度が出ないときは、RAGハルシネーションの解決法が切り分けの参考になります。

動かない・精度が出ないときの分岐

ここからは、実際に動かして詰まったときの切り分けです。症状ごとに原因の見当が違うので、まず何が起きているかを特定するところから始めます。

抽出が空・壊れるときの切り分け

返ってきたJSONがパースできない、あるいは全フィールドが空。この2つは原因が分かれます。

JSONがパースで落ちる場合、戻り値の文字列にモデルの余計な前置きや後置きが混じっていることがあります。json.loadsに渡す前に、最初の{から最後の}までを切り出す応急処置で通ることもあります。ただし文字列内の波括弧や複数JSONが混じると誤抽出を招くため、本番処理ではこの方式に頼らず、パースに失敗したら再試行するか要確認として人手に回すのが安全です。それでも壊れるなら、テンプレートの構造が複雑すぎてモデルが型を保てていない可能性。明細の配列を一度外し、単一項目だけにして通るか確認します。

フィールドが軒並み空で返るときは、入力画像の品質を疑います。解像度が低い、文書が傾いている、コントラストが弱いといった画像は、VLMでも読み取りが落ちます。PDFを画像化する際の解像度(DPI)を上げる、傾き補正を入れる、といった前処理で改善する場合がある。原因が画質なのかテンプレートなのかを切り分けるため、きれいなサンプル1枚で必ず動作確認をしてから、本番の汚れた文書へ進むのが順序です。

テンプレートのキー名が文書の項目と意味的に離れていると、該当値を見つけられず空が返りやすくなります。「金額」を抜きたいのにキーがvalueのように抽象的だと精度が落ちる傾向。total_amounttax_amountのように、何の値かが明確なキー名にすると改善することがあります。空が続くときは、画質と並んでキー名の具体性も疑ってください。

VRAM不足・速度が出ないとき

GPUのメモリが足りずに落ちる、いわゆるOOM(Out of Memory)。これはモデルのサイズと、扱う画像の解像度・枚数に対してVRAMが不足しているサインです。対処の方向はいくつかあります。量子化された軽量版のモデルを使う、入力画像の解像度を下げる、複数ページを一度に渡さず1ページずつ処理する。このあたりを順に試して、メモリ使用量を抑えます。

量子化版を使えば、1ページ単位の請求書抽出は数GBのメモリに収まり、8GBクラスのGPUやCPU実行でも動かせます。ただし、複数ページを束ねて高解像度で一度に渡すと、メモリ消費が跳ね上がります。実際に必要なメモリ量は、選ぶモデルの量子化レベルと入力画像のサイズで変わるため、ご自身の環境で1枚を流しながら使用量を実測して見極めるのが確実です。具体的なシステム要件の数値は、使うモデルによって変わるので公式のモデルカードを参照してください。

速度が出ないときは、CPUで動かしていないかをまず確認します。VLMは画像処理を含むため、CPU推論では1枚あたりの時間が大きく延びがち。GPUに載っているかをモデル読み込み時のログで確認し、載っていなければデバイス指定を見直します。GPUに載っていてもなお遅いなら、画像解像度を処理に必要な最低限まで下げることで時間を縮められる場合がありました。

NuExtract3の仕様まとめ

種別 構造化出力特化のローカル実行VLM
提供元 NuMind(公式モデルカードを参照)
入力 文書画像・PDF(画像化して入力)
出力 テンプレートで指定した型のJSON
実行形態 手元のGPU/サーバーでローカル実行可
ライセンス・対応言語・モデルサイズ バージョンにより異なるため公式の最新情報を確認

まとめ

請求書PDFをJSONに落とす作業は、テンプレート設計から始まります。単一項目で土台を固め、明細の配列を後から足し、必須フィールドの欠落と金額の整合をプログラムでチェックする。この流れを押さえれば、人が全件を目視しなくても怪しい1枚を機械的に拾い上げられるようになります。

ローカルのNuExtract3とクラウドのGPT-5.5は、どちらも構造化抽出をこなせる以上、精度の優劣で選ぶより、文書を外部に送ってよいか・どれだけの量を回すか・初期構築にどこまで手をかけられるかで選び分けるのが実務的でした。機密文書を手元で完結させたい、大量に回してコストを抑えたいならローカル。とにかく手早く汎用的に処理したいならクラウド、という住み分け。クラウド側のモデル選びはClaude vs ChatGPT 徹底比較ローカルではなくクラウドで使うべきかの比較も参考になります。ただしローカルの「外部に出ない」利点は、外部APIを呼ばない構成にして初めて成り立つ条件付きである点は崩せません。

測れる次元と測れない次元を分けることも忘れないでください。JSON構文の安定性は構造化出力モデルが保ちやすい一方、あらゆる帳票での抽出品質は文書の種類に左右されます。最終的な値の確定は、必ず元帳との突合に委ねる。まずはきれいなサンプル1枚を1ページ・1テンプレートで通し、成功体験を土台に請求書の本番スキーマへ広げていくのが、遠回りに見えて確実な道です。

よくある質問

Q. NuExtract3は商用利用できますか?

ライセンス条件はバージョンや配布形態によって異なります。商用利用の可否は、必ず公式のモデルカードとライセンス表記を一次ソースで確認してください。本記事執筆時点で条件を断定することは避けます。

Q. 日本語の請求書でも精度は出ますか?

テンプレートのキー名を具体的にし、入力画像の解像度を確保すれば、日本語請求書でも構造化抽出は試せます。ただし公式が示すのは多言語対応であって、自社帳票での精度を保証するものではありません。手書きやかすれたスキャンでは読み取りが揺れるため、必ず自社の実データで検証してください。

Q. GPT-5.5などのクラウドAPIより安く運用できますか?

処理量によります。クラウドAPIは文書量に比例して従量課金が積み上がるため、月に大量処理するならローカル実行のほうが処理ごとの課金がなく有利になりやすい。少量なら構築の手間が小さいクラウドが手軽です。

Q. 必要なVRAMはどのくらいですか?

選ぶモデルの量子化レベルと入力画像のサイズで変わります。4bit量子化版ならモデルの占有は3〜4GB程度で、8GBクラスのGPUはもちろんCPU実行でも1ページ単位の抽出は収まりました。正確な要件は公式モデルカードと、ご自身の環境での実測で確認してください。

Q. OCRは別途必要ですか?

NuExtract3はVLMとして画像を直接読めるため、用途によってはOCR工程を挟まずに構造化できます。ただし既存のOCRパイプラインがある場合は、読み取りをOCR、型への整形を構造化モデルに分担させる構成も選べます。

参考資料

タイトルとURLをコピーしました