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層で構成される:
- Value(型付きプリミティブ):
Amount(value, currency, unit)、DateValue(iso, original)、TableValue(headers, rows)など - Item(ValueとSpanの組み合わせ):
AmountItem(amount, spans) - Answer(ItemのリストとAnswerBaseの共有フィールド)
- 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換算レートに依存してしまう点だ。監査証跡もなく、再現性もない。
正しい手順は:
Amount(value, currency, unit)を抽出させる- 変換は明示的にPythonで実施:
amount.value * RATE[amount.currency]["USD"] - 閾値と比較する
換算レートが変わっても、モデルを再呼び出しせずに再計算できる。すべてのステップが可視化され、監査可能になる。
住所抽出の例: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を参照していただきたい。