コンテンツにスキップ

STUDY NOTES

第23回: マルチエージェントの2大パターン — Supervisor と Swarm を langgraph-supervisor / langgraph-swarm で比較

第9回(Research Agent)や第18回(データ分析エージェント)でも「複数エージェントの協調」は扱ってきたが、それらは自前で StateGraph を組んで分岐を書くスタイルだった。第23回は LangChain 公式が 2025 年に提供開始した 2 つのプリビルトパッケージを紹介する回:

パッケージ パターン 構造 比喩
langgraph-supervisor Supervisor(階層型) 1 名の supervisor LLM が specialists 群を指示・統括 工事現場の現場監督 + 職人。意思決定は中央集権
langgraph-swarm Swarm(フラット型) エージェント同士が handoff_tool直接バトンを渡し合う コールセンターのオペレータ間で「これは技術担当に回しますね」と相互転送

同じ「マルチエージェント協調」でも、意思決定が中央にあるか分散しているかで構造がまるで違う。本回はそれぞれの最小サンプル(2エージェント構成)で書き味の違いを体感する。

2 つのパターンの判断軸: - タスクの分解が事前に決められる(情報収集 → 計算など、ステップが固定)→ Supervisor - タスクの遷移が動的(FAQ→技術サポート→FAQ と行ったり来たり)→ Swarm - 単純に少数(2–3)の専門家なら Swarm のほうが軽い。多数(5+)の specialists + ルーティングなら Supervisor のほうが拡張しやすい


全体像

23/
├── main.py                                ← Supervisor / Swarm 両方を CLI で走らせる検証スクリプト
├── langgraph.json                         ← Studio が "supervisor" と "swarm" 2 グラフを発見
└── src/sd_23/
    ├── supervisor_graph.py                ← create_supervisor() でラップ
    ├── swarm_graph.py                     ← create_swarm() でラップ
    └── agents/
        ├── math_agent.py                  ← create_react_agent (add/multiply/divide)
        ├── research_agent.py              ← create_react_agent (web_search/wikipedia, mock)
        ├── faq_agent.py                   ← create_react_agent + handoff_tool→tech_support
        └── tech_agent.py                  ← create_react_agent + handoff_tool→faq_support
Supervisor パターンの実行フロー
flowchart TD
    User([ユーザ質問]) --> Sup[supervisor LLM]
    Sup -->|tool_call: transfer_to_research_expert| Research[research_expert<br/>create_react_agent<br/>tools: web_search, wikipedia]
    Research -->|結果| Sup
    Sup -->|tool_call: transfer_to_math_expert| Math[math_expert<br/>create_react_agent<br/>tools: add, multiply, divide]
    Math -->|結果| Sup
    Sup -->|最終回答| User

    style Sup fill:#FFF2E8,stroke:#ED7100
    style Research fill:#E1F5FE,stroke:#0288D1
    style Math fill:#E1F5FE,stroke:#0288D1

ポイント: 常に supervisor が中央。research_expert と math_expert は直接通信せず、必ず supervisor を経由する。

Swarm パターンの実行フロー
flowchart TD
    User([ユーザ質問]) --> FAQ[faq_support<br/>tools: check_faq_database<br/>+ handoff→tech_support]
    FAQ -->|tool_call: transfer_to_tech_support| Tech[tech_support<br/>tools: check_system_status, read_manual<br/>+ handoff→faq_support]
    Tech -->|tool_call: transfer_to_faq_support| FAQ
    FAQ -->|最終回答| User
    Tech -->|最終回答| User

    style FAQ fill:#E8F5E9,stroke:#3F8624
    style Tech fill:#FCE4EC,stroke:#E7157B

ポイント: supervisor が存在しないdefault_active_agent="faq_support" で起点を指定するだけで、後は agents が自分で「次は誰に渡すか」を tool_call で決める。

main.py の検証フロー(両パターン共通)
sequenceDiagram
    participant CLI as main.py
    participant WF as workflow (compiled)
    participant CP as InMemorySaver

    CLI->>WF: app.astream({messages: [HumanMessage(...)]}, stream_mode="updates", subgraphs=True)
    loop chunk ごと
        WF-->>CLI: (parent_ns, {node_name: output})
        Note over CLI: subgraphs=True なので<br/>サブグラフの内部実行も流れてくる<br/>例: ("research_expert:abc", {agent: {messages: ...}})
        CLI->>CLI: _print_node で整形 → 標準出力
    end

