Skip to content

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 的感觉会更明显

只跑一轮时,handoffSupervisor 的差别还不算大。到了多轮对话里,差别会落到「下一轮是否还要重新经过总控」上。

下面这段先演示最小版本,只保留当前接管角色,再继续往下走。这样最容易看清 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,重点是总控怎么派活。

这一篇的 handoffswarm,重点开始变成控制权怎么在角色之间流动。

它们处理的不是同一个问题,不需要放在一起比较谁更高级。需要统一汇总时,用 Supervisor;希望用户进入某个角色的上下文里连续对话时,用 handoff;如果角色之间还会继续彼此接力,就开始接近 swarm

下一篇讲 层级团队与并行协作。那一篇会继续往前走一步,处理多个角色怎样同时工作,以及结果怎样再汇回来。

基于 MIT 协议开源