技術 約10分で読めます

信頼度スコアで文書抽出の人手確認を絞る

いけさん目次

PDFからJSONを抜くデモは、だいたい気持ちよく動く。
でも請求書、契約書、スキャン画像、手書きメモが混ざった瞬間に、失敗の出方が面倒になる。
全部間違うわけではなく、あるフィールドだけ自信満々に外す。

Iteration LayerがDEV Communityに出したHuman in the Loop: Using Confidence Scores to Build Reliable Document Extractionは、この「一部だけ怪しい」状態をフィールド単位の信頼度スコアで扱う話だった。
同社のDocument Extraction APIは、抽出値ごとに0から1のconfidenceとsource citationを返す。
文書全体をまとめて合否判定するのではなく、請求書番号、取引先名、合計金額、配送先住所を別々に見る。

この粒度が効く。
住所だけ0.72で、金額や請求書番号が0.95前後なら、文書全体を人間に戻す必要はない。
住所フィールドだけレビューキューに積み、残りは自動処理に進められる。

confidenceは正解率ではない

原典でも強調されているが、confidenceはaccuracyではない。
0.95だから必ず正しい、0.65だから必ず間違い、という意味ではない。
モデルがその値をどれくらい確からしいと見ているかの信号であり、運用側はその信号を使って「人間に見せる場所」を絞る。

ここを取り違えると事故る。
confidenceが高いフィールドでも、ラベルを読み違えたり、表の別列を拾ったり、文書内の古い金額を選んだりする。
特に金額や日付のように、見た目はもっともらしいが下流影響が大きい値は、confidenceだけで通すより計算チェックや別文書との照合を噛ませるほうがいい。

原典の例では、金額系フィールドに0.95、取引先名に0.88、請求書番号に0.90のような別々の閾値を置いている。
この発想はわりと現実的だ。
「名前の表記ゆれ」は後から直せるが、「支払金額の誤り」はそのまま送金事故になる。

フィールド自動通過の目安外したときの痛さ
請求書番号0.90照合が面倒になる
取引先名0.88表記ゆれとして吸収できることがある
税額・合計金額0.95支払い・会計処理に直撃する
住所0.90前後物流や本人確認で事故りやすい

OCR単体の問題とは少し違う

以前、2025年のウェブOCR実装の限界で、ブラウザOCR、NDLOCR、Cloud Vision、Vision LLMを比べた。
そこでは「読めるか」「日本語に強いか」「ブラウザで動くか」が主題だった。

今回の話は、その一段後ろにある。
文字が読めたとして、その値を業務データとして採用していいのか。
請求書の「Total」と小計欄を区別できたのか。
OCRやVLMが返したJSONを、そのままDBへ入れていいのか。

NDLOCRの段組認識をヒストグラムで切った記事では、レイアウト認識が崩れると読み順そのものが壊れる問題を扱った。
信頼度スコアは、こういう前処理の失敗を魔法のように直すものではない。
むしろ「この領域は怪しい」と検出できたあとに、レビュー対象を狭めるための部品だ。

VLMでJSONを返す系の処理にも同じ問題がある。
ローカルVision LLMでRPGパラメータを抽出した実験では、JSON形式自体は安定していても、中身はモデルの解釈や創作に寄る。
文書抽出でも、スキーマ通りのJSONが返っただけでは足りない。
値の根拠、信頼度、レビュー後の訂正履歴がないと、後からどこで間違えたのか追えない。

レビューキューはフィールド単位に寄せる

人間を挟む設計で重いのは、「結局ぜんぶ見る」状態になることだ。
原典のレビューキュー設計は、フィールド単位の確認を前提にしている。
抽出値、confidence、引用元テキスト、元文書ビューアを同じ画面に置き、怪しいフィールドだけ直せるようにする。

流れはこうなる。

flowchart TD
    A["文書アップロード"] --> B["スキーマ指定で抽出"]
    B --> C["フィールドごとのconfidenceを付与"]
    C --> D{閾値を超えたか}
    D -->|高い| E["自動採用"]
    D -->|中間| F["値を入れたまま人間が確認"]
    D -->|低い| G["手入力または再抽出"]
    F --> H["訂正履歴を保存"]
    G --> H
    E --> I["下流処理へ"]
    H --> I

