← 返回首页

LangGraph supervisor 与 swarm 实测对照

LangGraphMulti-AgentTroubleshooting

LangGraph 多 Agent 两拓扑实测:supervisor 与 swarm 的 6 处差异(本机零 Key 对照)

TL;DR:LangGraph 官方给了两个「多 Agent」库,名字听起来像是同一个东西的两种叫法,我实际测试之后的结论是:它们决定的是「下一个说话的是谁」这件事的答案来源——supervisor 由中央节点重新决策,swarm 由状态字段 active_agent 直接决定,而这个字段会被 checkpointer 记住。这条差别在单轮对话里几乎看不出来,在第二轮对话里会变成「你的追问根本没有重新分诊」。另外两处更隐蔽:supervisor 默认会把 worker 的中间过程从主历史里删掉;以及两种拓扑都没有真正的并行委派,但失败形态完全不同。本文所有数字来自本机实测(langgraph 1.2.18 + langgraph-supervisor 0.0.31 + langgraph-swarm 0.1.0,Python 3.13,Windows 11),不含任何联盟推广链接。

先把两句话摆正

官方文档里对两种模式的描述各只有一句话,但对错就是从这两句里分出来的。docs.langchain.com/oss/python/langchain/multi-agent 的 Patterns 表原文:

官方仓库 langchain-ai/langgraph-swarm 的 README 第一句更直白:「The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent.」

一句话总结:supervisor 是「所有人都通过前台转接」,swarm 是「电话直接转到上一个人手里」。前者多一轮翻译,后者省一轮但会「粘」在最后一个专家身上。LangChain 官方博客《Benchmarking Multi-Agent Architectures》也把 supervisor 的额外成本归因于这层「translation」:子 Agent 不能直接回用户,supervisor 必须复述一遍,因此成本更高、也更容易失真。

如果你已经在本站读过 LangGraph 持久化那一篇,可以把本文当成它的续篇:checkpointer 记住的是「状态」,而 swarm 记住的是「谁拿着话筒」。

实测方法与可复现环境

我不想为了跑一次对照就在每篇文章背后绑一张账单,所以这里用了一个零成本做法:把「模型」换成一个按脚本吐消息的 Python 对象。LangGraph 的 create_react_agent 支持「动态模型」——只要传入的对象是 callable、签名是 (state, runtime)、返回值是一个 LangChain Runnable 即可(langgraph/prebuilt/chat_agent_executor.py 里的 _resolve_model)。这样图怎么路由、工具怎么执行、状态怎么写全都照常,唯独「模型思考」这一步被换成了写死的剧本。

环境安装不污染主环境,全部落到隔离目录:

pip install --target D:/openclaw-blog/tmp/ma-env langgraph-supervisor langgraph-swarm

实测拿到的版本组合(版本与发布时间取自 GitHub Releases API):

驱动用的假模型(create_supervisor 内部会对模型调 bind_tools,所以这个对象要同时提供返回自身的 bind_tools):

from langchain_core.messages import AIMessage
from langchain_core.runnables import RunnableLambda

class ScriptedModel:
    def __init__(self, script):
        self.script = script   # 一串 AIMessage,第 n 次调用返回第 n 条
        self.n = 0
    def bind_tools(self, tools, **kw):
        return self
    def __call__(self, state, runtime=None):
        i = self.n
        self.n += 1
        msg = self.script[i] if i < len(self.script) else AIMessage(content="(exhausted)")
        return RunnableLambda(lambda _: msg)

用它造一个 worker 就是一行:bob = create_react_agent(ScriptedModel([AIMessage(content="Bob worked")]), tools=[], name="bob")。

差异一:第二轮对话谁接(最容易踩的一个)

同一件事「让 bob 干活」,两个拓扑跑完的路径完全不同。用 stream_mode="updates" 抓节点轨迹:

差别不止是步数。带上 checkpointer 再问第二句「and now?」(同一个 thread_id):

