0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LangGraphで実装!マルチエージェントRAGの設計とワークフロー自動化

0
Posted at

多くのRAGシステム開発者が「複雑な推論や高度な情報処理が難しい」という壁にぶつかっています。単一のLLMエージェントだけでは、動的なクエリルーティングや多段階の評価・修正が困難で、結果としてRAGの精度や信頼性が頭打ちになることが少なくありません。

この記事では、LangGraphを用いたマルチエージェントRAGシステムの設計と実装を通じて、この課題を解決する具体的な手順を解説します。複数のエージェントを協調させ、洗練されたワークフロー自動化を実現することで、従来のRAGでは困難だった高度な情報検索や処理を可能にします。

LangGraphとは?複雑なワークフローをグラフで表現する利点

このセクションでは、LangGraphの基本的な概念と、なぜ複雑なエージェントワークフローに適しているのかを解説します。

LangGraphは、LangChainエコシステムの一部でありながら、エージェントのオーケストレーションに特化した低レベルのフレームワークです。その最大の特徴は、アプリケーションのロジックを「グラフ」としてモデル化する点にあります。これにより、以下の3つのコアコンポーネントを組み合わせて、エージェントの意思決定プロセスや情報フローを視覚的かつ構造的に定義できます。

  • State (状態): アプリケーションの現在のスナップショットを表す共有データ構造です。エージェント間で共有される情報や、ワークフローの進行状況を管理します。TypedDictPydanticで型付けすることで、状態の整合性を保ちやすくなります。
  • Nodes (ノード): グラフ内で実行される処理単位です。LLMの呼び出し、外部APIの実行、データ処理、条件分岐の評価など、エージェントの具体的なロジックを実装します。現在の状態を入力として受け取り、計算や副作用を実行し、更新された状態を返します。
  • Edges (エッジ): ノード間の遷移を定義します。現在の状態に基づいて次に実行するノードを決定する「条件付きエッジ」や、固定された順序で実行する「固定エッジ」があります。これにより、動的な意思決定や反復処理を持つマルチエージェントワークフローを柔軟に構築できます。

LangGraph v1は安定版であり、LangChain v1と連携するように設計されています。LangChainのcreate_agentはLangGraph上に構築されており、高レベルな開発から始めて、必要に応じて低レベルな制御に移行できるのが強みです。

なぜマルチエージェントRAGにLangGraphが適しているのか

従来のRAGシステムでは、単一のエージェントが「クエリの解釈」「ドキュメント検索」「回答生成」といった一連のタスクを順次実行することが一般的でした。しかし、複雑な質問や動的なコンテキストでは、この線形的なアプローチでは限界があります。

マルチエージェントRAGは、複数の専門エージェントが協調することで、この課題を克服します。例えば、以下のようなシナリオが考えられます。

  1. ルーターエージェント: ユーザーの質問を分析し、「事実質問」「意見質問」「ウェブ検索が必要な質問」などに分類。
  2. 検索エージェント: 分類された質問に基づいて、適切な検索戦略(ベクトルDB検索、キーワード検索、ウェブ検索など)を実行。
  3. 評価エージェント: 検索結果や生成された回答の関連性、正確性を評価し、必要に応じて修正を指示。
  4. 回答生成エージェント: 最終的な回答を生成。

LangGraphは、このようなエージェント間の複雑な連携や動的なルーティングを、明確なグラフ構造で表現・実行できるため、マルチエージェントRAGの構築に最適なフレームワークと言えます。

LangGraphの基本パターンでワークフローを構築する

ここからは、LangGraphの基本的な使い方をコード例を交えて解説します。まずはシンプルなグラフの構築から始めましょう。

状態の定義とノードの実装

LangGraphでワークフローを構築する最初のステップは、共有状態を定義し、個々の処理ロジックをノードとして実装することです。状態はTypedDictで型付けすることで、堅牢性と可読性を高めます。

from typing import TypedDict, List, Dict, Any
from langgraph.graph import StateGraph, START, END

