📚

LangGraphでSupervisor型Agentを試す

に公開

いつの間にかLangGraph Supervisorというものがリリースされていたのに気づきました。

https://github.com/langchain-ai/langgraph-supervisor-py

ぱっと見た感じ、関数を一つ呼び出すだけでSupervisor型(階層型)エージェントを容易に作成できるようです。

コードを見ていると、create_react_agentのインターフェースがパブリッククラウドのエージェント系サービスやMastra、Agnoのように、今年話題になっているエージェントの基本的な構造を備えていたことや、API提供される代表的なチャットモデルの初期化が大幅に簡略化されていたことなど、これまで見落としていた点が多々ありました。
そこで、このLangGraph Supervisorの基本的な動作を確認しつつ、現状のLangGraphの使い勝手を見てみたいと思います。

今回は、Supervisorを含む複数のAIエージェントをそれぞれ独立したDockerコンテナとして起動し、RemoteGraphを用いて連携させる構成を試します。

また、LangChain陣営は基本的にLangGraph Cloudの利用を推奨しているようです。

https://github.com/langchain-ai/langserve/issues/791

そのため、LangGraph CLIを活用してLangGraph Platform上での動作を前提としつつ、LangGraph Cloudへデプロイする代わりに、ローカルのDocker環境でどの程度動作させられるのかも確認します。
LangServeはどうやら今後のサポート停止ですし。

プロジェクトのセットアップ

まず、LangGraph CLI を使用して、LangGraphプロジェクトの雛形を作成します。
今回は最小構成のプロジェクトから開始。

LangGraphプロジェクトを最小構成で作成
langgraph new

上記コマンドを実行すると、対話形式で設定項目を尋ねられるので、適宜設定します。
指定したディレクトリに必要なファイル群が生成されます。

ディレクトリ構成は以下のようにしました。
各エージェントを独立させられるかを確認することが主な目的であるため、それぞれが異なるLangGraphプロジェクトとして構成されていれば問題ありません。

.
├── researcher
│   ├── docker-compose.yaml
│   ├── langgraph.json
│   ├── pyproject.toml
│   └── src
│       └── agent
│           ├── __init__.py
│           └── graph.py
├── scheduler
│   ├── docker-compose.yaml
│   ├── langgraph.json
│   ├── pyproject.toml
│   └── src
│       └── agent
│           ├── __init__.py
│           └── graph.py
└── supervisor
    ├── docker-compose.yaml
    ├── langgraph.json
    ├── pyproject.toml
    └── src
        └── agent
            ├── __init__.py
            └── graph.py

システム構成

今回試すシステムは、以下の3つのエージェントで構成される階層型エージェントアーキテクチャです。

  1. Supervisorエージェント (supervisor): 各エージェントの呼び出しや処理の振り分けを行う中心的な役割を担う。
  2. Web検索エージェント (researcher_agent): Web上の情報を検索する。
  3. スケジュール設定エージェント (scheduler_agent): Googleカレンダーへのスケジュール登録を行う。

これらのエージェントは、それぞれ個別のDockerコンテナとして動作させます。
結果としては以下のようになりました。


なんかAgentになってから複雑さがModelに押し付けられてフローがシンプルになってきたよね

エージェント間の連携は、LangGraphのRemoteGraph機能を利用して実現します。
これはLangChainにおけるRemoteRunnableに相当しますね。
これにより、各エージェントの独立性を保ちつつ協調動作させることが、フレームワークレベルでサポートされていると言えるでしょう。

https://langchain-ai.github.io/langgraph/how-tos/use-remote-graph/

Supervisorエージェントの実装

Supervisorエージェントは、冒頭で紹介したlanggraph-supervisorのREADMEをほぼそのまま採用しました。

他のエージェント(Web検索エージェント、スケジュール設定エージェント)はRemoteGraphとして定義し、それらを統括するエージェントとして指定します。
LangChain/Graphはやや複雑な印象がありますが、ヘルパー関数的なものがかなり充実していて、簡易的に組むだけであれば内部実装がうまく隠蔽され、優れた抽象化が提供されていると感じます。

また、簡単なツールとして現在時刻を取得する関数も用意します。
これを渡しておかないと、「今」や「明日」といった表現をエージェントが解釈できず、実用的ではありません。
システムプロンプトやユーザーメッセージに毎回固定でタイムスタンプを埋め込む方法もありますが、今回はエージェントくんに自ら時間を見てもらうことにします。

supervisor/src/agent/graph.py
supervisor/src/agent/graph.py
import datetime
import os

from langchain.chat_models import init_chat_model
from langgraph.pregel.remote import RemoteGraph
from langgraph_supervisor import create_supervisor

