RAG(検索拡張生成)を導入したものの、AIが参照ドキュメントにない情報を事実のように語り、対応に苦慮した経験はないでしょうか。
プロンプトで指示しても、ハルシネーション(幻覚)を完全にゼロにするのは難しいのが現状です。特にビジネスの現場では、誤った情報が信頼を損なう可能性があり、PoC(概念実証)から実運用への移行を阻む大きな壁となります。
この記事では、プロンプト調整に頼らず、コードで確実性を担保する実践的なアプローチを紹介します。PythonとLLM APIを使って、自前の「ファクトチェック&自己修正システム」を構築していきます。
1. ハルシネーション検知のアーキテクチャ設計
実装するシステムの全体像を論理的に整理します。単に「回答が正しいか?」とAIに問うだけでは不十分です。人間が文章を校正するときのように、プロセスを体系的に分解する必要があります。
なぜRAGでも嘘をつくのか?
LLMは「次に来る確率の高い言葉」を予測しています。RAGでコンテキストを与えても、学習済みデータの記憶が優先されたり、コンテキストの読み落としが発生したりします。これを防ぐには、生成された回答をそのままユーザーに出す前に、検証器(Verifier)というフィルターを通すアーキテクチャが有効です。
「主張抽出」と「事実検証」の2段階プロセス
精度の高い検証を行うための鉄則は、「文章をアトミックな(最小単位の)事実に分解すること」です。
長文のまま検証しようとすると、一部が合っていて一部が間違っている場合に、LLMの判定が曖昧になります。そこで、以下の2段階プロセスを採用します。
- Claim Extraction(主張抽出): 回答文を「主語・述語・目的語」が明確な、検証可能な個別の主張(Claim)リストに変換する。
- Verification(事実検証): 各Claimについて、参照ドキュメント(Evidence)と照らし合わせ、「支持(Entailment)」「矛盾(Contradiction)」「情報なし(Neutral)」を判定する。
実装するパイプラインの全体像
今回構築するのは、以下のフローを持つパイプラインです。
- Generate: ユーザーの質問と検索結果から、初期回答を生成。
- Extract: 回答からClaimを抽出。
- Verify: 各Claimを検証。
- Self-Correct: 「矛盾」判定が出た場合、その理由をフィードバックして再生成。
このループを回すことで、ハルシネーションを抑制し、実用的な精度を確保できると考えられます。
2. 検証環境のセットアップと依存ライブラリ
RAG向けのファクトチェック機能を実装するために、まずは基盤となる開発環境を整えます。
必要なPythonライブラリ
今回は、LLMのオーケストレーションに LangChain、構造化データの出力制御に Pydantic を使用します。
# 必要なライブラリのインストール
# pip install langchain langchain-openai pydantic
import os
from typing import List, Optional
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.output_parsers import PydanticOutputParser
# APIキーの設定(環境変数から読み込むことを推奨)
os.environ["OPENAI_API_KEY"] = "sk-...";
検証用LLMの選定
メインの回答生成には表現力豊かな最新モデルを利用できますが、検証タスク(Claim抽出や論理判定)は比較的単純な処理の繰り返しとなります。そのため、プロジェクトのROI(投資対効果)と速度のバランスを考慮し、検証用には高速かつコスト効率の高いモデルを採用するのが効果的です。
OpenAIの公式情報によると、以前広く利用されていた gpt-3.5-turbo や gpt-4o、gpt-4.1 mini などの旧モデルは、2026年2月をもって順次廃止されています。そのため、これから実装を行う場合や既存のシステムを改修する際は、2026年の最新主力モデルである GPT-5.2 シリーズへの移行が必須となります。
検証用途の具体的な代替手段としては、処理速度とコスト効率が最適化された GPT-5.2 Instant を指定することをおすすめします。以前のコードで model="gpt-3.5-turbo" や model="gpt-4o-mini" と記述していた箇所を、単純に model="gpt-5.2-instant" に書き換えるだけで対応可能です。これにより、より高い推論力と応答速度の恩恵を受けながら、スムーズな移行を実現できます。
本記事ではロジックの明確さを優先して解説を進めますが、実際の運用では、複雑な推論が必要なメインの回答生成には GPT-5.2 Thinking を、単純なファクトチェックや構造化データの抽出には GPT-5.2 Instant を使い分けることで、精度を落とさずにコストを最適化できます。
テストデータの準備
ハルシネーション検知の動作確認を行うには、意図的に矛盾を含ませたテストデータが不可欠です。例えば、以下のようなケースを用意します。
- 参照元ドキュメント: 「当社製品Xのバッテリー寿命は10時間です。」
- LLMの生成した誤回答: 「製品Xのバッテリーは24時間持続します。」
このような明確な事実のズレをシステムが正確に検知できるかどうかをテストすることで、ファクトチェック機能の信頼性を評価する目安になります。本番環境へデプロイする前に、こうしたエッジケースを複数用意して検証を重ねることが重要です。
3. Step 1:回答からの「検証可能な主張」の抽出
最初のステップは、LLMが生成した長文の回答を、論理的で検証可能な「主張(Claim)」のリストに変換する処理です。
なぜこの作業が必要なのでしょうか?
例えば「製品Xは2023年に発売され、バッテリーは24時間持続します」という複合文を想像してみてください。このままでは、発売年とバッテリー性能のどちらか一方が間違っていた場合、システムは「この文全体が嘘である」と大雑把に判断するしかありません。精度の高いファクトチェックを実現するためには、より細かい粒度での検証が不可欠です。
文章を事実単位(Claim)に分解する
ハルシネーションを正確に特定するには、複雑な文章を「主語・述語・目的語」が明確な単文に分解し、主観的な意見と客観的な事実を切り離す必要があります。
ここでは、Pythonのデータ検証ライブラリである Pydantic を活用し、LLMからの出力を構造化データ(JSON)として厳密に制御する実装を解説します。
from typing import List
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.output_parsers import PydanticOutputParser
# 出力データの構造定義
class Claim(BaseModel):
text: str = Field(description="検証可能な単一の事実を示す文")
class ClaimList(BaseModel):
claims: List[Claim]
# 抽出器の実装
class ClaimExtractor:
# 旧来のgpt-3.5-turboは廃止されているため、最新の高速モデルを指定します
def __init__(self, model_name="gpt-5.2-instant"):
self.llm = ChatOpenAI(model=model_name, temperature=0)
self.parser = PydanticOutputParser(pydantic_object=ClaimList)
self.prompt = ChatPromptTemplate.from_template(
"""
以下のテキストを、検証可能な個別の事実(Claim)に分解してください。
複合文は分割し、主観的な意見ではなく客観的な事実として抽出すること。
テキスト:
{text}
{format_instructions}
"""
)
def run(self, text: str) -> List[str]:
chain = self.prompt | self.llm | self.parser
result = chain.invoke({
"text": text,
"format_instructions": self.parser.get_format_instructions()
})
return [c.text for c in result.claims]
# 使用例
if __name__ == "__main__":
extractor = ClaimExtractor()
text = "製品Xは2023年に発売され、バッテリーは24時間持続します。"
claims = extractor.run(text)
print(claims)
# 出力例: ['製品Xは2023年に発売された', '製品Xのバッテリーは24時間持続する']
このように事実を最小単位に分解することで、「発売年は正しいが、バッテリーの持続時間に関する情報が誤っている」といった、ピンポイントでのファクトチェックが可能になります。これはRAGシステムにおいて、回答の信頼性を担保するための非常に重要なプロセスです。
また、APIで利用するモデルの選定も重要なポイントです。OpenAIの公式情報によると、かつて広く使われていた gpt-3.5-turbo などの旧モデルはすでにサービス終了・廃止されています。そのため、現行のシステムでは GPT-5.2 Instant(高速に処理可能な基本モデル)や、指示追従能力が向上した GPT-4.1 mini(GPT-4o miniの後継)といった最新モデルを明示的に指定する必要があります。
常に公式ドキュメントで最新のモデル名を確認し、適切なモデルを選択することで、システムの安定性と処理速度を維持することをおすすめします。特に、大量のテキストを分解するような処理では、コストと速度のバランスに優れた軽量・高速モデルの活用が効果的です。
4. Step 2:参照元との突き合わせと矛盾判定
次に、抽出した各主張(Claim)が、RAGで検索したドキュメント(Context)の内容と本当に合致しているかを判定します。これは自然言語処理の分野で NLI(Natural Language Inference:自然言語推論) と呼ばれるタスクに相当します。
単なるキーワードの有無ではなく、文脈や意味的な整合性を深く評価するため、「支持(Supported)」「矛盾(Contradicted)」「中立(Neutral)」の厳密な3値分類を用いるのが一般的です。この分類を適用することで、AIがもっともらしい嘘をつくリスクをシステム的にブロックできます。
判定ロジックの実装
ここでも構造化出力を活用し、判定結果だけでなく「理由(Reasoning)」も明示的に出力させるのが精度を高める重要なポイントです。理由を言語化させることで、LLM自身の推論プロセスが明確になり(Chain-of-Thought効果)、ハルシネーションの検知精度が飛躍的に向上します。また、この推論の記録は、後続の修正ステップやシステムのデバッグ時にも非常に価値のある情報源となります。
なお、この判定処理は抽出した主張の数だけ繰り返し実行されるため、処理速度とAPIコストのバランス設計が不可欠です。OpenAIの公式ドキュメントによれば、gpt-3.5-turboなどの旧来の軽量モデルはすでに廃止されており、現在は推論力と応答速度が最適化された最新モデルへの移行が推奨されています。具体的には、基本モデルとして展開されている「GPT-5.2 Instant」や、指示追従能力が向上した「GPT-4.1 mini」などを指定することで、高いコストパフォーマンスを発揮します。以下の実装例でも、これらの最新高速APIモデルを指定する想定としています。
from enum import Enum
from pydantic import BaseModel, Field
# ※ChatOpenAI, PydanticOutputParser, ChatPromptTemplateは適宜インポートしてください
class VerificationResult(str, Enum):
SUPPORTED = "supported" # 支持される:ドキュメントから論理的に導き出せる
CONTRADICTED = "contradicted" # 矛盾する:ドキュメントの内容と明らかに異なる
NEUTRAL = "neutral" # 情報なし:ドキュメントに記載がない、または判断不能
class FactCheckOutput(BaseModel):
claim: str
result: VerificationResult
reason: str = Field(description="判定の根拠となる理由")
class FactChecker:
# 廃止された旧モデルを避け、GPT-5.2 Instant や GPT-4.1 mini などの最新高速モデルを指定して処理速度とコストを最適化
def __init__(self, model_name="gpt-5.2-instant"):
self.llm = ChatOpenAI(model=model_name, temperature=0)
self.parser = PydanticOutputParser(pydantic_object=FactCheckOutput)
self.prompt = ChatPromptTemplate.from_template(
"""
あなたは厳格なファクトチェッカーです。
以下の「主張(Claim)」が、提供された「参照ドキュメント(Context)」に基づいているか検証してください。
判定基準:
- supported: ドキュメントの内容と一致する、または論理的に導き出せる。
- contradicted: ドキュメントの内容と明らかに矛盾する。
- neutral: ドキュメントに記載がない、または判断できない。
参照ドキュメント:
{context}
主張:
{claim}
{format_instructions}
"""
)
def verify(self, claim: str, context: str) -> FactCheckOutput:
chain = self.prompt | self.llm | self.parser
return chain.invoke({
"claim": claim,
"context": context,
"format_instructions": self.parser.get_format_instructions()
})
この FactChecker クラスをループ処理で呼び出し、抽出したすべての主張を一つずつ厳密に検査します。もし一つでも CONTRADICTED(矛盾)や、意図しない NEUTRAL(情報なし)が含まれていれば、生成された回答にハルシネーションが混入していると判断し、次の修正ステップへと進むための明確なトリガーとなります。
5. Step 3:自己修正(Self-Correction)ループの構築
矛盾を検知したら、単にエラーを返すのではなく、LLMに間違いを指摘して書き直させるプロセスを自動化します。
修正指示プロンプトの動的生成
検証結果を集約し、修正のための具体的な指示を作成します。
class SelfCorrectionLoop:
def __init__(self, generator_llm, extractor, checker):
self.generator = generator_llm
self.extractor = extractor
self.checker = checker
self.max_retries = 3 # 無限ループ防止のため回数制限を設定
def run(self, query: str, context: str) -> str:
# 1. 初回回答生成
current_answer = self.generator.invoke(f"質問: {query}\n資料: {context}").content
for attempt in range(self.max_retries):
print(f"--- 試行回数: {attempt + 1} ---")
# 2. 主張抽出
claims = self.extractor.run(current_answer)
# 3. 検証
contradictions = []
for claim in claims:
check_result = self.checker.verify(claim, context)
if check_result.result == VerificationResult.CONTRADICTED:
contradictions.append(check_result)
# 矛盾がなければループ終了
if not contradictions:
print("検証OK: 矛盾は見つかりませんでした。")
return current_answer
# 4. 修正指示(Feedback)の作成
feedback = "以下の点について、参照資料と矛盾しています。修正してください:\n"
for c in contradictions:
feedback += f"- 「{c.claim}」は誤りです。理由: {c.reason}\n"
print(f"修正指示: {feedback}")
# 5. 再生成(修正)
correction_prompt = f"""
前回の回答には誤りがありました。
質問: {query}
参照資料: {context}
前回の回答: {current_answer}
指摘事項: {feedback}
指摘事項を踏まえて、回答を修正してください。
"""
current_answer = self.generator.invoke(correction_prompt).content
print("最大試行回数に達しました。")
return "申し訳ありません。正確な回答を生成できませんでした。" # フォールバック
このループにより、システムが自律的に回答の精度を高められます。
6. 実運用に向けた最適化とトレードオフ
このファクトチェック機能は強力ですが、すべてのリクエストで実行するとレイテンシ(応答速度)とコストが増加します。実プロダクトへの導入時には、プロジェクトの要件に応じたトレードオフを考慮する必要があります。
レイテンシとコストの管理
検証プロセスには追加のAPIコールが必要です。例えば、回答に5つのClaimが含まれていれば、検証のために5回(またはまとめて1回)の推論が発生します。
- 全件検査: 金融や医療など、誤りが許されない領域向け。
- サンプリング検査: ログを蓄積し、事後的に10%のデータを検証してモデル改善に活かす。
- 非同期処理: ユーザーには先に回答を表示し、バックグラウンドで検証。もし誤りが発見されたら、UI上で「この回答には不正確な可能性があります」と警告バッジを後から表示する。
ユーザー体験(UX)への配慮
検証中の待ち時間をどう扱うかも重要です。「回答を確認しています...」といったステータス表示を入れることで、ユーザーに安心感を与えることができます。また、何度修正しても矛盾が解消しない場合は、無理に回答せず「資料からは回答できません」と伝えることも重要です。
まとめ
生成AIのハルシネーションは、確率論的なモデルである以上避けられない特性です。しかし、それを理由に導入を諦めるのではなく、周辺のエンジニアリングでカバーすることが重要です。
今回紹介したのは、以下の3ステップによる実践的な実装です。
- Extract: 曖昧な文章を明確な「主張」に分解する
- Verify: 参照元と突き合わせて論理的に検証する
- Correct: 検知した矛盾をフィードバックして自己修正させる
このロジックを組み込むことで、RAGシステムの信頼性は大きく向上し、PoCの壁を越えた実用的なAI導入に近づくはずです。ぜひ、実際のプロジェクトで試してみてください。
コメント