Python & AI Tutorials Logo
LangChain & LangGraph

18. 多代理系统 —— 监督者模式

回想一下我们在第 16 章构建的客服代理。它只有一个工具 —— 订单查询 —— 所以当客户询问订单时,它会报告发货状态。只有一个工具的情况下,它根本没有可能选错工具。

现在假设我们把这个代理发展成一个真正的生产服务。仅有订单查询是不够的。我们还需要订单取消、物流追踪、地址修改、换货请求、退货请求、退款资格检查、退款处理、库存检查、优惠券发放、积分查询、创建支持工单等等。机制很简单:不断添加工具,并把业务规则追加到系统提示中。

但随着工具和规则不断堆积,会出现三个问题。

  • 工具选择的准确性下降。 每一轮模型都会读取所有工具描述并决定调用哪个。当你添加的工具接收相同输入、用途上又相互重叠 —— 比如换货请求退货请求 —— 选错的概率就会攀升。

  • 上下文被当前用不到的信息填满。 即使只是在处理退款,库存检查工具的模式、优惠券发放规则、换货请求流程以及其他所有内容都会在每次调用时随行。你每次都在发送完整的工具模式集合和每个领域的业务规则。当上下文被与当前任务无关的材料塞满时,真正重要的部分就被淹没,准确性也随之受损。

  • 变得难以修改。 调整单条退款规则意味着要去编辑一个把所有领域规则纠缠在一起的提示,而你无法确定这次改动不会波及换货或发货。如果不同领域由不同负责人管理,问题只会更严重。

本章教你一种应对方法。与其把更多工具和规则堆到单个代理上,我们把工作拆分为每个领域一个代理。延续第 16 章的客服场景,我们将构建一个由只处理订单查询的代理和只处理退款的代理组成的团队。每个代理都有自己的工具和自己的提示。然后我们再加一个代理来指挥它们 —— 这个指挥者被称为监督者(supervisor)。像这样由多个代理组成的架构就是多代理系统(multi-agent system),而由监督者指挥其余代理的这种安排就是监督者模式(supervisor pattern)

多代理系统确实是有代价的。因为监督者必须在每一步决定委派给哪个工作者,LLM 调用次数就更多,这意味着更高的延迟和更多的开销。所以如果你没有那么多工具和规则,根本没必要动用多代理系统。

计划如下。18.1 我们看看什么是多代理系统以及它如何运作。18.2 我们构建工作者代理。18.3 我们用 LangGraph 的 Command 对象手动构建一个监督者。18.4 我们把工作者包装成工具,用少得多的代码重新构建同一个团队。

18.1) 理解多代理系统

18.1.1) 什么是多代理系统

我们来设计一个能处理这个请求的代理:"我的订单一直没到 —— 如果有问题,就退款。"我们可以用目前学过的一切来构建它:给单个代理挂上订单查询工具和退款工具,让代理循环运行直到任务完成。单个代理查询订单、确认配送失败、看到这个结果、请求退款,然后写出最终回复。

多代理系统是一种由多个代理彼此分工的结构。你按领域拆分代理,并在它们之上放置一个监督者,每个代理只持有它需要的工具。比如我们刚刚勾勒的这个代理,自然可以拆分成一个订单查询代理和一个退款代理。监督者先调用订单查询代理来检查发货状态;当它得到配送失败的回报后,再调用退款代理来处理退款;然后它汇总两方结果来回答客户。曾经是一个代理按顺序调用工具,现在变成了一个监督者按顺序调用代理。

如果只有两个工具,就没有理由这样拆分。一个代理就够了,加个监督者只会增加 LLM 调用。但正如我们在引言里看到的,一旦工具很多,代理就可能选错工具,它的上下文会被与手头任务无关的信息填满,它的提示也会变得难以修改。

拆分解决了这些问题。订单查询代理只看到几个订单相关的工具,所以从数十个中选择就缩减为从少数几个里选。它的提示只包含订单查询规则,所以退款政策、优惠券条件以及其他与订单查询无关的事项不会填满它的上下文。而当你需要修改退款规则时,你只需碰退款代理,所以改动不会波及其他代理。

