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 の Supervisor エージェント から LangChain の ReAct エージェント を呼び出す方法

Posted at

はじめに

本記事では、 LangGraphのSupervisorエージェント から LangChainのReActエージェント を呼び出す方法について確認します。

LangGraphのSupervisorエージェントLanggraphのReActエージェント を呼び出す前提で設計されているので、LangChainのReActエージェント をそのままだと呼び出せません。しかし、LangChainのReActエージェントを使って作成したエージェントをそのまま利用したい場合ということもあるでしょう。

異なるフレームワーク間のインターフェース不整合を解決し、複雑なマルチエージェントシステムを構築する方法を紹介します。

背景と課題

これまで次のような2つの記事を投稿しました。

2つめの記事で作成した Supervisorエージェント の1つの Workerエージェント として コンテンツ検索エージェント を利用する際にトラブルが発生しました。

発生した不整合問題

LangChainとLangGraphは、どちらも「エージェント実行」という共通の目的を持っていますが、入力の扱い方状態管理の設計思想 が大きく異なります。そのため、片方で動くコードをもう片方に持っていくと、そのままでは互換性がありません。

具体的には以下のような違いがありました。

LangChain Agent Executor の設計

# 単純な辞書形式を期待
agent_executor.invoke({"input": "検索クエリ"})
  • 入力形式: {"input": string} という単純な辞書構造
  • 設計思想: 1回の実行に対して1つの入力文字列を処理
  • 状態管理: 内部的に会話履歴やコンテキストを管理

LangGraph Supervisor の設計

# メッセージ配列を期待
supervisor.stream({
    "messages": [
        {"role": "user", "content": "タスク内容"}
    ]
})
  • 入力形式: {"messages": [Message]} というメッセージ配列構造
  • 設計思想: 会話の全履歴を状態として管理し、グラフ内で状態を更新
  • 状態管理: 明示的な状態グラフとして会話履歴を保持

なぜ不整合が発生したのか?

  • 入力形式の違い: AgentExecutorは単純な文字列入力を期待するのに対し、Supervisorは「メッセージ履歴」ごと受け取る必要がある
  • 出力形式の違い: Supervisorは「履歴更新」形式で返す必要があり、AgentExecutorのように単純な辞書返却では不十分
  • 責務の違い: AgentExecutorは「今の質問に答えること」に集中しているが、Supervisorは「会話全体の流れ」を制御する役割を持つ

このギャップを埋めるために、両者のインターフェースを変換するアダプタadapt_agent_executor_for_supervisor を作成しました

解決策:アダプタの実装

