Home ブログ解説: LangChain Self-Reflective RAG with LangGraph — 条件付きエッジで実現する自己修正型検索生成
投稿
キャンセル

✍️ ブログ解説: LangChain Self-Reflective RAG with LangGraph — 条件付きエッジで実現する自己修正型検索生成

ブログ概要(Summary)

LangChainのAnkush Golaが2024年2月に公開した「Self-Reflective RAG with LangGraph」は、RAGパイプラインに自己反省ループを組み込む実装パターンを解説したテックブログです。LangGraphの状態グラフ(StateGraph)と条件付きエッジ(conditional edges)を活用し、Document Grading → Query Rewriting → Answer Generationの3段階フローを実装しています。Corrective RAGとSelf-RAGの概念をファインチューニング不要でLangGraph上に再現する実践的なアプローチが特徴です。

この記事は Zenn記事: LangGraph×Claude Sonnet 4.6エージェント型RAGの精度評価と最適化 の深掘りです。

情報源

技術的背景(Technical Background)

RAGの3つの認知アーキテクチャ

ブログでは、RAGパイプラインを3つの認知アーキテクチャに分類しています。

Chain(チェイン型): 最も単純なパターンで、検索→生成の固定パイプラインです。LLMは検索結果に基づいて回答を生成するだけで、検索結果の品質を評価しません。

Routing(ルーティング型): LLMがクエリの種類に応じて異なるリトリーバー(ベクトルDB、SQL、Web検索等)を選択します。検索先の動的選択は行いますが、結果の評価ループはありません。

State Machine(状態機械型): ループとフィードバックをネイティブにサポートするアーキテクチャです。検索結果の品質評価、クエリの書き換え、再検索、ハルシネーション検証を、条件付きの状態遷移として表現します。

Zenn記事のLangGraph実装はこの第3のState Machine型に該当し、ブログで提案されているパターンを実際のプロダクション環境に適用したものです。

なぜLangGraphか

ブログがLangGraphを選択した理由は、条件付きエッジ(conditional edges)によるフロー制御にあります。

従来のLangChain(LCEL: LangChain Expression Language)はDAG(有向非巡回グラフ)のみをサポートしており、ループ構造を表現できませんでした。RAGの自己修正には「検索→評価→不十分なら再検索」のループが不可欠であり、LangGraphのadd_conditional_edges()がこれを実現します。

実装アーキテクチャ(Implementation Architecture)

状態グラフの全体設計

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
                    START
                      │
                      ▼
         ┌─ generate_query_or_respond ──┐
         │            │                  │
         │   (tool_call?)               │
         │    yes ↓        no ──────────┘──→ END
         │            │
         │   retrieve (ToolNode)
         │            │
         │            ▼
         │   grade_documents
         │    │              │
         │  relevant?    not relevant?
         │    ↓              ↓
         │  generate    rewrite_question
         │  _answer          │
         │    │              │
         │    ▼              └────→ generate_query_or_respond
         │   END                     (ループバック)
         └──────────────────────────────────────────────┘

GraphState定義

ブログの公式実装ではMessagesStateを使用し、メッセージリストで全状態を管理します。

1
2
3
4
5
from langgraph.graph import MessagesState, StateGraph, START, END

# MessagesStateはmessagesキーを持つTypedDict
# messages: list[BaseMessage]
workflow = StateGraph(MessagesState)

Zenn記事のGraphStateは、MessagesStateを拡張してretry_countgrade_scoreis_hallucination等の追加フィールドを定義しています。これはプロダクション環境でのデバッグ・モニタリングに必要な設計拡張です。

ノード実装の詳細

1. generate_query_or_respond(エントリポイント)

LLMにリトリーバーツールをバインドし、クエリに応じて検索を実行するか直接回答するかを判断します。

1
2
3
4
5
6
7
8
9
10
11
12
from langchain.chat_models import init_chat_model

response_model = init_chat_model("gpt-4.1", temperature=0)

def generate_query_or_respond(state: MessagesState):
    """LLMがツール呼び出しの要否を判断するノード"""
    response = (
        response_model
        .bind_tools([retrieve_blog_posts])
        .invoke(state["messages"])
    )
    return {"messages": [response]}

設計のポイント: bind_tools()によりLLMがツール呼び出しのJSON構造を生成し、tools_conditionがこれを検出してルーティングを行います。この設計はOpenAI Function Callingの仕組みに依存しています。

Zenn記事では、Claude Sonnet 4.6のAdaptive Thinkingを活用してこの判断を行っており、Function Callingではなくeffortパラメータによる推論深度制御を採用しています。