这件事本身是官方设计(README 原话已在上节引过),在官方文档的语境里它是优点:「后续交互可以从同一个专家继续」。但它有一个很容易被忽略的代价:用户的第二句话可能根本不属于 bob 的领域。举例:第一轮「帮我算个账」转去了财务 Agent;第二轮随口问「今天天气怎么样」,swarm 会把这句直接交给财务 Agent,而 supervisor 至少会让中央节点再看一眼。所以用了 swarm 就必须配套做两件事之一:要么在每个 Agent 的系统提示里明确「不属于你的领域要主动交回某某」,要么在应用层每次新任务开始时把 active_agent 重置一次。

差异二:默认配置会悄悄删掉 worker 的中间过程

create_supervisor 有个参数 output_mode,默认是 last_message。它的作用是:worker 跑完之后,把 worker 内部的消息历史以什么粒度写回主图。

我给 bob 加了一个工具,让它内部产生一个工具调用回合(一次 add 工具及其 ToolMessage),然后对比:

源码在 langgraph_supervisor/supervisor.py 的 _make_call_agent,它不是 bug,是一段写死的取舍:

elif output_mode == "last_message":
    if isinstance(messages[-1], ToolMessage):
        messages = messages[-2:]
    else:
        messages = messages[-1:]

什么时候这会让人生不如死:你的 supervisor 需要根据 worker「实际调用过什么工具、传了什么参数」来做下一步决策(比如审计、计费、把上下文递给另一个 worker),这些证据在默认配置下被删掉了,而且不报错、不告警。两条应对:显式传 output_mode="full_history";或者干脆在 worker 的最后一条消息里把关键中间结果复述一遍。反过来,如果你只想要答案、想压 context,默认值反而是对的——默认值不是错,是你需要知道它存在。

差异三:并行委派——两种拓扑都不并行,但崩法完全不同

官方文档的「Choosing a pattern」表里有一列 Parallelization,Handoffs(swarm)那一格是 --(明确不支持),Subagents(supervisor)是有星的。我实测的结果比表格更微妙。

先在 supervisor 上试:让 supervisor 在一条 AIMessage 里同时发出两个 handoff 工具调用(transfer_to_alice 与 transfer_to_bob)。langgraph_supervisor/handoff.py 里确实有专门处理并行的分支——一旦检测到 len(last_ai_message.tool_calls) > 1,就用 Send 列表把跳转包进同一个 Command,注释原文是这么写的:

# NOTE: we are using Send here to allow the ToolNode in langgraph.prebuilt
# to handle parallel handoffs by combining all Send commands into a single command
goto=[Send(agent_name, {**state, "messages": handoff_messages})],

顺着这条线索我预期两个 worker 都会跑。实际结果:6 次独立运行,6 次都只有一个 worker 真的执行了(bob 4 次、alice 2 次,谁活下来不确定),另一个 worker 既不报错也不留痕,最终历史里只有 5 条消息。我用 monkeypatch 抓了 ToolNode._combine_tool_outputs 的调用记录,它被执行了 2 次、每次 n_input=1——也就是两个 handoff 工具各自产出了一条只含一个 Send 的父命令,没有落进同一批,源码里那条负责累加合并的分支(replace(parent_command, goto=parent_command.goto + output.goto))压根没被走到。

那么在 swarm 上做同样的事呢?直接抛异常,而且是个看起来跟 handoff 毫不相干的错:

ValueError: Found AIMessages with tool_calls that do not have a corresponding ToolMessage.
Here are the first few of those tool calls:
[{'name': 'transfer_to_charlie', 'args': {}, 'id': 'w2', 'type': 'tool_call'}].
Every tool call (LLM requesting to call a tool) in the message history MUST have a
corresponding ToolMessage (result of a tool invocation to return to the LLM) ...
For troubleshooting, visit: https://docs.langchain.com/oss/python/langgraph/errors/INVALID_CHAT_HISTORY

