LangGraphでSupervisor型Agentを試す
いつの間にかLangGraph Supervisor
というものがリリースされていたのに気づきました。
ぱっと見た感じ、関数を一つ呼び出すだけでSupervisor型(階層型)エージェントを容易に作成できるようです。
コードを見ていると、create_react_agent
のインターフェースがパブリッククラウドのエージェント系サービスやMastra、Agnoのように、今年話題になっているエージェントの基本的な構造を備えていたことや、API提供される代表的なチャットモデルの初期化が大幅に簡略化されていたことなど、これまで見落としていた点が多々ありました。
そこで、このLangGraph Supervisor
の基本的な動作を確認しつつ、現状のLangGraphの使い勝手を見てみたいと思います。
今回は、Supervisorを含む複数のAIエージェントをそれぞれ独立したDockerコンテナとして起動し、RemoteGraph
を用いて連携させる構成を試します。
また、LangChain陣営は基本的にLangGraph Cloudの利用を推奨しているようです。
そのため、LangGraph CLIを活用してLangGraph Platform上での動作を前提としつつ、LangGraph Cloudへデプロイする代わりに、ローカルのDocker環境でどの程度動作させられるのかも確認します。
LangServeはどうやら今後のサポート停止ですし。
プロジェクトのセットアップ
まず、LangGraph CLI を使用して、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つのエージェントで構成される階層型エージェントアーキテクチャです。
- Supervisorエージェント (supervisor): 各エージェントの呼び出しや処理の振り分けを行う中心的な役割を担う。
- Web検索エージェント (researcher_agent): Web上の情報を検索する。
- スケジュール設定エージェント (scheduler_agent): Googleカレンダーへのスケジュール登録を行う。
これらのエージェントは、それぞれ個別のDockerコンテナとして動作させます。
結果としては以下のようになりました。
なんかAgentになってから複雑さがModelに押し付けられてフローがシンプルになってきたよね
エージェント間の連携は、LangGraphのRemoteGraph
機能を利用して実現します。
これはLangChainにおけるRemoteRunnable
に相当しますね。
これにより、各エージェントの独立性を保ちつつ協調動作させることが、フレームワークレベルでサポートされていると言えるでしょう。
Supervisorエージェントの実装
Supervisorエージェントは、冒頭で紹介したlanggraph-supervisor
のREADMEをほぼそのまま採用しました。
他のエージェント(Web検索エージェント、スケジュール設定エージェント)はRemoteGraph
として定義し、それらを統括するエージェントとして指定します。
LangChain/Graphはやや複雑な印象がありますが、ヘルパー関数的なものがかなり充実していて、簡易的に組むだけであれば内部実装がうまく隠蔽され、優れた抽象化が提供されていると感じます。
また、簡単なツールとして現在時刻を取得する関数も用意します。
これを渡しておかないと、「今」や「明日」といった表現をエージェントが解釈できず、実用的ではありません。
システムプロンプトやユーザーメッセージに毎回固定でタイムスタンプを埋め込む方法もありますが、今回はエージェントくんに自ら時間を見てもらうことにします。
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
で適宜設定します。
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のモデルを読み込む以下の部分。
model = init_chat_model(
os.getenv("MODEL_NAME"),
model_provider="openai",
base_url=os.getenv("OPENAI_BASE_URL"),
)
最初に見たときは「ChatOpenAI
やChatBedrock
ではないのか?」と思いましたが、どうやらlitellm
のようにopenai/gpt-4o
といった指定で適切なモデルクラスを選択してくれるヘルパーのようです。
なお、私はlitellm
で手元のLLM利用環境を統合し、OpenAI互換APIで統一的に利用できるようにしているため、上記のように初期化しました。
Web検索エージェントの実装
Web検索エージェントは、LangChainに統合されているTavilySearch
を利用して実装します。
create_react_agent
を使用して、ReActフレームワークに基づいたエージェントを構築します。
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
を利用しました。
Googleアカウントに接続するMCPはいくつか候補がありましたが、安定動作し、かつコンテナ化しやすかったこちらを選択しました。
FastMCPは最近Streamable HTTPに対応したため、transportはstdio
ではなくそちらを利用するように改変しました。
これをDockerコンテナとして起動し、langchain-mcp-adapters
経由で接続することで、スケジュール登録機能を実現します。
なお、MCPを利用したのは単なる興味で記事の主題ではないため、このようなOSSをベースにコンテナを準備し、LangGraphエージェントから利用した、という程度の紹介に留めます。
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として読み取れるため簡単ですね。
# 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によってエージェントがサービングされます。
まず、各エージェントのディレクトリ(supervisor
, researcher
, scheduler
)で、それぞれ以下のコマンドを実行してDockerイメージをビルドします。
イメージ名は任意です。
# 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
で使用するコンテナ名と全体的なポート番号を空いているものに変更すれば、同様に設定できました。
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_NAME
、OPENAI_BASE_URL
、そしてLANGSMITH_API_KEY
を定義しておきます。
残念ながら、この仕組み(LangGraph Platform、LangGraph APIによるサービング)を利用するにはStandalone Containerであったとしてもライセンスが必要なようで、検証目的であっても最低限LangSmithへの認証が必須となるようでした。
そのため、動作確認にはLangSmithのAPIキーが必要です。
ただし、これはあくまで起動時の認証とLangSmithが紐づいているがための仕様であり、LANGSMITH_TRACING=false
を設定しておけばトレーシング自体は無効化できるように見えています。
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://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