2. retrieve(検索実行)

1
2
3
4
5
6
7
from langchain.tools import tool

@tool
def retrieve_blog_posts(query: str) -> str:
    """検索ツールの定義。LangGraphのToolNodeが自動的に実行"""
    docs = retriever.invoke(query)
    return "\n\n".join([doc.page_content for doc in docs])

LangGraphのToolNodeがツール呼び出しを自動的にディスパッチし、結果をToolMessageとしてstateに追加します。

3. grade_documents(文書評価 — 条件付きエッジ)

検索文書の関連性を二値分類し、結果に応じてフローを分岐させる条件付きエッジ関数です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from pydantic import BaseModel, Field
from typing import Literal

class GradeDocuments(BaseModel):
    """検索文書の関連性スコア"""
    binary_score: str = Field(
        description="'yes' if relevant, or 'no' if not relevant"
    )

grader_model = init_chat_model("gpt-4.1", temperature=0)

GRADE_PROMPT = (
    "You are a grader assessing relevance of a retrieved document "
    "to a user question."
)

def grade_documents(
    state: MessagesState
) -> Literal["generate_answer", "rewrite_question"]:
    """文書の関連性を評価し、フローを分岐"""
    question = state["messages"][0].content
    context = state["messages"][-1].content

    response = (
        grader_model
        .with_structured_output(GradeDocuments)
        .invoke([{
            "role": "user",
            "content": GRADE_PROMPT.format(
                question=question, context=context
            )
        }])
    )

    if response.binary_score == "yes":
        return "generate_answer"   # → 回答生成へ
    else:
        return "rewrite_question"  # → クエリ書き換えへ

Zenn記事との対応: Zenn記事のDocument Gradingノードはこのgrade_documents関数の拡張版です。Zenn記事では二値分類に加えてスコアリング(0.0-1.0)を行い、閾値ベースの判定で精度を向上させています。

4. rewrite_question(クエリ書き換え)

検索結果が不十分な場合に、元のクエリを意味的に書き換えて再検索を試みます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from langchain.messages import HumanMessage

REWRITE_PROMPT = (
    "Look at the input and try to reason about "
    "the underlying semantic intent / meaning."
)

def rewrite_question(state: MessagesState):
    """セマンティック意図を推論してクエリを書き換え"""
    question = state["messages"][0].content
    response = response_model.invoke([{
        "role": "user",
        "content": REWRITE_PROMPT.format(question=question)
    }])
    return {"messages": [HumanMessage(content=response.content)]}

書き換え後のクエリはHumanMessageとして追加され、generate_query_or_respondノードに戻ることで再検索ループが形成されます。

5. generate_answer(回答生成)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GENERATE_PROMPT = (
    "You are an assistant for question-answering tasks. "
    "Use the following retrieved context to answer the question."
)

def generate_answer(state: MessagesState):
    """検索コンテキストに基づいて回答を生成"""
    question = state["messages"][0].content
    context = state["messages"][-1].content
    response = response_model.invoke([{
        "role": "user",
        "content": GENERATE_PROMPT.format(
            question=question, context=context
        )
    }])
    return {"messages": [response]}

グラフの組み立て

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from langgraph.prebuilt import ToolNode, tools_condition

workflow = StateGraph(MessagesState)

# ノード登録
workflow.add_node(generate_query_or_respond)
workflow.add_node("retrieve", ToolNode([retrieve_blog_posts]))
workflow.add_node(rewrite_question)
workflow.add_node(generate_answer)

# エッジ定義
workflow.add_edge(START, "generate_query_or_respond")
workflow.add_conditional_edges(
    "generate_query_or_respond",
    tools_condition,
    {"tools": "retrieve", END: END}
)
workflow.add_conditional_edges("retrieve", grade_documents)
workflow.add_edge("generate_answer", END)
workflow.add_edge("rewrite_question", "generate_query_or_respond")

graph = workflow.compile()

条件付きエッジの2つの使い方:

  1. tools_condition: LLMのツール呼び出し有無を検出するビルトイン関数(ToolNode向け)
  2. grade_documents: カスタムロジックで次のノードを決定する関数(文字列を返す)

パフォーマンス最適化(Performance Optimization)

ブログで示された最適化パターン

Tavily Search APIによるWeb検索フォールバック: Corrective RAGパターンでは、ベクトルDB検索が不十分な場合にTavily Search APIでWeb検索にフォールバックします。Zenn記事では同様のフォールバック機能をLangGraphの条件付きエッジで実装しています。