subgraphs=True を付けると、Supervisor / Swarm の内側で動いている react_agent の中間 step も全部見える。デバッグ時の重要オプション。


使用ライブラリ・原理

langgraph_supervisor.create_supervisor(agents, model, prompt, ...)
from langgraph_supervisor import create_supervisor

workflow = create_supervisor(
    agents=[math_agent, research_agent],
    model=supervisor_model,
    prompt=supervisor_prompt,
    add_handoff_messages=True,
    output_mode="full_history",
)

内部でやっていること:

  1. supervisor ノードを中心に置く StateGraph を組み立てる
  2. 各 sub-agent を Send API で呼び出す transfer_to_<agent_name> ツールを自動生成して supervisor に渡す
  3. sub-agent が応答したら supervisor に戻る固定エッジを張る
  4. output_mode で「sub-agent の全メッセージを supervisor の state に追加するか、最終応答だけか」を切り替え

create_react_agent との対比: create_react_agent は「LLM + tools + while ループ」の単一エージェント。create_supervisor は「supervisor LLM が transfer_to_X という tool を介して sub-agent を呼び出す」というメタ構造で、sub-agent 自体も内部で create_react_agent だったりする。ReAct パターンの再帰的入れ子だと思うと分かりやすい。

主要パラメータ:

パラメータ 役割
agents sub-agent のリスト。各 agent は name= 属性を持っている必要あり(supervisor が transfer_to_<name> で呼ぶ)
model supervisor が使う LLM
prompt supervisor 用のシステムプロンプト。「どのエージェントが何の専門家か」を明記する責任は呼び出し側にある
add_handoff_messages True にすると state に handoff の旨を示すメッセージが残る
output_mode "full_history" で全 sub-agent メッセージを保持、"last_message" で最終だけ
langgraph_swarm.create_swarm(agents, default_active_agent)
from langgraph_swarm import create_swarm

workflow = create_swarm(
    agents=[faq_agent, tech_agent],
    default_active_agent="faq_support",
)

内部:

  1. active_agent という追加 state フィールドを持つ StateGraph を構築
  2. 起点として default_active_agent を設定
  3. 各 agent が handoff_tool を呼ぶと active_agent が切り替わり、グラフが別の agent ノードに遷移する
  4. agent が tool_call なしで応答したら終了
langgraph_swarm.create_handoff_tool(agent_name, description)
from langgraph_swarm import create_handoff_tool

tech_handoff = create_handoff_tool(
    agent_name="tech_support",
    description="技術的な問題が発生した場合に技術サポートに転送",
)
tools = [check_faq_database, tech_handoff]

このツールは LLM から見ると「transfer_to_tech_support を呼ぶと制御が tech_support に移る」と振る舞う。中身は「state の active_agent"tech_support" に変える Command(goto="tech_support") を返す」だけのシンプル実装(第18回でも見た Command パターンの再利用)。

app.astream(..., stream_mode="updates", subgraphs=True)