async def get_current_time() -> dict[str, str]:
    """現在時刻(ISO 8601形式の文字列)を返すTool.

    Returns:
        dict[str, str]:
            - "current_time": 現在時刻(例: "2025-05-19T12:34:56.789123")
    """
    now = datetime.datetime.now().isoformat()
    return {"current_time": now}

model = init_chat_model(
    os.getenv("MODEL_NAME"),
    model_provider="openai",
    base_url=os.getenv("OPENAI_BASE_URL"),
)

researcher_agent = RemoteGraph(
    "agent", name="researcher_agent", url="http://host.docker.internal:8124"
)
scheduler_agent = RemoteGraph(
    "agent", name="scheduler_agent", url="http://host.docker.internal:8125"
)

# Create supervisor workflow
workflow = create_supervisor(
    agents=[researcher_agent, scheduler_agent],
    model=model,
    tools=[get_current_time],
    prompt=(
        "You are a team supervisor managing a research expert and a scheduling expert. "
        "For current events, use research_agent. "
        "For scheduling tasks, use scheduler_agent."
    ),
)

# Compile
graph = workflow.compile()

前述の通り、他のエージェントはRemoteGraphを使用してリモート接続します。
なお、おそらくLangGraph APIが生えているエンドポイントに対して接続することが可能なはずです。

今回はまず動作させることが目的だったため、ポート番号のみを変更して各コンテナを起動しました。
そのため、URLにはhost.docker.internalを使用し、Dockerホスト側で実行されている他のエージェントコンテナを指定しています。
ポート番号は後述するdocker-compose.yamlで適宜設定します。

supervisor/src/agent/graph.py
researcher_agent = RemoteGraph(
    "agent", name="researcher_agent", url="http://host.docker.internal:8124"
)
scheduler_agent = RemoteGraph(
    "agent", name="scheduler_agent", url="http://host.docker.internal:8125"
)

ところでLLMのモデルを読み込む以下の部分。

supervisor/src/agent/graph.py
model = init_chat_model(
    os.getenv("MODEL_NAME"),
    model_provider="openai",
    base_url=os.getenv("OPENAI_BASE_URL"),
)

最初に見たときは「ChatOpenAIChatBedrockではないのか?」と思いましたが、どうやらlitellmのようにopenai/gpt-4oといった指定で適切なモデルクラスを選択してくれるヘルパーのようです。
なお、私はlitellmで手元のLLM利用環境を統合し、OpenAI互換APIで統一的に利用できるようにしているため、上記のように初期化しました。

Web検索エージェントの実装

Web検索エージェントは、LangChainに統合されているTavilySearchを利用して実装します。
https://python.langchain.com/docs/integrations/tools/tavily_search

create_react_agent を使用して、ReActフレームワークに基づいたエージェントを構築します。

researcher/src/agent/graph.py
researcher/src/agent/graph.py
import datetime
import os
from typing import Any, Optional, cast

from langchain.chat_models import init_chat_model
from langchain_tavily import TavilySearch
from langgraph.prebuilt import create_react_agent

async def search(query: str) -> Optional[dict[str, Any]]:
    """Search for general web results.

    This function performs a search using the Tavily search engine, which is designed
    to provide comprehensive, accurate, and trusted results. It's particularly useful
    for answering questions about current events.
    """
    wrapped = TavilySearch(max_results=10)
    return cast(dict[str, Any], await wrapped.ainvoke({"query": query}))

async def get_current_time() -> dict[str, str]:
    """現在時刻(ISO 8601形式の文字列)を返すTool.

    Returns:
        dict[str, str]:
            - "current_time": 現在時刻(例: "2025-05-19T12:34:56.789123")
    """
    now = datetime.datetime.now().isoformat()
    return {"current_time": now}

model = init_chat_model(
    os.getenv("MODEL_NAME"),
    model_provider="openai",
    base_url=os.getenv("OPENAI_BASE_URL"),
)

prompt = """
You are a world class researcher with access to web search.
You are given a question and you need to answer it.
You can use the web search tool to find information.
"""

research_agent = create_react_agent(
    model=model,
    tools=[search, get_current_time],
    name="research_expert",
    prompt=prompt,
)

今回の検証では、エージェントが外部ツールを利用して情報収集を行うという基本的な動作を確認できれば十分であり、検索結果の品質については深く追求しないため、これで問題ありません。
Supervisorエージェントで述べたように、ここでも時刻取得ツールを渡しておきます。

スケジュール設定エージェントの実装

スケジュール設定エージェントは、Googleカレンダーとの連携を実現するために、オープンソースのfastmcp-gsuiteを利用しました。

https://github.com/tumf/fastmcp-gsuite

Googleアカウントに接続するMCPはいくつか候補がありましたが、安定動作し、かつコンテナ化しやすかったこちらを選択しました。