Structured Output by Pydantic: with_structured_output(GradeDocuments)により、LLMの出力をPydanticモデルに型安全にパースします。これによりグレーディング結果の信頼性が向上し、パース失敗のハンドリングが不要になります。

LangSmithによるトレーシング: LangSmithを統合することで、各ノードの実行時間、LLM呼び出し回数、トークン使用量を可視化できます。Zenn記事ではDeepEvalとRAGASによる定量評価を行っていますが、LangSmithは開発時のデバッグ・最適化ツールとして相補的に機能します。

Zenn記事での拡張

ブログの基本実装に対して、Zenn記事は以下の拡張を行っています。

観点ブログ実装Zenn記事の拡張
LLMGPT-4.1Claude Sonnet 4.6 + Adaptive Thinking
文書評価二値分類(yes/no)スコアリング(0.0-1.0) + 閾値
ハルシネーション検証なしHallucination Checkノード
再試行制御暗黙的ループretry_countによる明示的制限
評価フレームワークなしRAGAS + DeepEval(Faithfulness 0.91達成)
推論深度制御なしeffortパラメータ最適化(medium/high)

運用での学び(Operational Insights)

条件付きエッジ設計のベストプラクティス

ブログの実装から得られる設計指針:

  1. 条件付きエッジは単一責務にする: grade_documentsは文書評価のみ、tools_conditionはツール呼び出し検出のみ。複合条件を1つのエッジに詰め込まない
  2. ループには終了条件を明示する: ブログ実装ではrewrite→re-retrieve→grade→rewriteの無限ループが理論上可能。Zenn記事のretry_countは本番運用に必須の安全弁
  3. Structured Outputで型安全性を確保: Pydantic BaseModelによる出力型定義は、LLM応答のパースエラーを防止

LangGraphの限界と対策

ブログでは言及されていませんが、実運用で注意すべき点:

  • ステートの肥大化: MessagesStateのメッセージリストはループのたびに増大。長い会話ではコンテキストウィンドウの上限に注意
  • LLM呼び出し回数: Document Grading + Answer Generation + (必要に応じてRewrite + 再検索)で、1クエリあたり最低3回のLLM呼び出しが発生
  • 評価の不安定性: 二値分類(yes/no)はLLMの温度パラメータに影響されやすい。temperature=0でも確率的な変動が残る

学術研究との関連(Academic Context)

Corrective RAG (CRAG) との関係

ブログのDocument Gradingパターンは、Yan et al. (2024) のCorrective RAG(arXiv:2401.15884)の設計思想をLangGraph上で簡略化したものです。オリジナルのCRAGは3段階評価(Correct/Incorrect/Ambiguous)ですが、ブログ実装は二値分類に簡略化しています。

Self-RAG との関係

ブログタイトルの「Self-Reflective RAG」は、Asai et al. (2023) のSelf-RAG(arXiv:2310.11511)の反省メカニズムをファインチューニング不要で近似するアプローチです。Self-RAGの4つの反省トークン([Retrieve], [IsREL], [IsSUP], [IsUSE])を、LangGraphの条件付きエッジとStructured Outputで代替しています。

Self-RAGのトークンブログの対応実装Zenn記事の対応実装
[Retrieve]tools_conditionLangGraphのルーティング
[IsREL]grade_documentsDocument Grading(スコアリング)
[IsSUP]なしHallucination Check
[IsUSE]なしRAGAS Answer Relevancy

RAGLAB との関係

RAGLAB(arXiv:2408.15712)はブログと同じSelf-RAG/CRAGアルゴリズムを統一フレームワークで再現していますが、目的が異なります。RAGLABは公平な定量比較を目的とする研究フレームワークであり、ブログの実装はプロダクションアプリケーション構築を目的としています。

まとめ

LangChainのSelf-Reflective RAGブログは、LangGraphの条件付きエッジを活用した自己修正型RAGパイプラインの実装パターンを確立しました。Document Grading → Query Rewriting → Answer Generationの3段階フローは、Zenn記事のLangGraph実装の直接的な基盤となっています。

Zenn記事はこのブログの基本設計を拡張し、Claude Sonnet 4.6のAdaptive Thinking、Hallucination Check、RAGAS/DeepEvalによる定量評価を追加することで、Faithfulness 0.91・ハルシネーション率2.3%のプロダクション品質を達成しています。ブログが提供する「設計パターン」と、Zenn記事が提供する「本番運用品質の実装」は、Self-Reflective RAGの理論と実践を橋渡しする相補的な関係にあります。

参考文献

この投稿は CC BY 4.0 でライセンスされています。

Anthropic解説: Contextual Retrieval — チャンク文脈付与で検索失敗率67%削減

-