LiteLLMとLangGraphでSupervisor・Hierarchical・Swarmの3パターンのマルチエージェントを構築してみる
はじめに
データ事業本部のkobayashiです。
これまでの記事ではLiteLLMの基本、クラウド経由でのLLM利用、LangGraphでのエージェント構築、LangGraphでのRAG構築を紹介してきました。今回はその続編として、マルチエージェントを構築する方法を紹介します。
ここまでで扱ったエージェント(create_agent()によるReActエージェントやAgentic RAG)は、いずれも1つのLLMが全てのツールを使い分ける単一エージェントでした。タスクが複雑になると、1つのエージェントに「調査」「計算」「執筆」「校正」など様々な役割を持たせるとプロンプトが肥大化し、ツール選択の精度も落ちていきます。
マルチエージェントは、こうした複雑なタスクを役割ごとに分解し、それぞれの専門エージェントを協調させるアーキテクチャです。LiteLLMと組み合わせることで、エージェントごとに最適なモデル(高速・安価なモデルで調査、高品質モデルで執筆、など)を選べる点が大きな強みになります。
マルチエージェントとは
マルチエージェントシステムは、単一のLLMでは扱いきれない複雑なタスクを、複数の専門エージェントに分解して解決する設計パターンです。
単一エージェントの限界
- プロンプトの肥大化: 役割や指示を1つのシステムプロンプトに詰め込む必要がある
- ツール選択の精度低下: ツール数が増えるほどLLMが正しいツールを選べなくなる
- コンテキスト圧迫: 多様な情報が1つのコンテキストに混ざる
- モデル選択の硬直化: タスクの一部だけ高性能モデルが欲しい場合でも全体に高いモデルを使うことになる
マルチエージェントの利点
- 責任の分離: 各エージェントが単一の役割に集中できる
- モデルの使い分け: タスクの難易度に応じてエージェントごとにモデルを選択できる
- 再利用性: エージェント単位で独立しているため別のシステムにも組み込みやすい
- 観測性: どのエージェントが何をしたかが明確に追跡できる
主要な3つのアーキテクチャパターン
LangGraphでマルチエージェントを構築する際、主に以下3つのパターンが使われます。
1. Supervisor パターン
中央のSupervisorエージェントが指揮を執り、配下のエージェントにタスクを振り分けます。各エージェントの結果はSupervisorに戻り、最終回答もSupervisorが組み立てます。
- 制御が明示的でフローを把握しやすい
- Supervisor自体がボトルネックになり得る
2. Hierarchical(階層型)パターン
Supervisorパターンを多階層化したもの。TopレベルのSupervisorが配下のSupervisorを管理し、その下にWorkerエージェントが並ぶ構造です。チームを複数束ねる大規模ワークフローに向いています。
3. Swarm(群知能)パターン
Supervisorを置かず、**エージェント同士がハンドオフ(引き継ぎ)**で会話を進めます。「いま誰が応対しているか」という状態を共有し、必要に応じて担当エージェントが切り替わります。
- 中央集権がなく、エージェントが対等
- カスタマーサポートのように担当領域が明確に分かれるシナリオに向く
環境構築
環境
今回使用した環境は以下の通りです。
Python 3.13
litellm 1.83.14
langgraph 1.1.10
langgraph-supervisor 0.1.0
langgraph-swarm 0.0.13
langchain-litellm 0.6.4
langchain-core 1.3.2
インストール
pipでインストールします。
$ uv pip install litellm langgraph langgraph-supervisor langgraph-swarm langchain-litellm langchain-core
APIキーの設定
$ export OPENAI_API_KEY="sk-..."
$ export ANTHROPIC_API_KEY="sk-ant-..."
本記事では、LiteLLMの「ロールごとに異なるプロバイダーを使い分けられる」という強みを実演するため以下の構成にしてあります。
- メインのLLM呼び出しはOpenAIのGPTシリーズ
- 品質が重要なライターやTopレベルのSupervisorだけClaude(
claude-sonnet-4-6)
エージェントロジックは同一のまま、ChatLiteLLM(model="...")の文字列を変えるだけでこの使い分けを実現できます。
なお、LiteLLM経由でAnthropicモデルを使う場合、ツール無しのエージェントを呼び出す際に「tools=が必須」と弾かれることがあります。今回は各スクリプトの先頭で次の設定を入れています。
import litellm
litellm.modify_params = True
これによりLiteLLMが必要に応じてダミーツールを補完し、tools=[]のエージェント(純粋に文章生成だけを担うWriterなど)でもAnthropicを使えるようになります。
Supervisor パターン(langgraph-supervisor 使用版)
まずは公式パッケージのlanggraph-supervisorを使った最もシンプルな実装です。「調査エージェント」と「執筆エージェント」をSupervisorが束ねる構成にします。
import litellm
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_litellm import ChatLiteLLM
from langgraph_supervisor import create_supervisor
litellm.modify_params = True
# =============================================
# ツール定義
# =============================================
@tool
def search_web(query: str) -> str:
"""Webから情報を検索します。"""
# デモ用のダミー実装。実運用ではTavilyやSerperなどを使う
fake_db = {
"東京 観光": "2026年の東京観光トレンド: 浅草・豊洲・渋谷のナイトマーケット、お台場の没入型アート展示、神田の日本酒バー巡り",
"京都 観光": "2026年の京都観光トレンド: 嵐山の早朝座禅体験、伏見の酒蔵ツアー、二条城のプロジェクションマッピング",
"大阪 観光": "2026年の大阪観光トレンド: 万博会場跡地のリニューアル、道頓堀の屋形船ディナー、新世界のレトロ酒場巡り",
}
for key, value in fake_db.items():
if all(k in query for k in key.split()):
return value
return f"'{query}' に関する情報は見つかりませんでした"
@tool
def get_weather(city: str) -> str:
"""指定された都市の天気を取得します。"""
weather = {
"東京": "晴れ、最高気温24°C、観光に最適",
"京都": "曇りのち晴れ、最高気温22°C、屋外散策におすすめ",
"大阪": "晴れ、最高気温25°C、屋外イベント日和",
}
return weather.get(city, f"{city}の天気データはありません")
# =============================================
# エージェントごとに最適なモデルを割り当てる
# =============================================
# Researcher: 大量の検索を捌く高速・安価なモデル
researcher_llm = ChatLiteLLM(model="openai/gpt-5-mini")
# Writer: 文章品質が重要なので高品質モデル(Anthropic)
writer_llm = ChatLiteLLM(model="anthropic/claude-sonnet-4-6")
# Supervisor: 全体の指揮を執るため賢いモデル
supervisor_llm = ChatLiteLLM(model="openai/gpt-5.4")
# =============================================
# 各エージェントの定義
# =============================================
researcher = create_agent(
model=researcher_llm,
tools=[search_web, get_weather],
name="researcher",
system_prompt=(
"あなたは調査担当エージェントです。"
"与えられたテーマについて search_web や get_weather ツールで情報を集め、"
"事実ベースで簡潔に整理して報告してください。"
"推測や創作は行わないでください。"
),
)
writer = create_agent(
model=writer_llm,
tools=[],
name="writer",
system_prompt=(
"あなたは文章作成担当エージェントです。"
"researcherから渡された情報をもとに、読みやすく構造化されたレポートを作成してください。"
"見出し・箇条書き・見どころの3要素で整理してください。"
),
)
# =============================================
# Supervisorで束ねる
# =============================================
app = create_supervisor(
agents=[researcher, writer],
model=supervisor_llm,
prompt=(
"あなたは調査チームを統括するマネージャーです。"
"ユーザーの依頼を受けたら、まず researcher に必要な情報を調査させ、"
"その結果を writer に渡してレポートを作成させてください。"
"両方の作業が完了したら、最終結果をユーザーに返してください。"
),
).compile()
# =============================================
# 実行
# =============================================
result = app.invoke(
{
"messages": [
{
"role": "user",
"content": "東京の最新の観光トレンドと現在の天気を踏まえて、週末旅行の簡潔なレポートを書いてください",
}
]
}
)
print("=== 実行ログ ===")
for msg in result["messages"]:
name = getattr(msg, "name", None) or msg.type
content = msg.content if isinstance(msg.content, str) else str(msg.content)
print(f"\n[{name}]")
print(content[:400])
create_supervisor()の主なパラメータは以下です。
| パラメータ | 説明 |
|---|---|
agents |
配下のエージェントのリスト。各エージェントにはnameを必ず付ける |
model |
Supervisor自身が使うLLM |
prompt |
Supervisorのシステムプロンプト。配下エージェントへの委任ルールをここに書く |
ポイントは 3つのエージェントすべてで ChatLiteLLM を使いつつ、モデル名だけを変えている点です。「Supervisorだけ高性能、Researcherは大量呼び出すので安いモデル、Writerは品質重視」という構成を、コードを書き換えることなくモデル名の差し替えだけで実現できます。
実行結果は以下のようになります。
$ python supervisor_basic.py
=== 実行ログ ===
[human]
東京の最新の観光トレンドと現在の天気を踏まえて、週末旅行の簡潔なレポートを書いてください
[supervisor]
[transfer_to_researcher]
Successfully transferred to researcher
[researcher]
要点まとめ(ウェブ検索と天気予報に基づく)
1) 最新の観光トレンド(ウェブ検索で確認)
- 浅草・豊洲・渋谷などでのナイトマーケットの開催が増加している。
- お台場の没入型アート展示(体験型・フォトジェニックな展示)が注目されている。
- 神田周辺での日本酒バー巡りなど、地元飲食体験の需要が高まっている。
2) 現在の天気(天気予報で確認)
- 晴れ。最高気温 24°C。屋外観光に適した気候。
(以下略)
[transfer_back_to_supervisor]
Successfully transferred back to supervisor
[supervisor]
[transfer_to_writer]
Successfully transferred to writer
[writer]
# 🗼 東京 週末旅行レポート
### ~最新トレンドと天気を活かした充実の2日間プラン~
## ☀️ 1. 現在の天気情報
| 項目 | 詳細 |
|------|------|
| 天候 | ☀️ 晴れ |
| 最高気温 | 24°C |
| 総合評価 | **屋外観光に最適なコンディション** |
## 🔥 2. 東京の最新観光トレンド
- 🎨 **没入型アート体験**:お台場を中心に、フォトジェニックで体験型の展示が急増中
- 🏮 **ナイトマーケットの台頭**:浅草・豊洲・渋谷などで夜の屋外マーケットが人気上昇
- 🍶 **地元飲食体験**:神田エリアの日本酒バー巡り
(以下略)
[transfer_back_to_supervisor]
Successfully transferred back to supervisor
[supervisor]
以下、東京の最新観光トレンドと現在の天気を踏まえた、週末旅行の簡潔なレポートです。
(以下略)
Supervisorがtransfer_to_researcher → transfer_to_writer の順にハンドオフを発行し、最終的にSupervisor(GPT-5.4)が全体をまとめてユーザーへ返している様子が確認できます。transfer_to_* / transfer_back_to_supervisor はlanggraph-supervisorが内部で自動生成するハンドオフ用のツール呼び出しで、Supervisor↔Worker間の制御移譲がここで行われています。
注目したいのは、Researcher(GPT-5-mini、軽量モデル)が箇条書き中心の素朴な報告に留まる一方、Writer(Claude Sonnet 4.6)がークダウン表や絵文字、ヘッダー階層を駆使したリッチな成果物を出している点になります。エージェントロジックは同一でも、ロールに応じてプロバイダーを切り替えるだけで出力に適したレスポンスになります。これがLiteLLMをマルチエージェントと組み合わせる最大の特徴になります。
Supervisor パターン(StateGraph 手書き版)
create_supervisor()は便利ですが、内部で何が起きているかを理解するために、同等の処理をStateGraphで手書きしてみます。これによりSupervisorパターンの本質が明確になります。
from typing import Literal
import litellm
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_litellm import ChatLiteLLM
from langgraph.graph import END, START, MessagesState, StateGraph
litellm.modify_params = True
# =============================================
# ツールとエージェント(前節と同じ)
# =============================================
@tool
def search_web(query: str) -> str:
"""Webから情報を検索します。"""
return f"'{query}'の検索結果: 浅草・豊洲のナイトマーケットが人気"
@tool
def get_weather(city: str) -> str:
"""指定された都市の天気を取得します。"""
return f"{city}: 晴れ、24°C"
researcher_llm = ChatLiteLLM(model="openai/gpt-5-mini")
writer_llm = ChatLiteLLM(model="anthropic/claude-sonnet-4-6")
supervisor_llm = ChatLiteLLM(model="openai/gpt-5.4")
researcher = create_agent(
model=researcher_llm,
tools=[search_web, get_weather],
system_prompt="あなたは調査担当です。ツールで情報を集めて簡潔に報告してください。",
)
writer = create_agent(
model=writer_llm,
tools=[],
system_prompt="渡された情報をもとに見出し付きの簡潔なレポートを作成してください。",
)
# =============================================
# Supervisor用のルーティングロジック
# =============================================
SUPERVISOR_PROMPT = (
"あなたはチームのマネージャーです。次に呼ぶべきエージェントを決定してください。"
"選択肢: 'researcher'(調査担当)、'writer'(執筆担当)、'FINISH'(完了)。"
"ユーザーの依頼に対し、まずresearcherで情報収集し、次にwriterで執筆、"
"完了したらFINISHを返してください。"
"回答は 'researcher' / 'writer' / 'FINISH' のいずれか1語のみ。"
)
def supervisor_node(state: MessagesState) -> dict:
"""次に動くエージェントを決定する。"""
messages = [{"role": "system", "content": SUPERVISOR_PROMPT}] + [
{"role": m.type if m.type != "ai" else "assistant", "content": str(m.content)}
for m in state["messages"]
]
response = supervisor_llm.invoke(messages)
decision = response.content.strip().lower()
print(f" [Supervisor判断] next = {decision}")
return {"messages": [response]}
def researcher_node(state: MessagesState) -> dict:
"""調査エージェントを実行する。"""
# 直前のユーザー指示を取得
user_msg = next(
(m for m in reversed(state["messages"]) if m.type == "human"),
None,
)
query = user_msg.content if user_msg else ""
result = researcher.invoke({"messages": [HumanMessage(content=query)]})
final = result["messages"][-1]
print(f" [Researcher] {final.content[:80]}...")
return {"messages": [final]}
def writer_node(state: MessagesState) -> dict:
"""執筆エージェントを実行する。"""
# 直前の調査結果を渡す
last = state["messages"][-1]
result = writer.invoke({"messages": [HumanMessage(content=str(last.content))]})
final = result["messages"][-1]
print(f" [Writer] {final.content[:80]}...")
return {"messages": [final]}
def route_next(state: MessagesState) -> Literal["researcher", "writer", "__end__"]:
"""Supervisorの判断に応じて次のノードを決定する。"""
last = state["messages"][-1]
decision = str(last.content).strip().lower()
if "researcher" in decision:
return "researcher"
if "writer" in decision:
return "writer"
return END
# =============================================
# グラフ構築
# =============================================
graph = StateGraph(MessagesState)
graph.add_node("supervisor", supervisor_node)
graph.add_node("researcher", researcher_node)
graph.add_node("writer", writer_node)
graph.add_edge(START, "supervisor")
graph.add_conditional_edges(
"supervisor",
route_next,
{"researcher": "researcher", "writer": "writer", END: END},
)
# 各Workerが終わったら必ずSupervisorに戻る
graph.add_edge("researcher", "supervisor")
graph.add_edge("writer", "supervisor")
app = graph.compile()
# =============================================
# 実行
# =============================================
result = app.invoke(
{
"messages": [
{
"role": "user",
"content": "東京の観光トレンドを調べてレポートを書いてください",
}
]
}
)
print("\n=== 最終出力 ===")
print(result["messages"][-1].content)
このコードのポイントは以下になります。
グラフの構造
- supervisor: 次に呼ぶWorkerを判断するハブ
- researcher / writer: 専門タスクを実行するWorker
- 各WorkerからSupervisorに必ず戻ることで、Supervisorが「次は誰?」を継続的に判断できる
create_supervisor() との対応
langgraph-supervisorは内部的にこの構造を組み立ててくれているだけで、手書き版を一度書いておくと「なぜSupervisorパターンと言うのか」「ハンドオフはどう実装されているのか」がよく理解できるようになります。
特定の条件でSupervisorを介さず直接エージェントを呼ぶといった実運用で細かくフローを制御したい場合はこの手書きパターンを使うことで実現可能です。
実行結果は以下のようになります。
$ python supervisor_manual.py
[Supervisor判断] next = researcher
[Researcher] 調査報告(概要) — 東京の観光トレンド(2023–2024、要点)
注記:今回の自動検索ツールからは有意な統計ページが取得できなかったため、報告は私の保有知...
[Supervisor判断] next = finish
=== 最終出力 ===
FINISH
[Supervisor判断]の出力からSupervisor(GPT-5.4)がresearcherを呼び、その結果を見て次の行動を決めている流れがわかります。今回はSupervisorが「Researcherの調査内容で十分」と判断してwriter(Claude)を呼ばずにFINISHへ遷移しました。Supervisorのプロンプト次第でこの判断は変わるため、運用時はWorker全員を確実に通したい場合に明示的なルールベースの分岐を追加するなどのチューニングが必要になります。
Hierarchical パターン
Supervisorをさらに多階層化するとHierarchicalになります。「リサーチチーム」「執筆チーム」のようにチーム単位でSupervisorを置き、Topレベルのチームリーダーがそれらを束ねる構成です。
import litellm
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_litellm import ChatLiteLLM
from langgraph_supervisor import create_supervisor
litellm.modify_params = True
# =============================================
# Worker レベルのツール
# =============================================
@tool
def search_news(query: str) -> str:
"""ニュースを検索します。"""
return f"{query}に関する最新ニュース: AI業界で大規模モデルの統合が進行中"
@tool
def search_papers(query: str) -> str:
"""論文を検索します。"""
return f"{query}に関する論文: マルチエージェントの協調学習に関する研究が多数発表"
@tool
def write_summary(text: str) -> str:
"""テキストを要約します。"""
return f"要約: {text[:30]}..."
@tool
def write_blog(text: str) -> str:
"""ブログ記事を作成します。"""
return f"ブログ記事ドラフト: {text[:30]}..."
# =============================================
# モデルの定義
# =============================================
worker_llm = ChatLiteLLM(model="openai/gpt-5-mini")
mid_llm = ChatLiteLLM(model="openai/gpt-5.1")
top_llm = ChatLiteLLM(model="anthropic/claude-sonnet-4-6")
# =============================================
# リサーチチーム: news_searcher + paper_searcher
# =============================================
news_searcher = create_agent(
model=worker_llm,
tools=[search_news],
name="news_searcher",
system_prompt="ニュース検索担当。search_newsで最新ニュースを取得します。",
)
paper_searcher = create_agent(
model=worker_llm,
tools=[search_papers],
name="paper_searcher",
system_prompt="論文検索担当。search_papersで関連論文を取得します。",
)
research_team = create_supervisor(
agents=[news_searcher, paper_searcher],
model=mid_llm,
prompt="リサーチチームのリーダー。ニュースと論文を両方調査してください。",
supervisor_name="research_lead",
).compile(name="research_team")
# =============================================
# 執筆チーム: summarizer + blogger
# =============================================
summarizer = create_agent(
model=worker_llm,
tools=[write_summary],
name="summarizer",
system_prompt="要約担当。write_summaryで短くまとめます。",
)
blogger = create_agent(
model=worker_llm,
tools=[write_blog],
name="blogger",
system_prompt="ブログ記事作成担当。write_blogで記事を作成します。",
)
writing_team = create_supervisor(
agents=[summarizer, blogger],
model=mid_llm,
prompt="執筆チームのリーダー。要約とブログ記事の両方を作成してください。",
supervisor_name="writing_lead",
).compile(name="writing_team")
# =============================================
# トップレベルのSupervisor
# =============================================
app = create_supervisor(
agents=[research_team, writing_team],
model=top_llm,
prompt=(
"あなたは編集長です。"
"research_teamに調査を依頼し、その結果をwriting_teamに渡して記事化してください。"
),
).compile()
# =============================================
# 実行
# =============================================
result = app.invoke(
{
"messages": [
{
"role": "user",
"content": "マルチエージェントの最新動向について記事を作ってください",
}
]
}
)
print(result["messages"][-1].content)
このように、サブグラフ自体をエージェントとして上位Supervisorに渡せるのがHierarchicalの特徴です。Worker → Mid Supervisor → Top Supervisor の3層構造により、大規模なワークフローを役割単位で整理できます。
LiteLLMの観点では、層ごとにモデルランクを揃える設計が便利です。
| 層 | モデル例 | 理由 |
|---|---|---|
| Worker | openai/gpt-5-mini |
大量の単純タスクを高速・低コストで処理 |
| Mid Supervisor | openai/gpt-5.1 |
チーム内の判断は中程度の知性で十分 |
| Top Supervisor | anthropic/claude-sonnet-4-6 |
全体を俯瞰する重要な判断は高品質モデル(プロバイダーを切り替えてClaudeに統合役を担わせる例) |
実行結果は以下のようになります。最終出力はTopレベルのSupervisor(Claude Sonnet 4.6)がリサーチチームと執筆チームの結果を統合した1本のレポートです。
$ python hierarchical.py
両チームから成果物が上がってきました。以下が最終的な記事です!
# 📰 マルチエージェントの最新動向
## 🔍 要約
| ポイント | 内容 |
|------|------|
| **なぜ今?** | LLMとの融合・大規模化で第二の波が到来 |
| **主な技術** | LLM×RL・CTDE・GNN・ポピュレーション学習・発現的コミュニケーション |
| **応用分野** | ロボット群・自動運転・スマートグリッド・ゲームAI・市場シミュレーション |
| **主な課題** | 非定常性・クレジット割当・スケール・LLMコスト・倫理ガバナンス |
| **今後** | ハイブリッドエージェントの標準化・ガバナンス整備・社会インフラへの実装 |
## 1. なぜ今マルチエージェントなのか
(中略)
## 5. 実務で使い始めるためのヒント
- ✅ まず「何を最適化するか」を決める(個別最適 vs 全体最適)
- ✅ 既存ベースライン(MAPPO, QMIX, PettingZoo)から始める
- ✅ まず5〜10体の小規模プロトタイプで課題を把握してからスケール
- ✅ 評価指標は「報酬」だけでなく、公平性・安全性・説明可能性も含める
(以下略)
Top Supervisor(Claude Sonnet 4.6) → リサーチチーム(GPT-5-mini × 2 を GPT-5.1 がまとめる)→ 執筆チーム(同じく GPT-5-mini × 2 を GPT-5.1 がまとめる)と3階層を経由し、最後にTop Supervisorが統合した1本のレポートを返しています。Worker層を高速・安価なGPT-5-miniで実行しつつ、最終統合だけClaudeに任せることで「層ごとにモデルランクを揃える」設計となっています。
Swarm パターン
Swarmはエージェント同士がハンドオフで会話を引き継ぐパターンです。中央のSupervisorが存在せず、現在の担当エージェントが「自分の領域外だ」と判断したら、明示的に別のエージェントへ会話を引き渡します。
カスタマーサポートのように「営業」「技術」「請求」など担当が明確に分かれるシナリオに向きます。
import litellm
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_litellm import ChatLiteLLM
from langgraph.checkpoint.memory import InMemorySaver
from langgraph_swarm import create_handoff_tool, create_swarm
litellm.modify_params = True
# =============================================
# 業務固有ツール
# =============================================
@tool
def get_pricing(plan: str) -> str:
"""料金プランの情報を取得します。"""
pricing = {
"basic": "Basicプラン: 月額1,000円、ユーザー5名まで",
"pro": "Proプラン: 月額5,000円、ユーザー50名まで",
"enterprise": "Enterpriseプラン: 個別見積もり、無制限ユーザー",
}
return pricing.get(plan.lower(), f"プラン '{plan}' は見つかりませんでした")
@tool
def diagnose_error(error_code: str) -> str:
"""エラーコードから原因と対処法を返します。"""
errors = {
"E001": "認証エラー: APIキーが正しいか確認してください",
"E002": "レート制限: しばらく待ってから再試行してください",
"E003": "通信エラー: ネットワーク接続を確認してください",
}
return errors.get(error_code.upper(), f"エラーコード '{error_code}' は不明です")
# =============================================
# ハンドオフ用ツール
# =============================================
to_tech = create_handoff_tool(
agent_name="tech_support",
description="技術的な質問・エラー対応の場合に技術サポートへ引き継ぐ",
)
to_sales = create_handoff_tool(
agent_name="sales",
description="料金・契約に関する質問の場合に営業へ引き継ぐ",
)
# =============================================
# 各エージェント
# =============================================
sales_llm = ChatLiteLLM(model="openai/gpt-5.4")
tech_llm = ChatLiteLLM(model="anthropic/claude-sonnet-4-6")
sales = create_agent(
model=sales_llm,
tools=[get_pricing, to_tech],
name="sales",
system_prompt=(
"あなたは営業担当です。"
"料金プランや契約に関する質問に答えてください。"
"技術的な質問・エラー対応が来たら、必ず tech_support にハンドオフしてください。"
),
)
tech_support = create_agent(
model=tech_llm,
tools=[diagnose_error, to_sales],
name="tech_support",
system_prompt=(
"あなたは技術サポート担当です。"
"エラーや技術的な質問に答えてください。"
"料金・契約に関する質問が来たら、必ず sales にハンドオフしてください。"
),
)
# =============================================
# Swarm を構築
# =============================================
swarm = create_swarm(
agents=[sales, tech_support],
default_active_agent="sales",
)
# Swarmは「現在の担当エージェント」を状態として持つため checkpointer が必須
checkpointer = InMemorySaver()
app = swarm.compile(checkpointer=checkpointer)
# =============================================
# 実行: 同じスレッドで担当が切り替わる様子を観察
# =============================================
config = {"configurable": {"thread_id": "user-001"}}
queries = [
"Proプランの料金を教えてください", # → sales
"エラーコード E002 が出ています", # → sales が tech_support にハンドオフ
"Enterpriseプランの料金は?", # → tech_support が sales にハンドオフ
]
for query in queries:
print(f"\n{'=' * 60}")
print(f"USER: {query}")
print(f"{'=' * 60}")
result = app.invoke(
{"messages": [{"role": "user", "content": query}]},
config=config,
)
last = result["messages"][-1]
active = result.get("active_agent", "?")
print(f"[現在の担当: {active}]")
print(f"AI: {last.content}")
Swarmの動作原理
create_handoff_tool(): エージェント間ハンドオフを実現するツールを生成。LLMが「自分の領域外」と判断したらこのツールを呼び、会話の主導権が別エージェントに移るactive_agent: 現在誰が応対しているかをステートで追跡checkpointer: 会話の継続性が前提なので、状態を保存するチェックポイントが必要
実行結果は以下のようになります。
$ python swarm.py
============================================================
USER: Proプランの料金を教えてください
============================================================
[現在の担当: ?]
AI: Proプランは月額5,000円、ユーザー50名までご利用いただけます。
============================================================
USER: エラーコード E002 が出ています
============================================================
[現在の担当: tech_support]
AI: エラーコード **E002** の診断結果をお伝えします。
---
**原因:レート制限**
短時間に多数のリクエストが送信されたため、レート制限がかかっています。
**対処法:**
しばらく時間をおいてから、再度お試しください。
---
改善しない場合や、さらにご不明な点がございましたら、お気軽にお知らせください!
============================================================
USER: Enterpriseプランの料金は?
============================================================
[現在の担当: sales]
AI: Enterpriseプランは**個別見積もり**、**ユーザー数は無制限**です。
同じスレッド内で、質問内容に応じてエージェントがsales(GPT-5.4)→tech_support(Claude Sonnet 4.6)→salesと動的に切り替わっているのが確認できます。active_agentがチェックポイントに保存されているため、3回目のEnterpriseプラン質問でtech_supportからsalesへハンドオフが返ったあとも、その状態が次の問い合わせに引き継がれます(1回目は初期状態のため?と表示されています)。
技術サポート担当だけClaudeに切り替えているのは、エラーコードに対する詳細な原因分析と落ち着いた説明が品質ポイントになるためです。営業担当(sales)は料金回答という比較的単純なタスクなので、汎用性の高いGPT-5.4を当てています。担当者ごとに最適なモデルを割り当てるという、Swarmパターンの典型的な使い方です。
3つのパターンの比較
| 観点 | Supervisor | Hierarchical | Swarm |
|---|---|---|---|
| 制御構造 | 中央集権(1階層) | 中央集権(多階層) | 分散(中央なし) |
| 適したシナリオ | タスクをWorkerに振り分けたい | 大規模で役割が階層的 | 担当領域が明確で会話継続が必要 |
| 実装の手軽さ | ◎(公式パッケージ) | ○(サブグラフを組む) | ◎(公式パッケージ) |
| 状態管理 | ステートレスでも可 | ステートレスでも可 | チェックポイント必須 |
| 代表的な用途 | リサーチ+執筆など | 編集部・大規模ワークフロー | カスタマーサポート |
| LiteLLMでの強み | Supervisor/Worker でモデル分離 | 層ごとにモデルランク分け | 担当ごとに最適モデル |
各パターンは排他ではなく、Hierarchicalの最下層をSwarmにするといった組み合わせも可能です。例えば「Top SupervisorがCS部門に振った後、CS部門内ではSwarmで担当者が切り替わる」といった構成が考えられます。
LiteLLMでのモデル使い分け
3パターンを通して、本記事のサンプルでは以下の構成でプロバイダー混在を実演しています。
| 役割 | モデル | 採用理由 |
|---|---|---|
| Researcher / Worker | openai/gpt-5-mini |
大量に呼ばれる調査・実行タスクは安価・高速モデル |
| Mid Supervisor | openai/gpt-5.1 |
チーム単位の判断は中程度の知性で十分 |
| Top Supervisor / Writer / Tech | anthropic/claude-sonnet-4-6 |
統合役・文章生成・専門応答は高品質モデル |
| 全体 Supervisor / Sales | openai/gpt-5.4 |
フロー全体の指揮役 |
エージェントロジックは一切変えずに、ChatLiteLLM(model="...") の文字列を変えるだけでこの使い分けが実現できます。本番ではここにRouterによるロードバランシングやfallbacksによるプロバイダー障害対策を重ねることで、コスト・性能・可用性のバランスを取れます。
まとめ
LangGraphでマルチエージェントを構築する3つのパターン(Supervisor、Hierarchical、Swarm)を試してみました。
LiteLLMと組み合わせる最大のメリットは、エージェントごとに最適なモデルを選べる点です。Supervisorには高性能モデル、Workerには高速・安価モデル、というようにロール単位でコスト最適化できます。これは単一エージェントでは実現できない、マルチエージェントならではの利点です。
最後まで読んでいただきありがとうございました。