排到这里可以给一条工程结论:真要并行就别指望这两种拓扑自动给你。需要「同时派几个 worker」的场景,用 Send 手工搭 fan-out / fan-in 子图,或者在 supervisor 侧强制每轮只发一个 handoff。这比依赖一条「注释里说支持、实测只跑一个」的路径可靠得多。

Troubleshooting:7 个真实报错与复现方式

下面 7 条全部在本机复现过,都给了抛出位置与绕过办法。

报错 1:ParentCommand——把带 handoff 工具的 agent 单独 invoke

现象:为了调试单独跑一个 worker,bob.invoke(...) 直接炸,异常类名是 langgraph.errors.ParentCommand,str(e) 打印出来是一整个 Command 对象的千字符 repr(含全部消息),既没有错误说明也没有修复提示。

原因:create_handoff_tool 返回的工具本体是 Command(goto=..., graph=Command.PARENT, ...),而 langgraph/graph/state.py 第 1762 行的处理是 raise ParentCommand(command),没有做友好封装。ParentCommand 继承自 GraphBubbleUp,本来是给「子图向父图传话」用的内部信号——一旦没有父图可传,它就变成了一个没有文案的裸异常。

修复:handoff 工具只能在被 create_swarm / create_supervisor 包起来的图里用。要单测某个 worker,就临时构造一个只含它的 swarm,或者把工具列表换成不含 handoff 的版本。

报错 2:KeyError: 'charlie'——给 active_agent 写了不存在的名字

现象:手动传参、update_state、或被带偏的模型写入了一个不在 agent 列表里的 active_agent,报出来的只有一句 KeyError: 'charlie',定位在 langgraph/graph/_branch.py 第 203 行。

原因:create_swarm 会在内部把 schema 里的 active_agent: str | None 收窄成 Literal['alice','bob'] | None(langgraph_swarm/swarm.py 的 _update_state_schema_agent_names),随后 add_active_agent_router 从 START 挂了一条带 path_map=route_to 的条件边;路由函数返回一个不在 path_map 里的值,就是一声不吭的 KeyError。

修复:校验优先。入口处就断言 active_agent in {a.name for a in agents},别指望图给你友好报错。

报错 3:Error: transfer_to_Bob is not a valid tool——工具名的大小写与空格

现象:模型输出 transfer_to_Bob,日志里出现一条 ToolMessage:Error: transfer_to_Bob is not a valid tool, try one of [transfer_to_bob]。不抛异常,handoff 静默失败,模型通常会重试或者直接开摆。

原因:create_handoff_tool 里有一行归一化 _normalize_agent_name:WHITESPACE_RE.sub("_", agent_name.strip()).lower()。于是:

也就是说:工具名和节点名是两套不同的字符串。修复:agent 名一律用小写加下划线的 snake_case;给 handoff 工具写 description 时把目标节点名写死进去,而不是让模型猜。

报错 4:ValueError: Please specify a name when you create your agent

抛点 langgraph_supervisor/supervisor.py:400。只要传给 create_supervisor 的图 name is None or name == "LangGraph"(即 compile() 时没命名)就炸。修复:create_react_agent(..., name="xxx") 或 graph.compile(name="xxx")。

报错 5:ValueError: Agent with name 'dup' already exists. Agent names must be unique.

抛点 supervisor.py:406,与报错 4 是同一个循环的两条支路。注意判据是名字而不是对象同源:两个用途不同但同名的 worker 也会踩。

报错 6:ValueError: Invalid agent output mode: all_history. Needs to be one of ('full_history', 'last_message')

抛点 supervisor.py:73。合法值只有两个(源码注释原文:full_history 加整个历史 / last_message 只加最后一条),拼写成 all_history、history 都会被直接拒绝——这条反而是最友好的一个。

报错 7:ValueError: Missing required key 'active_agent' in state_schema 与 Default active agent 'charlie' not found in agent names ['alice']

