powered by TechFeed
表示モード
Deep Dive

RAGの誤答はハルシネーションではなく抽出の設計問題 — 型付きPydanticオブジェクトで防ぐ7つのパターン

7月24日、Towards Data Scienceが「Most RAG Hallucinations Are Extraction Errors: Seven Patterns for a Typed Generation Contract」と題した記事を公開した。RAGにおける誤答の多くはLLMの"ハルシネーション"ではなく抽出パイプラインの構造的な問題であるという主張と、それを防ぐ7つのパターンが詳しく解説されている。「ハルシネーション」という言葉が問題を隠しているRAGのシステムが間違った答えを返したとき、チームはすぐに「ハルシネーションだ」と言う。しかしこの記事はその診断を正面から否定する。厳密な意味でのハルシネーションとは、モデルがコンテキストとは無関係にパラメトリック記憶から事実を捏造することだ。RAGではモデルは取得したドキュメントを読んでいる。だとすれば誤答の原因は上流、つまりパース・クエリ生成・検索・生成コントラクトのいずれかにある。「ハルシネーション」と呼ぶことで、どこを直すべきかの議論が閉じてしまう。正確な原因を名付けることで、議論が再び開く。記事が提示する対案...

7月24日、Towards Data Scienceが「Most RAG Hallucinations Are Extraction Errors: Seven Patterns for a Typed Generation Contract」と題した記事を公開した。RAGにおける誤答の多くはLLMの"ハルシネーション"ではなく抽出パイプラインの構造的な問題であるという主張と、それを防ぐ7つのパターンが詳しく解説されている。

「ハルシネーション」という言葉が問題を隠している

RAGのシステムが間違った答えを返したとき、チームはすぐに「ハルシネーションだ」と言う。しかしこの記事はその診断を正面から否定する。

厳密な意味でのハルシネーションとは、モデルがコンテキストとは無関係にパラメトリック記憶から事実を捏造することだ。RAGではモデルは取得したドキュメントを読んでいる。だとすれば誤答の原因は上流、つまりパース・クエリ生成・検索・生成コントラクトのいずれかにある。「ハルシネーション」と呼ぶことで、どこを直すべきかの議論が閉じてしまう。正確な原因を名付けることで、議論が再び開く。

記事が提示する対案はシンプルだ。LLMへの入出力を「文字列」ではなく「型付きPydanticオブジェクト」として扱え、というものだ。PydanticはPythonの型バリデーションライブラリで、データクラスにスキーマと自動バリデーションを付与できる。スキーマが契約であり、バリデーターがユーザーに届く前に走る。LLMはオラクル(神託)ではなく、関数である。


最も重要なパターン:小さいモデルはスキーマを分解せよ(Pattern 7)

7つのパターンの中で最後に紹介されるPattern 7は、実装エンジニアにとって最も即効性が高い。なぜこのパターンが核心かといえば、他の6つのパターンが「何を」「どう」設計すべきかを示すのに対し、Pattern 7は「モデルのサイズによって設計の粒度そのものを変えなければならない」という制約を明示するからだ。コスト削減目的で小規模モデルを採用した瞬間に崩壊するシステムを防ぐ、実践的な安全弁といえる。

問い:「月額保険料は100ユーロを超えるか?」

複合スキーマ PremiumComparison(raw_amount: Amount, converted_eur: float, exchange_rate: float, over_100_eur: bool) を使って検証する。

  • GPT-4.1(大規模モデル):1回の呼び出しで全フィールドを正確に埋める。$124/月を抽出し、可視の為替レートで114.08 EURに変換し、over_100_eur=True を返す。
  • llama-3.2-3B(小規模モデル)raw_amount は正しく埋めるが、converted_eur は架空のレート(0.95)で117.6と返す。JSONバリデーションは通る。4フィールドのうち3つが捏造だ。

小規模モデルはJSONとして壊れていない。スキーマの形は満たしている。しかし中間計算を「発明」する。

解決策は契約を段階に分解することだ:

  1. 第1呼び出しAmount(value=124, currency="USD", unit="month") だけを抽出させる
  2. Python処理:為替レートをログに記録し変換(124 × 0.92 = 114.08 EUR)、閾値比較(114.08 > 100 = True
  3. 第2呼び出し(省略可):自然言語サマリーが必要なら、完成した複合行をフォーマットプロンプトに渡す

結論:モデルのサイズが抽出呼び出しの粒度を決める。スキーマの形を決めるのではない。


残り6つのパターン:契約の構造

パターン 要点
Pattern 1:LLMは関数、オラクルではない 答えは文字列ではなく、引用・信頼性フラグ付きのPydanticオブジェクト
Pattern 2:抽出するが計算しない Amount(value, currency, unit) を抽出し、比較・変換はPythonが行う。モデル内計算は監査証跡を消す
Pattern 3:完全性は構造から判定する 「リストは完全か?」をモデルに聞かない。次ページが同じセクションかを検索時に構造的に判定する
Pattern 4:信頼度はfloatではなく2つのbool answer_found(答えがあるか)と complete_answer_found(リストが完全か)を分離する。0.6という数値は「部分的に見つかった」と「全部捏造」を区別できない
Pattern 5:形状ごとに1プロンプト、実行時にディスパッチ BASE + 形状フラグメント + 制約で構成し、使用フラグメントをログに記録する。巨大な1枚のプロンプトは6ヶ月後のバグ調査を不可能にする
Pattern 6:JSON抽出に推論モデルを使わない スキーマが出力フォーマットを事前に制約しているため、モデルが「どう返すか」を推論する余地がなく、推論トークンはレイテンシとコストを増やすだけで精度は変わらない

「答えを文字列で返す」がいかに危険か

Pattern 2の具体例が分かりやすい。

「保険料は月100ユーロより高いか?」 という問いに対し、ナイーブなアプローチでLLMに直接聞くと "yes, the premium is around 130 EUR / month" と返ってくる。$124をユーロに換算しているが、使った為替レートはどこにも記録されない。 翌月同じ質問をすると、別の見えないレートで別の答えが返るかもしれない。

対して、型付き契約では Amount(value=124, currency="USD", unit="month") を抽出し、Pythonが 1 USD = 0.92 EUR という監査ログに残るレートで変換する。抽出・レート・比較のすべてが再現可能だ。


実装コード

記事にはGitHubリポジトリ doc-intel/notebooks-vol1 が付属しており、7つのパターンを実際のブローカー業界のドキュメントで試せるノートブックが公開されている。型付き AnswerWithEvidence スキーマを埋め、各自己評価フラグが切り替わる様子を確認し、スキーマを分解した場合に小規模モデルがどこまで追いつくかを比較できる。

Pydanticの基本的な使い方については公式ドキュメントを、RAGアーキテクチャの概要についてはLangChain公式のRAGガイドも参照されたい。


詳細はMost RAG Hallucinations Are Extraction Errors: Seven Patterns for a Typed Generation Contractを参照していただきたい。