レビュー画面で大事なのは、モデル出力を空欄に戻さないことだと思う。
低confidenceでも、候補値と引用元があれば確認は速い。
完全な手入力に戻すのは、値がnullに近い、引用元が取れていない、計算チェックに失敗した、という場面に絞れる。

もうひとつ見たいのは、レビュー後の訂正率だ。
confidence 0.70から0.90のレビュー対象のうち、ほとんどがそのまま承認されているなら閾値が厳しすぎる。
逆に、レビュー対象の訂正率が高いなら、自動採用側にも誤りが流れている疑いがある。
confidenceの平均値より、confidence帯ごとの訂正率のほうが運用の調整に使いやすい。

エージェントに渡すなら分岐条件を明示する

原典はMCP経由のエージェント利用にも触れている。
Iteration LayerのMCPドキュメントを見ると、文書・画像処理APIをClaudeやCursorなどのツールから呼ぶ導線を用意している。
エージェントが文書を受け取り、抽出ツールを呼び、confidenceを見て続行・確認・拒否を分岐する形だ。

ここは人間向けのレビューキューより危ない。
人間なら「この住所おかしくない?」と立ち止まれるが、エージェントは次のツール呼び出しへ進む。
請求書の金額を読んで支払いワークフローへ進む、契約日の抽出結果でアラートを作る、履歴書のスキル欄を評価に使う、といった処理では、confidenceが単なるメタデータではなく実行条件になる。

Cloudflare Browser Runの記事では、ログインやMFAで人間へ制御を戻すHuman in the Loopを扱った。
ブラウザ操作のHITLは「エージェントが操作できない壁」で止まる。
文書抽出のHITLはもう少し地味で、「操作はできるが、その値を信じていいか分からない」場面で止まる。

Pinterest社内MCP基盤の記事で見たように、企業内MCPでは危険な操作に承認フローを挟む設計が出てくる。
文書抽出でも同じで、confidenceが閾値未満ならツール実行を止め、ユーザー確認かレビューキューへ送る。
エージェント側のプロンプトに「低confidenceなら確認する」と書くだけでは弱い。
ツール呼び出し後のコード側で分岐を固定したほうがいい。

閾値は最初から当てにいかない

原典は、数百件処理してレビュー結果を見てから閾値を調整する流れを提案している。
これはかなり大事だ。
最初から0.92や0.95に意味を持たせすぎると、文書の種類が変わっただけで崩れる。

まず保守的に始める。
レビュー対象を多めに取り、フィールドごとに「承認された」「訂正された」「手入力になった」を記録する。
そのあとで、訂正がほとんどないconfidence帯を自動採用へ寄せる。
訂正が多い帯は閾値を上げるか、スキーマ説明、前処理、別文書照合、計算フィールドで補強する。

confidenceだけで閉じないほうがいい場面もある。
小計、税額、合計のような金額は、各フィールドのconfidenceが高くても合算が合わなければ止める。
契約書の日付なら、本文、表紙、更新条項の複数箇所が食い違うことがある。
履歴書なら、連絡先は高confidenceで拾えても、スキル要約は抽象化されすぎる。

文書抽出の本番化で欲しいのは、完璧な自動化ではなく、間違った値が静かに下流へ流れない配管だ。
confidenceはそのためのバルブになるが、バルブの開き具合はレビュー結果で調整するしかない。

多段フィルタとして組む

ここまでの閾値判定は、フィールドごとに1段の閾値で「通す・確認・弾く」を分ける設計だった。
これを多段の分類器のように組むアプローチもある。

最初の段はconfidenceで粗く振り分ける。
次の段で計算整合やフォーマット検証(日付形式、郵便番号、税額の合算)をかける。
さらに外部マスタとの照合(取引先名の部分一致、請求書番号の採番ルール)を通す。

flowchart TD
    A["フィールド抽出値 + confidence"] --> B{"Stage 1<br/>confidence閾値"}
    B -->|0.95以上| C["仮採用"]
    B -->|0.60未満| D["手入力送り"]
    B -->|0.60〜0.95| E["Stage 2へ"]
    C --> F{"Stage 2<br/>計算・形式チェック"}
    E --> F
    F -->|パス| G["自動採用"]
    F -->|不整合| H{"Stage 3<br/>外部マスタ照合"}
    H -->|一致| G
    H -->|不一致| I["レビューキュー"]
    D --> I

