powered by TechFeed
表示モード
Deep Dive

RAGのハルシネーションはプロンプトでは防げない — 出力を「型付きオブジェクト」に縛ることで構造的に抑制する

7月4日、Towards Data Scienceが「Stop Returning Text from RAG: The Typed Answer Contract That Prevents Hallucination」と題した記事を公開した。この記事では、RAGシステムの生成フェーズにおいてテキストの代わりに型付きスキーマ(Typed Answer Contract)を返すことでハルシネーションを構造的に抑制する実装パターンについて詳しく紹介されている。「嘘をつくな」と書いても何も変わらない——その前提から出発し、出力形式そのものを設計で縛るという根本的な発想の転換を、具体的なコードとともに提示している点が本記事の核心だ。

7月4日、Towards Data Scienceが「Stop Returning Text from RAG: The Typed Answer Contract That Prevents Hallucination」と題した記事を公開した。この記事では、RAGシステムの生成フェーズにおいてテキストの代わりに型付きスキーマ(Typed Answer Contract)を返すことでハルシネーションを構造的に抑制する実装パターンについて詳しく紹介されている。「嘘をつくな」と書いても何も変わらない——その前提から出発し、出力形式そのものを設計で縛るという根本的な発想の転換を、具体的なコードとともに提示している点が本記事の核心だ。


「答えを返せ」という曖昧な指示がハルシネーションを招く

RAGシステムを構築するとき、多くの開発者はプロンプトに「嘘をつくな」「文書に基づいて答えよ」と書き加える。しかし記事は明確に述べる。「do not make things up」と書いても何も変わらない。

LLMがやっていることは、次のトークンを予測することだ。学習データに豊富に存在するトピックなら予測は信頼できる。しかし、あなたの契約書や社内文書は学習データにほぼ存在しない。モデルは同じ流暢さ、同じ自信で「それらしい続き」を生成する。これはバグではなく、生成AIの本質的な動作だ。

では何が有効か。記事が提示する答えは「controlled execution」——モデルが答える形式そのものを型で縛ること、だ。


テキストではなく型付きオブジェクトを返す

記事の核心は、RAGの生成レイヤーにTyped Answer Contract(型付き回答スキーマ)を導入するという設計だ。

AnswerWithEvidence のような最小構成のRAGは「文字列の答え」と「証拠テキスト」を返す。これに対して提案されるアプローチは、回答をPydanticモデルで型定義し、モデルに「文字列を埋める」のではなく「構造体を埋める」よう強制することだ。

スキーマは4層で構成される:

  1. Value(型付きプリミティブ): Amount(value, currency, unit)DateValue(iso, original)TableValue(headers, rows) など
  2. Item(ValueとSpanの組み合わせ): AmountItem(amount, spans)
  3. Answer(ItemのリストとAnswerBaseの共有フィールド)
  4. Programmatic completeness(モデルではなくパイプラインが算出するシグナル)
class Span(BaseModel):
    line_start: int
    line_end: int
    quote: str | None = None

class Amount(BaseModel):
    value: float
    currency: str
    unit: str | None = None

class DateValue(BaseModel):
    iso: str
    original: str

class TableValue(BaseModel):
    headers: list[str]
    rows: list[list[str]]

「抽出はLLM、比較はPython」という鉄則

記事で最も実践的な指摘の一つが「モデルに比較・集計・計算を委ねるな」という原則だ。

例として挙げられるのが「保険料が100万ドルを超えているか?」という問いだ。LLMに直接「Yes/No」で答えさせると、3つの処理が暗黙に起きる——プレミアムの抽出、通貨の解析、そして比較。問題は 100,000,000 JPY が "Yes" になるか "No" になるかが、モデルが内部で持っているJPY/USD換算レートに依存してしまう点だ。監査証跡もなく、再現性もない。

正しい手順は:

  1. Amount(value, currency, unit) を抽出させる
  2. 変換は明示的にPythonで実施: amount.value * RATE[amount.currency]["USD"]
  3. 閾値と比較する

換算レートが変わっても、モデルを再呼び出しせずに再計算できる。すべてのステップが可視化され、監査可能になる。


住所抽出の例:4回のAPI呼び出しを1回に

もう一つ具体的な例として「住所を取得せよ」というケースが紹介されている。

ナイーブな実装では「住所は?」「郵便番号は?」「都市は?」「国は?」と4回のクエリを発行する。これは4回のラウンドトリップ、4回のハルシネーション機会、4倍のコストだ。

Address(street, postal_code, city, country) を一つのPydanticクラスとして定義し、レジストリに登録するだけで、1回の呼び出しで INSERT INTO addresses(...) に直接マッピングできる構造体が返ってくる。スキーマは契約であり、同時に指示でもある——モデルはフィールド名を見ることで自然に住所を分解する。


モデルに自己評価させる:フィードバックフィールドの設計

スキーマには、モデル自身に評価させる自己評価フィールドも含まれる:

  • confidence: 回答の確信度
  • answer_found / complete_answer_found: 答えが文書中に存在したか
  • caveats: 注意すべき留保事項
  • conflicting_evidence: 矛盾する証拠の有無
  • suggested_clarification: ユーザーへの質問提案
  • llm_discovered_keywords: モデルが発見した関連キーワード

また extraction_method フィールドは、回答がどのように得られたかを示す:

  • "verbatim": 文書の記述をそのまま抽出
  • "computed": 複数要素を組み合わせて算出
  • "inferred": モデルが補完(=要人間レビュー)
  • "na": 回答なし

inferred は信頼できない、とはっきり位置づけられている点が重要だ。


「スキーマにフィールドを足す」だけで機能が拡張できる

この設計のもう一つの強みは拡張性だ。新しい検出ロジックを追加したいとき——「墨消しされたブロックをフラグする」「法令を引用している条項の管轄を返す」——は、フィールド1つの宣言とプロンプトの1フラグメントで済む。スキーマがパイプラインとモデルの間のインターフェースになっているため、追加コストが構造的に小さい。

スキーマの強制はPydantic v2とOpenAIの client.responses.parse(...) で実現される。Structured Outputsとして公式にサポートされている機能であり、制約デコーディングをサポートしないプロバイダー向けのフォールバック階層については、本記事の続編にあたるシリーズ第8B回で扱うとされている。


RAGのハルシネーション対策として「プロンプトを工夫する」アプローチには限界がある。本記事が示すのは、出力形式そのものを型で縛るという、より根本的な設計の転換だ。エンタープライズRAGを構築している開発者にとって、参照価値の高い実装パターンだ。

詳細はStop Returning Text from RAG: The Typed Answer Contract That Prevents Hallucinationを参照していただきたい。