subgraphs=True の効果:

  • 通常の streamトップレベルグラフのノード遷移のみを流す
  • subgraphs=True を付けると 入れ子の sub-graph(=create_react_agent が内部で持つグラフ)の中間遷移も流れる
  • chunk が (parent_namespace, outputs) のタプル形式になる(普段は dict だけ)
  • parent_ns[0] で「どのエージェントの中の出来事か」が分かる(例: "research_expert:abc123"

主要 stream_mode 一覧(第22回からの再掲):

mode 流れるもの
"values" state スナップショット全体
"updates" 直近 step の差分のみ ← 本サンプルで使用
"messages" LLM トークン単位
"custom" StreamWriter ペイロード

ファイル別の役割

ファイル 役割
main.py Supervisor と Swarm の 2 サンプル質問を投げて、astream(subgraphs=True) で全エージェントの中間 step を整形して標準出力
src/sd_23/supervisor_graph.py create_supervisor の 1 行で組み立て。supervisor 用プロンプトを直書き
src/sd_23/swarm_graph.py create_swarm の 1 行で組み立て。プロンプトすら supervisor 側に持たない(各 agent のプロンプトに分散)
src/sd_23/agents/math_agent.py create_react_agent(tools=[add, multiply, divide], name="math_expert")
src/sd_23/agents/research_agent.py create_react_agent(tools=[web_search (mock), wikipedia (mock)], name="research_expert")
src/sd_23/agents/faq_agent.py FAQ DB tool + create_handoff_tool("tech_support")
src/sd_23/agents/tech_agent.py check_system_status + read_manual + create_handoff_tool("faq_support")
langgraph.json Studio に "supervisor" と "swarm" 2 グラフを公開(同時に並べて比較できる)

行レベルの工夫(中核ロジックの抜粋)

① Supervisor の組み立て — プロンプトに「誰が何の専門家か」を書くだけ (supervisor_graph.py:13-44)
def create_supervisor_workflow():
    math_agent = create_math_agent()                                          # ①
    research_agent = create_research_agent()

    supervisor_model = ChatAnthropic(temperature=0, model_name="claude-sonnet-4-20250514")

    supervisor_prompt = """あなたはタスクコーディネーターです。

利用可能なエージェント:
- research_expert: 情報収集専門
- math_expert: 計算専門

タスク実行方法:
1. 情報収集が必要な場合、research_expertに委譲
2. 計算が必要な場合、具体的な数値と計算内容をmath_expertに伝える"""                  # ②

    workflow = create_supervisor(                                             # ③
        agents=[math_agent, research_agent],
        model=supervisor_model,
        prompt=supervisor_prompt,
        add_handoff_messages=True,
        output_mode="full_history",
    )
    return workflow
やってること なぜそうする
sub-agent をそれぞれ create_react_agent で組む sub-agent 側は普通の ReAct エージェント。supervisor から見ると単なる「name 付きのツール」になる
プロンプトに全エージェントの専門分野を箇条書きで書く supervisor は「自分が呼べるエージェント一覧」をプロンプトとツール定義の両方から知るが、自然言語で説明があったほうがルーティング精度が上がる(古典テク)
create_supervisor の 1 行で完成 中で Send API を使った fan-out / fan-in グラフを自動構築。自分で StateGraph + conditional_edges を書くと 50 行は必要
② Swarm の組み立て — プロンプトすら supervisor 側にない (swarm_graph.py:12-23)
def create_swarm_workflow():
    faq_agent = create_faq_agent()
    tech_agent = create_tech_agent()

    workflow = create_swarm(                                                  # ①
        agents=[faq_agent, tech_agent],
        default_active_agent="faq_support",                                   # ②
    )
    return workflow
やってること なぜそうする
create_swarm(agents, default_active_agent) だけ Swarm にはコーディネーター LLM が存在しないprompt 引数すら要らない
default_active_agent で起点を指定 ユーザ最初の質問はこのエージェントが受ける。以降は agent 内の handoff_tool で動的に遷移
③ Handoff tool の宣言 — ツール定義 1 行で agent 間の橋を架ける (faq_agent.py:31-36)
def create_faq_agent() -> CompiledGraph:
    model = ChatAnthropic(temperature=0, model_name="claude-sonnet-4-20250514")

    tech_handoff = create_handoff_tool(                                       # ①
        agent_name="tech_support",
        description="技術的な問題や詳細なサポートが必要な場合に技術サポートに転送",
    )

    tools = [check_faq_database, tech_handoff]                                # ②

    prompt = """あなたの名前は「faq_support」です。FAQサポートエージェントとして動作しています。
...
- エラーコードの詳細診断など技術的な質問は、tech_supportへ転送してください"""               # ③

    agent = create_react_agent(model=model, tools=tools, name="faq_support", prompt=prompt)
    return agent
やってること なぜそうする
create_handoff_tool("tech_support", description=...) で「tech_support へバトンを渡すツール」を生成 LLM はこれを transfer_to_tech_support という普通の tool として扱う。実態は Command(goto="tech_support") を返す薄いラッパ
通常 tool と handoff tool を同じ tools リストに並べる LLM 側はどれが handoff か意識しない(プロンプトで書くだけ)。LangGraph runtime が tool 結果を見て graph 遷移を判断
プロンプトで「自分の name は X」「Y に送ること」と明記 LLM が自分の役割と「いつ handoff すべきか」を理解する必要がある。プロンプトが Swarm の振る舞いの 80%を決める
④ main.py の subgraphs ストリーミング (main.py:46-75)
async for chunk in app.astream(*stream_args, stream_mode="updates", subgraphs=True):  # ①
    if isinstance(chunk, tuple):                                              # ②
        parent_ns, outputs = chunk
        if parent_ns and len(parent_ns) > 0:
            parent_info = parent_ns[0] if isinstance(parent_ns[0], str) else str(parent_ns[0])
            parent_name = parent_info.split(":")[0] if ":" in parent_info else parent_info  # ③
        else:
            parent_name = None
        for node_name, output in outputs.items():
            _print_node(node_name, output, is_swarm, seen_messages, parent_name)
    else:
        for node_name, output in chunk.items():                               # ④
            _print_node(node_name, output, is_swarm, seen_messages)
やってること なぜそうする
subgraphs=True で sub-agent の中間 step も購読 これがないと「supervisor → 結果」しか見えず、各 sub-agent のツール呼び出しがブラックボックスになる
chunk がタプルなら subgraph 由来 subgraphs=True の時だけ来るフォーマット
parent_info.split(":")[0] で UUID を剥がしてエージェント名だけ取り出し LangGraph は内部で "research_expert:abc-123" のような形で sub-agent をラベル付けする。表示用には UUID 部分が邪魔なので削る
タプルでない場合は通常 chunk(トップレベル node) subgraphs=True でも親グラフ自身の遷移は dict 形式で来る

学んだこと(要点)

  • Supervisor vs Swarm の選び方:
  • Supervisor: タスクが事前に分解できる、ルーティングロジックを 1 箇所に集めたい、新規エージェント追加で supervisor プロンプトだけ変えれば済む → 規模拡張に強い
  • Swarm: エージェント間の遷移が動的、専門家が対等な関係、プロンプトがエージェントごとに自然に書ける → 小規模で書きやすい
  • langgraph-supervisor / langgraph-swarm はどちらも公式プリビルト。「マルチエージェント書くなら毎回 StateGraph を組む」時代は終わった。自前 graph は「これらでは表現できない複雑なフロー」のときだけ書く
  • create_react_agent(name=...)name を付けることが両パターンの前提。supervisor は transfer_to_<name> 経由で呼び出し、swarm は Command(goto=<name>) で遷移する
  • subgraphs=Trueマルチエージェント観察の必須オプション。これなしでは「supervisor が何を考えてどこに振ったか」が見えない。LangGraph Studio でも subgraphs=True 相当の可視化が効いている
  • handoff の宣言は ツール定義として 書ける。LLM 側のコードに特別な API は要らず、create_handoff_tool の戻り値を tools リストに混ぜるだけ
  • Swarm では プロンプトに「自分の name」と「いつ handoff すべきか」を必ず書く。書かないと LLM は handoff を呼ばずに自分で答えようとする
  • 第18回(自前で Command(goto=...) をハンドラブルしていた)は Swarm の手作り実装だったと理解できる。langgraph-swarm は同じことを 1 行で書けるラッパ
  • stream_mode="updates" + subgraphs=Truechunk 形式が dict と tuple で混在する点に注意。isinstance(chunk, tuple) で分岐する古典的な処理が必要

拡張アイデア

  1. 3 つ目以降の specialist を Supervisor に足す — 例えば translation_agent(DeepL ラッパ)を作って create_supervisor(agents=[math, research, translation], ...) に追加。プロンプトを更新するだけで supervisor が新エージェントに振り分けるかを観察
  2. Swarm に 3 エージェント目を入れて handoff グラフを複雑化billing_support を追加して FAQ↔Tech↔Billing の三角形を作る。各 agent に 2 つずつ handoff_tool を持たせて「無限ループに陥らないか」を実験
  3. Supervisor + Swarm の混在 — Supervisor 配下の sub-agent として「Swarm として組まれた sub-workflow」を入れる。ホテルのコンシェルジュ(supervisor)の下に「フロント / コールセンター / 客室サービス」の Swarm を置く構造
  4. LangGraph Studio で並列観察langgraph.json に既に 2 グラフ登録済みなので、uv run langgraph dev で Studio を立ち上げて同じ質問を両方に投げ、ノード遷移を視覚的に比較
  5. Supervisor プロンプトの A/B 比較 — 同じタスクに対して「specialist の説明が箇条書き」vs「自由文」vs「task type → agent の対応表」でルーティング精度がどう変わるか実験
  6. output_mode="last_message" に変えてみるfull_history だと state に sub-agent の全 ToolMessage が残る。last_message にすると最終応答だけ。token 経済性と デバッグ性のトレードオフを体験
  7. ハンドオフ無限ループ対策 — Swarm で「両方の agent が相手にバトンを渡し続ける」状況を意図的に作って、recursion_limit でどう止まるかを観察。本来は agent 側に「同じ handoff を 2 回続けない」プロンプトが必要

現代版に移植するなら

1. 既にモデルは Claude 4 Sonnet 系(2026 年現在も新しい)

claude-sonnet-4-20250514 は本サンプル時点で十分新しい。2026 年現在さらに新しい claude-sonnet-4-6 も存在するので、必要なら差し替え可能。temperature=0 で deterministic にしているのは Supervisor / Swarm のルーティング学習のため適切。

2. web_search / wikipedia のモックを Tavily 実装に置き換える

第20回で使った TavilyClientresearch_agent.pyweb_search に注入する:

from tavily import TavilyClient
_tavily = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])

