ふくふくHukuhuku Inc.
EP.20Prep 14分公開: 2026-09-01

LLM で項目を抽出する ── 非決定的な処理を工程に組み込む作法

非構造なテキストから項目を取り出す作業は、LLM が得意です。ただし毎回同じ結果になるとは限りません。工程に組み込むための検証と運用を扱います。

#前処理#LLM#検証#Python
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

請求書、契約書、問い合わせのメール。非構造なテキストから項目を取り出す作業は、これまで人がやるか、正規表現で頑張るしかありませんでした。

はこれが得意です。ただし毎回同じ結果になるとは限りません。この回は、非決定的な処理を前処理の工程に組み込むための作法を扱います。

1. 何が変わるか

従来の抽出手法と比べると、得意なことと苦手なことが逆転しています。

抽出手法の比較
対象正規表現などLLM
書式が完全に固定確実使う必要がない
書式に揺れがある規則が爆発する得意
言い回しが多様対応困難得意
同じ結果の再現保証される保証されない
処理速度速い遅い
費用ほぼゼロ件数に比例

4行目が、工程に組み込むときの本質的な違いです。前処理は「同じ入力から同じ結果」を前提に組まれていることが多く、そこが崩れます。

使い分けが基本

書式が固定されている部分は従来の手法、揺れる部分だけ LLM。全部を LLM に任せる必要はありません。1行目のように、確実な方法があるならそちらを使うほうが速く安く確実です。

2. 形式を必ず検証する

最初にやるべきは、出力の形式が正しいかの検証です。LLM時代のテスト戦略 EP.07 で扱ったとおり、ここは決定的に判定できます

抽出結果を、決定的に検証する
Python
import jsonimport refrom dataclasses import dataclass

@dataclassclass Extracted:    invoice_no: str    issued_on: str      # YYYY-MM-DD    total: int    tax_rate: float

class ExtractionError(Exception):    pass

def parse_and_validate(raw: str) -> Extracted:    """モデルの出力を検証する。おかしければ例外を投げる。"""    try:        data = json.loads(raw)    except json.JSONDecodeError as e:        raise ExtractionError(f"JSON として読めない: {e}") from e
    required = {"invoice_no", "issued_on", "total", "tax_rate"}    missing = required - set(data)    if missing:        raise ExtractionError(f"項目が足りない: {sorted(missing)}")
    if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", str(data["issued_on"])):        raise ExtractionError(f"日付の形式が違う: {data['issued_on']!r}")
    if not isinstance(data["total"], int) or data["total"] < 0:        raise ExtractionError(f"金額が不正: {data['total']!r}")
    if not 0 <= float(data["tax_rate"]) <= 1:        raise ExtractionError(f"税率が範囲外: {data['tax_rate']!r}")
    return Extracted(        invoice_no=str(data["invoice_no"]),        issued_on=str(data["issued_on"]),        total=int(data["total"]),        tax_rate=float(data["tax_rate"]),    )

この検証は完全に決定的です。非決定的なのは抽出そのものだけで、その結果が使える形かどうかは確実に判定できます。ここを通らないものは、再試行するか人に回します。

範囲の検査も入れてください。税率が 3.0(300%)になっているといった誤りは、形式としては正しいので JSON の検証だけでは通ってしまいます。

3. 一致率を測る

形式が正しくても、中身が合っているとは限りません。実際のデータで一致率を測る必要があります。

  1. 1正解を人が作った検証データを用意する — 数十件でよい
  2. 2項目ごとに一致率を出す — 全体ではなく項目別に
  3. 3外れ方を分類する — どういう文書で外れるか
  4. 4閾値を決める — この項目はこの精度が必要、という基準
項目ごとの一致率を出す
Python
from collections import defaultdict

def field_accuracy(cases):    """項目ごとの一致率と、外れた例を出す。
    cases: [{"expected": {...}, "actual": {...}, "id": "..."}, ...]    """    hit = defaultdict(int)    total = defaultdict(int)    misses = defaultdict(list)
    for c in cases:        for field, expected in c["expected"].items():            total[field] += 1            actual = c["actual"].get(field)            if actual == expected:                hit[field] += 1            else:                misses[field].append((c["id"], expected, actual))
    print(f"{'項目':16s}{'一致率':>8}{'件数':>7}")    for field in sorted(total, key=lambda f: hit[f] / total[f]):        rate = hit[field] / total[field]        print(f"{field:16s}{rate:8.1%}{total[field]:7d}")
    print("\n外れた例(各項目3件まで)")    for field, ms in misses.items():        for case_id, exp, act in ms[:3]:            print(f"  {field:14s} {case_id}: 期待 {exp!r} / 実際 {act!r}")
    return {f: hit[f] / total[f] for f in total}