FastMCPは最近Streamable HTTPに対応したため、transportはstdioではなくそちらを利用するように改変しました。
これをDockerコンテナとして起動し、langchain-mcp-adapters経由で接続することで、スケジュール登録機能を実現します。

https://github.com/langchain-ai/langchain-mcp-adapters

なお、MCPを利用したのは単なる興味で記事の主題ではないため、このようなOSSをベースにコンテナを準備し、LangGraphエージェントから利用した、という程度の紹介に留めます。

scheduler/src/agent/graph.py
scheduler/src/agent/graph.py
import datetime
import os

from langchain.chat_models import init_chat_model
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.graph.graph import CompiledGraph
from langgraph.prebuilt import create_react_agent

async def get_current_time() -> dict[str, str]:
    """現在時刻(ISO 8601形式の文字列)を返すTool.

    Returns:
        dict[str, str]:
            - "current_time": 現在時刻(例: "2025-05-19T12:34:56.789123")
    """
    now = datetime.datetime.now().isoformat()
    return {"current_time": now}

async def make_graph() -> CompiledGraph:
    """Create and compile a StateGraph for the agent's reasoning and tool use.

    Returns:
        The compiled StateGraph object representing the agent's workflow.
    """
    # Define the two nodes we will cycle between
    client = MultiServerMCPClient(
        {
            "google-gsuite": {
                "url": "http://host.docker.internal:8000/mcp", # fastmcp-gsuiteコンテナのURL
                "transport": "streamable_http",
            },
        }
    )
    tools = await client.get_tools()
    tools = tools + [get_current_time] # get_current_timeツールも追加

    model = init_chat_model(
        os.getenv("MODEL_NAME"),
        model_provider="openai",
        base_url=os.getenv("OPENAI_BASE_URL"),
    )

    prompt = """
    You are a personal scheduler agent.
    You are given a task and you need to schedule it.
    You must use time zone of JST (UTC+9).
    """

    scheduler_agent = create_react_agent(
        model=model,
        tools=tools,
        name="scheduler_agent",
        prompt=prompt,
    )

    return scheduler_agent

MCP接続もこれだけでToolとして読み取れるため簡単ですね。

scheduler/src/agent/graph.py
    # Define the two nodes we will cycle between
    client = MultiServerMCPClient(
        {
            "google-gsuite": {
                "url": "http://host.docker.internal:8000/mcp", # fastmcp-gsuiteコンテナのURL
                "transport": "streamable_http",
            },
        }
    )
    tools = await client.get_tools()
    tools = tools + [get_current_time] # get_current_timeツールも追加

ただ、闇雲に連携するMCPを増やすと際限なくツールが増える可能性があるため、実用性やエージェントの商用利用においては、連携上限を設けたりリクエスト時のツールbindingを選択的にするなど、細かな調整が必要になると思われます。
一方で、最近のモデルは高性能なため、とりあえず全てのツールを提供しても問題なく機能するのではないか、という気もしますが、実際はどうなんでしょうかね。

LangGraph CLI と Standalone Container による実行

各エージェントは、LangGraph CLIを利用してLangGraph Platform上で動作するコンテナとしてビルドできます。
具体的には、LangGraphが提供するStandalone Containerをデプロイ形式として選択し、ローカルのDocker環境で各エージェントを起動する形になります。

以下の手順で起動すると、上でも少し触れたLangGraph APIによってエージェントがサービングされます。

https://langchain-ai.github.io/langgraph/concepts/langgraph_standalone_container

まず、各エージェントのディレクトリ(supervisor, researcher, scheduler)で、それぞれ以下のコマンドを実行してDockerイメージをビルドします。
イメージ名は任意です。

langgraphコマンドでコンテナビルド
# supervisorディレクトリに移動して
langgraph build -t supervisor-agent
# researcherディレクトリに移動して
langgraph build -t researcher-agent
# schedulerディレクトリに移動して
langgraph build -t scheduler-agent

次に、これらのコンテナとLangGraphが必要とするRedisおよびPostgreSQLをまとめて起動するために、docker-compose.yamlを作成します。
参考までにsupervisorのものを掲載しますが、他のエージェントについてはlanggraph-apiで使用するコンテナ名と全体的なポート番号を空いているものに変更すれば、同様に設定できました。

https://langchain-ai.github.io/langgraph/cloud/deployment/standalone_container/?h=compose#docker

docker-compose.yaml
docker-compose.yaml
volumes:
  langgraph-data:
    driver: local