@tool
def web_search(query: str) -> str:
    """ウェブ検索を実行します"""
    response = _tavily.search(query, max_results=5)
    return "\n".join(f"{r['title']}: {r['content'][:200]}" for r in response.get("results", []))

これだけで「実在の AI 市場規模」を取りに行く本物のリサーチエージェントになる。

3. Swarm で循環防止の tools_condition を入れる

現状の Swarm は handoff の往復に上限がない(recursion_limit 頼り)。各 agent のプロンプトに「同じ相手に 2 回連続で handoff したら自分で答える」を追記、または state に handoff_history: List[str] を持たせて agent 側で参照する設計が堅い。

4. print()logging

main.py の整形ロジックは print まみれ。CI 等で動かすときに log level で抑えられない。logging + RichHandler に置き換えると色付き表示も得られる。

5. Supervisor プロンプトのハードコードをファイル分離

supervisor_graph.pysupervisor_prompt = """..."""agent が増えるたびに編集が必要prompts/supervisor.txt に分離して、agents リストから自動的に「利用可能エージェント」セクションを生成する(第20回の tool_descriptions 同様)。


既知の不具合・注意点

  • Swarm の Supervisor 非存在ゆえの応答終端の曖昧さ: Swarm は「どの agent がいつ最終回答を返すか」が agent 任せ。今回のサンプルでは tech_support が「解決方法を返したら終了」とプロンプトで示唆しているが、handoff を呼ばずに普通に応答した瞬間に終了する仕様。意図せず途中で終わるリスク
  • Handoff Loop の検出機構なし: 上記の通り、A↔B が handoff を続けると recursion_limit(デフォルト 25)まで回る。本サンプルでは事故らない簡単な質問だが、本番では循環検出が必要
  • tech_agent.py:43# type: ignore[call-arg] が抜けている: 他の agent ファイルは付けているのに tech_agent.py だけ抜けている。mypy strict 設定だと警告が出る
  • langgraph_supervisor / langgraph_swarm のバージョン未固定: pyproject.toml でバージョン指定がないので、uv sync のタイミングで API 破壊変更を踏む可能性。両ライブラリは 2025 年中も急速に進化中
  • research_agent.web_search がハードコードのモック: 「2025年AI市場規模 5200億ドル」を返すだけで、実際にはモデルが「知っていそうな数字」を返すリスクと変わらない。実装意図を理解せず本番に持ち込むと事故る
  • InMemorySaver の state がプロセス再起動で消える: 検証スクリプトだから問題ないが、production では SqliteSaver / PostgresSaver に差し替え必須
  • main.py:43 の handoff 判定: "transfer_to" in tool_name or "handoff" in tool_name で判別しているが、ツール名が create_handoff_tool のデフォルトに依存。カスタム名前にすると検出が外れる

記事参照

  • Software Design 2025 年 8 月号(推定)連載第23回「マルチエージェントの 2 大パターン」
  • 関連: 第18回 STUDY_NOTES — 自前で Command(goto=...) を書いた multi-agent。Swarm の手作り版
  • 関連: 第9回 STUDY_NOTES — Research Agent。タスク分解型 (= Supervisor 的) の手作り版
  • 公式 langgraph-supervisor: https://github.com/langchain-ai/langgraph-supervisor-py
  • 公式 langgraph-swarm: https://github.com/langchain-ai/langgraph-swarm-py
  • LangGraph Multi-Agent ガイド: https://langchain-ai.github.io/langgraph/concepts/multi_agent/

作成: 2026-05-25 / 最終更新: 2026-06-10