让一个"主管"带一群专家干活
本文基于 LangGraph 官方库 langgraph-supervisor(v0.0.31)源码,简单介绍Supervisor怎么用、为什么这么设计。读前只需了解 LangGraph 的基本概念(图、节点、状态)。
一、先看场景:它到底解决什么问题
在讲任何代码之前,先看三个你可能遇到过的场景。
场景一:客服工单分派
用户说"我上个月多扣了两次钱,能退吗?"。系统里有三个Agent专家:账单 Agent、退款 Agent、技术支持 Agent。你需要有个角色先判断"这是退款问题",派给退款Agent专家;退款专家给出结论后,再有人决定是结束流程还是转给技术支持。
场景二:写一份研究报告
用户问"2024 年 FAANG 五家公司加起来有多少员工?"。这需要两步:先查数据,再做加法。中间必须有人盯着——查完之后知道下一步该派数学Agent专家,而不是写文件的 Agent。
场景三:多步骤任务要有人盯着
任务不是一步能做完的,而中间结果会影响下一步。比如订行程:先查机票,发现没有合适的航班,就得回头改日期,再重新查酒店。这个"回头看、再决定"的动作需要有一个固定的负责人。
这三个场景的共同点是:有一群分工明确的Agent专家,但还需要一个固定的"大脑"来决定下一步派谁、以及对结果是否满意。
这就是 Supervisor(主管)模式。先看一眼它的全貌:
(实线是"派工下去",虚线是"结果写回共享状态"。)
什么时候不该用它
- 只要一个 Agent 就够了:一个问题只有一个专家能答,加个主管纯属浪费 token
- 用户纯闲聊:不需要分工,直接给对话 assistant 就行
- 任务路径完全固定:如果永远都是"步骤一→步骤二→步骤三",写一个固定顺序的流水线比让主管用 LLM 决策更便宜、更快。
一句话总结适用场景:任务需要拆给多个专家,且每一步派谁要根据中间结果动态决定。
二、技术铺垫:先搞懂 handoff(交接)是什么
Supervisor 是建立在 handoff 之上的,所以先花三分钟把它讲清楚。
2.1 一句话解释
handoff 就是"转接"。 就像客服电话里的"我帮您转接退款专员"——当前这个人不再跟你对话了,换另一个人来,而且新来的人能看到你们之前的全部对话记录。
2.2 为什么要把"转接"做成一个工具?
先把一件容易搞混的事说清楚:大模型不会调用工具,更不会执行任何代码。
模型做的事从头到尾只有一件——看着前面的对话,预测并生成下一个 token。在支持 function calling 的模型里,它可能输出一段特殊格式的内容,意思大概是:
我建议执行这个动作:transfer_to_refund_agent(参数:无)
所以模型负责的是"决策"——要不要用工具、用哪个、参数填什么;真正把函数跑起来的是外面那一层程序(在 LangGraph 里就是图上的工具节点)。完整流程是这样:
- 你把工具清单(名字、用途说明、参数格式)随对话一起发给模型
- 模型可能返回一个类似"请执行某某工具"这样的结构化请求
- 外面那层程序解析返回的请求,找到对应的真实函数并执行
- 执行结果被包装成一条工具回执消息,追加到对话历史
- 程序带着更新后的对话历史,再次请求模型
明白了这一点,"为什么转接要做成工具"就很自然了:模型能影响外部世界的唯一出口,就是"提议执行某个工具"。
于是工程师想了个办法:把"转接给某人"也登记成一个工具。
给模型一个叫 transfer_to_refund_agent 的工具(针对退款Agent),用途说明写着"转接给退款专家"。模型看到用户说要退款,就会像提议调用普通工具一样提议调用它。区别只在于——真正被调用到的那个函数并不干活,它返回的是一条指令: "请把我换掉,让 refund_agent 接着干" 。
2.3 LangGraph 里具体是怎么实现的
接着 2.2 的那个循环,把"执行"这一步放大看:转接工具被执行的瞬间,到底发生了什么?答案是四个动作的接力。之所以容易看晕,是因为这四个动作分属不同的"人"——先记住谁做什么,代码就顺了:
| 动作 | 谁来做 | 发生了什么 |
|---|---|---|
| ① 出题 | 大模型 | 输出"我建议调用 transfer_to_refund_agent" |
| ② 接单 | 工具节点 | 按名字找到这个工具函数,执行它 |
| ③ 发通行证 | 工具函数 | 所执行的工具不查数据,只返回一张 Command |
| ④ 兑现 | 外层图 | 使用返回的Command 中的update 写回状态,再跳到Command中定义的目标节点 |
① 大模型出题
模型输出一段结构化内容,指示所要调用的工具相关的信息,包含工具名称、参数等内容,示意如下:
注意这只是"提议",模型到这里就完事了。
AIMessage: 建议执行 transfer_to_refund_agent
② 工具节点接单
LangGraph 的工具节点收到请求,按名字找到对应的工具函数并执行——这一步全是自己的应用程序的事,和模型无关。
③ 工具函数不干活,只发一张"通行证"
这是整个设计最"狡猾"的地方。handoff_to_agent 这个函数的参数里有两个是自动注入的(模型看不到它们):state(当前完整状态)和 tool_call_id(这次调用的编号)。靠着 state,它才能拼出新的消息列表:
# langgraph_supervisor/handoff.py 中的 handoff_to_agent
def handoff_to_agent(state, tool_call_id):
tool_message = ToolMessage(
content=f"Successfully transferred to {agent_name}", ...
)
return Command(
goto="refund_agent", # 下一个要执行的Agent专家节点
graph=Command.PARENT, # 这张通行证交给外层图兑现
update={**state, "messages": state["messages"] + [tool_message]},
# └─ 整段历史 + 一条"已转接"回执
)
注意 update 里写回的是 state["messages"] + [tool_message] ——在整段已有的对话历史后面再加上一条新回执,而不是只有那条回执。专家接手时能不能看到来龙去脉,全靠这里。
④ 外层图兑现通行证
为什么需要 graph=Command.PARENT 这一行?因为交接工具是在 Agent 自己的子图里被执行的,而 refund_agent 这个节点挂在外层大图上——子图内部根本够不着。PARENT 就是给通行证盖的章:"别在本地兑现,送到外层去"。
外层图收到后按固定顺序做两件事:
- 先应用 update:把新的消息列表写进共享状态(此时那条"已转接"回执已经在历史里了)
- 再跳到 goto 指定的节点:refund_agent 启动,它读到的就是完整对话——包括主管说过的话和这条回执
那条回执是给技术规则用的,不是给人看的:消息流要求"每次工具调用请求都必须紧跟一条工具结果",缺了它,下次请求模型会直接报错。
2.4 一张图看懂
关于Handoff机制记住这四件事就够了:
- 模型只选择工具,不答题:它只输出"建议调用某某工具",真正执行的是工具节点
- 交接工具是个空壳:在模型眼里它和普通工具没区别,但它的函数不查数据,只发一张通行证
- 通行证 = Command(goto, PARENT, update) :goto 是下一个要执行的Agent专家节点,update 是要写回的东西,两者一起送达
- PARENT 是关键那一行:专家在子图里、目标节点在外层图上,所以要盖这个章把指令送出去;最后由外层图先写状态、再跳节点
三、Supervisor 怎么用
3.1 最小可运行例子
最小样例代码,两个专家,一个主管:
from langchain_openai import ChatOpenAI
from langgraph_supervisor import create_supervisor
from langgraph.prebuilt import create_react_agent
model = ChatOpenAI(model="gpt-4o")
# --- 两个专家 Agent ---
def web_search(query: str) -> str:
"""模拟搜索信息工具"""
return "FAANG 2024 员工数:Meta 67317,Apple 164000,..."
def add(a: float, b: float) -> float:
"""做加法的数学计算工具"""
return a + b
# 创建一个负责搜索的专家,专家都是采用 ReAct 机制的 Agent
research_agent = create_react_agent(
model=model,
tools=[web_search],
name="research_expert",
prompt="你是搜索专家,只负责查资料。",
)
# 创建一个负责计算的专家,专家都是采用 ReAct 机制的 Agent
math_agent = create_react_agent(
model=model,
tools=[add],
name="math_expert",
prompt="你是数学专家,只负责算数。",
)
# --- 一个主管 ---
# 创建一个 Supervisor,其下包含 research_agent 和 math_agent 两个专家
workflow = create_supervisor(
[research_agent, math_agent],
model=model,
prompt="你管理一个搜索专家和一个数学专家。先查资料,再算数。",
)
app = workflow.compile()
result = app.invoke(
{
"messages": [
{"role": "user", "content": "FAANG 2024 总员工数是多少?"}
]
}
)
跑起来之后是这样的:
注意图里最重要的是:专家干完活,必须返回信息到主管。主管再根据新结果决定下一步,而不是让专家之间互相接着聊,这就是"中心化"。
3.2 几个最常用的开关
控制专家Agent的输出内容哪些信息要进总的messages 列表(output_mode)
# 默认:只把专家最后一句话写回主管的对话记录
create_supervisor(agents, model=model, output_mode="last_message")
# 或者:专家的每一步思考、每一次工具调用都记录下来
create_supervisor(agents, model=model, output_mode="full_history")
默认是 last_message,因为专家中间那些"试算、重来"类的过程对主管没用,还白白占 token。这是 Supervisor 相对 Swarm 一个很实用的优势:能过滤噪音。
让主管一次派多个人(parallel_tool_calls)
create_supervisor(agents, model=model, parallel_tool_calls=True)
适合"这份事实需要查三家数据,可以并行去查"的场景。注意目前只对 OpenAI 和 Anthropic 等部分模型生效。
把专家的话原封不动交出去(forward_message)
在这个架构里,最终回复用户的那句话是主管生成的。专家把结论写好交回来之后,主管还要再跑一次模型,用它自己的话把专家的结论总结一遍——这一步既多花一次模型调用,又有转述失真的风险。官方 README 对这个工具的定位说得很直白:避免 "misrepresentation of the worker's response through paraphrasing" (转述歪曲了专家的回答),也省掉 "further processing or summarization by the supervisor itself" (主管自己的再加工)。
create_forward_message_tool 就是给主管开的一条"懒人通道":
from langgraph_supervisor.handoff import create_forward_message_tool
workflow = create_supervisor(
agents, model=model,
tools=[create_forward_message_tool("supervisor")]
)
它生成一个叫 forward_message 的工具,由主管调用,参数是 from_agent(转发哪个专家的最后一条消息)。注意 create_forward_message_tool("supervisor") 里的参数不是主管的节点名,而是给转发出去的那条消息署的名字。
沿用 3.1 的例子。假设用户这次问的是"FAANG 五家 2024 年各有多少员工"(注意:不是问总数,所以不需要再派数学专家),主管判断搜索专家的回答已经够用,于是:
用户: FAANG 五家 2024 年各有多少员工?
主管: [派工 transfer_to_research_expert]
搜索专家: Meta 67,317;Apple 约 164,000;Amazon 约 155 万(含大量兼职);
Netflix 约 14,000;Alphabet 约 183,000。其中 Amazon 口径含仓储配送,
与其他几家不完全可比。
搜索专家: [交回 transfer_back_to_supervisor]
主管: Meta 67,317;Apple 约 164,000;Amazon 约 155 万(含大量兼职);
Netflix 约 14,000;Alphabet 约 183,000。其中 Amazon 口径含仓储配送,
与其他几家不完全可比。
↑ 和上一行一字不差,但署名变成了"主管"
注意最后两行:内容完全相同,发言者却从"搜索专家"换成了"主管"。如果不用这个工具,主管会用自己的话把这些数字重新组织一遍("根据查询结果,五家公司的情况如下……"),既多一次模型调用,也容易把"Amazon 口径含兼职"这种关键限定词在转述中弄丢。
实现上靠的是一条 Command:
return Command(
graph=Command.PARENT,
goto="__end__",
update={"messages": [AIMessage(content=专家的原文, name=supervisor_name)]},
)
专家的原文被以主管的名义写回共享消息流,而图的最终输出就是这条共享消息流,于是它自然成为最终回复。
所以严格说:它不是"绕过主管直接发给用户",而是"主管主动选择不再加工,把原文当成自己的最终发言" 。要不要转发、转发谁,都是主管的模型决定的;省掉的只是那次转述所花的模型调用。
3.3 基于Supervisor机制组建多层级团队
编译好的 supervisor 可以作为上一级的"专家"再被管理:
research_team = create_supervisor([research_agent, math_agent], ...).compile(
name="research_team"
)
writing_team = create_supervisor([writing_agent, publish_agent], ...).compile(
name="writing_team"
)
top = create_supervisor([research_team, writing_team], model=model).compile()
像公司架构一样:总监管着两个经理,经理管着自己的组员:
这里最妙的一点是中间那两层既是主管也是专家:research_team 编译之后带着自己的名字,对 top 来说它就是个普通专家节点——派活、写回、被 output_mode 过滤,用的全是同一套机制。层级可以一直往下叠,但每多一层就多一次模型调用,延迟和 token 都会上去,一般两三层就够。
Agent的记忆
app = workflow.compile(checkpointer=InMemorySaver())
不配置checkpointer,多轮对话就没有历史,涨价后的总人数是多少 这种追问会答不上来。
四、内部原理:它到底怎么搭起来的
Supervisor 的核心代码只有几百行,去掉注释更少。它的实现方式可以概括为一句话:
把"主管"本身做成一个普通 Agent,把"派活"做成它的工具,把"干完必须回来"做成图上的一条固定边。
4.1 外层图的样子
三条固定规则:
- 开始一定先进主管。没有复杂的路由判断,主管就是入口
- 每个专家到主管都有一条固定返回的边。这就是为什么控制权永远不会"卡"在专家手里
- 专家被包了一层函数,不是直接把子图嵌进来(下面解释)
4.2 为什么要给专家包一层?
源码里专家是这样挂上去的:
builder.add_node( # 在拓扑图中,一个专家就是一个节点,和节点一样对待
agent.name, # 节点名称
_make_call_agent(agent, output_mode, ...), # 注意:包了一层,节点所执行的函数
)
builder.add_edge(agent.name, supervisor_agent.name) # 添加边
这层包装函数做两件事:
一是过滤信息。专家跑完是一个子图调用,返回一长串消息;包装函数按照 output_mode 只挑有用的写回外层状态:
if output_mode == "last_message":
messages = messages[-1:] # 只要最后一句
二是补一条"回归"记录。回来时会自动追加一对消息:"Transferring back to supervisor" + 对应的结果回执。目的和前面说的一样——让消息序列合法,让模型看得懂"刚才谁把控制权还回来了"。
正是这层包装,让 Supervisor 能实现 Swarm 做不到的上下文过滤。
4.3 状态存在哪里
外层状态就一个字段:
class _OuterState(TypedDict):
messages: Annotated[Sequence[AnyMessage], add_messages]
没有额外的记录追踪器。原因很简单:每轮都从主管开始,主管看着完整的消息历史自己判断——历史本身就是它的工作计划。
所以 checkpointer 一定要配,否则主管就失忆了。
4.4 并行派工怎么实现的
当模型一次回复里同时提议多个派工工具时,handoff 工具的执行体会改用 LangGraph 的 Send:
goto=[Send(agent_name, {**state, "messages": handoff_messages})]
每个专家收到的是一份裁剪过的历史——只保留属于自己的那次工具调用,避免多个专家互相看到对方的指令产生混乱。这就是多 Agent 里常说的 fan-out(扇出)。
五、一句话总结 + 它的边界
Supervisor = 一个大管家 + 一群专家 + 图上的回路。
它好在哪:
- 有人统管全局,能看到中间结果再决定下一步
- 专家的中间过程可以过滤掉,上下文干净
- 支持并行派工,支持多级层级
- 决策集中在一处,出了问题好排查
它的边界在哪:
- 主管是瓶颈:每次任务流转都要过主管一次大脑,延迟叠加;它的上下文也会随着任务变长而膨胀,token 成本上升明显
- 没有"计划本" :每一步派谁,依然是模型临时决定的,不是一张可以检查、修改、失败重试的任务清单。任务高度不确定时,需要你自己再加一个规划节点
- 专家名单开工前定死:运行时不能动态增加新角色
- 单机执行:所有 Agent 在同一进程、同一张图里。只有 RemoteGraph 能调用远端图,没有通用的"把某专家调度另一台机器"机制
如果你想要的是"不要主管、专家之间互相传球"的模式,那是 Swarm(下一篇)。两者的选型详细对比见系列第三篇。