# 1. 状態の定義: アプリケーションの共有データをここに集約
class AgentState(TypedDict):
    """
    RAGエージェントの状態を定義するTypedDict。
    エージェント間で共有される情報や、ワークフローの進行状況を管理します。
    """
    query: str # ユーザーからの元のクエリ
    documents: List[str] # 検索されたドキュメントのリスト
    answer: str # 生成された回答
    history: List[Dict[str, Any]] # 会話履歴やステップ履歴など
    # エラーハンドリング用のフィールドも追加可能
    error_message: str | None

# 2. ノード関数の定義例: 各エージェントの処理ロジック
def retrieve_documents(state: AgentState) -> AgentState:
    """
    クエリに基づいてドキュメントを検索するノード。
    実際にはベクトルDB検索やキーワード検索などを実行します。
    """
    print(f"--- Retrieving documents for query: {state['query']} ---")
    # 実際にはここで検索ロジックを実装
    retrieved_docs = [
        f"Document 1 related to {state['query']}",
        f"Document 2 related to {state['query']}"
    ]
    return {"documents": retrieved_docs}

def generate_answer(state: AgentState) -> AgentState:
    """
    検索されたドキュメントとクエリに基づいて回答を生成するノード。
    実際にはLLMを呼び出して回答を生成します。
    """
    print(f"--- Generating answer based on documents: {state['documents']} ---")
    if not state.get("documents"):
        return {"answer": "No documents found to generate an answer."}

    # 実際にはLLM呼び出しロジック
    generated_answer = (
        f"Based on documents {state['documents']}, "
        f"the answer to '{state['query']}' is a synthesized response."
    )
    return {"answer": generated_answer}

def final_output(state: AgentState) -> AgentState:
    """
    最終的な出力を行うノード。
    """
    print(f"--- Final Answer: {state['answer']} ---")
    return state # 最終状態をそのまま返す

AgentStateは、複数のエージェントが共有する「状態」を定義しています。retrieve_documentsgenerate_answerは、それぞれ特定のタスクを実行する「ノード」の例です。これらのノードはAgentStateを入力として受け取り、処理後に更新されたAgentStateを返します。

グラフの構築と実行

ノードが定義できたら、それらを組み合わせてグラフを構築します。StateGraphクラスを使用し、add_nodeでノードを追加し、add_edgeでノード間の遷移を定義します。STARTENDは、グラフの開始と終了を示す特別なエッジです。

# 3. グラフの構築
workflow = StateGraph(AgentState)

workflow.add_node("retrieve", retrieve_documents)
workflow.add_node("generate", generate_answer)
workflow.add_node("final_output", final_output) # 最終出力ノードを追加

# エッジの定義: ワークフローの順序を決定
workflow.add_edge(START, "retrieve") # グラフ開始 -> ドキュメント検索
workflow.add_edge("retrieve", "generate") # ドキュメント検索 -> 回答生成
workflow.add_edge("generate", "final_output") # 回答生成 -> 最終出力
workflow.add_edge("final_output", END) # 最終出力 -> グラフ終了

# グラフのコンパイル
graph = workflow.compile()

# 4. グラフの実行
initial_state = {"query": "What is LangGraph?", "documents": [], "answer": "", "history": []}
result = graph.invoke(initial_state)

print("\n--- Execution Result ---")
print(f"Query: {result['query']}")
print(f"Documents: {result['documents']}")
print(f"Answer: {result['answer']}")

このコードを実行すると、retrieve_documentsノードが実行され、次にgenerate_answerノード、そしてfinal_outputノードが順に実行され、最終的な回答が出力されます。これは最もシンプルな線形的なワークフローの例です。

マルチエージェントRAGの高度なワークフロー設計と実装

より複雑な推論を必要とするRAGシステムでは、複数のエージェントが協調し、動的にタスクを委譲したり、結果を評価・修正したりするマルチエージェントワークフローが不可欠です。

条件付きエッジによる動的ルーティング

LangGraphの強力な機能の一つが「条件付きエッジ」です。これにより、現在の状態に基づいて次に実行するノードを動的に決定できます。これは、マルチエージェントシステムにおける「ルーターエージェント」の実装に非常に有効です。

