RAG(Retrieval Augmented Generation)システムを実運用している方であれば、「なぜか自信満々に嘘をつく(ハルシネーションを起こす)」問題に一度は直面し、頭を抱えた経験があるのではないでしょうか。特に、Agentic RAGはハルシネーション対策の切り札として注目されていますが、その設計と実装には特有の難しさがあります。
この記事では、Agentic RAGを導入してハルシネーションを抑制し、評価駆動でRAGの精度を最大化するための3つの実践的なアプローチを解説します。LangChainとLangGraphを用いた具体的な実装例、よくあるハマりどころとその回避策まで踏み込み、読者の皆さんが自身のプロジェクトでAgentic RAGを成功させるための実践的な知見を提供します。
Agentic RAGとは?従来のRAGが抱える「幻覚」問題とどう戦うか
まず、Agentic RAGとは何か、そしてなぜそれがRAGシステムにおけるハルシネーション(幻覚)問題の強力な解決策となり得るのかを解説します。
従来のRAGとハルシネーションの限界
従来のRAGシステムは、ユーザーのクエリに基づいて関連ドキュメントを検索し、その情報をLLM(大規模言語モデル)に渡して回答を生成します。この「検索→生成」というシンプルなパイプラインは多くの場面で有効ですが、以下のような限界がありました。
- 単一パスの脆弱性: 検索結果が不十分だったり、関連性が低かったりすると、LLMは誤った情報に基づいて回答を生成したり、自信満々に嘘をついたり(ハルシネーション)するリスクがあります。特に複雑なクエリや複数ソースからの情報統合が必要な場合に顕著です。
- クエリの曖昧さへの対応不足: ユーザーのクエリが曖昧な場合、適切なドキュメントを検索できず、結果として不正確な回答が生成されます。
- 自己修正能力の欠如: 生成された回答が不適切であっても、システム自身がその間違いを認識し、修正するためのメカニズムがありません。
Agentic RAGがハルシネーション抑制にもたらす革新
Agentic RAGは、従来のRAGパイプラインに「自律的なAIエージェント」の概念を導入することで、これらの課題を克服します。具体的には、エージェントが以下のような振る舞いをすることで、ハルシネーションを大幅に抑制し、回答の精度を高めます。
- 計画と反復 (Planning & Iteration): エージェントは、単一の検索・生成ではなく、タスクを達成するための計画を立て、必要に応じて複数回検索や推論を繰り返します。
- ツール利用 (Tool Use): ベクトルストア検索だけでなく、Web検索(Tavily Searchなど)、API呼び出し、コード実行など、多様なツールを状況に応じて使い分けます。
- 自己評価と修正 (Self-Correction & Evaluation): 検索結果や生成された回答の品質をエージェント自身が評価し、不十分であればクエリを再定式化したり、別のツールを使ったりして修正を試みます。これはGoogle Researchが提唱する「Sufficient Context」の概念にも通じ、十分な証拠が揃うまで検索を続けることで、標準RAGよりも最大34%精度が向上するという研究結果もあります(ICLR 2025論文)。
- メモリ (Memory): 過去の対話履歴や学習した知識を記憶し、より文脈に即した推論と行動を可能にします。
これにより、Agentic RAGは「与えられた情報で回答する」だけでなく、「信頼できる回答を生成するために何をすべきか」を自律的に判断し実行できるため、複雑な情報探索やハルシネーション対策に非常に強力なアプローチとなります。
Agentic RAGの評価駆動開発:3つの実践アプローチ
Agentic RAGを導入し、ハルシネーションを抑制しながら精度を高めるためには、評価駆動開発が不可欠です。ここでは、具体的な3つの実践アプローチと実装例を紹介します。
1. LangGraphとReActエージェントによる柔軟なワークフロー構築
Agentic RAGの核となるのは、エージェントが計画(Reasoning)と行動(Acting)を繰り返すReActフレームワークと、そのプロセスをオーケストレーションするLangGraphです。
前提環境
- Python 3.9+
langchain==0.1.20langchain-community==0.0.17langchain-openai==0.1.6langgraph==0.0.30chromadb==0.4.24- OpenAI API Key(またはGroqなどの互換LLMサービスAPI Key)
実装例:シンプルなAgentic RAGワークフロー
以下のコードは、ユーザーのクエリに対して、ユーザー情報と社内ポリシーを検索するツールを持つReActエージェントが、LangGraphで定義されたワークフロー内で動く例です。
import os
import operator
from typing import List, Tuple, Annotated, TypedDict
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain.docstore.document import Document
from langchain_community.vectorstores import Chroma
from langchain.tools import tool
from langchain.agents import AgentExecutor, create_react_agent
from langchain import hub
from langgraph.graph import StateGraph, END
# 環境変数の設定 (例: OpenAI APIキー)
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"
# os.environ["TAVILY_API_KEY"] = "YOUR_TAVILY_API_KEY" # Web検索ツールを使う場合
# 1. ベクトルデータベースのセットアップ
# 社内ドキュメントのダミーデータ
documents = [
Document(page_content="会社の休暇ポリシーは年間20日です。病気休暇は別途5日あります。", metadata={"source": "policy_doc"}),
Document(page_content="プロモーションボーナスは、年間売上目標の120%達成で支給されます。", metadata={"source": "hr_doc"}),
Document(page_content="ユーザーAは営業部門に所属しており、入社3年目です。", metadata={"source": "user_db", "user_id": "userA"}),
Document(page_content="ユーザーBは開発部門に所属しており、入社1年目です。", metadata={"source": "user_db", "user_id": "userB"}),
]
embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 最新の埋め込みモデル
vectorstore = Chroma.from_documents(
documents=documents,
embedding=embeddings,
persist_directory="./chroma_db"
)
retriever = vectorstore.as_retriever()
# 2. エージェントツールの定義
@tool
def get_user_context(user_id: str) -> str:
"""ユーザーIDに基づいてユーザーのコンテキスト情報を取得するツール。ユーザーIDは必須。"""
# 実際にはDBやCRMからユーザー情報を取得
for doc in documents:
if doc.metadata.get("user_id") == user_id:
return doc.page_content
return f"User {user_id} の情報は見つかりませんでした。"
@tool
def search_policy(query: str) -> str:
"""会社のポリシー(休暇、ボーナスなど)を検索するツール。検索クエリは必須。"""
# ベクトルストアから関連ポリシーを検索
docs = retriever.invoke(query)
if docs:
return "\n".join([f"Source: {doc.metadata.get('source', 'Unknown')}\nContent: {doc.page_content}" for doc in docs])
else:
return "関連するポリシー情報は見つかりませんでした。"
# Web検索ツールを追加する例 (Tavily Search APIが必要)
# from langchain_community.tools.tavily_research import TavilySearchResults
# tavily_tool = TavilySearchResults(max_results=3)
# tools = [get_user_context, search_policy, tavily_tool]
tools = [get_user_context, search_policy]
# 3. ReActエージェントの作成
# ReActプロンプトのロード
prompt = hub.pull("hwchase17/react") # ReActエージェント用のプロンプト
# LLMの初期化 (ここではgpt-4o-miniを使用)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# ReActエージェントのインスタンス作成
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
# 4. LangGraphを用いたワークフローの構築
class AgentState(TypedDict):
"""LangGraphの状態を定義するTypedDict"""
input: str # ユーザーからの初期入力
chat_history: List[Tuple[str, str]] # 会話履歴 (HumanMessage, AIMessage)
agent_outcome: str # エージェントの最終的な出力または中間結果
intermediate_steps: Annotated[List[Tuple[str, str]], operator.add] # エージェントの思考とツール利用の履歴
def run_agent(state: AgentState) -> dict:
"""エージェントを実行するノード"""
print("---RUN AGENT---")
# agent_executor.invoke の input は、プロンプトテンプレートに依存
# ReActプロンプトは 'input' と 'agent_scratchpad' を期待
result = agent_executor.invoke({
"input": state["input"],
"agent_scratchpad": state["intermediate_steps"] # 以前の思考とツール出力を渡す
})
# agent_executor の出力は辞書形式で、'output' キーに最終回答が含まれることが多い
# または、ツール呼び出しの場合は 'tool_output' など
return {"agent_outcome": result["output"], "intermediate_steps": state["intermediate_steps"]}
def decide_to_stop(state: AgentState) -> str:
"""エージェントが停止するか、続行するかを判断するノード"""
print("---DECIDE TO STOP---")
# エージェントが最終的な回答を生成したか、または停止条件を満たしたかを判断するロジック
# ここでは、agent_outcomeが最終回答と見なせるかを簡易的に判断
# より堅牢な停止条件は、エージェントの出力構造や特定のキーワードに依存する
if "final answer:" in state["agent_outcome"].lower(): # ReActプロンプトは通常"Final Answer:"で終了
return "end"
else:
# エージェントがまだツールを呼び出す必要がある、または思考を続ける必要がある場合
return "continue"
# ワークフローの定義
workflow = StateGraph(AgentState)
workflow.add_node("agent", run_agent)
# 条件付きエッジ: agentノードの出力に基づいて、agentノードに戻るか、ENDに到達するかを決定
workflow.add_conditional_edge("agent", decide_to_stop, {"continue": "agent", "end": END})
workflow.set_entry_point("agent")
app = workflow.compile()
# クエリの実行例
print("\n---実行例1: ユーザーAの休暇ポリシー---")
result1 = app.invoke({"input": "ユーザーAの休暇ポリシーについて教えてください。", "chat_history": [], "intermediate_steps": []})
print(f"最終結果1: {result1['agent_outcome']}")
print("\n---実行例2: プロモーションボーナス---")
result2 = app.invoke({"input": "プロモーションボーナスはどのような条件で支給されますか?", "chat_history": [], "intermediate_steps": []})
print(f"最終結果2: {result2['agent_outcome']}")
print("\n---実行例3: 存在しないユーザー---")
result3 = app.invoke({"input": "ユーザーCの部署はどこですか?", "chat_history": [], "intermediate_steps": []})
print(f"最終結果3: {result3['agent_outcome']}")
この実装では、decide_to_stop関数がエージェントの出力に"final answer:"というキーワードが含まれているかをチェックすることで、停止条件を判断しています。この柔軟なワークフローにより、エージェントは状況に応じて最適な行動を選択し、ハルシネーションのリスクを軽減します。
2. 評価駆動で「十分なコンテキスト」を判断し、幻覚を抑制する
ハルシネーションの主な原因の一つは、LLMが不十分な情報に基づいて回答を生成することです。これを防ぐためには、エージェントが「十分なコンテキストが揃ったか」を判断し、必要に応じて追加の検索や情報収集を行うメカニズムが重要です。
実践アプローチ:評価指標の導入
RAGシステムにおける評価には、RAGASのようなライブラリが非常に強力です。RAGASは以下の主要な指標でRAGシステムの品質を定量的に評価できます。
- 忠実度 (Faithfulness): 生成された回答が、与えられたコンテキストに基づいて事実と合致しているか。ハルシネーション対策に直結します。
- 回答の関連性 (Answer Relevance): 生成された回答が、ユーザーの質問にどれだけ関連しているか。
- コンテキストの関連性 (Context Relevance): 検索されたコンテキストが、ユーザーの質問にどれだけ関連しているか。
- コンテキストの網羅性 (Context Recall): 検索されたコンテキストが、回答に必要な情報をどれだけ含んでいるか。
これらの指標をAgentic RAGの評価ループに組み込むことで、システムは自己修正能力を高められます。
実装例:RAGASを用いた評価の組み込み(概念)
LangGraphのワークフローにRAGASを用いた評価ステップを直接組み込むのは複雑ですが、開発段階でのオフライン評価や、生成された回答の品質をチェックする「評価エージェント」として組み込むことは可能です。
例えば、decide_to_stopノードのロジックを拡張し、生成されたagent_outcomeを評価エージェントに渡し、その忠実度スコアが閾値以下であれば再度検索・生成を促す、といった実装が考えられます。
# RAGASのインストール
# pip install ragas
# from ragas.metrics import faithfulness, answer_relevance, context_recall, context_precision
# from ragas import evaluate
# ... (AgentState, run_agent の定義は上記と同じ) ...
def evaluate_and_decide(state: AgentState) -> str:
"""生成された回答を評価し、停止するか、改善のために続行するかを判断するノード"""
print("---EVALUATE AND DECIDE---")
final_answer = state["agent_outcome"]
input_query = state["input"]
retrieved_context = " ".join([step[1] for step in state["intermediate_steps"] if "search_policy" in step[0]]) # 例としてポリシー検索結果をコンテキストと見なす
# ここにRAGASなどの評価ロジックを組み込む
# 例: 簡易的なキーワードベースの忠実度チェック (RAGASを使う場合はもっと堅牢)
is_faithful = "policy" in final_answer.lower() and "day" in final_answer.lower() if "休暇" in input_query else True
if "final answer:" in final_answer.lower() and is_faithful:
print("評価: 回答は最終的かつ忠実と判断。停止します。")
return "end"
else:
print("評価: 回答が不十分または忠実でない可能性。続行します。")
# ここでエージェントに「なぜ不十分だったか」をフィードバックし、
# クエリの再定式化や別のツール利用を促すプロンプトを agent_scratchpad に追加することも可能
return "continue"
# ワークフローの再定義(decide_to_stop を evaluate_and_decide に置き換え)
workflow_with_eval = StateGraph(AgentState)
workflow_with_eval.add_node("agent", run_agent)
workflow_with_eval.add_conditional_edge("agent", evaluate_and_decide, {"continue": "agent", "end": END})
workflow_with_eval.set_entry_point("agent")
app_with_eval = workflow_with_eval.compile()
print("\n---実行例4: 評価駆動での改善(架空のシナリオ)---")
# このシナリオでは、agent_executorが"Final Answer:"を出すが、
# evaluate_and_decideが「忠実でない」と判断し、再度agentノードに戻るような挙動を想定
result4 = app_with_eval.invoke({"input": "会社の休暇ポリシーについて教えてください。", "chat_history": [], "intermediate_steps": []})
print(f"最終結果4: {result4['agent_outcome']}")
この例は概念的なものですが、evaluate_and_decideノードを導入することで、エージェントが自己評価を行い、ハルシネーションのリスクを低減するサイクルを回せるようになります。
3. セマンティックチャンキングとParent Document Retrieverによる検索品質向上
ハルシネーションは、検索結果の品質が低いことにも起因します。不適切なチャンキングは、検索の関連性を低下させ、LLMに不完全なコンテキストを与えることになります。
実践アプローチ:チャンキング戦略の最適化
- Semantic Chunking: 文の埋め込みベクトル間の類似度に基づいてチャンクを分割することで、意味的なまとまりを維持します。これにより、検索時に意味的に関連性の高いチャンクが取得されやすくなります。
- Parent Document Retriever: 小さなチャンク(通常、数文〜段落)を検索し、その小さなチャンクの「親ドキュメント」(より大きなコンテキスト、例えばセクション全体や元のドキュメント全体)をLLMに渡す手法です。これにより、検索の粒度を細かく保ちつつ、LLMには十分なコンテキストを提供できます。
実装例:Parent Document Retrieverの利用(概念)
# Parent Document Retriever のインストール
# pip install langchain-text-splitters
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 1. ドキュメントの準備
# ここでは、より長いドキュメントを想定
long_documents = [
Document(page_content="""
## 会社の休暇ポリシー
当社の年次有給休暇は、正社員に対し年間20日が付与されます。入社初年度は入社月に応じて按分され、2年目以降は毎年1月1日に付与されます。
病気休暇は、別途年間5日まで取得可能です。これは有給休暇とは別に管理され、医師の診断書が必要です。
慶弔休暇や介護休暇などの特別休暇も用意しており、詳細は社内ポータルサイトをご確認ください。
## プロモーションボーナス規定
プロモーションボーナスは、個人の年間売上目標の120%達成、またはチームの年間目標達成に大きく貢献した場合に支給されます。
評価は毎年12月に行われ、翌年1月に支給されます。詳細な評価基準は人事部までお問い合わせください。
""", metadata={"source": "company_handbook"}),
# 他の長いドキュメント...
]
# 2. 親ドキュメントと子ドキュメントの分割器
# 親ドキュメント(大まかな単位)
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=200)
# 子ドキュメント(検索用の小さな単位)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=50)
# 3. インメモリのストアとベクトルストアの初期化
# ドキュメントの内容を保存するストア(InMemoryStoreは開発用、本番ではPersistentなDBを)
store = InMemoryStore()
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(embedding_function=embeddings, persist_directory="./chroma_db_parent")
# 4. ParentDocumentRetrieverの作成
parent_document_retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)
# ドキュメントの追加
parent_document_retriever.add_documents(long_documents)
# 検索例
# 実際のAgentic RAGでは、このリトリーバーをツールとしてエージェントに渡す
print("\n---Parent Document Retriever 検索例---")
query = "病気休暇の条件について教えてください。"
retrieved_docs = parent_document_retriever.invoke(query)
print(f"検索された親ドキュメント数: {len(retrieved_docs)}")
for doc in retrieved_docs:
print(f"--- Source: {doc.metadata.get('source', 'Unknown')} ---")
print(doc.page_content[:200] + "...") # 一部表示
Parent Document Retrieverを使用することで、エージェントはまず詳細な情報を検索し、その詳細情報を含むより大きなコンテキストをLLMに提示できるようになります。これにより、LLMは必要な情報を見つけやすくなり、文脈を失わずに正確な回答を生成できるため、ハルシネーションの抑制に貢献します。
よくあるエラー・ハマりどころと回避策
Agentic RAGの実装は強力である反面、いくつかの共通の課題に直面しやすいです。
1. エージェントの無限ループ
ハマりどころ: decide_to_stopのような停止条件が曖昧だったり、ツールが常に何らかの出力を返し、エージェントが「最終回答」と認識できない場合に発生します。
回避策:
- 明確な停止条件をプロンプトで指示: エージェントのプロンプトに「最終回答を生成する際は'Final Answer:'で始めること」のように明示的な指示を含めます。
-
LangGraphでのループ回数制限:
StateGraphの状態にカウンタを含め、特定のノードの実行回数を制限し、上限を超えたらエラー終了させるロジックを追加します。 - 「十分なコンテキスト」判断の強化: GoogleのSufficient Contextのように、エージェントの思考プロセスに「十分な情報が揃ったか」を判断するステップを組み込みます。
2. 不適切なチャンキングと検索品質の低下
ハマりどころ: 固定長や単純な区切り文字によるチャンキングは、意味のある文脈を分断したり、関連性の低い情報を混入させたりして、検索の精度を著しく低下させます。
回避策:
-
セマンティックなチャンキング戦略:
-
RecursiveCharacterTextSplitter: 区切り文字のリストを定義し、意味のある単位で分割を試みます。 - Semantic Chunking: 文の埋め込みベクトル間の類似度に基づいてチャンクを分割し、意味的なまとまりを維持します。
- Parent Document Retriever: 上記の例のように、検索には小さなチャンクを使い、LLMにはより大きな親ドキュメントを渡します。
-
- チャンキング戦略の評価: RAGASなどのツールで異なるチャンキング戦略を評価し、最もパフォーマンスの良いものを選定します。
3. 高レイテンシと計算コスト
ハマりどころ: Agentic RAGは、計画、複数回の検索、評価、自己修正といった多くのステップを含むため、従来のRAGよりもレイテンシが増加し、LLMのトークン使用量も増えがちです。
回避策:
- 効率的なLLMの選択: タスクの複雑度に応じて、GPT-4o-miniやLlama 3 8Bなどの高速でコスト効率の良いLLMを選択します。Groqのような高速推論サービスも検討の価値があります。
- 積極的なキャッシュ: 以前の検索結果、エンベディング、LLMの推論結果などをキャッシュし、重複する計算を避けます。
- 早期終了条件の最適化: 十分な情報が得られたと判断された場合、不必要な追加ステップをスキップして早期に回答を生成するロジックを強化します。
- 並列処理: 可能な限り、複数のエージェントやツール呼び出しを並列で実行し、全体の処理時間を短縮します。
設計上のトレードオフとベストプラクティス
Agentic RAGを実運用に導入する際には、設計上のトレードオフを理解し、ベストプラクティスを適用することが重要です。
設計上のトレードオフ
- 複雑なクエリへの対応 vs レイテンシとコスト: Agentic RAGは複雑なクエリに強いですが、その分レイテンシとトークン使用量が増えます。シンプルな事実検索には、従来のRAGの方が効率的です。
- 汎用性 vs 特化性: 汎用的なツールは多くのクエリに対応できますが、コストとレイテンシが増え、強力なLLMが必要です。特定のタスクに特化したツールは効率的ですが、適用範囲が限定されます。
- 精度 vs 速度: 評価駆動の反復やファクトチェックは精度を高めますが、処理時間を長くします。リアルタイム性が求められるアプリケーションでは、許容できるレイテンシとのバランスを取る必要があります。
ベストプラクティス
- 段階的な導入: まず従来のRAGパイプラインを構築し、それが解決できない複雑な問題に対してのみAgentic RAGを導入します。シンプルな問題にはシンプルな解決策を。
- モジュール化されたアーキテクチャ: クエリ分析、検索、合成、評価といった役割をそれぞれ専用のエージェントやモジュールに分離します。LangGraphはこのようなモジュール化された設計を強力にサポートします。
- 評価駆動の開発: RAGASなどのツールを用いて、回答の忠実度、関連性、コンテキストの網羅性などを定量的に評価し、その結果に基づいてシステムを継続的に改善するサイクルを回します。
- 信頼度の追跡: LLMの出力に信頼度スコアを付与したり、参照ドキュメントの数を考慮したりするメカニズムを導入し、自信を持って誤った回答を避けます。
- メモリの活用: 短期記憶(現在のセッション)と長期記憶(永続的な知識)をエージェントに組み込み、より洗練された推論を可能にします。
- データ品質の重視: RAGの基盤となる知識ベースのデータ品質を最優先します。正確で最新のデータが、ハルシネーション対策の最も基本的な要素です。
まとめ
本記事では、RAGシステムにおけるハルシネーション問題に対し、Agentic RAGの導入と評価駆動での改善サイクルに焦点を当て、以下の3つの実践的なアプローチを解説しました。
- LangGraphとReActエージェントによる柔軟なワークフロー構築: エージェントが計画・行動・反復することで、複雑なクエリに対応し、ハルシネーションのリスクを軽減します。
- 評価駆動で「十分なコンテキスト」を判断し、幻覚を抑制: RAGASなどの評価ツールを導入し、生成された回答の品質を自己評価・修正するメカニズムを構築します。
- セマンティックチャンキングとParent Document Retrieverによる検索品質向上: 適切なチャンキング戦略と検索手法により、LLMに与えるコンテキストの質を高め、ハルシネーションを根本から抑制します。
Agentic RAGは、RAGシステムの精度と信頼性を飛躍的に向上させる可能性を秘めています。この記事で紹介した具体的な実装例やベストプラクティスを参考に、ぜひご自身のプロジェクトでAgentic RAGを導入し、評価駆動で精度を高めるサイクルを回してみてください。
さらなる詳細については、LangChain公式ドキュメント や LangGraph公式ドキュメント を参照し、最新の情報をキャッチアップすることをお勧めします。