这里的订单查询代理和退款代理没什么特别的。它们和你在第 16 章构建的是同一种代理。你通过向 create_agent 传入模型、工具和提示来创建它们,并用 invoke 调用它们。它们只是覆盖的范围更小。

那么监督者做什么呢?订单查询代理和退款代理都不知道对方的存在。每个只做自己的工作;谁都不知道该谁先上。在上面的例子里,先调用订单查询、并且只在看到其结果之后才调用退款,这是监督者的判断。

谁在何时调用谁。 这就是我们所说的编排(orchestration)。

18.1.2) 监督者模式

监督者模式是一种由单个中央监督者编排多个工作者代理的结构。它遵循这些规则:

  • 监督者自己从不做实际工作。 它不查询订单也不处理退款。它只决定交给谁,然后把返回的结果汇总成最终答案。
  • 工作者之间从不互相调用。 订单查询代理从不直接调用退款代理。每条路径都要经过监督者。
  • 只有监督者与客户对话。 工作者向监督者汇报,而不是向客户。

那么监督者如何决定调用哪个代理呢?由 LLM 决定。 监督者读取迄今为止的整段对话并做出判断。如果它还不知道订单状态,就调用订单查询代理;一旦确认配送失败且需要退款,就调用退款代理。

监督者不断重复这一判断,直到用户的请求完成。它调用一个代理、收到一份汇报、在汇报被加入后重新阅读对话,然后决定下一个调用哪个代理。

这个循环与你在第 14 章构建的循环结构相同。

  • 思考 —— 读取迄今为止的对话并决定调用哪个代理。
  • 行动 —— 运行选定的代理。
  • 观察 —— 拿到代理的汇报并将其加入对话。

第 14 章你调用的是工具;这里你调用的是代理。变的只有这一点。

委派

委派

汇报

汇报

完成

客户咨询

监督者

订单查询代理

退款代理

回复客户

注意箭头是如何循环回到监督者的。当一个工作者完成时,它向监督者汇报,监督者读取该汇报并决定下一步。

结束循环的是监督者。一旦它判断客户的请求已完全处理完毕,它就产出最终答案并停止。

18.2) 构建工作者代理

我们来构建 18.1 中设计的两个工作者代理:一个检查订单状态的订单查询工作者,和一个处理退款的退款工作者。我们将在 18.3 构建监督者。

工作者代理就是你在第 16 章构建的那种普通代理。我们将用 create_agent 快速构建它们。

首先,我们来设置两个工作者共享的订单数据。

python
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": "配送失败(已退回 —— 未找到收件人)"},
}

订单查询工作者只有一个工具。

python
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"
        "不要判断是否应当退款,也不要以任何方式提及退款。你的职责仅限于订单查询和状态报告。"
    ),
)

退款工作者有两个工具:一个决定订单是否符合退款资格,一个实际处理退款。

python
@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 用作节点名和交接工具名的东西。

工作者系统提示需要的四个要素

工作者的系统提示由四个要素构成 —— 这是 Anthropic 在构建自家多代理研究系统时提炼出的原则。当这些要素薄弱时,工作者会重复工作、遗漏任务,或找不到它们需要的信息。

要素order_agentrefund_agent
角色你是订单查询专家你是退款处理专家
工具指引务必先用 check_refund_eligibility 确认,只有在那之后才调用 issue_refund
输出格式在你的最终答案中包含订单号、商品、支付金额和发货状态如果你处理了退款,在最终答案中包含金额和审批编号
任务边界不要判断是否应当退款,也不要以任何方式提及退款。你的职责仅限于订单查询和状态报告。如果订单不符合资格,不要处理它 —— 报告原因

角色用一句话固定这个工作者的身份。用"你是订单查询专家"钉住它的身份,能让模型专注于自己的工作,不太可能游荡到别人的地盘。

工具指引。 当有些事情工具模式本身无法传达时才写这一项 —— 比如工具应当在何种顺序和条件下使用。如果除了模式之外没什么要补充的,可以省略它。

输出格式和任务边界在多代理场景中至关重要。