confidence単体で最終判定しない構成にするのが肝だ。
Stage 1でconfidenceが高ければ仮採用にするが、Stage 2の計算チェックは素通りしない。
confidence 0.98でも、小計+税額≠合計なら止まる。

逆に、confidence 0.72で中間ゾーンに入ったフィールドでも、郵便番号の形式が通り、外部マスタの住所と一致すれば自動採用に格上げできる。
信頼度が低い=人間に全部見せる、ではなくて、別の検証で補強できるなら自動化の余地が残る。

機械学習のカスケード分類器と発想は同じだ。
Viola-Jonesの顔検出が段階的に非顔を弾くように、各ステージで「明らかにOK」「明らかにNG」を先に消し、残ったグレーゾーンだけを次のステージに回す。
文書抽出ではモデルの再推論ではなく、ルールベースの検証やマスタ照合で段を重ねる形になる。

段の順序と通過条件の管理が実装上の面倒なところだ。
Stage 2で使う照合先がない文書種(フリーフォーマットの見積書など)では、confidenceと引用元テキストだけで判定するしかない。
文書タイプごとにどの段が有効か変わるので、段の構成自体を文書スキーマに紐づける設計になる。

単段のconfidence閾値より設計は重くなるが、「高confidenceだが計算が合わない」と「低confidenceだがルールで確定できる」の両方を拾える。

freee MCPで仕訳を自動化しようとして踏んだ壁

ここまで書いてきた話が、freee MCPでファイルボックスの書類から仕訳を起こそうとしたときにそのまま降りかかった。

freeeのファイルボックスは、領収書や請求書の画像をアップロードして保管する機能だ。
Web UIでは内部OCRが走って、金額や取引先名が読み取られた状態で表示される。
ところがAPI経由でファイルボックスのデータを取ると、このOCR結果がついてこない。
MCP経由で「ファイルボックスの書類を読んで仕訳を起こす」をやろうとすると、自前でOCRするところから始まる。

自前OCRで請求書の金額を読む段階で、上で書いた「confidenceが高くても間違える」話が直撃する。
仕訳の金額を1桁読み違えると、会計帳簿に入り、場合によってはそのまま振込処理まで進む。
今回やったのは、閾値を実質最大に振って、絶対に合っている確証があるものだけ自動処理する方式だった。
confidenceが少しでも落ちたら全部人間に回す。

「間違った仕訳が静かに通る」事故は潰せる。
でも自動化率は低い。
ほとんどの書類が人間確認送りになって、手で仕訳を切るのと大差ない。

freeeの内部OCRは精度が高い。
Web UIで見ると、スキャンが荒い領収書でもかなり正確に読めている。
さらにfreeeには仕訳ルールがある。
取引先名や摘要のパターンで勘定科目を自動設定する機能で、一度ルールを作ればそれ以降は同じパターンの書類が自動で仕訳される。
API外で自前OCRをかける経路では、この精度も仕訳ルールも使えない。

「ファイルボックスに入った書類を自動で仕訳して、明細にアップロードして添付する」一気通貫のパイプラインを組むには、多段フィルタの設計だけでは足りない。
freeeの仕訳ルール相当のロジックを外部に再実装するか、freee側がOCR結果をAPIで返すようになるのを待つか。
現状だと、閾値を最大に振って確実なものだけ流す保守的な運用か、人間が最終確認するフローを残すかの二択になる。

OCRの精度とは別に、勘定科目の推定という壁もある。
金額と取引先が正確に読めたとしても、「アマゾンからの3,980円を消耗品費にするか新聞図書費にするか」「スターバックス680円は会議費か交際費か」は、その事業者の経理判断で決まる。
freeeの仕訳ルールは過去の仕訳パターンからこのマッピングを蓄積していく仕組みだが、API経由ではこの蓄積を参照できない。
外部パイプラインから勘定科目を推定しようとすると、取引先名と金額だけを頼りに自前でルールを書くか、毎回LLMに推測させるかになる。
どちらも、freeeの中で人間が積み上げた判断の蓄積には遠い。

OCRの認識精度と仕訳ルールの蓄積、この2つがどちらもAPI境界の内側に閉じていて、外部エージェントからは届かない。
ファイルボックスAPIが書類の画像バイナリとメタデータだけ返す限り、エージェント側は認識と判断を丸ごと自前でやり直すことになる。

参考