两个都在 langgraph_swarm/swarm.py(257 / 267 行)。第一种最常见的触发姿势是「照着 supervisor 的写法写代码」——把 MessagesState 直接传给 create_swarm;swarm 必须用自己的 SwarmState(或自定义 schema,但一定要带 active_agent)。第二种是 default_active_agent 与 agents 的 name 对不上,大小写敏感。另外 agents=[] 也会被直接拒绝(agents list cannot be empty,swarm.py:261)。

顺带记录一条 supervisor 侧的同类报错方便搜索:一旦开始自定义 handoff 工具,就必须给所有子 agent 配齐,否则 supervisor.py:167 会抛 ValueError: When providing custom handoff tools, you must provide them for all subagents. Missing handoff tools for agents '{'bob'}'.

什么时候用哪个

FAQ

Q:不用 checkpointer 的话,swarm 还会记住上一个 active agent 吗?

不会。active_agent 是图 state 的一个字段,两轮之间靠 checkpoint 存活;没有 checkpointer,每次 invoke 都从 default_active_agent 出发。上文差异一的结论正是建立在带 checkpointer 的前提上的。

Q:supervisor 每轮都要回中央节点,是不是一定更贵?

官方博客的结论是 supervisor 持续比 swarm 贵,贵在复述(translation)。但如果你本来就需要中央节点做校验或审计,这笔开销买到的是控制力,不是浪费。另外别忘了默认的 output_mode 会砍掉 worker 的中间过程,在压成本时反而是加分项。

Q:能不能两个混着用?

可以,而且官方也鼓励这么想:外层用 handoff 做专家切换,每个专家内部再把别的 agent 当工具调用。本站 LangGraph 流式三口径 那篇讲的是同一套运行时在 streaming 上的表现,人审中断那篇 讲的是把人放进这条链路时的六条硬规则,与本拓扑问题组合着看更容易定位。

Q:我只想知道现在该装哪个库?

短期维护老项目就继续用这两个库(版本号见前文,均为发稿时 GitHub Releases 上的最新版);新项目先看官方 supervisor 教程里「用工具包一层」的写法。两个库不冲突,但要注意它们依赖同一个 langgraph 版本基线。

参考来源(一手)

本文的边界

三条要说在前面。第一,全程没有真实 LLM:模型由脚本驱动,因此结论覆盖的是「拓扑与运行时行为」,不覆盖真实模型会不会发出并行工具调用——后者取决于模型与 provider。官方文档在 supervisor 教程里的说法是「许多 LLM 会在一次响应里发出多个工具调用,runtime 会并行执行」,与我实测到的 langgraph-supervisor 0.0.31 手牌路径的表现并不一致,这是本文最值得你自己复算的一点。第二,只测了 InMemorySaver,没有测 SQLite / Postgres 后端下的并发一致性。第三,全部数据在 Windows 11 + Python 3.13 上取得,未做 Linux 对照;拓扑结论与平台无关,但文中的源码行号是对着 langgraph 1.2.18 的。本文不含任何联盟推广链接。

👉 立即参与小米 MiMo 开放平台:国内领先的 AI 大模型开放平台,高性价比推理服务

👉 立即参与阿里云 AI:汇集爆款 AI 产品,热门模型专属权益优惠券助力企业创新加速

📌 本文由 AI 辅助生成并经人工审核发布 | TechPassive — AI 驱动的内容测试站点,专注于效率工具与 SaaS 真实评测

🔗 精选推荐工具

使用以下链接支持我们持续产出高质量内容(点击可直接前往购买):

☁️ DigitalOcean 云服务器 ⚡ Vultr 高性能 VPS 🤖 QoderWork 中国版(推荐有奖) ☁️ 阿里云爆款 AI 产品 📚 WordPress 实用书单 🔍 WordPress SEO 书单 🌐 虚拟主机书单 🐳 Docker 书单 🐧 Linux 书单 🐍 Python 书单 💰 联盟营销书单 💵 被动收入书单 🖥️ 服务器书单 ☁️ 云计算书单 🚀 DevOps 书单 🤖 小米 MiMo 开放平台
← 返回首页