項目ごとに出すのが要点です。全体の一致率が90%でも、特定の項目だけ50%ということがあります。EP.19 で扱った「平均が少数派を隠す」のと同じ構造です。この連載で繰り返し出てくる形なので、集計する前に、分けて見る軸がないかを問う習慣をつけてください。

そして外れた例を実際に見てください。数字だけでは、どういう文書で外れるかが分かりません。見ると、「手書きの部分がある」「表が複雑」といった共通点が見つかることがあります。

4. 項目ごとに要求水準を変える

すべての項目に同じ精度を求める必要はありません。誤ったときの影響で分けます。

項目ごとの要求水準
項目要求水準外れたときの影響
金額極めて高い計算が狂う。人の確認が要る
日付高い期限の判断に影響
識別番号高い突き合わせができなくなる
取引先名表記揺れは後段で吸収できる
備考・摘要低い参考情報

金額と識別番号は、人の確認を前提にするのが現実的です。逆に備考のような項目は、多少外れても実害が小さい。一律に「精度が足りないから使えない」と判断しないでください。全項目を人が確認するなら自動化の意味が薄れますが、重要な項目だけに絞れば、確認の量は大幅に減ります

金額の誤りは静かに流れる

形式としては正しい数字なので、検証を通過して下流へ流れます。そして集計してから「合計が合わない」と気づく。レガシー再生の現場 EP.06 で扱った突き合わせと同じで、元と照合する手順が必要です。

5. 工程への組み込み方

検証と一致率の測定ができたら、工程に組み込みます。要点は、非決定的な部分を隔離することです。

工程の中で非決定的なのは一段だけ
段階決定的か扱い
文書の取り込み・整形決定的通常のテスト
抽出(LLM)非決定的この一段だけ
形式・範囲の検証決定的通常のテスト
元との突き合わせ決定的重要項目のみ
保存・後処理決定的通常のテスト

非決定的なのは中央の一段だけです。この形にしておけば、大半の工程は通常のテストで守れます。全体が非決定的な塊になっていると、検証のしようがありません。

検証を通らなければ人に回す
Python
def extract_with_fallback(client, text, max_retries=2):    """抽出して検証。通らなければ再試行し、それでも駄目なら人へ。"""    last_error = None
    for attempt in range(max_retries + 1):        raw = client.extract(text)        try:            return {"status": "ok", "data": parse_and_validate(raw),                    "attempts": attempt + 1}        except ExtractionError as e:            last_error = e
    # 検証を通らなかったものは、捨てずに人の確認へ回す    return {        "status": "needs_review",        "reason": str(last_error),        "raw_text": text,        "attempts": max_retries + 1,    }

通らなかったものを捨てないのが要点です。EP.17 でも書いたとおり、黙って落とすと件数が合わなくなり、後から気づけません。人の確認に回す枠を作っておきます。 の観点でも、処理できなかった件数は必ず記録に残すべき対象です(EP.22)。

6. 運用してからの確認

組み込んだ後も、継続して確認が要ります。モデルが更新されると、挙動が変わることがあります。

  • 検証を通らなかった割合 — 上がっていれば何かが変わった
  • 人の確認に回った件数 — 増えていれば調査
  • 再試行の回数 — 増えていれば不安定になっている
  • 定期的な一致率の測定 — 同じ検証データで測り直す
  • モデル更新の記録 — 変化があったとき、原因の候補になる

4番目が重要です。同じ検証データを取っておいて、定期的に測り直す。数字が下がっていれば、モデルか入力の傾向が変わっています。これは LLM時代のテスト戦略 EP.08 で扱った評価セットと同じ役割です。

ここまでのまとめ

書式が固定なら従来の手法、揺れる部分だけ LLM。出力の形式と範囲の検証は決定的にできるので必ず入れる。一致率は項目ごとに測り、外れた例を実際に見る項目ごとに要求水準を変える(金額は人の確認前提)。工程では非決定的な一段だけを隔離し、検証を通らなかったものは捨てずに人へ回す。そして同じ検証データで定期的に測り直してください。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp