18. マルチエージェントシステム — スーパーバイザーパターン
第16章で構築したカスタマーサービスエージェントを思い出してみましょう。それはツールをちょうど1つ——注文照会——だけ持っていたので、お客様が注文について尋ねると、配送状況を報告しました。ツールが1つしかないため、間違ったツールを選ぶ余地がそもそもありませんでした。
さて、そのエージェントを本物の本番サービスへと成長させると仮定しましょう。注文照会だけでは十分ではありません。注文キャンセル、配送追跡、住所変更、交換依頼、返品依頼、返金資格チェック、返金処理、在庫チェック、クーポン発行、ポイント照会、サポートチケット作成など、さまざまな機能が必要になります。仕組みは単純です。ツールを追加し続け、システムプロンプトにビジネスルールを追記していくだけです。
しかし、ツールとルールが積み重なるにつれて、3つの問題が浮かび上がります。
-
ツール選択の精度が下がります。 モデルはターンごとにすべてのツールの説明を読み、どれを呼び出すかを決めます。交換依頼と返品依頼のように、同じ入力を取り、目的が重なるツールを追加していくと、間違ったものを選ぶ確率が上がります。
-
現在必要としない情報でコンテキストが埋まります。 返金を処理している最中でさえ、在庫チェックツールのスキーマ、クーポン発行ルール、交換依頼手順、その他すべてが呼び出しごとに一緒についてきます。ツールスキーマの全セットと各ドメインのビジネスルールを毎回送っているのです。現在のタスクに無関係な資料でコンテキストが混み合うと、重要な部分が埋もれてしまい、精度が損なわれます。
-
変更が難しくなります。 たった1つの返金ルールを調整するには、すべてのドメインのルールが絡み合ったプロンプトを編集する必要があり、その変更が交換や配送に波及しないと確信することはできません。ドメインごとに異なる担当者がいる場合、問題はさらに大きくなります。
この章では、これに対処する1つの方法を教えます。単一のエージェントにツールとルールをさらに積み重ねる代わりに、作業をドメインごとに1エージェントに分割します。第16章のカスタマーサービスのシナリオを継続して、注文照会だけを扱うエージェントと、返金だけを扱うエージェントからなるチームを構築します。それぞれが独自のツールと独自のプロンプトを持ちます。そして、それらを指揮するもう1つのエージェントを追加します——この指揮者はスーパーバイザー(supervisor)と呼ばれます。このように複数のエージェントを持つ構成がマルチエージェントシステム(multi-agent system)であり、スーパーバイザーが残りを指揮する配置がスーパーバイザーパターン(supervisor pattern)です。
マルチエージェントシステムにはコストがかかります。スーパーバイザーはステップごとにどのワーカーに委譲するかを決めなければならないため、LLM 呼び出しが増え、それは遅延の増加とコストの増加を意味します。ですから、ツールとルールがそれほど多くない場合は、マルチエージェントシステムに手を伸ばす必要はまったくありません。
計画は次のとおりです。18.1 ではマルチエージェントシステムとは何か、そしてどのように機能するかを見ます。18.2 ではワーカーエージェントを構築します。18.3 では LangGraph の Command オブジェクトを使って手作業でスーパーバイザーを構築します。18.4 ではワーカーをツールとしてラップし、はるかに少ないコードで同じチームを再構築します。
18.1) マルチエージェントシステムの理解
18.1.1) マルチエージェントシステムとは
「注文が届かなかった——何か問題があれば返金して」というリクエストを処理できるエージェントを設計してみましょう。これまでに学んだすべてを使って構築できます。注文照会ツールと返金ツールを1つのエージェントにアタッチし、仕事が完了するまでエージェントをループさせるのです。単一のエージェントが注文を照会し、配送が失敗したことを確認し、その結果を見て、返金をリクエストし、最終的な返信を書きます。
マルチエージェントシステム(multi-agent system)は、複数のエージェントがこの作業を分担する構造です。エージェントをドメインごとに分割し、その上にスーパーバイザーを配置します。各エージェントは必要なツールだけを保持します。たとえば、たった今スケッチしたエージェントは、注文照会エージェントと返金エージェントに自然に分割されます。スーパーバイザーはまず注文照会エージェントを呼び出して配送状況を確認します。配送が失敗したという報告を受け取ると、返金エージェントを呼び出して返金を処理します。そして両方の結果を集めてお客様に回答します。かつては1つのエージェントが順番にツールを呼び出していたものが、スーパーバイザーが順番にエージェントを呼び出すものになったのです。
ツールが2つしかなければ、このように分割する理由はありません。1つのエージェントで十分であり、スーパーバイザーを追加することは LLM 呼び出しを増やすだけです。しかし、序文で見たように、ツールが多くなると、エージェントは間違ったものを選び、そのコンテキストは手元のタスクに無関係な情報で埋まり、そのプロンプトは変更しにくくなります。
分割はこれらの問題を解決します。注文照会エージェントは注文関連のいくつかのツールしか見ないので、数十個のリストから選ぶことが、ほんの一握りから選ぶことに縮小します。そのプロンプトには注文照会のルールしか含まれないので、返金ポリシー、クーポン条件、その他注文照会に無関係な事柄がコンテキストを埋めることはありません。そして返金ルールを変更する必要があるときは、返金エージェントにだけ手を触れればよいので、その変更は他には及びません。
ここでの注文照会エージェントと返金エージェントは何も特別なものではありません。第16章で構築したのと同じ種類のエージェントです。モデル、ツール、プロンプトを create_agent に渡して作成し、invoke で呼び出します。ただ、カバーする範囲が狭いだけです。
では、スーパーバイザーは何をするのでしょうか? 注文照会エージェントと返金エージェントは、互いの存在を知りません。それぞれが自分の仕事をするだけで、どちらが先に動くべきかを知りません。上記の例で、まず注文照会を呼び出し、その結果を見た後にのみ返金を呼び出したのは、スーパーバイザーの判断でした。
誰が誰を、いつ呼び出すか。 これがオーケストレーションと呼ばれるものです。
18.1.2) スーパーバイザーパターン
スーパーバイザーパターン(supervisor pattern)は、単一の中央スーパーバイザーが複数のワーカーエージェントをオーケストレーションする構造です。これは次のルールに従います。
- スーパーバイザーは自分では作業をしません。 注文を照会したり、返金を処理したりしません。誰にハンドオフするかを決め、返された結果を最終的な回答にまとめるだけです。
- ワーカーは互いを呼び出しません。 注文照会エージェントが返金エージェントを直接呼び出すことはありません。すべての経路はスーパーバイザーを通ります。
- お客様と話すのはスーパーバイザーだけです。 ワーカーはお客様にではなく、スーパーバイザーに報告します。
では、スーパーバイザーはどのエージェントを呼び出すかをどのように決めるのでしょうか? LLM が決めます。 スーパーバイザーはこれまでの会話全体を読み、判断します。まだ注文状況を知らなければ、注文照会エージェントを呼び出します。配送が失敗して返金が必要だと確認できたら、返金エージェントを呼び出します。
スーパーバイザーは、ユーザーのリクエストが完了するまでこの判断を繰り返します。エージェントを呼び出し、報告を受け取り、その報告が追加された今の会話を読み直し、次にどのエージェントを呼び出すかを決めます。
このループは、第14章で構築したものと同じ構造を持ちます。
- Think(考える) — これまでの会話を読み、どのエージェントを呼び出すかを決める。
- Act(行動する) — 選ばれたエージェントを実行する。
- Observe(観察する) — エージェントの報告を受け取り、会話に追加する。
第14章ではツールを呼び出しましたが、ここではエージェントを呼び出します。変わるのはそれだけです。
矢印がスーパーバイザーに戻ってループしていることに注目してください。ワーカーが完了すると、スーパーバイザーに報告し、スーパーバイザーはその報告を読んで次の動きを決めます。
ループを終わらせるのはスーパーバイザーです。お客様のリクエストが完全に処理されたと判断すると、最終的な回答を生成して停止します。
18.2) ワーカーエージェントの構築
18.1 で設計した2つのワーカーエージェントを構築しましょう。注文状況をチェックする注文照会ワーカーと、返金を処理する返金ワーカーです。スーパーバイザーは 18.3 で構築します。
ワーカーエージェントは、第16章で構築した普通のエージェントにすぎません。create_agent を使ってすばやく構築します。
まず、2つのワーカーが共有する注文データを設定しましょう。
ORDERS = {
"12345": {"item": "ワイヤレスイヤホン", "amount": 89,
"status": "in_transit", "status_text": "配送中(明日到着予定)"},
"67890": {"item": "メカニカルキーボード", "amount": 129,
"status": "delivered", "status_text": "配達完了"},
"24680": {"item": "ノイズキャンセリングヘッドホン", "amount": 249,
"status": "delivery_failed", "status_text": "配達失敗(返送 — 受取人不明)"},
}注文照会ワーカーはツールをちょうど1つだけ持ちます。
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_order_status(order_id: str) -> str:
"""注文番号について、商品、支払金額、配送状況を照会します。"""
order = ORDERS.get(order_id)
if order is None:
return f"注文 {order_id} が見つかりません。"
return (f"注文 {order_id}: {order['item']}, "
f"${order['amount']:,}, ステータス: {order['status_text']}")
order_agent = create_agent(
name="order_expert",
model="openai:gpt-5.4-mini",
tools=[get_order_status],
system_prompt=(
"あなたは注文照会の専門家です。注文状況を照会して回答してください。\n"
"最終的な回答には、注文番号、商品、支払金額、配送状況を含めてください。\n"
"返金が妥当かどうかを判断したり、いかなる形でも返金に言及したりしないでください。あなたの役割は注文照会と状況報告のみです。"
),
)返金ワーカーは2つのツールを持ちます。1つは注文が返金資格を持つかどうかを判断するもの、もう1つは実際に返金を処理するものです。
@tool
def check_refund_eligibility(order_id: str) -> str:
"""注文が返金の対象となるかどうかを判断します。配達失敗の注文のみが対象となります。"""
order = ORDERS.get(order_id)
if order is None:
return f"注文 {order_id} が見つかりません。"
if order["status"] == "delivery_failed":
return f"注文 {order_id} は返金の対象です(理由: 配達失敗)。"
return f"注文 {order_id} は返金の対象外です(現在のステータス: {order['status_text']})。"
@tool
def issue_refund(order_id: str) -> str:
"""返金を処理します。これを呼び出す前に、必ず check_refund_eligibility で資格を確認してください。"""
order = ORDERS.get(order_id)
if order is None:
return f"注文 {order_id} が見つかりません。"
return (f"返金完了: 注文 {order_id} の ${order['amount']:,} は "
f"3〜5営業日以内に返金されます。(承認番号: RF-{order_id})")
refund_agent = create_agent(
name="refund_expert",
model="openai:gpt-5.4-mini",
tools=[check_refund_eligibility, issue_refund],
system_prompt=(
"あなたは返金処理の専門家です。\n"
"必ず最初に check_refund_eligibility で資格を確認し、その後に "
"のみ issue_refund を呼び出してください。\n"
"返金を処理した場合は、金額と承認番号を最終的な回答に含めてください。\n"
"注文が対象外の場合は処理せず、代わりに理由を報告してください。"
),
)両方のワーカーに name を与えました。この名前は、18.5 の create_supervisor がノード名およびハンドオフツール名として使うものです。
ワーカーのシステムプロンプトに必要な4つのこと
ワーカーのシステムプロンプトは4つの要素から構築されます——Anthropic が独自のマルチエージェントリサーチシステムを構築する中で抽出した原則です。これらが弱いと、ワーカーは作業を重複させたり、タスクを未完了のまま残したり、必要な情報を見つけられなかったりします。
| 要素 | order_agent | refund_agent |
|---|---|---|
| 役割 | あなたは注文照会の専門家です | あなたは返金処理の専門家です |
| ツールガイダンス | 必ず最初に check_refund_eligibility で確認し、その後にのみ issue_refund を呼び出す | |
| 出力フォーマット | 最終的な回答に注文番号、商品、支払金額、配送状況を含める | 返金を処理した場合は、金額と承認番号を最終的な回答に含める |
| タスク境界 | 返金が妥当かどうかを判断したり、いかなる形でも返金に言及したりしない。あなたの役割は注文照会と状況報告のみ。 | 注文が対象外の場合は処理せず、理由を報告する |
役割は、このワーカーが誰であるかを一文で固定します。「あなたは注文照会の専門家です」でそのアイデンティティを釘付けにすることで、モデルは自分の仕事に集中し続け、他人の仕事に迷い込みにくくなります。
ツールガイダンス。 ツールスキーマだけでは伝えられないことがあるときに書きます——たとえば、ツールを使うべき順序と条件などです。スキーマを超えて追加すべきことが何もなければ、省略してもかまいません。
出力フォーマットとタスク境界は、マルチエージェントの設定において非常に重要です。
出力フォーマット。 ワーカーの最終的な回答は、お客様への返信ではありません——スーパーバイザーに提出される報告です。そこに書かれていないことは、決してスーパーバイザーに届きません。ワーカーがツールで金額を照会しても、それを最終的な回答から漏らすと、スーパーバイザーにはそれを知る手立てがありません。
タスク境界。 これは、ワーカーがどこまで進んでよいか、そして何をしてはならないかを定義する範囲です。注文照会ワーカーは照会だけをすべきで——決して返金してはいけません。だからこそ、返金ツールを与えなかったのです。しかし、ツールを与えないだけでは不十分です。なぜなら、モデルはツールがまったくなくても「配達に失敗したので、返金いたします」と言うことができるからです。もしその文がスーパーバイザーに届くと、スーパーバイザーは返金がすでに進行中だと思い込み、返金ワーカーを決して呼び出さないかもしれません。そのため、プロンプトには「返金が妥当かどうかを判断したり、いかなる形でも返金に言及したりしないでください」とも書いてあり、言葉の上でも返金を持ち出すことすら防いでいます。
モデルの選択
ワーカーには gpt-5.4-mini、スーパーバイザーには gpt-5.4 を使っています。ワーカーはいくつかのツールを決まった順序で呼び出すという単純な仕事をするので、小さいモデルで十分です。スーパーバイザーは会話全体を読み、次に誰を呼び出すかを判断しなければならないので、より大きなモデルが必要です。仕事の難易度に合わせてエージェントごとにモデルを選べることは、分割のもう1つの利点です。
これでスーパーバイザーを追加できます。
18.3) 手作業でスーパーバイザーを構築する
手作業でスーパーバイザーを構築してみましょう。実際には、フレームワークがこれを代わりにやってくれるアプローチ(次のセクションで扱います)をほとんどの場合使うことになりますが、内部で何が起こっているかを理解するには、一度自分で構築する必要があります。
18.1 で見たように、スーパーバイザーがすることは1つのループです。ワーカーを呼び出し、報告を読み、次に誰を呼び出すか——あるいは停止するかどうか——を何度も何度も決めることです。
18.3.1) ハンドオフと Command
このループが回るためには、制御がスーパーバイザーとワーカーの間を行き来しなければなりません。スーパーバイザーは制御をハンドオフし——「次はこのワーカー」——ワーカーが完了すると、制御をスーパーバイザーに返します。このように1つのノードから別のノードへ制御を渡すことをハンドオフ(handoff)と呼びます。
ハンドオフには2つの情報が必要です。どこへ行くか(宛先)と何を渡すか(ペイロード)です。宛先は常に必須ですが、ペイロードは渡すものがあるときにのみ含まれます。LangGraph では、ノードは Command を返すことで両方を指定します。
from typing import Literal
from langgraph.graph import MessagesState
from langgraph.types import Command
def some_node(state: MessagesState) -> Command[Literal["refund_expert_proxy"]]:
return Command(
goto="refund_expert_proxy", # どこへ: 次に実行するノード
update={"messages": [...]}, # 何を: ワーカーの報告(State に追加されるペイロード)
)goto は宛先、update はペイロードです。戻り値の型ヒント Command[Literal["refund_expert_proxy"]] は、このノードが行ける宛先を前もってリストアップしています。この Command が実際にどう使われるかは、スーパーバイザーノードとワーカープロキシを構築する次のセクションで見ていきます。
18.3.2) スーパーバイザーループの構築
構造そのものは単純です。1つのスーパーバイザーノードと、必要なだけのワーカープロキシを作ります。ワーカープロキシは、スーパーバイザーに代わって割り当てられたワーカーエージェントを呼び出すノードです。エントリーポイントはスーパーバイザーで、各ワーカープロキシは完了するとスーパーバイザーに戻ります——これがループを形成します。各ノードが返す Command が、次にどこへ制御が行くかを決めます。
実線はノード間のハンドオフ(goto)で、点線はワーカープロキシがワーカーエージェントを呼び出すこと(invoke)です。スーパーバイザーはワーカープロキシにハンドオフし、ワーカープロキシは完了するとスーパーバイザーに戻ります。スーパーバイザーが FINISH を決めると、END に抜けます。
では、この図をコードに変えましょう。まず、Route クラスです。Route は、次にどのワーカープロキシを呼び出すかを LLM に尋ねるとき、スーパーバイザーの回答を構造化レスポンスとして受け取るためのスキーマです。もし LLM が自由形式の自然言語で答えたら、どのワーカープロキシを実行すべきか判別するのが難しくなります。Route は、次にどのワーカープロキシを呼び出すか(next)と、そう決めた理由(reason)を保持します。
from typing import Literal
from pydantic import BaseModel, Field
class Route(BaseModel):
reason: str = Field(description="この決定の理由。")
next: Literal["order_expert_proxy", "refund_expert_proxy", "FINISH"] = Field(
description="次に実行するワーカーノード。リクエストが完全に処理されていれば FINISH。"
)reason が next の前に宣言されているのには理由があります。構造化出力はスキーマにフィールドが現れる順序で生成されるので、reason を先にすると、LLM はワーカーを選ぶ前に推論を書きます。先に推論して後で決めるほうが、より良い選択につながります。順序を逆にして——next を先にすると——LLM はまったく推論する前にワーカーを選び、それから、すでに間違えているかもしれない選択に合わせて理由を後付けします。
次に、スーパーバイザーノードです。
from typing import Literal
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
supervisor_llm = ChatOpenAI(model="gpt-5.4")
SUPERVISOR_PROMPT = (
"あなたはカスタマーサービスチームのスーパーバイザーです。2人のワーカーを管理します:\n"
"- order_expert_proxy: 注文状況を照会します。\n"
"- refund_expert_proxy: 返金資格をチェックし、返金を処理します。\n"
"返金が必要かどうかを判断するには、まず注文状況を確認しなければなりません。\n"
"一度に1人のワーカーに割り当て、リクエストが完全に処理されたら FINISH で応答してください。"
)
def supervisor(
state: MessagesState,
) -> Command[Literal["order_expert_proxy", "refund_expert_proxy", "__end__"]]:
messages = [{"role": "system", "content": SUPERVISOR_PROMPT}, *state["messages"]]
decision = supervisor_llm.with_structured_output(Route).invoke(messages)
print(f"[supervisor] → {decision.next} ({decision.reason})")
if decision.next == "FINISH":
final = supervisor_llm.invoke(
[{"role": "system", "content": "これまでの会話に基づいて、お客様への返信を書いてください。"},
*state["messages"]]
)
return Command(goto=END, update={"messages": [final]}) # END にハンドオフ
return Command(goto=decision.next) # ワーカープロキシにハンドオフこれまで、ノードは変更された State だけを返していました。しかしスーパーバイザーノードは Command を返します。ノードが Command を返すと、LangGraph は2つのことを行います。update の内容を State に適用し、goto で指名されたノードを次に実行します。上記のコードでは、decision.next から取り出したワーカープロキシ名を goto に入れているので、LLM が選んだノード——decision.next——が実行されます。
次に、ワーカープロキシです。ワーカープロキシは自分の仕事をしてスーパーバイザーに制御を返します——だからこそ goto="supervisor" でハンドオフするのです。
ワーカープロキシの仕事は単純です。invoke で自分のワーカーエージェントを呼び出すことです。
def order_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
result = order_agent.invoke(state)
last = result["messages"][-1]
return Command(
goto="supervisor",
update={"messages": [HumanMessage(content=last.content, name="order_expert")]},
)
def refund_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
result = refund_agent.invoke(state)
last = result["messages"][-1]
return Command(
goto="supervisor",
update={"messages": [HumanMessage(content=last.content, name="refund_expert")]},
)ワーカーの最終メッセージだけを取り出して、それをスーパーバイザーに渡していることに注目してください。スーパーバイザーは結論だけを必要とします。ワーカーが内部で何回ツールを呼び出したかを知る必要はありません。
18.3.3) グラフの配線と実行
3つのノードを登録し、エントリーポイントをスーパーバイザーだけに接続します。それ以外のすべての動きは各ノードの Command によって決まるので、それ以上のエッジは必要ありません。
builder = StateGraph(MessagesState)
builder.add_node("supervisor", supervisor)
builder.add_node("order_expert_proxy", order_expert_proxy)
builder.add_node("refund_expert_proxy", refund_expert_proxy)
builder.add_edge(START, "supervisor")
team = builder.compile()
result = team.invoke(
{"messages": [HumanMessage(
content="注文 24680 がまだ届きません。何か問題があれば返金してください。"
)]},
config={"recursion_limit": 15},
)出力:
[supervisor] → order_expert_proxy (返金を決める前に注文状況を確認する必要がある。)
[supervisor] → refund_expert_proxy (配達失敗が確認されたので、返金資格をチェックして処理する。)
[supervisor] → FINISH (注文確認と返金処理の両方が完了した。)スーパーバイザーはまず注文照会にルーティングしました。配達が失敗したという報告が返ってくると返金にルーティングし、返金完了の報告が届くと終了しました。前のエージェントの報告を読むことで、次の宛先をそれぞれ選んだのです。
ワーカープロキシは invoke(state) を通じて共有 State 全体をワーカーに渡すので、各ワーカーはこれまでの会話全体を見ます。ワーカーが2人だけならこれでかまいませんが、ワーカーと会話が増えると、各ワーカーは自分の仕事とは無関係なメッセージまで読むことになります。次のセクションではこれを別の方法で解決します。
18.4) ツールを通じてワーカーに委譲する
18.3 でスーパーバイザーの内部を手作業で構築したので、次は実際のプロジェクトで推奨される方法で同じチームを再構築しましょう。このアプローチには新しい API は必要ありません。各ワーカーを @tool でツールに変え、それらのツールをスーパーバイザーエージェントに与えます。スーパーバイザーエージェントは create_agent でシンプルに構築します。
鍵となる考え方は一文に収まります。スーパーバイザー自体もただのエージェントであり、各ワーカーはスーパーバイザーが呼び出すツールになる。
こう見ると、スーパーバイザーは第16章で構築したツール呼び出しエージェントと同じ構造を持っています。ただ、get_order_status のような低レベルのツールの代わりに、エージェントを呼び出す高レベルのツールを保持しているだけです。
18.4.1) ワーカーをツールとしてラップする
18.2 の order_agent と refund_agent をそのまま使います。やることは、それぞれを @tool 関数でラップするだけです。
from langchain.tools import tool
# order_agent と refund_agent は 18.2 のワーカーです
@tool
def lookup_order(request: str) -> str:
"""注文の商品、支払金額、配送状況を照会します。注文の状況を知る必要があるときに使ってください。
入力: 自然言語の照会リクエスト(例: '注文 24680 の配送状況を教えて')。
"""
print("[tool call] lookup_order")
print(f" request: {request}")
result = order_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].content
@tool
def handle_refund(request: str) -> str:
"""返金資格をチェックし、返金を処理します。お客様が返金を希望し、すでに注文状況を確認済みのときに使ってください。
入力: 自然言語の返金リクエスト。注文番号と、照会で確認した配送状況を含めてください。
(例: '注文 24680 は配達失敗の状態です。対象であれば返金を処理してください。')
"""
print("[tool call] handle_refund")
print(f" request: {request}")
result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].content3つのことが変わりました。
-
ツールの説明がルーティングロジックを置き換えます。 18.3 では、スーパーバイザーにワーカーのリストと選択肢を伝えるために、
SUPERVISOR_PROMPTとRouteスキーマを手作業で書きました。ここではツールの docstring がその仕事をします。スーパーバイザーの LLM はツールの説明を読み、いつ何を呼び出すかを決めます。 -
各ワーカーはクリーンなコンテキストから始まります。 18.3 と並べて置くと、違いが明確になります。
python# 18.3 (手作業のグラフ): 共有 State 全体を渡す result = refund_agent.invoke(state) # 18.4 (ツール委譲): スーパーバイザーが書いたタスク説明だけを渡す result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})18.4 では、返金ワーカーは自分のタスクを説明するたった一文を受け取ります。お客様の元の言葉遣いも、スーパーバイザーの推論も、別のワーカーのツール呼び出し履歴も、決して見ません。ワーカーが10人いて会話が100ターンあっても、各ワーカーのコンテキストは依然としてそのたった一つのタスク説明だけです。
-
その代わり、スーパーバイザーが情報を渡す仕事を引き受けます。 ワーカーは会話履歴を見ることができないので、必要なものは何であれスーパーバイザーが
request文字列に詰め込まなければなりません。だからこそhandle_refundの docstring は「注文番号と、照会で確認した配送状況を含めてください」と明記しているのです。その指示がなければ、スーパーバイザーは"返金を処理してください"だけを渡してしまい、返金ワーカーはそもそもどの注文についてなのか分からなくなるかもしれません。
18.4.2) スーパーバイザーの組み立てと実行
このアプローチでは、スーパーバイザーもただのエージェントです。Command も Route スキーマも必要ありません。
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
TOOL_SUPERVISOR_PROMPT = (
"あなたはカスタマーサービスチームのスーパーバイザーです。\n"
"返金が必要かどうかを判断するには、まず注文状況を確認しなければなりません。\n"
"自分では作業をせず——ワーカーに委譲してください。\n"
"ワーカーはこの会話を見ることができません。委譲するときは、必要なものすべてをリクエストに入れてください。\n"
"すべての作業が完了したら、ワーカーの結果を統合してお客様への返信にまとめてください。"
)
supervisor_agent = create_agent(
model="openai:gpt-5.4",
tools=[lookup_order, handle_refund],
system_prompt=TOOL_SUPERVISOR_PROMPT,
)
result = supervisor_agent.invoke(
{"messages": [HumanMessage(
content="注文 24680 がまだ届きません。何か問題があれば返金してください。"
)]}
)
print("\n\n[final response]")
print(result["messages"][-1].content)最終結果は 18.3 と同じです。
[tool call] lookup_order
request: お客様は注文 24680 がまだ届いていないとおっしゃっています。返金が必要かどうかを
判断するため、注文 24680 の商品、支払金額、現在の配送状況を教えてください。
[tool call] handle_refund
request: 注文 24680 はノイズキャンセリングヘッドホン、$249 で、その配送状況は
'配達失敗(返送 — 受取人不明)' と確認されています。お客様は
返金を希望しているので、資格をチェックして対象であれば処理してください。
[final response]
確認したところ、注文 24680 は配達失敗の状態でした(返送 — 受取人不明)。
返金の対象となり、返金を完了しました。
- 商品: ノイズキャンセリングヘッドホン
- 返金額: $249
- 返金承認番号: RF-24680
お支払い方法によって、返金が反映されるまで通常数営業日かかります。しかし、その過程でスーパーバイザーが行ったツール呼び出しを見てください——そこに 18.3 との違いが表れています。handle_refund の request を見てください。スーパーバイザーは先の照会結果を要約し、タスク説明を自分で書きました。 返金ワーカーはこのたった一文だけを受け取ります。18.3 ではワーカーに会話全体を渡して掘り起こさせたのに対し、ここではスーパーバイザーが必要なものだけを選び出して渡します。
18.4.3) チェックポインターでスーパーバイザーにメモリを追加する
スーパーバイザーが普通のエージェントだと言うことは、第17章で学んだチェックポインティングがそのまま動くということです。
from langgraph.checkpoint.memory import InMemorySaver
supervisor_agent = create_agent(
model="openai:gpt-5.4",
tools=[lookup_order, handle_refund],
system_prompt=TOOL_SUPERVISOR_PROMPT,
checkpointer=InMemorySaver(), # チェックポインターはトップレベルのエージェントにのみ付ける
)
config = {"configurable": {"thread_id": "cs-1"}}
supervisor_agent.invoke(
{"messages": [HumanMessage(content="私の注文 12345 はどこですか?")]},
config,
)
follow_up = supervisor_agent.invoke(
{"messages": [HumanMessage(content="それはいくらでしたか?")]},
config,
)
print(follow_up["messages"][-1].content)出力:
あなたの注文 12345、ワイヤレスイヤホンは $89 でした。スーパーバイザーは、フォローアップの中のそれを前のターンの注文 12345 として正しく読み取ります。チェックポインターはスーパーバイザーでも同じように動きます。
ワーカーエージェント(
order_agent、refund_agent)にはチェックポインターを付けないでください。付けると、ワーカーは前回の呼び出しの結果を今回の呼び出しに持ち込んでしまい、手元のタスクの妨げになることがあります。付けなければ、ワーカーはスーパーバイザーのリクエストだけで動きます。サブエージェントについては、これが推奨されるデフォルトです。
18.5) create_supervisor: レガシーコードで出会う形
既存のコードベースや古いチュートリアルでは、langgraph-supervisor パッケージの create_supervisor ヘルパーに出くわすでしょう。エージェントのリストとプロンプトを与えると、スーパーバイザーグラフ全体を代わりに構築してくれます。
from langgraph_supervisor import create_supervisor
from langchain_openai import ChatOpenAI
workflow = create_supervisor(
agents=[order_agent, refund_agent], # 各エージェントには name が設定されていなければならない
model=ChatOpenAI(model="gpt-5.4"),
prompt="注文確認を order_expert に、返金を refund_expert に割り当ててください。",
)
app = workflow.compile()create_supervisor は、スーパーバイザーチームを1回の関数呼び出しで組み立てるヘルパーです(18.3 のハンドオフアプローチ)。ただし、新しいプロジェクトでは使わないでください。これは LangChain がもはや推奨しないレガシーであり、内部では create_react_agent に依存していますが、これは v1 で非推奨となりました(v2 での削除が予定されています)。LangChain は、18.4 で学んだツールベースのスーパーバイザーを推奨しています。
この章では、単一のグラフの中に存在していたエージェントを、チームへと拡張しました——ドメインで分割し、スーパーバイザーでオーケストレーションしました。同じチームを3つの方法で構築しました。18.3 の手作業の Command グラフ、18.4 のツール委譲アプローチ、そして 18.5 のレガシー create_supervisor ヘルパーです。実際の仕事では、ツール委譲アプローチをデフォルトにしてください。