例えば、ユーザーのクエリが「単純な質問」か「複雑な質問」かによって、異なる処理フローにルーティングするケースを考えてみましょう。

from typing import Literal # Python 3.8+ for Literal
from langgraph.graph import ConditionalEdge

# 状態定義はAgentStateを再利用

# 新しいノードの定義
def decide_route(state: AgentState) -> Literal["simple_qa", "complex_rag"]:
    """
    クエリの複雑性に基づいて次のルートを決定するノード。
    実際にはLLMを使ってクエリ分類を行う。
    """
    print(f"--- Deciding route for query: {state['query']} ---")
    if "complex" in state["query"].lower() or "?" not in state["query"]:
        return "complex_rag" # 複雑なクエリは詳細なRAGへ
    return "simple_qa" # 単純なクエリは直接回答へ

def simple_qa_agent(state: AgentState) -> AgentState:
    """
    単純な質問に直接回答するエージェント(LLMのパラメトリック知識を利用)。
    """
    print(f"--- Simple QA Agent processing: {state['query']} ---")
    return {"answer": f"Simple answer to: {state['query']}"}

def complex_rag_coordinator(state: AgentState) -> AgentState:
    """
    複雑なRAGワークフローを開始するコーディネーターノード。
    ここでは便宜上、単なるマーカーとして機能。
    """
    print(f"--- Complex RAG Coordinator started for: {state['query']} ---")
    return state

# グラフの再構築
workflow_complex = StateGraph(AgentState)

workflow_complex.add_node("decide_route", decide_route)
workflow_complex.add_node("simple_qa", simple_qa_agent)
workflow_complex.add_node("retrieve", retrieve_documents) # 既存のノードを再利用
workflow_complex.add_node("generate", generate_answer) # 既存のノードを再利用

# 開始からルート決定へ
workflow_complex.add_edge(START, "decide_route")

# 条件付きエッジ: decide_routeの結果に基づいて遷移
workflow_complex.add_conditional_edges(
    "decide_route",
    decide_route, # ルーティング関数を再利用
    {
        "simple_qa": "simple_qa",
        "complex_rag": "retrieve" # 複雑な場合はretrieveノードへ
    }
)

# 単純QAの終了
workflow_complex.add_edge("simple_qa", END)

# 複雑RAGのフロー(既存のretrieve -> generate -> END)
workflow_complex.add_edge("retrieve", "generate")
workflow_complex.add_edge("generate", END) # generateから直接ENDへ

graph_complex = workflow_complex.compile()

# 実行例1: 単純なクエリ
print("\n--- Running with simple query ---")
result_simple = graph_complex.invoke({"query": "What is a cat?", "documents": [], "answer": "", "history": []})
print(f"Final Answer (Simple): {result_simple['answer']}")

# 実行例2: 複雑なクエリ
print("\n--- Running with complex query ---")
result_complex = graph_complex.invoke({"query": "Explain the complex history of quantum mechanics.", "documents": [], "answer": "", "history": []})
print(f"Final Answer (Complex): {result_complex['answer']}")

decide_routeノードが、クエリの内容に応じてsimple_qaまたはretrieveノードに処理をルーティングします。このように、条件付きエッジを使うことで、動的で柔軟なマルチエージェントワークフローを構築できます。

フォールトトレランスとエラーハンドリング

本番環境で堅牢なマルチエージェントRAGを運用するには、エラーハンドリングが不可欠です。LangGraphは、フォールトトレランスのための3つの強力なプリミティブを提供します。

  • RetryPolicy: 一時的なエラー(ネットワーク障害、レート制限など)に対して、自動的にノードの実行をリトライします。バックオフ(再試行間隔の延長)やジッター(ランダムな遅延)を設定できます。
  • TimeoutPolicy: ノードの実行時間に対して上限を設定します。長時間の処理やハングアップを防ぎます。
  • error_handler: リトライが尽きた後や、特定のタイプのエラーが発生した場合に実行されるノードです。エラー情報を状態に記録したり、別のエージェントに通知したりするのに使用できます。