設計方針

  • 疎結合: 既存の AgentExecutor 実装は改変せず、アダプタで包むだけでSupervisorに統合できるようにする
  • 最小API: Supervisorが期待するのは「`callable(input_data) -> {messages: [...]}」」。この入出力だけを満たす
  • 安全な失敗: 例外は握りつぶさず、Supervisor側の履歴にエラーメタデータ付きで返す(グラフを落とさない)
  • デバッグ容易性: debug=True で、入力メッセージの型・件数・コンテンツの一部を安全に出力
  • 将来拡張: マルチターン文脈をAgentExecutorへ明示的に渡す余地を残す(現状は直近ユーザー発話のみ)

インターフェース設計

入力(Supervisor → アダプタ)

input_data = {
  "messages": [
    {"role": "system", "content": "..."},
    {"role": "user", "content": "質問テキスト"},
    # ... 過去履歴
  ],
  # 必要に応じて他のキーが含まれる場合あり
}

出力(アダプタ → Supervisor)

{
  **input_data,
  "messages": [
    *input_data["messages"],
    {"role": "assistant", "content": "<AgentExecutorの出力文字列>", "name": "<登録名>"}
  ]
}

注: Supervisorは履歴が増え続ける前提のため、出力は「追記」する。上書きはNG。

メインアダプター関数

def adapt_agent_executor_for_supervisor(agent_executor, name, debug=False):
    """LangChainのAgentExecutorをLangGraph Supervisor用に変換"""
    
    def supervisor_compatible_agent(input_data, config=None):
        """Supervisor互換エージェントラッパー"""
        try:
            # デバッグ情報の出力(debug=Trueの場合のみ)
            if debug:
                print("=== TMDB Agent Debug Info ===")
                print(f"Input data keys: {list(input_data.keys())}")
                print(f"Input data type: {type(input_data)}")
                # メッセージ詳細のデバッグ出力
                if "messages" in input_data:
                    messages_dbg = input_data["messages"]
                    print(f"Messages count: {len(messages_dbg)}")
                    for i, msg in enumerate(messages_dbg):
                        print(f"Message {i}: {type(msg)}")
                        if isinstance(msg, dict):
                            print(f"  - Role: {msg.get('role', 'N/A')}")
                            print(f"  - Content preview: {str(msg.get('content', 'N/A'))[:100]}")

            # ユーザー入力の抽出
            user_input = extract_user_input_multiple_patterns(input_data)
            
            if not user_input:
                raise ValueError("ユーザー入力が見つかりません")

            # LangChain AgentExecutorの呼び出し
            result = agent_executor.invoke({"input": user_input})
            output = result.get("output", "検索結果を取得できませんでした")

            # Supervisor形式でのレスポンス生成
            messages = input_data.get("messages", [])
            updated_messages = messages + [{
                "role": "assistant",
                "content": output,
                "name": name
            }]

            return {**input_data, "messages": updated_messages}

        except Exception as e:
            # エラーハンドリング
            error_message = f"エラーが発生しました: {str(e)}"
            messages = input_data.get("messages", [])
            updated_messages = messages + [{
                "role": "assistant",
                "content": error_message,
                "name": name,
                "metadata": {"error": True}
            }]
            return {**input_data, "messages": updated_messages}

    # Supervisor互換のインターフェース提供
    supervisor_compatible_agent.name = name
    supervisor_compatible_agent.invoke = supervisor_compatible_agent
    return supervisor_compatible_agent

ユーザー入力抽出関数

def extract_user_input_multiple_patterns(input_data):
    """様々なメッセージ形式からユーザー入力を抽出"""
    messages = input_data.get("messages", [])
    
    for message in reversed(messages):
        # 辞書型メッセージの処理
        if isinstance(message, dict) and message.get("role") == "user":
            return message.get("content", "")
        
        # LangChain HumanMessageクラスの処理
        elif hasattr(message, "content") and type(message).__name__ == "HumanMessage":
            return message.content
        
        # 汎用的なクラス型メッセージの処理
        elif hasattr(message, "role") and hasattr(message, "content"):
            if message.role in ["user", "human"]:
                return message.content
    
    # デバッグ情報の出力
    print("=== Extract Debug ===")
    for i, message in enumerate(messages):
        print(f"Message {i}: {type(message)}")
        if hasattr(message, "role"):
            print(f"  Role: {message.role}")
        if hasattr(message, "content"):
            print(f"  Content: {message.content}")
    
    raise ValueError("ユーザー入力が見つかりません")

利用方法 (擬似コード)

  1. 既存のLangChainエージェント(AgentExecutor)を用意
  2. アダプタでSupervisor互換にラップ
  3. 他のワーカー(四則演算など)と一緒にSupervisorへ登録
  4. 実行
# 1) 既存のLangChainエージェント(AgentExecutor)を用意
tmdb_agent = create_tmdb_agent(
    llm=ChatOpenAI(model="gpt-4.1-mini", temperature=0.1),
    verbose=True,
)

# 2) アダプタでSupervisor互換にラップ
tmdb_supervisor_compatible = adapt_agent_executor_for_supervisor(
    agent_executor=tmdb_agent.agent_executor,
    name="tmdb_search_agent"
)

# 3) 他のワーカー(四則演算など)と一緒にSupervisorへ登録
supervisor = create_supervisor(
    model=init_chat_model("openai:gpt-4.1-mini"),
    agents=[
        arithmetic_agent,
        unit_conversion_agent,
        tmdb_supervisor_compatible,  # TMDBエージェントを追加
    ],
    ...

# 4) 実行
result_stream = supervisor.stream({
    "messages": [{"role": "user", "content": "スターウォーズ1と2の公開年の合計は?"}]
})

システム構成

エージェント定義

# 四則演算エージェント (Langgraph の ReAct Agent)
arithmetic_agent = create_react_agent(
    model=init_chat_model("openai:gpt-4.1-mini"),
    tools=[add, subtract, multiply, divide],
    name="ArithmeticAgent",
    prompt="算術演算専用エージェント..."
)

# 単位変換エージェント (Langgraph の ReAct Agent)
unit_conversion_agent = create_react_agent(
    model="openai:gpt-4.1-mini",
    tools=[meters_to_feet, celsius_to_fahrenheit, ...],
    name="UnitConversionAgent",
    prompt="単位変換専用エージェント..."
)

# TMDBエージェント (LangChain の ReAct Agent なのでアダプタを利用する)
tmdb_agent = create_tmdb_agent(
    llm=ChatOpenAI(model="gpt-4.1-mini", temperature=0.1),
    verbose=True,
)

tmdb_supervisor_compatible = adapt_agent_executor_for_supervisor(
    agent_executor=tmdb_agent.agent_executor,
    name="tmdb_search_agent"
)

Supervisor設定

supervisor = create_supervisor(
    model=init_chat_model("openai:gpt-4.1-mini"),
    agents=[
        arithmetic_agent,
        unit_conversion_agent,
        tmdb_supervisor_compatible,
    ],
    prompt=(
        "You are a supervisor managing three agents:\n"
        "- ArithmeticAgent: Handles arithmetic operations and mathematical calculations.\n"
        "- UnitConversionAgent: Handles unit conversion tasks (length, weight, temperature, etc.).\n"
        "- TMDBSearchAgent: Handles movie, TV show, and celebrity information searches using TMDB API. "
        "Supports multilingual queries and can search for cast/crew information, plot details, release dates, ratings, etc.\n\n"
        
        "ASSIGNMENT RULES:\n"
        "1. Assign work to ONE agent at a time - do not call agents in parallel.\n"
        "2. Do not perform any work yourself - always delegate to appropriate agents.\n"
        "3. For arithmetic: Always use ArithmeticAgent for ALL calculations, even simple additions.\n"
        "4. For unit conversions: Use UnitConversionAgent.\n"
        "5. For movie/TV/celebrity queries: Use TMDBSearchAgent for any entertainment content questions.\n\n"
        
        "TASK ROUTING:\n"
        "- Movie/TV show information (plot, cast, release date, ratings) → TMDBSearchAgent\n"
        "- Celebrity/actor/director information → TMDBSearchAgent\n"
        "- Entertainment industry questions → TMDBSearchAgent\n"
        "- Mathematical calculations → ArithmeticAgent\n"
        "- Unit conversions → UnitConversionAgent\n\n"
        
        "COMPLEX TASKS:\n"
        "If a task involves multiple domains (e.g., unit conversion + arithmetic), handle in sequence:\n"
        "1. First use UnitConversionAgent for conversions\n"
        "2. Then use ArithmeticAgent for calculations\n"
        "3. Use TMDBSearchAgent if entertainment content is involved\n\n"
        
        "Always respond in the same language as the user's query."
    ),
    add_handoff_back_messages=True,
    output_mode="full_history",
).compile()

実行例とテスト結果

ここでは、実際に Supervisor が TMDBエージェント四則演算エージェント を組み合わせて、複合タスクを解決する様子を示します。

TMDBエージェントがスターウォーズのエピソード1と2の公開年をTMDBから取得し、その公開年を四則演算エージェントが加算して、最後に回答を作成しています。意図した通りの動作となりました。

入力

スターウォーズ1の公開された年と2の公開された年を足すと何年になるか?

実行結果

スターウォーズ1(エピソード1)の公開年1999年とスターウォーズ2(エピソード2)の公開年2002年を足すと、合計は4001年になります。

処理フロー

  1. TMDBSearchAgent
    • スターウォーズエピソード1(1999年)とエピソード2(2002年)の公開年を検索
  2. ArithmeticAgent
    • 1999 + 2002 = 4001を計算

実行ログ

実行ログ (縦に長いので少し整形しています)
Task: スターウォーズ1の公開された年と2の公開された年を足すと何年になるか?
Update from node supervisor:
================================= Tool Message =================================
Name: transfer_to_tmdb_search_agent
Successfully transferred to tmdb_search_agent

> Entering new AgentExecutor chain...
Thought: スターウォーズ1と2の公開年を調べる必要がある。スターウォーズ1は「スター・ウォーズ エピソード1 ファントム・メナス」、スターウォーズ2は「スター・ウォーズ エピソード2 クローンの攻撃」として知られている。これらの公開年を調べるためにtmdb_multi_searchで「Star Wars Episode I」と検索して確認する。
Action: tmdb_multi_search
Action Input: Star Wars Episode Imovie_title: Robot Chicken: Star Wars Episode III
release_date: 2010-12-19
vote_average: 7.424
overview: Robot Chicken: Star Wars Episode III, directed by Chris McKay, combines the satirical sensibilities ...

movie_title: Star Wars: Episode I - The Phantom Menace
release_date: 1999-05-19
vote_average: 6.564
overview: Anakin Skywalker, a young slave strong with the Force, is discovered on Tatooine. Meanwhile, the evi...

movie_title: Star Wars: Episode III - Revenge of the Sith
release_date: 2005-05-17
vote_average: 7.5
overview: The evil Darth Sidious enacts his final plan for unlimited power – and the heroic Jedi Anakin Skywal...

movie_title: Star Wars: Episode II - Attack of the Clones
release_date: 2002-05-15
vote_average: 6.582
overview: Following an assassination attempt on Senator Padmé Amidala, Jedi Knights Anakin Skywalker and Obi-W...

<省略>...

language: en-USThought: スターウォーズ エピソード1の公開年は1999年、エピソード2の公開年は2002年であることがわかった。これらを足すと1999 + 2002 = 4001年になる。

Final Answer: スターウォーズ1(1999年)と2(2002年)の公開年を足すと、4001年になります。

> Finished chain.
Update from node tmdb_search_agent:

================================= Tool Message =================================
Name: transfer_back_to_supervisor
Successfully transferred back to supervisor
Update from node supervisor:
================================= Tool Message =================================
Name: transfer_to_arithmeticagent
Successfully transferred to ArithmeticAgent

Adding 1999.0 and 2002.0

Update from node ArithmeticAgent:
================================= Tool Message =================================
Name: transfer_back_to_supervisor
Successfully transferred back to supervisor
Update from node supervisor:
================================== Ai Message ==================================
Name: supervisor
スターウォーズ1の公開年1999年と2の公開年2002年を足すと、合計は4001年になります。

Final Result: スターウォーズ1の公開年1999年と2の公開年2002年を足すと、合計は4001年になります。

技術的なポイント

1. インターフェース統一

  • LangChainの{"input": string}形式からLangGraphの{"messages": [Message]}形式への変換
  • レスポンスの逆変換とメッセージ履歴の維持

2. 堅牢なメッセージ抽出

  • 複数のメッセージ形式(辞書、HumanMessage、カスタムクラス)に対応
  • エラー時のデバッグ情報提供

3. エラーハンドリング

  • 例外発生時の適切なエラーメッセージ生成
  • デバッグモードでの詳細情報出力

4. 状態管理

  • 会話履歴の保持と更新
  • エージェント間の状態受け渡し

まとめ

本記事では、LangChainとLangGraphの設計思想の違いを理解し、アダプターパターンを使用してこれらを統合する方法を示しました。この手法により、既存のLangChainエージェントをLangGraphのSupervisorシステムに組み込み、複雑なマルチエージェントワークフローを構築することが可能になります。

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?