主题
Handoff 与 Swarm
要点
- 上一篇我们把 Supervisor 跑起来了
- handoff 的核心是把当前对话的控制权交给另一个角色
- 状态里需要明确保留「当前谁在接管」
- 与 Supervisor 相比,handoff 更强调后续多轮对话由接手角色继续处理
- 多轮对话里,handoff 和 Supervisor 的差异会更明显
- Swarm 更接近一组角色之间持续转交控制权
内容
1. 总控模式写顺以后,新的问题就来了
上一篇我们把 Supervisor 跑起来了。
用户只面对一个入口,总控 Agent 在后面找资料 Agent、编辑 Agent 这些专门角色来做事。
这种方式适合「所有事最后都回到一个总控来汇总」的场景。
但业务再往前走一步,你会碰到另一类需求。用户并不总是想一直和总控对话。有些时候,他其实更希望直接进入某个角色的上下文里,把接下来的几轮话都交给这个角色处理。
比如用户先说:
「我想请你帮我做一篇文章。」
总控当然可以理解这句话,然后去问内容 Agent,拿回结果再转述给用户。可一旦用户接着追问:
「接下来都按公众号风格改写。」
「标题要更适合搜索。」
「案例尽量贴近初学者。」
如果这几轮都还要先经过总控,再转给内容 Agent,交互路径会变长。更合适的做法,是直接把当前对话交给内容 Agent,让它连续处理后面的写作约束。
这就是 handoff 想解决的事。
对照官方 multi-agent 资料,Supervisor 和 handoff 的差异主要不在「有没有多个 Agent」,而在控制权怎么流动。Supervisor 像派单中心:专门 Agent 做完以后,结果回到总控。Handoff 像把当前会话切到另一个工作台:接手角色拿到控制权后,后续几轮可以继续由它处理。
放到内容创作场景:
| 模式 | 控制权 | 更适合的问题 |
|---|---|---|
| Supervisor | 总控始终在最外层 | 一次请求里要查资料、润色、汇总 |
| Handoff | 当前对话切给专门角色 | 后续多轮都围绕文章风格、标题、结构修改 |
| Swarm | 多个角色可以继续互相转交 | 内容、SEO、事实核查之间反复接力 |
因此 handoff 最关键的技术动作,是把「当前谁在接管」写进状态。只靠提示词说「现在交给内容助理」不够,下一轮调用时图还得能从 state 里读出 activeAgent。
2. Handoff 说白了,就是把控制权交出去
先把名字放轻一点看。handoff 指的是一件很直接的事:
当前这个角色不继续处理了,把后面的对话交给另一个角色。
和上一篇的 Supervisor 放在一起看,可以先按控制权流向区分:
Supervisor更像总控派活,结果还会收回来Handoff更像把用户带进另一个角色的工作台,让那个角色继续往下聊
这两种模式处理的不是替代关系,而是不同场景里的控制权组织方式。
如果你要的是集中汇总、统一对外回复,Supervisor 会更合适。
如果你要的是让用户直接进入某个领域角色的上下文里连续对话,handoff 会更自然。
3. 先看一个最简单的 handoff 场景
先用一个生活化的例子来讲。系统里有两个角色:
generalAgent负责接第一句话contentAgent负责文章创作
一开始用户先和 generalAgent 对话。只要问题还比较泛,它自己就能接住。但当它判断用户已经明确进入「内容创作」这个领域以后,就不继续硬接,而是把对话交给 contentAgent。
typescript
// handoff-scene.ts
type AgentName = 'general' | 'content'
type ChatState = {
activeAgent: AgentName
messages: Array<{ role: 'user' | 'assistant'; content: string }>
}
const initialState: ChatState = {
activeAgent: 'general',
messages: [],
}这里需要先看 activeAgent 字段。
只要这个字段从 general 变成了 content,后面的消息就不再交给总控,而是直接交给内容创作角色。
这件事本身不复杂。关键是把「有些对话需要换角色继续」写进状态,而不是只停留在提示词里。
4. 用状态切换把 handoff 落下来
这一类模式常见的做法,是在状态里保留「当前谁在接管」,再让路由逻辑根据这个字段决定下一步。
下面用一张很小的图,把这件事落成代码。
typescript
// handoff-graph.ts
import {
StateGraph,
StateSchema,
ReducedValue,
START,
END,
Command,
} from '@langchain/langgraph'
import type { GraphNode, ConditionalEdgeRouter } from '@langchain/langgraph'
import { z } from 'zod'
const appendMessages = (
current: Array<{ role: 'user' | 'assistant'; content: string }>,
update: Array<{ role: 'user' | 'assistant'; content: string }>,
) => {
return [...current, ...update]
}
const State = new StateSchema({
activeAgent: z.enum(['general', 'content']).default('general'),
messages: new ReducedValue(
z
.array(
z.object({
role: z.enum(['user', 'assistant']),
content: z.string(),
}),
)
.default([]),
{ reducer: appendMessages },
),
})
const generalAgent: GraphNode<typeof State> = (state) => {
const lastMessage = state.messages.at(-1)?.content ?? ''
// 如果用户已经明确在聊内容创作,这里就不继续硬接了
if (
lastMessage.includes('文章') ||
lastMessage.includes('草稿') ||
lastMessage.includes('改写')
) {
return new Command({
update: {
activeAgent: 'content',
messages: [
{
role: 'assistant',
content: '接下来由内容创作助理继续帮你推进文章。',
},
],
},
goto: 'contentAgent',
})
}
return {
messages: [
{
role: 'assistant',
content:
'我先帮你判断一下需求方向,如果是内容创作,我会把对话切给内容创作助理。',
},
],
}
}
const contentAgent: GraphNode<typeof State> = (state) => {
const lastMessage = state.messages.at(-1)?.content ?? ''
// 这里假设控制权已经切到了内容创作角色,
// 所以后面的回复会直接站在内容创作助理的视角继续往下接
return {
messages: [
{
role: 'assistant',
content: `内容创作助理已接手,当前收到的新要求是:${lastMessage}`,
},
],
}
}
const shouldContinue: ConditionalEdgeRouter<typeof State> = (state) => {
if (state.activeAgent === 'content') return 'contentAgent'
return END
}
const graph = new StateGraph(State)
.addNode('generalAgent', generalAgent, { ends: ['contentAgent'] })
.addNode('contentAgent', contentAgent)
.addEdge(START, 'generalAgent')
.addConditionalEdges('generalAgent', shouldContinue, ['contentAgent'])
.addEdge('contentAgent', END)
.compile()这段代码里,更应该关注的是状态更新:
typescript
// handoff-core.ts
activeAgent: 'content'handoff 的本质,就是把当前活跃角色切过去。这里我们用的是一个最容易看懂的版本:状态切换以后,后面的处理节点直接换成 contentAgent。
5. 连续对话时,handoff 的感觉会更明显
只跑一轮时,handoff 和 Supervisor 的差别还不算大。到了多轮对话里,差别会落到「下一轮是否还要重新经过总控」上。
下面这段先演示最小版本,只保留当前接管角色,再继续往下走。这样最容易看清 handoff 的核心是「控制权已经切过去了」。
typescript
// handoff-turns.ts
let state = await graph.invoke({
messages: [{ role: 'user', content: '我想做一篇 LangGraph 入门文章。' }],
})
console.log(state.activeAgent)
// → content
state = await graph.invoke({
// 这里把当前活跃角色继续传回去,
// 所以下一轮不会再回到 generalAgent 重新判断
activeAgent: state.activeAgent,
messages: [{ role: 'user', content: '接下来都按公众号风格改写。' }],
})
console.log(state.messages.at(-1)?.content)
// → 内容创作助理已接手,当前收到的新要求是:接下来都按公众号风格改写。到了第二轮,系统已经不需要再问「这是不是内容创作问题」。因为 activeAgent 已经切成了 content,后面就直接在内容创作角色的上下文里继续走。
这也是 handoff 的主要价值。它不要求每一轮都重新路由,而是让某个角色接手以后,连续处理后面的对话。
不过这里要注意一件事:上面这段只是为了演示角色切换,所以第二轮只把 activeAgent 传了回去,没有把整段历史消息一起带上。如果你希望内容创作角色继续看到完整对话,要么把历史消息一并传回去,要么接上前面讲过的 checkpointer。
6. 那 Swarm 又是什么
Swarm 可以先理解成比 handoff 再往前走一步。
如果说 handoff 还是在做「一个角色把控制权交给另一个角色」,那 Swarm 更像一组角色之间可以彼此转交,谁觉得下一步该找谁,就继续往下交。
它不一定总有一个固定总控站在最上面。控制权可能在多个角色之间流动。
比如还是内容创作场景:
- 内容顾问先接到需求
- 它发现搜索标题是关键,于是把问题交给 SEO 顾问
- SEO 顾问给出标题方向以后,再把问题交回内容顾问
- 内容顾问继续整理正文结构和段落表达
这时候系统更像是一张角色网络,而不是一棵单向分发的树。
7. 什么时候适合 handoff,什么时候更像 swarm
可以把这两个模式放回使用感受里看。
如果你的系统里仍然有一个比较明确的起点角色,只是中途会把用户带进某个更专业的角色里继续聊,那通常还是 handoff 更贴切。
如果你的系统里,多个角色本来就可能互相接力,而且你不太想设一个永远站在最上面的总控,那它就会越来越像 swarm。
所以区别不只是「有没有多个 Agent」,而在于:
控制权是一次性交出去,还是可能在多个角色之间持续流动。
8. 写这一类模式时最容易出的问题
最常见的问题,是明明已经 handoff 了,结果历史上下文还是按总控那套思路在塞。这样角色虽然换了,实际看到的还是一锅混在一起的内容,接手的意义就会打折。
第二个问题,是没有把「当前谁在接管」这件事落成状态。写的时候好像知道现在是谁在说话,跑到第二轮、第三轮以后,系统自己却不知道了。结果就是一会儿回总控,一会儿又回专门角色,行为会很飘。
还有一个问题,是把 swarm 写成一堆互相乱跳的 handoff。角色越多,这种问题越明显。所以一旦开始走到 swarm 这种模式,角色边界、共享状态和路由条件就得比前面更清楚。
9. 总结
到了这里,多 Agent 这条线又往前走了一步。
上一篇的 Supervisor,重点是总控怎么派活。
这一篇的 handoff 和 swarm,重点开始变成控制权怎么在角色之间流动。
它们处理的不是同一个问题,不需要放在一起比较谁更高级。需要统一汇总时,用 Supervisor;希望用户进入某个角色的上下文里连续对话时,用 handoff;如果角色之间还会继续彼此接力,就开始接近 swarm。
下一篇讲 层级团队与并行协作。那一篇会继续往前走一步,处理多个角色怎样同时工作,以及结果怎样再汇回来。