05 · Supervisor 多智能体:一个 Agent 装不下的活
零、开始之前
这篇的目标:搞清楚多智能体到底解决什么问题(不是你以为的那个),能搭出中心化和去中心化两种结构,并且知道大多数情况下你不该用它。
需要的前置知识:读过 01 ReAct(特别是 4.6 讲 messages 增长的部分)和 03 Plan。
读完你会明白:
- 多智能体的两个真实理由是什么(都和「更聪明」无关)
- 「交接」(handoff)具体是怎么实现的 —— 它就是一个普通工具
- 星型和网状两种拓扑差在哪,各适合什么场景
- 为什么多智能体是本专题最容易被过度使用的范式
先说结论,免得你读完还是想拆:
多智能体的两个理由是上下文隔离和并行。这两个都是工程约束,不是智能问题。 说不出你隔离了什么、并行了什么,就别拆。
一、先看一个真实问题
给一个 ReAct Agent 派活:
「调研一下我们这三个候选的向量数据库,写一份对比报告。」
它会怎么做?
第 1 步 搜索 A 数据库的文档 → 返回 8000 token
第 2 步 打开官方性能测试页 → 返回 12000 token
第 3 步 搜索 A 的已知问题 → 返回 6000 token
第 4 步 搜索 B 数据库的文档 → 返回 9000 token
...
第 12 步 上下文超限,报错 ❌
问题很直接:调研过程产生的原始材料,比最终报告大两个数量级。
而这些原始材料,在写报告的时候一点都不需要。你需要的只是「A 的结论、B 的结论、C 的结论」这三段话。
再看第二个问题:这三个数据库的调研互不依赖,完全可以同时进行。但 ReAct 是单线程的,只能一个接一个查。
二、朴素的办法为什么不够
想法一:换个上下文窗口更大的模型。
能撑久一点,但治标不治本。而且长上下文有两个隐性代价:
- 贵:每一轮都要重发全部历史,token 消耗随轮数平方增长
- 笨:上下文越长,中段信息越容易被忽略(03 讲过的 lost in the middle)
想法二:每查完一次就做个摘要。
方向对了,这其实就是多智能体做的事的一半。但如果你在同一个 Agent 里做摘要,原始材料仍然在历史里 —— 你只是又加了一段摘要,历史反而更长了。
要真正扔掉原始材料,你 得让「查资料」这件事发生在另一个上下文里。
这就是多智能体的第一个真实理由:
让子 Agent 在自己独立的上下文里干脏活,只把结论带回来。
第二个理由是并行:三个数据库同时调研,墙上时间除以 3。
三、概念:子 Agent 其实就是一个工具
在讲拓扑之前,先破除一个常见的神秘感。
「派一个子 Agent 去干活」,在代码上跟「调用一个工具」是同一件事。
主 Agent 完全不知道子 Agent 内部跑了 10 轮。 对它来说,那就是一次工具调用,返回了一段文本。
这个视角很重要:理解了「子 Agent = 工具」,多智能体就没有神秘之处了,剩下的都是工程细节。