これらの機能を活用して、LLM呼び出し時のエラーをハンドリングする例を見てみましょう。

from langgraph.types import RetryPolicy, TimeoutPolicy
from langgraph.errors import NodeError
import random

# AgentStateにerror_messageフィールドがあることを前提とする

def call_llm_with_retry(state: AgentState) -> AgentState:
    """
    LLM呼び出しを模倣し、ランダムにエラーを発生させるノード。
    """
    print(f"--- Calling LLM for query: {state['query']} ---")
    if random.random() < 0.6: # 60%の確率で一時的なエラー
        raise ValueError("LLM API call failed temporarily due to network instability!")
    return {"answer": f"LLM successfully generated response for: {state['query']}"}

def handle_model_failure(error: NodeError, state: AgentState) -> AgentState:
    """
    LLM呼び出しノードでエラーが発生した場合に実行されるハンドラ。
    エラーメッセージを状態に記録し、人間による介入を促す。
    """
    print(f"!!! Error in node '{error.node}': {error.error} !!!")
    return {"error_message": f"Failed to call LLM after multiple retries. Error: {str(error.error)}"}

# グラフの再構築(エラーハンドリング版)
workflow_error = StateGraph(AgentState)

workflow_error.add_node("call_llm_robust",
                        call_llm_with_retry,
                        retry_policy=RetryPolicy(max_attempts=3, backoff_factor=1.5), # 最大3回リトライ、1.5倍のバックオフ
                        timeout=TimeoutPolicy(run_timeout=15), # 15秒でタイムアウト
                        error_handler=handle_model_failure # エラーハンドラを指定
                       )
workflow_error.add_edge(START, "call_llm_robust")

# エラーハンドリング後にグラフを終了させるか、別のノードに遷移させるかを決定
# ここではエラーハンドラが実行されたら、最終出力へ
def check_error_status(state: AgentState) -> Literal["has_error", "no_error"]:
    if state.get("error_message"):
        return "has_error"
    return "no_error"

workflow_error.add_conditional_edges(
    "call_llm_robust",
    check_error_status,
    {
        "has_error": END, # エラーがあれば終了
        "no_error": END # 成功しても終了 (本来は次の処理へ)
    }
)

graph_error = workflow_error.compile()

# 実行例: 複数回実行してリトライとエラーハンドリングの動作を確認
print("\n--- Running with error handling (may retry or fail) ---")
for i in range(3):
    print(f"\n--- Test Run {i+1} ---")
    initial_state_err = {"query": "Explain something important.", "documents": [], "answer": "", "history": [], "error_message": None}
    result_err = graph_error.invoke(initial_state_err)
    if result_err.get("error_message"):
        print(f"Handled error: {result_err['error_message']}")
    else:
        print(f"Success: {result_err['answer']}")

この例では、call_llm_with_retryノードでランダムにエラーが発生した場合、RetryPolicyに従って最大3回リトライが試みられます。それでも解決しない場合はhandle_model_failureが実行され、エラーメッセージが状態に記録されます。これにより、一時的な障害に対する耐性を高め、ワークフローの信頼性を向上させることができます。

LangGraph CLIとLangSmithによるデバッグと運用

複雑なマルチエージェントワークフローを開発する上で、デバッグと監視は非常に重要です。LangGraphは、開発効率を高めるためのCLIツールと、LangSmithとの連携による強力なデバッグ機能を提供します。

LangGraph CLIで開発サーバーを起動

langgraph-cliを使用すると、開発サーバーを起動し、コードの変更を自動的にリロードしながらグラフのUIをブラウザで確認できます。

# langgraph-cli[inmem] をインストール
pip install -U "langgraph-cli[inmem]"

# (推奨) 仮想環境を使用し、依存関係をインストール
# プロジェクトのルートディレクトリで実行
python -m venv .venv
source .venv/bin/activate
pip install . # プロジェクトの依存関係をインストール