输出格式。 工作者的最终答案不是给客户的回复 —— 而是提交给监督者的汇报。没有写在那里的任何内容都永远到不了监督者手中。如果工作者用工具查到了金额却在最终答案里漏掉了它,监督者就无从知晓。

任务边界。 这是定义工作者能走多远、以及绝不能做什么的范围。订单查询工作者应当只做查询 —— 绝不退款。这就是我们没给它退款工具的原因。但仅仅不给工具本身还不够,因为即便一个工具都没有,模型仍然可以"配送失败了,所以我来给您办理退款"。如果这句话到了监督者那里,监督者可能会以为退款已经在进行中,于是从不调用退款工作者。所以提示还说了"不要判断是否应当退款,也不要以任何方式提及退款",从话语上就阻止它连提都不提退款。

选择模型

我们给工作者用 gpt-5.4-mini,给监督者用 gpt-5.4。工作者做的是按固定顺序调用几个工具这样的简单工作,所以小模型足矣。监督者必须读取整段对话并判断下一个调用谁,所以它需要更大的模型。能够为每个代理挑选与其工作难度相匹配的模型,是拆分带来的又一个好处。

现在我们可以加上监督者了。

18.3) 手动构建监督者

我们来手动构建一个监督者。实践中你多半会使用由框架代劳的方式(下一节介绍),但要理解幕后发生了什么,你需要亲手构建一次。

正如我们在 18.1 看到的,监督者做的是单个循环:调用一个工作者、读取汇报、决定下一个调用谁 —— 或者是否停止 —— 一遍又一遍。

18.3.1) 交接与 Command

要让这个循环转起来,控制权必须在监督者和工作者之间来回传递。监督者交接控制权 —— "下一个轮到这个工作者" —— 当工作者完成时,它把控制权交回给监督者。这种控制权从一个节点传到另一个节点的过程称为交接(handoff)

一次交接需要两条信息:去哪里(目的地)和传递什么(载荷)。目的地总是必需的;载荷只在有东西要传时才包含。在 LangGraph 中,节点通过返回一个 Command 来同时指定这两者。

python
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) 构建监督者循环

结构本身很简单。我们做一个监督者节点,以及所需数量的工作者代理。工作者代理是一个代替监督者调用其指定工作者代理的节点。入口点是监督者,每个工作者代理一旦完成就移回监督者 —— 形成循环。每个节点返回的 Command 决定了控制权接下来去往何处。

goto=order_expert_proxy

goto=refund_expert_proxy

invoke

invoke

goto

goto

goto=END

START

supervisor

order_expert_proxy

refund_expert_proxy

order_agent

refund_agent

END

实线是节点之间的交接(goto);虚线是工作者代理调用其工作者代理(invoke)。监督者交接给一个工作者代理,工作者代理一旦完成就返回监督者。当监督者决定 FINISH 时,它退出到 END

现在我们把这张图变成代码。首先是 Route 类。Route 是当我们向 LLM 询问下一个调用哪个工作者代理时,用来以结构化响应接收监督者答案的模式。如果 LLM 用自由格式的自然语言回答,就难以判断该运行哪个工作者代理。Route 持有下一个调用哪个工作者代理(next)以及它如此决定的原因(reason)。

python
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 就会在完全还没推理时先选出工作者,然后再补一个理由去迎合一个它可能已经选错的选择。

接下来是监督者节点。

python
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 = (
    "你是一个客服团队的监督者。你管理两个工作者:\n"
    "- order_expert_proxy: 查询订单状态。\n"
    "- refund_expert_proxy: 检查退款资格并处理退款。\n"
    "要判断是否需要退款,你必须先检查订单状态。\n"
    "一次只指派一个工作者,一旦请求完全处理完毕就回复 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 做两件事:把 update 的内容应用到 State,并把 goto 中命名的节点作为下一个运行。在上面的代码中,我们把从 decision.next 取出的工作者代理名放进 goto,所以 LLM 选中的节点 —— decision.next —— 就是被运行的那个。

接下来是工作者代理。工作者代理做完自己的工作后把控制权交回监督者 —— 这就是它为什么用 goto="supervisor" 交接。

工作者代理的工作很简单:用 invoke 调用它的工作者代理。

python
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) 连线并运行图

