LangGraph マルチエージェント 写経レクチャー — 学習メモ+用語集¶
LangGraph で複数エージェントを協調させる典型パターン(Supervisor / Swarm /
Command(goto=)/ 共有 Store / TODO 駆動)を、5 本の短いサンプルで素手から段階的に学ぶ。 サンプルの並びと起動手順はREADME.md、連載本体の解説は第18回 (../../software-design/18/STUDY_NOTES.md)・第23回 (../../software-design/23/STUDY_NOTES.md)・第24回 (../../software-design/24/STUDY_NOTES.md) を参照。
対応連載回:
- 第18回: マルチエージェント協調(データ分析エージェント)
- 第23回: Supervisor / Swarm の 2 大パターン紹介
- 第24回: Supervisor + 共有メモリ + TODO で Claude Code 風 文章執筆エージェント
注: フォルダには
ex0N.py(短い名前。現行の実体)とex0N_*.py(説明的な旧名)の 2 系統が並んでいるが、中心ロジックは同じ。本メモはex0N.pyを基準に行番号を引く。
全体像¶
学ぶ順序¶
| # | ファイル | 学ぶ概念 | 中心 API | LLM |
|---|---|---|---|---|
| 01 | ex01.py |
最小 Supervisor。ルーター LLM が worker に委譲する型を「組み立て済み」で体験 | langgraph_supervisor.create_supervisor / create_react_agent(name=...) |
✅ |
| 02 | ex02.py |
ex01 の中身を素手で再現。ノードが Command(goto=...) を return して動的ルーティング |
StateGraph / Command(goto=) / with_structured_output |
✅ |
| 03 | ex03.py |
Swarm(ピアツーピア handoff)。窓口エージェントから専門担当へ LLM が tool で bounce | langgraph_swarm.create_swarm / create_handoff_tool |
✅ |
| 04 | ex04.py |
researcher → writer の橋渡しを InMemoryStore(共有メモリ)で。state とは別経路 |
InMemoryStore(BaseStore)/ store.put・search・get |
✅ |
| 05 | ex05.py |
TODO リスト + クロージャで「自分宛の宿題だけ取る」3 エージェント協調(第24回相当を最小化) | シングルトン TodoManager / クロージャ tool / create_supervisor |
✅ |
読む順番の意図: ex01 で「完成品の Supervisor」を体験 → ex02 で「その中身(ルーター LLM + handoff + Command(goto=))を素手で」開けて見せる → ex03 で「supervisor のいない別アーキ(Swarm)」へ → ex04 で「エージェント間でデータをどう渡すか(state でなく Store)」→ ex05 で「タスクを構造化して回す(TODO 駆動)」。ex01 と ex02 はペアで読むのが肝(同じ Supervisor を「黒箱」と「素手」の両方で見る)。
2 大アーキテクチャの形(Supervisor vs Swarm)¶
flowchart TD
subgraph SV["Supervisor(ex01, ex02, ex04, ex05)"]
U1([user]) --> S{{supervisor<br/>ルーターLLM}}
S -->|委譲| W1[worker A]
S -->|委譲| W2[worker B]
W1 -->|戻す| S
W2 -->|戻す| S
S -->|FINISH| OUT1([最終回答])
end
subgraph SW["Swarm(ex03)"]
U2([user]) --> A1[faq_support<br/>窓口]
A1 -.->|transfer_to_tech_support| A2[tech_support<br/>専門]
A2 -.->|transfer_to_faq_support| A1
A2 --> OUT2([最終回答])
end
要点: Supervisor は中央のルーターが毎回ハブとして経由する「ハブ&スポーク」。Swarm はエージェント同士が直接バトンを渡す「ピアツーピア」で中央司令塔がいない。点線(-.->)は「LLM が handoff tool を選んだときだけ起こる」遷移を表す。
ex02 の素手ルーティング(Command(goto=) の動き)¶
sequenceDiagram
participant START
participant SV as supervisor_node
participant LLM as ルーターLLM<br/>(structured output)
participant M as math_node
participant W as writer_node
START->>SV: 起動(add_edge は START→supervisor のみ)
SV->>LLM: 履歴を渡し RouteDecision を要求
LLM-->>SV: next="math"
SV-->>M: Command(goto="math")
M->>M: 計算 → AIMessage(name="math")
M-->>SV: Command(goto="supervisor")
SV->>LLM: 再度判断
LLM-->>SV: next="writer"
SV-->>W: Command(goto="writer")
W-->>SV: Command(goto="supervisor")
SV->>LLM: 再度判断
LLM-->>SV: next="FINISH"
SV-->>START: Command(goto=END)
ポイント: 静的な add_edge は START → supervisor の 1 本だけ。「次にどのノードへ行くか」はノード関数が実行時に返す Command(goto=...) が決める。これが LangGraph の動的ルーティングの核。
サンプル別の要点¶
ex01: create_supervisor で最小 Supervisor¶
「タスクコーディネーター(Supervisor)が、依頼内容を見て適切な worker に委譲する」型を、組み立て済みのヘルパーで体験する。
中心 API の役割:
create_react_agent(model, tools, name, prompt)— 1 つの ReAct エージェント(LLM が tool 呼び出しを自律的に回すループ)を作る。name=がマルチエージェントで必須: Supervisor がこの名前で worker を識別し、handoff 先として指定する。langgraph_supervisor.create_supervisor(agents, model, prompt, output_mode)— 内部で「ルーター LLM + 各 worker への handoff tool +Command(goto=...)配線」を自動生成する。つまり ex02 で素手で書くものを 1 関数で吐く。
# ex01.py:33-45
math_agent = create_react_agent(
model=model, tools=[add, multiply],
name="math_expert", # ① worker の識別名(必須)
prompt="あなたは計算専門のエージェント。…",
)
greeter_agent = create_react_agent(
model=model, tools=[get_user_name],
name="greeter", prompt="あなたは挨拶担当。…",
)
# ex01.py:48-60
workflow = create_supervisor(
agents=[math_agent, greeter_agent], # ② 順序は問わない
model=model,
prompt="あなたはタスクコーディネーター。…委譲してください。", # ③ ルーター LLM への指示
output_mode="full_history", # ④ worker のやり取りを全部 messages に残す
)
graph = build_workflow().compile() # ⑤ Studio/`langgraph dev` 用に compile 済みも公開
| 行 | やってること | なぜそうする |
|---|---|---|
| ① | worker に name を付ける |
Supervisor が「math_expert に委譲」と名前で指定するため。無名だと handoff 先を識別できない |
| ② | worker のリストを渡す | Supervisor がこの集合から「誰に振るか」を選ぶ |
| ③ | Supervisor 用 prompt | ルーティング判断は LLM がやる。worker の能力一覧を渡して選ばせる |
| ④ | output_mode="full_history" |
worker 内部の全メッセージを残す。"last_message" だと各 worker の最終発話だけになる |
ex02: Command(goto=) で素手 handoff¶
ex01 の create_supervisor が裏でやっていることを、StateGraph で素手に展開する。ex01 を読んだ直後に対比で読むのが効果的。
中心メカニズム:
Command(goto=..., update=...)— ノード関数の戻り値。gotoで次に行くノード名を、updateでstate への差分書き込みを同時に指定する。add_edgeを書かずに動的に行き先を決められる(条件分岐をadd_conditional_edgesで書く旧来法より素直)。llm.with_structured_output(RouteDecision)— LLM の出力を Pydantic モデルに強制する。自由テキストを regex で抜くのでなく、next: Literal["math","writer","FINISH"]という型に縛るので、ルーティング先が必ず有効値になる。
# ex02.py:15-18
class RouteDecision(BaseModel):
next: Literal["math", "writer", "FINISH"] = Field(...) # ① 行き先を型で縛る
# ex02.py:20-35
def supervisor_node(state: MessagesState) -> Command:
decision = (
llm.with_structured_output(RouteDecision) # ② 構造化出力
.invoke([("system", decision_prompt), *state["messages"]])
)
goto = END if decision.next == "FINISH" else decision.next # ③ FINISH→END に変換
return Command(goto=goto) # ④ 行き先だけ返す(update なし)
# ex02.py:37-47
def math_node(state: MessagesState) -> Command:
response = llm.invoke([("system", "あなたは計算専門。…"), *state["messages"]])
return Command(
goto="supervisor", # ⑤ 仕事後は必ず supervisor に戻す
update={"messages": [AIMessage(content=str(response.content), name="math")]}, # ⑥ 結果を state に積む
)
# ex02.py:65-72
builder.add_edge(START, "supervisor") # ⑦ 静的エッジはこの 1 本だけ
| 行 | やってること | なぜそうする |
|---|---|---|
| ① | next を Literal で限定 |
存在しないノード名へ goto する事故を型で防ぐ |
| ② | with_structured_output |
ルーティング判断を JSON スキーマに沿わせる。LLM が勝手な語を返さない |
| ③ | "FINISH" → END |
LLM には人間語の FINISH を返させ、コード側で LangGraph 終端 END に翻訳 |
| ④ | supervisor は goto だけ |
supervisor 自身は state を書かない(判断するだけ) |
| ⑤⑥ | worker は goto="supervisor" + update |
ハブ&スポーク。worker は結果を積んで必ずハブに戻す |
| ⑦ | add_edge 1 本 |
残りの遷移は全部 Command(goto) が実行時に決める。これが ex01 の create_supervisor の中身 |
ex03: Swarm(ピアツーピア handoff)¶
中央 Supervisor を置かない。エージェント同士が「これは自分の担当じゃない」と思ったら、handoff tool を自分で選んで相手に bounce する。最初の窓口(faq_support)→ 専門担当(tech_support)へ転送する FAQ パターン。
中心 API:
create_handoff_tool(agent_name, description)— 「agent_nameに処理を引き渡す tool」を生成する。LLM はこれを普通のツールと同じように呼ぶ。tool 名はtransfer_to_<agent_name>になる(ex03 の prompt がtransfer_to_tech_supportと書いているのはこの命名規約に合わせたもの)。create_swarm(agents, default_active_agent)— handoff tool 付きの agent 群をまとめる。default_active_agentが最初にユーザーを受けるエージェント。
# ex03.py:23-30
handoff_to_tech = create_handoff_tool(
agent_name="tech_support", # ① tool 名は transfer_to_tech_support になる
description="技術的な詳細質問は tech_support に転送する",
)
handoff_to_faq = create_handoff_tool(agent_name="faq_support", description="…")
# ex03.py:33-52
faq_agent = create_react_agent(
model=model, tools=[handoff_to_tech], # ② 窓口は「技術へ転送」だけ持つ
name="faq_support", prompt="…技術的詳細は transfer_to_tech_support で転送する。",
)
tech_agent = create_react_agent(
model=model, tools=[lookup_doc, handoff_to_faq], # ③ 専門は資料引き + 「FAQ へ戻す」
name="tech_support", prompt="…lookup_doc で資料を引いて回答する。",
)
# ex03.py:55-58
workflow = create_swarm(
agents=[faq_agent, tech_agent],
default_active_agent="faq_support", # ④ 最初の窓口
)
| 行 | やってること | なぜそうする |
|---|---|---|
| ① | handoff tool を生成 | 「相手へバトンを渡す」操作を LLM の選択肢(tool)にする。これが Swarm の本質 |
| ② | 窓口は転送 tool のみ | faq は簡単な質問に答え、難しいものは tech へ渡す役 |
| ③ | 専門は実務 tool + 戻す tool | 技術質問が終わって雑談に戻ったら faq へ bounce する |
| ④ | default_active_agent |
Swarm には司令塔がいないので「最初に誰が出るか」を明示する |
ex01/ex02 との違い: Supervisor では「supervisor が毎回呼ばれて次を決める」。Swarm では supervisor は存在せず、現在アクティブなエージェントが直接次を指名する。会話の「アクティブなエージェント」が state に保持され、handoff tool が呼ばれるとそれが切り替わる。
ex04: InMemoryStore で共有メモリ¶
researcher が調べたメモを writer に渡す経路を、state(messages)とは別の Store で作る。「会話履歴」と「作業用の蓄積データ」を分離する設計。
中心 API:
InMemoryStore(BaseStoreの実装)— key-value ストア。store.put(namespace, key, value)で書き、store.search(namespace)で一覧、store.get(namespace, key)で 1 件取得。namespaceはタプル(例("session", "research_notes"))で、セッションごとに区切れるのが利点。- なぜ state でなく Store か: messages はやり取りのたびに膨らむ会話ログ。一方で「調査メモ」は構造化された蓄積データ。Store に分けると、writer は
list_notes/read_noteで必要なメモだけ取り出せる(履歴を全部読み返さなくてよい)。
# ex04.py:11-14
store = InMemoryStore() # ① プロセス内共有 store
RESEARCH_NS = ("session", "research_notes") # ② namespace(セッション分離の鍵)
# ex04.py:17-21 researcher の書き込みツール
@tool
def save_note(topic: str, content: str) -> str:
store.put(RESEARCH_NS, topic, {"content": content}) # ③ topic を key にして保存
return f"'{topic}' を保存しました"
# ex04.py:24-38 writer の読み出しツール
@tool
def list_notes() -> str:
items = store.search(RESEARCH_NS) # ④ namespace 内を全件
return "\n".join(f"- {it.key}: {it.value['content']}" for it in items)
@tool
def read_note(topic: str) -> str:
item = store.get(RESEARCH_NS, topic) # ⑤ key 指定で 1 件
return item.value["content"] if item else f"'{topic}' は未保存"
| 行 | やってること | なぜそうする |
|---|---|---|
| ① | InMemoryStore() |
シングルトン dict でも動くが、BaseStore は LangGraph 公式の置き場で namespace・永続化差し替えに対応 |
| ② | namespace をタプルで | ("session", "<id>") のように切ればマルチセッションで混線しない(dict だと全セッション共用で衝突する) |
| ③ | put(ns, key, value) |
value は dict。topic を key にして「上書き保存」になる |
| ④⑤ | search / get |
writer は会話履歴ではなく「メモ集」を直接参照 → 文脈の節約になる |
注(推測ではなく README 記載の設計意図): README は「シングルトン dict だとマルチセッションで混線する。
BaseStore経由なら namespace でセッション分離がきれいに保てる」と明記している。本サンプル自体は単一セッションなので混線は起きないが、書き方の作法として Store を使っている。
ex05: TODO 駆動マルチエージェント(第24回相当の最小化)¶
「task_decomposer が依頼を TODO に分解 → researcher が research タスクを潰す → writer が writer タスクを潰す」を、シングルトン TodoManager とクロージャで自分の宿題だけ取る tool で実現する。Claude Code の TODO リスト方式の最小版。
中心メカニズム:
TodoManager(シングルトン dataclass)—add/for_agent(agent)/complete(id, result)を持つ。for_agentが「指定エージェント宛の未完了 TODO だけ」を返すのが肝。- クロージャ tool
make_get_my_todos(agent_name)—agent_nameを閉じ込めたget_my_todostool を返す。同じ実装から researcher 用・writer 用の別 tool を量産でき、各エージェントは「自分宛 TODO だけ」を見る。第24回sd_24のcreate_get_my_todos_for_agentと同じ発想。
# ex05.py:34-43 for_agent が「自分宛の未完了だけ」を絞る
def for_agent(self, agent: str) -> list[Todo]:
return [t for t in self.items if t.agent == agent and not t.done] # ① 担当 × 未完了
# ex05.py:61-70 クロージャで agent_name を埋め込む
def make_get_my_todos(agent_name: str):
@tool
def get_my_todos() -> str:
"""自分のエージェント宛の未完了 TODO 一覧を返す"""
todos = todo_manager.for_agent(agent_name) # ② 閉じ込めた agent_name を使う
return "\n".join(f"#{t.id}: {t.description}" for t in todos) or "(…ありません)"
return get_my_todos
# ex05.py:107-126 各 worker に「自分用 get_my_todos」を配る
researcher = create_react_agent(
tools=[make_get_my_todos("research"), mock_research, complete_todo], …) # ③
writer = create_react_agent(
tools=[make_get_my_todos("writer"), mock_write, complete_todo], …) # ④
# ex05.py:128-135 実行順を prompt で固定
workflow = create_supervisor(
agents=[decomposer, researcher, writer],
prompt="…task_decomposer → researcher → writer の順で必ず呼ぶ。…") # ⑤
| 行 | やってること | なぜそうする |
|---|---|---|
| ① | 担当 × 未完了でフィルタ | 各エージェントが「他人の宿題」「終わった宿題」を見ないようにする |
| ② | クロージャで agent_name を固定 |
同じ関数定義から researcher 用 / writer 用の別 tool を作る。tool 自身は引数なしで呼べる |
| ③④ | worker ごとに専用 tool | researcher には "research"、writer には "writer" を埋めて渡す |
| ⑤ | prompt で順序を固定 | 分解→調査→執筆の依存順を Supervisor に守らせる(自由に振らせない) |
注:
mock_research/mock_writeは Web 検索や実生成をせず固定文字列を返すモック(実行を速く・安くするため)。学ぶ対象は「TODO の受け渡し構造」であって調査内容ではない。
用語集(最重要)¶
写経しながら「言葉が混乱する」ポイントを 1 枚に整理する。
1. 一番混乱する対比: Supervisor と Swarm¶
両方とも「複数エージェントの協調」だが、司令塔がいるか / 誰が次を決めるかが真逆。
| 観点 | Supervisor | Swarm |
|---|---|---|
| 形 | ハブ&スポーク(中央に supervisor) | ピアツーピア(中央なし) |
| 次の担当を決めるのは | 中央の supervisor(ルーター LLM) | 今アクティブなエージェント自身 |
| 経由 | worker は毎回 supervisor に戻る | エージェント間を直接 bounce |
| 切り替えの仕組み | supervisor が Command(goto=worker) |
アクティブエージェントが handoff tool を呼ぶ |
| 使う API | create_supervisor / Command(goto=) |
create_swarm / create_handoff_tool |
| 具体例 | ex01・ex02・ex04・ex05 | ex03 |
| 向いている場面 | タスクの順序・配分を中央で統制したい | 担当が会話の流れで自然に切り替わる(FAQ→専門→FAQ) |
判定基準(1 文): 「誰が次を決めるか」を中央の 1 体に集約したいなら Supervisor、各エージェントに『自分で次へ渡す』判断を持たせたいなら Swarm。
よくある誤解: 「Swarm は supervisor が省略された版」ではない。Swarm にはそもそも supervisor が存在しない。ex03 を回しても supervisor ノードは一度も呼ばれない(README の観察ポイントにも明記)。
2. Command(goto=) によるハンドオフ vs 通常の関数呼び出し¶
ノードが「次どこへ行くか」を決める仕組みは、普通の Python の関数呼び出しとは別物。
| 観点 | Command(goto="X") |
通常の関数呼び出し x() |
|---|---|---|
| 何をする | グラフの次ノードを指定して戻る(制御を LangGraph に返す) | その場で関数本体を実行して値を受け取る |
| 戻った後 | LangGraph ランタイムが X ノードを次に実行 | 呼び出し元が続行 |
| state | update= で差分をリデューサ経由で反映 |
関数戻り値を自分で扱う |
| 具体例 | ex02 return Command(goto="supervisor", update={...}) |
ex01 の add/multiply(tool 内の純計算) |
判定基準: グラフのノード遷移を制御したいなら Command(goto=)、ノード内のローカルな計算なら普通の関数 / tool。Command を「return」する点が重要で、関数を呼ぶのではなくランタイムへの指示書を返す。
3. 共有 Store と state(MessagesState)の違い¶
エージェント間でデータを渡す経路が 2 系統ある。混同しやすい。
| 観点 | state(MessagesState の messages) |
Store(InMemoryStore / BaseStore) |
|---|---|---|
| 中身 | 会話履歴(人間⇄AI のメッセージ列) | 任意の key-value(構造化メモ・蓄積データ) |
| 増え方 | やり取りごとに追記され膨らむ | put した key だけ。上書き可 |
| 取り出し方 | 全履歴がプロンプトに載る | get/search で欲しい分だけ |
| 区切り | グラフ実行(thread)単位 | namespace タプルで自由に分離 |
| 具体例 | ex02 の update={"messages":[...]} |
ex04 の save_note/read_note |
| 使う API | MessagesState / Command(update=) |
store.put / search / get |
判定基準: 「会話の流れそのもの」は state、「会話とは別に貯めて後で参照したい作業データ」は Store。ex04 が調査メモを Store に置くのは、writer が全会話履歴を読み返さず read_note でピンポイント取得できるようにするため。
よくある誤解: 「Store はグローバル dict と同じ」→ 機能的には近いが、Store は namespace でセッション分離でき、InMemoryStore を PostgresStore 等に差し替えれば永続化できる。ex04 はシングルトン dict でも動くが、あえて公式の BaseStore 作法で書いている。
4. worker の作り方: create_react_agent の name と handoff¶
| 用語 | 役割 | 内部メカニズム | 出てくる回 |
|---|---|---|---|
create_react_agent |
1 つの ReAct エージェント(tool 呼び出しを自律的に回す LLM ループ)を生成 | LLM→tool→LLM…を tool 呼び出しが尽きるまで回す | ex01・ex03・ex04・ex05 |
name=(引数) |
そのエージェントの識別名 | Supervisor / Swarm が「誰に渡すか」をこの名で指定。マルチエージェントでは必須 | 全回 |
create_handoff_tool(agent_name=) |
「相手にバトンを渡す」tool を生成 | tool 名 transfer_to_<agent_name>。LLM が選ぶと active agent が切り替わる |
ex03 |
create_supervisor(agents=) |
ルーター LLM + handoff + 配線を自動生成 | ex02 を 1 関数に畳んだもの | ex01・ex04・ex05 |
output_mode |
Supervisor の messages の残し方 | "full_history"=worker の全やり取り / "last_message"=各 worker の最終発話のみ |
ex01 |
判定基準: worker を素手で配線するなら ex02 の StateGraph+Command、組み立て済みでよいなら create_supervisor。学習では一度 ex02 で素手を経験してから ex01 に戻ると create_supervisor のありがたみが分かる。
5. TODO 駆動まわりの語¶
| 用語 | 役割 | 具体例 |
|---|---|---|
TodoManager(シングルトン) |
全エージェント共通の TODO 置き場 | ex05 todo_manager |
for_agent(agent) |
「担当 × 未完了」で TODO を絞る | ex05:34-35 |
クロージャ tool(make_get_my_todos) |
agent_name を閉じ込めた tool を量産 |
ex05:61-70 |
complete_todo(id, result) |
TODO を完了に倒す | 各 worker が消化後に呼ぶ |
判定基準: 「各エージェントに自分の宿題だけ見せたい」→ クロージャで agent_name を埋めた tool を配る(引数で渡すと LLM が他人の名前を入れる事故が起きうる)。
6. クイック早見表(迷ったらここ)¶
| 困りごと | 見る/使うもの |
|---|---|
| 中央で順序・配分を統制したい | Supervisor(create_supervisor / ex01・ex02) |
| 会話の流れで担当を切り替えたい | Swarm(create_swarm + create_handoff_tool / ex03) |
create_supervisor の中身を知りたい |
ex02(StateGraph + Command(goto=) の素手版) |
| ノードから次のノードを指定したい | Command(goto="X", update={...}) を return(ex02) |
| LLM のルーティング先を有効値に縛りたい | with_structured_output(RouteDecision) + Literal(ex02) |
| エージェント間でメモ/データを渡したい | InMemoryStore(BaseStore)+ namespace(ex04) |
| 履歴を膨らませず必要分だけ参照したい | Store の get/search(ex04) |
| タスクを分解して順に消化させたい | TODO リスト + クロージャ tool(ex05) |
| 各エージェントに自分の宿題だけ見せたい | make_get_my_todos(agent_name) クロージャ(ex05) |
| worker を Supervisor/Swarm に識別させたい | create_react_agent(name=...) を必ず付ける |
学んだこと(要点)¶
- Supervisor と Swarm の本質的な違いは「次を決める主体」。Supervisor は中央ルーター LLM、Swarm は今アクティブなエージェント自身。Swarm に supervisor は存在しない。
Command(goto=..., update=...)が LangGraph 動的ルーティングの核。静的add_edgeは最小限(ex02 はSTART→supervisorの 1 本だけ)で、残りはノードが実行時に返すCommandで決まる。create_supervisorは ex02 の素手コードを 1 関数に畳んだもの。ルーター LLM・handoff tool・Command配線を自動生成している。一度素手で書くと中身が腑に落ちる。- Swarm の handoff は「LLM が tool として転送を選ぶ」。
create_handoff_toolがtransfer_to_<name>という tool を作り、それが呼ばれると active agent が切り替わる。 - エージェント間のデータ受け渡しは state と Store の 2 系統。会話の流れは state、貯めて後で引く作業データは Store(namespace でセッション分離・永続化差し替えが効く)。
- クロージャで
agent_nameを埋めた tool を配ると、各エージェントが「自分宛の宿題だけ」を安全に取れる。引数で渡すより事故が少ない。 create_react_agent(name=...)はマルチエージェントで必須。Supervisor/Swarm がこの名前で worker を識別・指名する。- ルーティング判断は
with_structured_output+Literalで型に縛ると、LLM が存在しないノード名を返す事故を防げる。
拡張アイデア¶
- ex02 にチェックポイント(メモリ)を追加 —
builder.compile(checkpointer=InMemorySaver())を入れ、thread_idを変えてマルチターン会話を試す。supervisor が前回の続きから判断する様子を観察する。 - ex03 に 3 体目(billing_support)を追加 — 課金質問を担当する agent と handoff tool を足し、faq → billing → tech と複数ホップする会話を作る。アクティブエージェントが state でどう保持されるかをログで追う。
- ex04 の Store を namespace でセッション分離 —
RESEARCH_NSを("session", session_id)に変え、2 セッションを交互に回して「混線しない」ことを実測する(README の主張をコードで確認)。さらにInMemoryStoreをPostgresStoreに差し替えて永続化を試す。 - ex05 に「再分解」ループを足す — researcher が「調査が足りない」と判断したら task_decomposer に TODO を追加させ、supervisor が再び researcher に振る自己修正ループを作る。
lectures/loop_basicsのトリガー設計と接続する。 - ex01 と ex02 の実行コストを Langfuse で比較 —
lectures/langfuse_basicsの@observeでラップし、create_supervisor(ex01)と素手 supervisor(ex02)で LLM 呼び出し回数・トークン・レイテンシがどう違うかをトレースで可視化する。 - Supervisor と Swarm の同一タスクでの挙動比較 — 同じ「FAQ→技術質問」タスクを ex01 系(Supervisor)と ex03(Swarm)の両方で解き、ホップ数・最終 messages 長・無駄な往復の差を測る。
現代版に移植するなら¶
- README の
.env手順は使用禁止(プロジェクト CLAUDE.md ルール)。cp .env.sample .env+vi .envではなく、.env.op+op run --env-file=.env.op -- uv run python ex01.pyを使う(詳細は~/.claude/docs/secret-handling.md)。 langgraph-supervisor/langgraph-swarmは比較的新しいヘルパー(pyproject.tomlで>=0.0.27/>=0.0.11)。API が動くうちは便利だが、本質を理解するには ex02 の素手版を読むのが確実。バージョン更新でcreate_handoff_toolの tool 命名規約(transfer_to_<name>)が変わる可能性があるので、prompt 内の tool 名指定は実際の生成名と突き合わせて確認する。with_structured_outputは Pydantic v2 前提(decision.nextでインスタンスのフィールドにアクセス。ex02:32-33 のコメントが明記)。旧来の dict 返しを期待するコードと混ぜない。
記事参照¶
- Software Design 連載「実践LLMアプリケーション開発」第18回(マルチエージェント協調)・第23回(Supervisor / Swarm)・第24回(TODO 駆動 文章執筆エージェント)
- 連載本体メモ:
../../software-design/18/STUDY_NOTES.md/../../software-design/23/STUDY_NOTES.md/../../software-design/24/STUDY_NOTES.md - 公式: langgraph-supervisor / langgraph-swarm / LangGraph multi-agent 概念
作成: 2026-06-12 / 最終更新: 2026-06-12