# LangGraph CLIの設定ファイル (langgraph.json) を作成
# 例えば、以下の内容でプロジェクトルートに保存
# {
#   "entrypoint": "your_module:graph_error", # ここは上記のgraph_errorオブジェクトを指すように変更
#   "graph_type": "compiled_runnable",
#   "input_schema": {
#     "type": "object",
#     "properties": {
#       "query": {"type": "string"},
#       "documents": {"type": "array", "items": {"type": "string"}},
#       "answer": {"type": "string"},
#       "history": {"type": "array", "items": {"type": "object"}},
#       "error_message": {"type": ["string", "null"]}
#     }
#   },
#   "output_schema": {
#     "type": "object",
#     "properties": {
#       "query": {"type": "string"},
#       "documents": {"type": "array", "items": {"type": "string"}},
#       "answer": {"type": "string"},
#       "history": {"type": "array", "items": {"type": "object"}},
#       "error_message": {"type": ["string", "null"]}
#     }
#   }
# }

# 開発サーバーを起動
# 例えば、上記のグラフを定義したPythonファイルが `my_graph.py` で、
# `graph_error` という変数名でコンパイルされたグラフがある場合:
# langgraph dev --config ./langgraph.json --entrypoint my_graph:graph_error

langgraph devコマンドを実行すると、ローカルサーバーが起動し、ブラウザでグラフの実行状況をリアルタイムに確認できます。これは、複雑なマルチエージェントワークフローの構造を理解し、デバッグする上で非常に役立ちます。

LangSmithによる詳細なトレースとデバッグ

LangSmithは、LangChainおよびLangGraphアプリケーションのデバッグ、テスト、監視のためのプラットフォームです。LangGraphの実行はLangSmithに自動的にトレースされ、各ノードの入力、出力、実行時間、エラーなどを詳細に確認できます。

  • トレースの視覚化: グラフの各ステップで何が起こったのか、どのエージェントがどのツールを呼び出したのかを視覚的に追跡できます。
  • 状態の変化の監視: 各ノードの実行前後の状態の変化を比較し、意図しない状態変更やデータ損失を特定できます。
  • 無限ループの検出: LangSmithは、グラフが無限ループに陥っていることを検出し、どのノードでループが発生しているかを特定するのに役立ちます。recursion_limitを設定することで、無限ループを早期に停止させることも可能です。

LangSmithを統合するには、環境変数にLANGCHAIN_TRACING_V2=trueLANGCHAIN_API_KEYLANGCHAIN_PROJECTを設定するだけです。

まとめ:LangGraphで実現する高度なマルチエージェントRAG

この記事では、LangGraphを用いたマルチエージェントRAGの設計とワークフロー自動化について、その基本的な概念から高度な実装、そしてデバッグ方法までを解説しました。

重要なポイントをまとめると以下のようになります。

  • LangGraphは、状態、ノード、エッジを組み合わせることで、複雑なエージェントワークフローをグラフとして表現します。これにより、マルチエージェントRAGの動的なルーティングや協調動作を柔軟に構築できます。
  • 条件付きエッジを活用することで、ユーザーのクエリや中間結果に基づいて、エージェントが動的に処理フローを決定する「ルーターエージェント」を実装できます。
  • **RetryPolicyTimeoutPolicyerror_handler**といった組み込みのフォールトトレランス機能により、本番環境で堅牢なRAGシステムを運用するためのエラーハンドリングが可能です。
  • LangGraph CLIとLangSmithは、開発中のデバッグや本番環境での監視において、ワークフローの可視化と問題特定に強力な支援を提供します。

LangGraphの低レベルな設計思想は、高い柔軟性を提供し、アダプティブRAG、修正RAG、自己反省RAGといった高度なRAGパターンを実装する基盤となります。ぜひ、このフレームワークを活用して、あなたのRAGシステムを次のレベルへと進化させてください。

さらに深く学びたい方は、LangGraphの公式ドキュメントを参照し、より複雑なエージェントパターンや永続化の仕組みについて探求することをお勧めします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?