图 5-1 主管-工人(Orchestrator-Workers)的官方示意
四、两种拓扑
多个 Agent 怎么组织,主要有两种形状。用两个生活场景来区分:
① 星型(Supervisor)—— 像项目经理派活
项目经理把任务拆开,分给三个人;三个人干完各自向经理汇报;经理整合。三个人之间不直接说话。
② 网状(Swarm)—— 像客服转接
你打客服电话,分诊台听完说「这是技术问题,我帮您转技术部」。转过去之后,分诊台就不管了,你直接和技术部对话。
| ① 星型 Supervisor | ② 网状 Swarm | |
|---|---|---|
| 谁决定下一个是谁 | 主管 | 当前这个 Agent 自己 |
| 干完了回哪 | 回主管 | 不回,就停在那个 Agent |
| 谁看得见全局 | 主管 | 没人 |
| 上下文 | 可以隔离 | 全量共享 |
| 典型场景 | 研究、写报告、代码任务 | 客服分诊 |
五、动手:把两种拓扑都搭出来
下面对照读两个库的源码:langgraph-supervisor(星型)和 langgraph-swarm(网状),都是 MIT。两个加起来不到 30KB,而「多智能体有几种形状」的答案,就在它们的差别里。
5.1 共同的基础:交接工具
两个库的核心都是一个叫 create_handoff_tool 的函数。它做的事就一件:
把「交给谁」这件事,做成一个模型可以调用的工具。
def create_handoff_tool(*, agent_name: str, name=None, description=None):
if name is None:
name = f"transfer_to_{_normalize_agent_name(agent_name)}"
if description is None:
description = f"Ask agent '{agent_name}' for help"
...
生成出来的工具名长这样:transfer_to_technical_support。
这里有个很重要的设计思想:路由不是你写的 if-else,是模型的一次工具调用。
模型看到工具列表里有 transfer_to_billing、transfer_to_technical、transfer_to_refund,它像选任何其他工具一样选一个。
同时这也是最大的实践隐患:默认描述只有 Ask agent 'X' for help 这么一句。模型就靠这一句判断该不该转给它。
生产上必须自己写描述,写清楚「什么情况下转给我」:
create_handoff_tool(
agent_name="refund",
description=(
"当用户要求退款、取消订单、或询问退款进度时转给我。"
"注意:只是询问账单金额的不要转,那是 billing 的活。"
),
)
注意后半句 —— 写清楚「什么情况不要转给我」,比写「什么情况转」更能减少误路由。
5.2 网状版:96 行,交出去就不回来
@tool(name, description=description)
def handoff_to_agent(state, tool_call_id) -> Command:
tool_message = ToolMessage(
content=f"Successfully transferred to {agent_name}",
name=name, tool_call_id=tool_call_id,
)
return Command(
goto=agent_name,
graph=Command.PARENT,
update={
"messages": [*state["messages"], tool_message],
"active_agent": agent_name, # ← swarm 的全部秘密在这一行
},
)
两个观察点:
① active_agent 就是 swarm 的全部状态。
「去中心化」听起来很复杂,实现只需要一个字段记住「现在谁在说话」。下一轮用户消息进来,直接路由到 active_agent 指向的那个 Agent。
没有主管,没有调度器,就一个变量。
② 历史是全量传过去的。
"messages": [*state["messages"], tool_message], # 完整历史,一条不删
新接手的 Agent 能看到之前所有对话。
这对客服场景是对的 —— 用户已经跟分诊台说了一遍问题,转到技术支持后不该再问一遍。
但代价是上下文只增不减。 转手三四次就很可观了。而且 swarm 没有提供任何裁剪选项 —— 这是它和星型最本质的分野。
5.3 星型版:多了「回来」,也多了并行
星型的交接工具复杂得多,因为它要处理两件网状不用管的事。
多出来的第一件事:可以选择性地隐藏交接痕迹。
if add_handoff_messages:
handoff_messages = state["messages"] + [tool_message]
else:
handoff_messages = state["messages"][:-1] # 连调用记录都抹掉
关掉这个开关,连「主管调用了 transfer_to_X」这条记录都从历史里删掉 —— 子 Agent 完全不知道自己是被派来的,它看到的就是一个干净的任务。
好处是子 Agent 不会被主管的措辞带偏。
多出来的第二件事:并行派活。
主管一轮里同时调 transfer_to_researcher 和 transfer_to_writer,两个子 Agent 并行启动。但这里有个必须处理的问题:
if len(last_ai_message.tool_calls) > 1:
handoff_messages = state["messages"][:-1]
handoff_messages.extend((
_remove_non_handoff_tool_calls(last_ai_message, tool_call_id), # ← 关键
tool_message,
))
源码注释说得很清楚:
如果主管在并行调用多个 agent,需要把不属于这个 agent 的 tool call 从消息里删掉,以保证消息历史合法。
为什么? 回想 01 的 6.2 节 那条硬约束:
消息历史里,每一个工具调用都必须有对应的工具结果。
研究员那个分支只会产生「转给研究员」的结果。如果历史里还留着「转给写手」的调用却没有对应结果,这份历史就是非法的,API 直接 400。
所以并行多智能体的第一个工程难点,不是调度,是维持每个分支的消息历史合法。 这是本篇最值得记住的一段。
多出来的第三件事:「交还」。
def create_handoff_back_messages(agent_name, supervisor_name):
return (
AIMessage(content=f"Transferring back to {supervisor_name}", ...),
ToolMessage(content=f"Successfully transferred back to {supervisor_name}", ...),
)
注意这对消息不是模型生成的,是框架凭空造出来插进历史的。子 Agent 从来没说过「我要交还控制权」,是框架帮它说的。
为什么要这么做?因为主管重新接手时,需要在历史里看到一个明确的分界标记,否则它分不清哪些话是自己说的、哪些是子 Agent 说的。
在多智能体里,消息历史是唯一的共享媒介,边界必须显式画出来。
六、星型版的三个开关,每个都很值钱
6.1 output_mode:子 Agent 回来时带多少东西
这是整个多智能体最值钱的一行代码。
OutputMode = Literal["full_history", "last_message"]
def _process_output(output: dict) -> dict:
messages = output["messages"]
if output_mode == "full_history":
pass # 全带回来
elif output_mode == "last_message":
if isinstance(messages[-1], ToolMessage):
messages = messages[-2:]
else:
messages = messages[-1:] # ← 只带最后一条
默认值是 "last_message"。
子 Agent 内部可能跑了 20 轮、读了 50 个文件、消耗 5 万 token,回到主管那里只留最后一条消息。
这就是第二节说的「上下文隔离」在代码里的样子 —— 就是这一行 messages[-1:]。多智能体的价值全部兑现在这里。
那个 if isinstance(messages[-1], ToolMessage) 分支也不是多余的:子 Agent 最后一个动作如果是调工具,光带一条工具结果回去是非法的(孤儿结果),必须把前面那条 AI 消息一起带上,所以取 [-2:]。又是消息历史合法性。
实践建议:保持默认。改成 full_history 之前先问自己 —— 主管真的需要看子 Agent 的全部过程吗?通常不需要。真需要的话,正确做法是让子 Agent 自己写一份摘要再回来。
6.2 parallel_tool_calls = False:官方默认关闭并行
def create_supervisor(..., parallel_tool_calls: bool = False, ...)
上一节刚讲完并行派活怎么实现,这里却默认关掉 —— 因为并行会带来结果合并、部分失败、多分支历史合并一堆问题。默认关掉更安全。
要用并行,明确打开,并且做好错误处理。
6.3 一个官方示例里的坑
Anthropic 的 patterns/agents/orchestrator_workers.ipynb 是主管-工人的官方演示。它的类 docstring 写着:
class FlexibleOrchestrator:
"""Break down tasks and run them in parallel using worker LLMs."""
但实际实现是串行的:
for i, task_info in enumerate(tasks, 1): # 就是个普通 for 循环
worker_response = llm_call(worker_input, model=self.model)
整个 notebook 里 ThreadPoolExecutor、asyncio、gather 一个都没有。docstring 说了两遍 parallel,代码是串行的。
照着它写,你会 得到一个「以为在并行、其实 N 倍延迟」的系统。
真正的并行版本在同目录的另一个文件:async_multi_agent_orchestration.ipynb,用的是:
await asyncio.gather(*helper_tasks, return_exceptions=True)
注意 return_exceptions=True —— 一个子 Agent 挂掉不会让整批结果丢失,正是 02 并行 那节讲过的坑。
七、上生产前必须回答的四个问题
拓扑图画得再漂亮,这四个问题答不上来就别上:
| 问题 | 星型的答案 | 网状的答案 |
|---|---|---|
| 子 Agent 之间能直接说话吗 | 不能,必须过主管 | 能,直接转 |
| 结果怎么回来、回多少 | output_mode,默认只回最后一条 | 不回,控制权就留在那 |
| 能不能套娃(子 Agent 再开子 Agent) | 能,主管本身也是个 Agent | 能,但没人管得住 |
| 谁付上下文的钱 | 主管付调度的,子 Agent 各付各的细节 | 全都堆在同一份历史里 |
第四个问题是选型的决定性因素。
网状没有任何上下文隔离机制。客服场景转手两三次没问题,但拿它做需要读大量资料的任务,必爆。
想看真实产品(Kimi CLI、Manus、DeerFlow)怎么回答这四个问题,去 Multi-Agent 产品源码分析。
八、常见故障与排查
| 你看到的现象 | 原因 | 怎么修 |
|---|---|---|
| API 报 400,提到 tool_use | 某个分支里有孤儿工具调用 | 并行分支要清理不属于自己的调用(5.3) |
| 拆了多智能体,上下文照样爆 | 用了 full_history,或者用的是网状 | 改回 last_message |
| A 转给 B,B 又转回 A,无限循环 | 两边的描述都觉得该对方管 | 递归上限 + 描述里写清「什么情况不要转给我」 |
| 主管的上下文比谁都长 | 所有信息都过主管中转 | 子 Agent 之间用共享文件/存储,别都走主管 |
| 说好的并行,延迟还是串行的 N 倍 | 照抄了 orchestrator_workers.ipynb | 用 asyncio.gather(6.3) |
| 4 个成功 1 个失败,结果全丢 | gather 没设 return_exceptions | 加上 return_exceptions=True |
| 派出去干 A,回来交了 B | 任务描述里没写边界和输出格式 | 见 06 的任务描述七要素 |
| 拆了之后反而更慢更贵了 | 任务本身不适合拆 | 看下一节 |
九、全局定位 + 什么时候千万别用
这一节比前面都重要,因为多智能体是本专题最容易被过度使用的范式。
不该用的四种情况:
① 单 Agent 的上下文还没爆。 那就别拆。每多一个 Agent,就多一层信息损耗 —— 子 Agent 的理解偏差 + 摘要时的信息丢失。
② 任务本身是串行的。 拆了也不能并行,纯粹增加交接开销和延迟。
③ 子任务之间需要频繁商量。 消息全走主管转发,成本和延迟都会爆炸。这种情况应该合并成一个 Agent。
④ 你只是想让架构图好看。 「研究员 Agent」「写手 Agent」「审校 Agent」这种角色分工,如果没有实际的上下文隔离或并行收益,就只是把一个 prompt 拆成了三个,还多了两次信息传递损耗。
一句话判断标准:
说不出「隔离了什么上下文」或者「并行了什么」,就别上多智能体。
十、小结与检查清单
核心三句话:
- 子 Agent 本质上就是一个工具,调用它和调用
bash没有结构差别 - 多智能体的价值全部兑现在
output_mode="last_message"这一行上 - 并行多智能体的第一难点不是调度,是维持每个分支的消息历史合法
自查清单:
- 你能说清楚这次拆分隔离了什么上下文、或并行了什么吗?
- 用的是星型还是网状?符合你的场景特性吗?
-
output_mode是last_message还是full_history?后者的话理由是什么? - 交接工具的描述,有没有写清「什么情况不要转给我」?
- 并行时,每个分支的消息历史是否合法(无孤儿工具调用)?
- 并行的异常处理,单个失败会不会拖垮全批?
- 有没有交接次数上限,防止 A↔B 乒乓?
- 主管的上下文长度,有没有随子 Agent 数量线性增长?
参考资料
| 资料 | 位置 | 协议 |
|---|---|---|
| 星型实现(主拆) | langgraph_supervisor/:supervisor.py 18KB + handoff.py 8KB | MIT |
| 网状实现(对照) | langgraph_swarm/:swarm.py 9.9KB + handoff.py 3.9KB | MIT |
| 官方示例(注意是串行) | claude-cookbooks/patterns/agents/orchestrator_workers.ipynb | MIT |
| 真并行示例 | claude-cookbooks/patterns/agents/async_multi_agent_orchestration.ipynb | MIT |
| 另一种交接实现 | openai-agents-python/src/agents/handoffs/,40KB | MIT |
| 教学实现 | s06_subagent 12KB、s13_agent_teams 70KB | MIT |
| 生产经验 | Anthropic — How we built our multi-agent research system | —— |
下一篇:06 · Deep Research —— 把这一篇推到极限,加上引用和综合。