services:
  langgraph-redis:
    image: redis:6
    healthcheck:
      test: redis-cli ping
      interval: 5s
      timeout: 1s
      retries: 5
  langgraph-postgres:
    image: postgres:16
    ports:
      - "5436:5432"
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - langgraph-data:/var/lib/postgresql/data
    healthcheck:
      test: pg_isready -U postgres
      start_period: 10s
      timeout: 1s
      retries: 5
      interval: 5s
  langgraph-api:
    image: langgraph-supervisor
    ports:
      - "8126:8000"
    depends_on:
      langgraph-redis:
        condition: service_healthy
      langgraph-postgres:
        condition: service_healthy
    env_file:
      - .env
    environment:
      REDIS_URI: redis://langgraph-redis:6379
      LANGSMITH_API_KEY: ${LANGSMITH_API_KEY}
      POSTGRES_URI: postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable

.envファイルには、今回の実装ではMODEL_NAMEOPENAI_BASE_URL、そしてLANGSMITH_API_KEYを定義しておきます。
残念ながら、この仕組み(LangGraph Platform、LangGraph APIによるサービング)を利用するにはStandalone Containerであったとしてもライセンスが必要なようで、検証目的であっても最低限LangSmithへの認証が必須となるようでした。
そのため、動作確認にはLangSmithのAPIキーが必要です。

ただし、これはあくまで起動時の認証とLangSmithが紐づいているがための仕様であり、LANGSMITH_TRACING=falseを設定しておけばトレーシング自体は無効化できるように見えています。

https://langchain-ai.github.io/langgraphjs/concepts/self_hosted/#self-hosted-lite

Self-Hosted Lite¶
The Self-Hosted Lite version is a limited version of LangGraph Platform that you can run locally or in a self-hosted manner (up to 1 million nodes executed).

When using the Self-Hosted Lite version, you authenticate with a LangSmith API key.

よって、今回はローカルでの動作確認が目的であり、LangGraph Cloudへの本格的なデプロイは行いませんが、起動用にLangSmithのAPIキーを設定しておきます。

docker-compose up コマンドで、定義した全てのサービスを起動します。

動作確認

構築したシステムの動作確認は、以下のフロントエンドUIを利用して対話形式で行いました。

https://agentchat.vercel.app/
(リポジトリ: https://github.com/langchain-ai/agent-chat-ui)

このUIの接続先を、ローカルで起動したSupervisorエージェントのURLに設定します。
UIを通じてSupervisorエージェントに指示を出し、各エージェントの動作や連携の様子を確認します。


万博で祝うほどの世界ミツバチの日

確かに、検索を行いカレンダーに登録するところまで、Supervisorくんが仲介してくれました。
エージェントが言うように、正しく登録されているかカレンダーも確認してみます。


しっかり登録された世界ミツバチの日

なるほど、正しく登録されていますね。
チャット全体は折りたたんで以下に掲載します。

チャット全体


localhostにあるエージェントを会話の対象に指定


感想

Supervisor、Web検索、スケジュール設定という3つの異なる機能を持つエージェントをそれぞれ独立したDockerコンテナとして起動し、RemoteGraphを介して連携させることが確認できました。
また、Supervisor型エージェント自体は関数を一つ呼び出すだけで作成できるため、動作検証だけであれば非常に簡単に構築できますね。

RemoteGraphによるエージェント呼び出し部分は、まさにGoogleが提唱するA2Aのコンセプトが具体的に活かされる箇所でしょう。
LangGraphは独自のプロトコルを採用していますが、各エージェントが標準化されたインターフェースを通じて通信することで、よりモジュール化された柔軟なエージェントシステムの構築が進むのではないかと期待されます。
MCPもA2Aも、現時点では世間の注目度ほどエンタープライズ要件で利用するには成熟しきっていない印象ですので、注視していきたいところです。

一方で、A2AやMCPには留意すべき点もあると考えています。
MCPやA2Aといった仕組みは、あくまでエージェント間の「通信プロトコル」や「連携方法」を定義するものであって、これらの技術を採用したからといって、LLMそのものの能力が向上したり、これまでできなかったタスクが魔法のように解決されたりするわけではありません。
本質的には、個々のエージェントが持つ能力(利用するツールやモデルの性能)に依存するという点は、冷静に認識しておく必要があるでしょうね。

巷じゃなんだか「MCPでできることが増える!」みたいな言い方されてるように感じますが、だって、実態はただのtool callじゃん?みたいなね。
もちろん、そのフォーマットを統一しようという試みはすごいことではありますし、統一されることで機能追加が容易になり、間接的にできることは増えると思いますが。

おわりに

LangGraph SupervisorとRemoteGraphを用いることで、マルチエージェントシステム、特に階層型アーキテクチャの構築が容易に実現できることが分かりました。
商用ケースにおいては別途考える部分もあると思いますが個人のツールや社内の業務改善程度であれば十分に使えると感じます。

とある通信会社の有志

Discussion