我们注册这三个节点,只把入口点连接到监督者。其余每一步移动都由各节点的 Command 决定,所以不需要更多的边。

python
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 传给它的工作者,所以每个工作者都能看到迄今为止的整段对话。只有两个工作者时这没问题,但随着工作者和对话增长,每个工作者最终都会读到与自己工作毫无关系的消息。我们将在下一节用不同的方式解决这个问题。

18.4) 通过工具委派给工作者

在 18.3 手动构建了监督者的内部机制之后,我们现在用推荐用于真实项目的方式重新构建同一个团队。这种方式不需要新的 API。你用 @tool 把每个工作者变成一个工具,然后把这些工具交给一个监督者代理。我们将用 create_agent 简单地构建监督者代理。

关键思想一句话就能概括:监督者本身也只是一个代理,而每个工作者成为监督者调用的一个工具。

lookup_order 工具调用

handle_refund 工具调用

结果字符串

结果字符串

客户

监督者代理

order_agent

refund_agent

回复客户

这样看来,监督者和你在第 16 章构建的工具调用代理结构相同。它只是持有调用代理的高层工具,而不是像 get_order_status 这样的底层工具。

18.4.1) 把工作者包装成工具

我们原封不动地使用 18.2 中的 order_agentrefund_agent。我们所做的只是把每个包装进一个 @tool 函数。

python
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].content

有三点变化。

  • 工具描述取代了路由逻辑。 在 18.3 中我们手写了 SUPERVISOR_PROMPTRoute 模式,来告诉监督者它的工作者清单和它的选项。这里由工具的文档字符串来做这件事。监督者的 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 中,退款工作者只收到一句描述其任务的话。它永远看不到客户的原始措辞、监督者的推理,或另一个工作者的工具调用历史。即便有十个工作者和一百轮对话,每个工作者的上下文仍然只是那一句任务描述。

  • 作为交换,监督者承担了传递信息的工作。 因为工作者看不到对话历史,它需要的一切都必须由监督者打包进 request 字符串。这就是为什么 handle_refund 的文档字符串明确写道"包含订单号以及查询所确认的发货状态"。没有那条指令,监督者可能只传 "处理退款",让退款工作者连它到底是哪个订单都搞不清楚。

18.4.2) 组装并运行监督者

在这种方式里,监督者也只是一个代理。不需要 Command,也不需要 Route 模式。

python
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_refundrequest。监督者总结了先前的查询结果并自己写出了任务描述。 退款工作者只收到这一句话。18.3 是把整段对话交给工作者让它自己去翻找,而这里监督者只挑出需要的东西并传递过去。

18.4.3) 用检查点器给监督者添加记忆

既然说监督者是一个普通代理,那么你在第 17 章学到的检查点机制就能原样在它身上工作。

python
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_agentrefund_agent)挂检查点器。如果你这么做,工作者会把它上一次调用的结果带到当前调用里,这可能干扰手头的任务。没有检查点器,工作者只在监督者的请求之上运行。对于子代理,这是推荐的默认设置。

18.5) create_supervisor:你会在遗留代码中遇到的形式

在既有的代码库和较旧的教程里,你会遇到来自 langgraph-supervisor 包的 create_supervisor 辅助函数。给它一个代理列表和一个提示,它就为你构建整个监督者图。

python
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 是一个用单次函数调用就组装出监督者团队的辅助函数(即 18.3 的交接方式)。不过不要在新项目中使用它。它是 LangChain 不再推荐的遗留物,而且内部依赖 create_react_agent,后者在 v1 中已被弃用(计划在 v2 中移除)。LangChain 推荐你在 18.4 学到的基于工具的监督者。


在本章中,我们把一个曾经存在于单个图中的代理扩展成了一个团队 —— 按领域拆分,由监督者编排。我们用三种方式构建了同一个团队:18.3 的手动 Command 图、18.4 的工具委派方式,以及 18.5 的遗留 create_supervisor 辅助函数。对于实际工作,把工具委派方式作为你的默认选择。