CRAG・Self-RAGで実装する自己修正RAGパイプライン実践ガイド
この記事でわかること
- CRAG(Corrective RAG)とSelf-RAGの理論的背景と両手法の違い
- LangGraphのStateGraphを使った自己修正RAGパイプラインの実装方法
- 文書関連性評価・クエリ変換・ハルシネーション検出の具体的な実装パターン
- RAGASフレームワークによるパイプライン品質評価の導入方法
- 本番運用を見据えたAdaptive RAGへの拡張設計
対象読者
- 想定読者: RAGシステムの構築経験があり、回答精度の改善に取り組むMLエンジニア
-
必要な前提知識:
- Python 3.11+の基礎文法
- LangChain / LangGraphの基本的な使い方
- ベクトルデータベース(Chroma、Pinecone等)の概念理解
- Retrieval-Augmented Generationの基本フロー
結論・成果
CRAG論文の報告によると、標準RAGと比較してPopQAで+7.0%、PubHealthで+36.6%、ARC-Challengeで+15.4%の精度改善が確認されています。Self-RAGとCRAGを組み合わせたパイプラインでは、検索品質の事前評価(CRAG)と生成品質の事後検証(Self-RAG)の両方をカバーでき、ハルシネーション率を大幅に削減できると報告されています。本記事では、この2つのアプローチをLangGraphで統合実装する手順を解説します。
CRAGとSelf-RAGの理論を理解する
CRAGの仕組み:検索結果を「使う前に」評価する
標準的なRAGは検索された文書をそのまま生成モデルに渡します。文書が無関係でも、古くても、そのまま使ってしまう点が根本的な弱点です。
CRAG(Corrective Retrieval Augmented Generation)は、Shi-Qi Yanらが2024年に提案した手法で、検索結果を生成に使う前に評価・修正するアプローチです。核となるのは以下の3つのコンポーネントです。
1. 軽量検索評価器(Retrieval Evaluator)
T5-Large(770Mパラメータ)をファインチューニングした軽量モデルで、検索文書の関連性を3段階で判定します。
| 判定結果 | 条件 | アクション |
|---|---|---|
| Correct | 信頼度が上位閾値を超える | 知識精製へ進む |
| Incorrect | 信頼度が下位閾値を下回る | 全文書を破棄しWeb検索 |
| Ambiguous | 上記の中間 | Web検索で補完 |
2. 知識ストリップ分解・再構成アルゴリズム
文書全体を使うのではなく、細かい「知識ストリップ」に分解し、各ストリップの関連性を個別評価します。関連性の高いストリップのみを連結して再構成することで、ノイズを除去した精製済みコンテキストを生成器に渡します。
3. Web検索フォールバック
ローカルコーパスの検索結果が不十分な場合、Web検索(Tavily API等)に自動切り替えします。検索クエリはLLMで最適化してから実行します。
Self-RAGの仕組み:生成プロセスを「自己批評」する
Self-RAG(Asai et al., ICLR 2024)は、モデル自身が検索・生成・批評の各段階を制御するフレームワークです。4種類のリフレクショントークンを学習時に組み込むことで、推論時に適応的な判断を行います。
| トークン | 役割 | 判定内容 |
|---|---|---|
| Retrieve | 検索必要性判断 | この時点で外部検索が必要か |
| IsRel | 関連性評価 | 検索文書がクエリに関連しているか |
| IsSup | 根拠性評価 | 生成テキストが文書に根拠づけられているか |
| IsUse | 有用性評価 | 最終回答がクエリに対して有用か |
従来のRAGが固定回数の検索を行うのに対し、Self-RAGはRetrieveトークンの確率に基づいて検索頻度を動的に調整できます。検索が不要な場合はスキップし、複数回の検索が必要な場合はループします。
両手法の相補性
CRAGは検索品質の事前保証(入力側の修正)、Self-RAGは生成品質の事後検証(出力側の修正)に強みがあります。CRAG論文では「plug-and-play」としてSelf-RAGとの組み合わせ実験も行われており、組み合わせることで両方の利点を享受できます。
LangGraphで自己修正RAGパイプラインを実装する
環境構築とプロジェクト設定
# pyproject.toml の依存関係
# [project]
# dependencies = [
# "langgraph>=0.2.0",
# "langchain-openai>=0.2.0",
# "langchain-community>=0.3.0",
# "chromadb>=0.5.0",
# "tavily-python>=0.5.0",
# "ragas>=0.2.0",
# ]
pip install langgraph langchain-openai langchain-community chromadb tavily-python ragas
グラフ状態の定義
LangGraphのStateGraphでは、パイプライン全体の状態をTypedDictで定義します。CRAGとSelf-RAGの両方のロジックを統合するため、標準的な3フィールドに加えて検索品質フラグを追加します。
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class GraphState(TypedDict):
question: str
documents: list[str]
generation: str
web_search_needed: bool
retry_count: int
retry_countは無限ループ防止のためのガードです。Self-RAGのフィードバックループで最大3回まで再試行し、それ以上は現在の最良回答を返します。
文書検索ノード
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
embedding = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(
collection_name="knowledge_base",
embedding_function=embedding,
persist_directory="./chroma_db",
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
def retrieve(state: GraphState) -> GraphState:
"""ベクトルストアから関連文書を検索する"""
question = state["question"]
documents = retriever.invoke(question)
return {
**state,
"documents": [doc.page_content for doc in documents],
"web_search_needed": False,
}
CRAG: 文書関連性評価ノード
CRAGの核心となる文書評価を実装します。本来はT5-Largeをファインチューニングしますが、実用上はLLMによる構造化出力で十分な精度が得られます。
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class RelevanceScore(BaseModel):
"""文書の関連性スコア"""
is_relevant: bool = Field(description="文書がクエリに関連しているか")
confidence: float = Field(description="信頼度 0.0-1.0", ge=0.0, le=1.0)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
relevance_grader = llm.with_structured_output(RelevanceScore)
GRADING_PROMPT = """あなたは検索文書の関連性を評価する専門家です。
以下の文書がユーザーの質問に関連するキーワードや意味的つながりを含むか判定してください。
質問: {question}
文書: {document}
関連性があれば is_relevant=True、なければ False を返してください。
判断の確信度を confidence (0.0-1.0) で示してください。"""
def grade_documents(state: GraphState) -> GraphState:
"""各文書の関連性を評価し、不適格文書をフィルタリングする"""
question = state["question"]
documents = state["documents"]
relevant_docs = []
for doc in documents:
result = relevance_grader.invoke(
GRADING_PROMPT.format(question=question, document=doc)
)
if result.is_relevant and result.confidence >= 0.5:
relevant_docs.append(doc)
# 関連文書が全体の30%未満ならWeb検索が必要
web_search_needed = len(relevant_docs) < len(documents) * 0.3
return {
**state,
"documents": relevant_docs if relevant_docs else documents,
"web_search_needed": web_search_needed,
}
なぜLLMベースの評価器を選んだか:
- T5-Largeのファインチューニングには大量のラベル付きデータが必要
- GPT-4o-miniの構造化出力は十分な判定精度を持ちつつ、コストが低い(入力$0.15/1Mトークン)
- Pydanticによる型安全な出力が得られる
注意点:
LLMベースの評価器はレイテンシが追加されます。文書数が多い場合(10件以上)は、並列実行(
asyncio.gather)またはバッチ処理を検討してください。評価器のレイテンシ(1文書あたり約200-500ms)が全体のボトルネックになりやすい点に注意が必要です。
Web検索フォールバックノード
from tavily import TavilyClient
tavily_client = TavilyClient()
def web_search(state: GraphState) -> GraphState:
"""Web検索で文書を補完する"""
question = state["question"]
# Tavily APIで関連性の高いWeb結果を取得
search_results = tavily_client.search(
query=question,
max_results=3,
search_depth="advanced",
)
web_docs = [result["content"] for result in search_results["results"]]
# 既存の関連文書にWeb検索結果を追加
combined_docs = state["documents"] + web_docs
return {
**state,
"documents": combined_docs,
"web_search_needed": False,
}
クエリ変換ノード
検索結果が不十分な場合、クエリ自体を最適化して再検索します。
REWRITE_PROMPT = """あなたはクエリ最適化の専門家です。
以下の質問をベクトル検索に最適化された形に書き換えてください。
意味は保ちつつ、より具体的なキーワードを含む形にしてください。
元の質問: {question}
最適化された質問:"""
def transform_query(state: GraphState) -> GraphState:
"""クエリを最適化して再検索に備える"""
question = state["question"]
retry_count = state.get("retry_count", 0)
optimized = llm.invoke(REWRITE_PROMPT.format(question=question))
return {
**state,
"question": optimized.content,
"retry_count": retry_count + 1,
}
回答生成ノード
from langchain_core.prompts import ChatPromptTemplate
RAG_PROMPT = ChatPromptTemplate.from_messages([
("system", "以下のコンテキストに基づいて質問に回答してください。"
"コンテキストに含まれない情報は推測せず、"
"「情報が不足しています」と回答してください。"),
("human", "コンテキスト:\n{context}\n\n質問: {question}"),
])
generation_llm = ChatOpenAI(model="gpt-4o", temperature=0.1)
def generate(state: GraphState) -> GraphState:
"""精製済み文書から回答を生成する"""
question = state["question"]
documents = state["documents"]
context = "\n\n---\n\n".join(documents)
response = generation_llm.invoke(
RAG_PROMPT.format(context=context, question=question)
)
return {
**state,
"generation": response.content,
}
Self-RAG: ハルシネーション検出ノード
生成された回答が検索文書に根拠づけられているか(IsSup相当)を検証します。
class HallucinationCheck(BaseModel):
"""ハルシネーション検出結果"""
is_grounded: bool = Field(
description="生成テキストが提供された文書に根拠づけられているか"
)
unsupported_claims: list[str] = Field(
default_factory=list,
description="文書に根拠のない主張のリスト"
)
hallucination_grader = llm.with_structured_output(HallucinationCheck)
HALLUCINATION_PROMPT = """あなたはファクトチェッカーです。
LLMの生成テキストが、提供された文書セットに根拠づけられているか評価してください。
文書セット:
{documents}
生成テキスト:
{generation}
生成テキスト内の各主張が文書に裏付けられているか確認し、
根拠のない主張があれば列挙してください。"""
def check_hallucination(state: GraphState) -> GraphState:
"""生成テキストのハルシネーションを検出する"""
documents = state["documents"]
generation = state["generation"]
result = hallucination_grader.invoke(
HALLUCINATION_PROMPT.format(
documents="\n---\n".join(documents),
generation=generation,
)
)
if not result.is_grounded:
# 根拠なし:再生成のためretry_countを増加
return {
**state,
"retry_count": state.get("retry_count", 0) + 1,
}
return state
Self-RAG: 回答有用性評価ノード
class UsefulnessCheck(BaseModel):
"""回答有用性の評価"""
is_useful: bool = Field(description="回答がユーザーの質問に有用か")
reason: str = Field(description="判定理由")
usefulness_grader = llm.with_structured_output(UsefulnessCheck)
USEFULNESS_PROMPT = """あなたは回答品質の評価者です。
以下の回答がユーザーの質問に対して有用で適切か評価してください。
質問: {question}
回答: {generation}
質問に直接答えており、具体的で実用的な情報を含んでいれば有用と判定してください。"""
def check_usefulness(state: GraphState) -> GraphState:
"""回答の有用性を評価する"""
result = usefulness_grader.invoke(
USEFULNESS_PROMPT.format(
question=state["question"],
generation=state["generation"],
)
)
if not result.is_useful:
return {
**state,
"retry_count": state.get("retry_count", 0) + 1,
}
return state
ルーティングロジックの定義
各ノード間の条件分岐を定義します。
def route_after_grading(state: GraphState) -> str:
"""文書評価後のルーティング"""
if state["web_search_needed"]:
return "web_search"
if not state["documents"]:
return "transform_query"
return "generate"
def route_after_hallucination_check(state: GraphState) -> str:
"""ハルシネーション検出後のルーティング"""
retry_count = state.get("retry_count", 0)
generation = state.get("generation", "")
# 最大リトライ回数に達したら現在の回答を返す
if retry_count >= 3:
return "end"
# ハルシネーション検出で再生成が必要な場合
if not generation:
return "generate"
return "check_usefulness"
def route_after_usefulness_check(state: GraphState) -> str:
"""有用性評価後のルーティング"""
retry_count = state.get("retry_count", 0)
if retry_count >= 3:
return "end"
# 有用でない場合はクエリ変換から再実行
return "transform_query" if retry_count > state.get("_prev_retry", 0) else "end"
グラフの組み立てと実行
def build_self_correcting_rag() -> StateGraph:
"""CRAG + Self-RAGの統合パイプラインを構築する"""
workflow = StateGraph(GraphState)
# ノードの追加
workflow.add_node("retrieve", retrieve)
workflow.add_node("grade_documents", grade_documents)
workflow.add_node("web_search", web_search)
workflow.add_node("transform_query", transform_query)
workflow.add_node("generate", generate)
workflow.add_node("check_hallucination", check_hallucination)
workflow.add_node("check_usefulness", check_usefulness)
# エッジの定義
workflow.add_edge(START, "retrieve")
workflow.add_edge("retrieve", "grade_documents")
# CRAG: 文書評価後の条件分岐
workflow.add_conditional_edges(
"grade_documents",
route_after_grading,
{
"web_search": "web_search",
"transform_query": "transform_query",
"generate": "generate",
},
)
workflow.add_edge("web_search", "generate")
workflow.add_edge("transform_query", "retrieve")
# Self-RAG: 生成後の検証ループ
workflow.add_edge("generate", "check_hallucination")
workflow.add_conditional_edges(
"check_hallucination",
route_after_hallucination_check,
{
"generate": "generate",
"check_usefulness": "check_usefulness",
"end": END,
},
)
workflow.add_conditional_edges(
"check_usefulness",
route_after_usefulness_check,
{
"transform_query": "transform_query",
"end": END,
},
)
return workflow.compile()
# パイプラインの実行
app = build_self_correcting_rag()
result = app.invoke({
"question": "LangGraphでCRAGを実装する方法は?",
"documents": [],
"generation": "",
"web_search_needed": False,
"retry_count": 0,
})
print(result["generation"])
RAGASによるパイプライン品質評価を導入する
パイプラインの改善効果を定量的に測定するには、RAGASフレームワークが有用です。RAGASは2026年時点で最も広く採用されているオープンソースRAG評価フレームワークです。
評価メトリクスの概要
| メトリクス | 評価対象 | 計算方法 | 目標値 |
|---|---|---|---|
| Context Precision | 検索精度 | 上位k件中の関連文書率 | > 0.8 |
| Context Recall | 検索網羅性 | 正解に必要な情報のカバー率 | > 0.7 |
| Faithfulness | 忠実度 | 生成文中の主張が文書に根拠を持つ割合 | > 0.9 |
| Answer Relevance | 回答関連性 | 回答が質問に対して的確な割合 | > 0.8 |
RAGASの実装例
from ragas import evaluate
from ragas.metrics import (
context_precision,
context_recall,
faithfulness,
answer_relevancy,
)
from datasets import Dataset
def evaluate_pipeline(
questions: list[str],
ground_truths: list[str],
pipeline_app,
) -> dict:
"""パイプラインの品質をRAGASで評価する"""
results = []
for q, gt in zip(questions, ground_truths):
output = pipeline_app.invoke({
"question": q,
"documents": [],
"generation": "",
"web_search_needed": False,
"retry_count": 0,
})
results.append({
"question": q,
"answer": output["generation"],
"contexts": output["documents"],
"ground_truth": gt,
})
dataset = Dataset.from_list(results)
scores = evaluate(
dataset,
metrics=[
context_precision,
context_recall,
faithfulness,
answer_relevancy,
],
)
return scores.to_pandas().mean().to_dict()
標準RAGとの比較評価
自己修正パイプラインの効果を測定するには、同じデータセットで標準RAG(検索→即生成)と比較します。
def build_naive_rag() -> StateGraph:
"""比較用:標準RAGパイプライン"""
workflow = StateGraph(GraphState)
workflow.add_node("retrieve", retrieve)
workflow.add_node("generate", generate)
workflow.add_edge(START, "retrieve")
workflow.add_edge("retrieve", "generate")
workflow.add_edge("generate", END)
return workflow.compile()
# 比較実行
naive_scores = evaluate_pipeline(test_questions, test_gt, build_naive_rag())
corrective_scores = evaluate_pipeline(test_questions, test_gt, app)
print(f"標準RAG Faithfulness: {naive_scores['faithfulness']:.3f}")
print(f"自己修正RAG Faithfulness: {corrective_scores['faithfulness']:.3f}")
注意点:
RAGASの評価自体にLLM呼び出しが必要です。評価コスト(1件あたり約$0.01-0.05)を考慮し、評価データセットは50-100件程度に絞ることを推奨します。CI/CDに組み込む場合は、コスト上限の設定が必須です。
本番運用に向けた拡張設計を検討する
Adaptive RAGによるクエリルーティング
すべてのクエリに自己修正パイプラインを適用するとコストとレイテンシが増大します。Adaptive RAGのアプローチでは、クエリの複雑度に応じて処理戦略を切り替えます。
class QueryComplexity(BaseModel):
"""クエリ複雑度の分類"""
level: str = Field(
description="クエリの複雑度: direct / single_hop / multi_hop"
)
reasoning: str = Field(description="分類理由")
complexity_classifier = llm.with_structured_output(QueryComplexity)
CLASSIFY_PROMPT = """クエリの複雑度を分類してください。
- direct: LLMの内部知識で回答可能(事実確認不要)
- single_hop: 1回の検索で回答可能(単一文書参照)
- multi_hop: 複数の検索・推論ステップが必要
クエリ: {question}"""
def route_by_complexity(state: GraphState) -> str:
"""クエリ複雑度に応じてパイプラインを選択する"""
result = complexity_classifier.invoke(
CLASSIFY_PROMPT.format(question=state["question"])
)
match result.level:
case "direct":
return "direct_answer" # 検索なしで回答
case "single_hop":
return "simple_rag" # 標準RAG
case "multi_hop":
return "corrective_rag" # 自己修正RAG
case _:
return "corrective_rag"
このルーティングにより、単純なクエリには高速な標準RAGを、複雑なクエリにのみ自己修正パイプラインを適用できます。論文の報告では、40-60%の処理効率改善が見込めるとされています。
よくある問題と解決方法
| 問題 | 原因 | 解決方法 |
|---|---|---|
| 無限ループに陥る | retry_count制限の欠如 |
retry_count >= 3 でループ脱出 |
| 評価器が常にIrrelevantを返す | プロンプトの閾値が厳しすぎる | confidence閾値を0.3に下げるか、プロンプトを緩和 |
| Web検索結果が低品質 | 検索クエリの最適化不足 | クエリ変換を2段階(要約→展開)に |
| レイテンシが5秒を超える | 直列LLM呼び出しが多い | 文書評価の並列化(asyncio) |
| ハルシネーション検出の偽陽性 | 文書外の一般知識を「根拠なし」と判定 | 「常識的知識は許容」をプロンプトに追加 |
パフォーマンス最適化のパターン
import asyncio
from typing import Coroutine
async def grade_documents_parallel(state: GraphState) -> GraphState:
"""文書評価を並列実行してレイテンシを削減する"""
question = state["question"]
documents = state["documents"]
async def grade_single(doc: str) -> tuple[str, bool]:
result = await relevance_grader.ainvoke(
GRADING_PROMPT.format(question=question, document=doc)
)
return doc, result.is_relevant and result.confidence >= 0.5
tasks: list[Coroutine] = [grade_single(doc) for doc in documents]
results = await asyncio.gather(*tasks)
relevant_docs = [doc for doc, is_relevant in results if is_relevant]
web_search_needed = len(relevant_docs) < len(documents) * 0.3
return {
**state,
"documents": relevant_docs if relevant_docs else documents,
"web_search_needed": web_search_needed,
}
直列実行では5文書の評価に約2秒かかるところ、並列実行では約500msに短縮できます。
まとめと次のステップ
まとめ:
- CRAGは検索結果を3段階(Correct/Incorrect/Ambiguous)で評価し、不十分な場合はWeb検索でフォールバックする
- Self-RAGは4種のリフレクショントークンで生成品質を自己検証し、ハルシネーションを検出・修正する
- 両手法をLangGraphのStateGraphで統合することで、検索品質の事前保証と生成品質の事後検証を実現できる
- RAGASフレームワークでFaithfulness/Context Precision等の定量評価が可能
- Adaptive RAGのクエリルーティングと組み合わせることで、コストとレイテンシのバランスを最適化できる
次にやるべきこと:
- 自社のドメインデータでベクトルストアを構築し、パイプラインの動作確認を行う
- RAGASで標準RAGとの比較評価を実施し、改善幅を定量化する
- LangSmithでトレースを可視化し、ボトルネックノードを特定する
参考
- Corrective Retrieval Augmented Generation (arXiv:2401.15884)
- Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection
- Self-Reflective RAG with LangGraph - LangChain Blog
- Corrective RAG (CRAG) Implementation With LangGraph - DataCamp
- Self-RAG: A Guide With LangGraph Implementation - DataCamp
- RAGAS Available Metrics - 公式ドキュメント
- RAG in 2025-2026: State of the Art
- Lightweight Query Routing for Adaptive RAG (arXiv:2604.03455)
注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。