Supervisor 模式

要点

  • 前面几篇一直在写一张图里的节点怎么配合
  • Supervisor 不是什么特殊 API,它更像一种组织方式
  • 到了 Supervisor 这一层,问题就变了
  • 现在可以把总控 Agent 接起来了
  • 这件事真正的差别,不在于角色数量,而在于决策压力

内容

1. 一个人开始忙不过来时,就会想拆角色

前面几篇一直在写一张图里的节点怎么配合。

到了这里,图已经能暂停、恢复、拆子图,也能把一段流程包成模块。

但业务再往前走一步,问题就不只是「流程有多长」了,而是「是不是所有事都该让一个角色来做」。

比如一个个人助理系统,用户可能一口气说一句话:

「帮我看看周五下午有没有空,再起草一封邮件给客户,说我们把会议改到三点。」

这句话里其实有两种完全不同的能力。一种是查日程、安排时间,一种是写邮件、组织措辞。把它们全塞给一个 Agent 当然也能写,但工具一多、提示词一长、上下文一杂,决策就开始变得不稳定。

这时候更顺的办法通常不是继续往一个 Agent 身上加东西,而是把角色拆开:让一个总控角色负责判断眼前要找谁处理,再把具体工作交给对应的专门 Agent。

这就是 Supervisor 模式。

2. Supervisor 到底在做什么

先把名字放轻一点看。Supervisor 不是什么特殊 API,它更像一种组织方式。

它做的事情其实很简单。自己不直接处理所有细节,手里握着几个专门 Agent,先判断当前请求应该交给谁,拿到结果以后,再决定要不要继续找下一个角色,或者直接回复用户。

Canvas actions78%Exit zen mode

Drawing canvas 如果把前面那句用户请求拆开看,Supervisor 可能会先把「看周五下午有没有空」交给日程 Agent,再把「起草邮件」交给邮件 Agent,最后把两边结果收回来,整理成一段最终回复。

这里真正重要的,不是「角色变多了」,而是上下文开始分层了。日程 Agent 不用知道怎么写邮件,邮件 Agent 也不用关心所有日历工具的细节。每个角色只管自己那一块。

3. 先把两个专门 Agent 准备出来

这一篇先用两个最容易理解的角色来讲:一个是 calendarAgent,只负责查空闲时间和安排会议;另一个是 emailAgent,只负责起草和发送邮件。

当前这套官方示例更常见的写法,是把专门 Agent 包成工具,让总控 Agent 去调用。因为总控 Agent 眼里不需要看见每个底层 API,只需要知道「这里有一个日程专家」和「这里有一个邮件专家」就够了。

这一层虽然写的是 createAgent(),但关注点已经不是单个 Agent 自己怎么调工具了,而是多个角色之间怎么分工、怎么把结果重新收回来。

// specialist-agents.ts
import { createAgent, tool } from 'langchain'
 
import { ChatOpenAI } from '@langchain/openai'
 
import { z } from 'zod'
 
const model = new ChatOpenAI({
 
  model: process.env.DEEPSEEK_MODEL || 'deepseek-chat',
 
  apiKey: process.env.DEEPSEEK_API_KEY,
 
  configuration: {
 
    baseURL: process.env.DEEPSEEK_BASE_URL,
 
  },
 
})
 
// 先准备底层工具。专门 Agent 真正做事时,还是靠这些工具落地。
 
const checkCalendar = tool(
 
  async ({ date, period }) => {
 
    return `${date} 的 ${period} 目前空闲,可以安排会议。`
 
  },
 
  {
 
    name: 'check_calendar',
 
    description: '查看某一天某个时间段是否空闲',
 
    schema: z.object({
 
      date: z.string(),
 
      period: z.string(),
 
    }),
 
  },
 
)
 
const draftEmail = tool(
 
  async ({ to, subject, body }) => {
 
    return `邮件草稿已生成:收件人 ${to},主题《${subject}》,正文:${body}`
 
  },
 
  {
 
    name: 'draft_email',
 
    description: '生成邮件草稿',
 
    schema: z.object({
 
      to: z.string(),
 
      subject: z.string(),
 
      body: z.string(),
 
    }),
 
  },
 
)
 
// 日程 Agent 只看日程领域的问题
 
const calendarAgent = createAgent({
 
  model,
 
  tools: [checkCalendar],
 
  systemPrompt: '你是一个日程助理,只处理时间、会议和安排相关的问题。',
 
})
 
// 邮件 Agent 只看邮件领域的问题
 
const emailAgent = createAgent({
 
  model,
 
  tools: [draftEmail],
 
  systemPrompt: '你是一个邮件助理,只处理邮件起草和发送相关的问题。',
 
})

这里先注意一个边界:专门 Agent 不一定要暴露给用户。很多时候,用户只和总控 Agent 对话,专门 Agent 只是系统内部角色。

4. 把专门 Agent 包成总控可调用的工具

到了 Supervisor 这一层,问题就变了。它不关心底层工具怎么用,它只关心:

「日程这件事交给谁。」

「邮件这件事交给谁。」

所以更顺的做法,是把 calendarAgentemailAgent 再包一层,变成总控 Agent 眼里的两个高层工具。

// wrap-subagents-as-tools.ts
import { tool } from 'langchain'
 
import { z } from 'zod'
 
const askCalendarAgent = tool(
 
  async ({ request }) => {
 
    // 这里把一段自然语言请求交给日程 Agent
 
    const result = await calendarAgent.invoke({
 
      messages: [{ role: 'user', content: request }],
 
    })
 
    // 总控 Agent 最终只需要看这个角色返回了什么,
 
    // 不需要关心日程 Agent 内部到底调了哪些底层工具
 
    return result.messages.at(-1)?.text ?? ''
 
  },
 
  {
 
    name: 'ask_calendar_agent',
 
    description: '把与日程、会议安排有关的事情交给日程助理处理',
 
    schema: z.object({
 
      request: z.string(),
 
    }),
 
  },
 
)
 
const askEmailAgent = tool(
 
  async ({ request }) => {
 
    // 邮件 Agent 自己决定该怎么起草邮件、调用什么工具
 
    const result = await emailAgent.invoke({
 
      messages: [{ role: 'user', content: request }],
 
    })
 
    return result.messages.at(-1)?.text ?? ''
 
  },
 
  {
 
    name: 'ask_email_agent',
 
    description: '把与邮件起草、修改、发送有关的事情交给邮件助理处理',
 
    schema: z.object({
 
      request: z.string(),
 
    }),
 
  },
 
)

这样包完以后,总控 Agent 根本不需要知道 check_calendardraft_email 的参数细节。它只会觉得自己手里有两个能力:

一个能处理日程,一个能处理邮件。

这也是 Supervisor 模式最顺手的地方。底层工具怎么变,可以留在专门 Agent 里慢慢演进,不需要每次都回头改总控。

5. 最上面这一层,就是 Supervisor

现在可以把总控 Agent 接起来了。

// supervisor-agent.ts
import { createAgent } from 'langchain'
 
const supervisor = createAgent({
 
  model,
 
  tools: [askCalendarAgent, askEmailAgent],
 
  systemPrompt: `
 
你是一个总控助理。
 
你的工作不是自己完成所有任务,而是判断当前请求应该交给哪个专门助理。
 
如果请求里同时包含多个任务,可以按顺序调用多个助理,再把结果整理给用户。
 
  `.trim(),
 
})

这时候,调用方式反而变简单了。用户还是只和一个入口对话。

// supervisor-run.ts
// 对用户来说,入口依然只有一个 supervisor
 
const result = await supervisor.invoke({
 
  messages: [{
 
    role: 'user',
 
    content: '帮我看看周五下午有没有空,再起草一封邮件给客户,说会议改到三点。',
 
  }],
 
})
 
console.log(result.messages.at(-1)?.text)

这段代码里,看上去只有一个 supervisor.invoke(...),但内部可能已经做了多步:

先找日程 Agent,拿到可用时间,再找邮件 Agent 起草邮件,最后回到总控整理结果。

这也是为什么 Supervisor 模式会比「一个 Agent 手里挂十几个底层工具」更稳一些。总控层负责分工,专门角色负责执行,每一层看到的上下文都收得更干净。

6. 为什么它比把所有工具都塞到一个 Agent 里更顺

这件事真正的差别,不在于角色数量,而在于决策压力。

如果只有一个 Agent,它每次都要同时面对: 日程工具、邮件工具、提示词、用户历史、格式要求和错误处理。

工具少的时候没问题,工具一多,模型开始做选择时就容易发散。它既要判断这句话属于什么领域,又要记住每个工具的参数要求,还要想怎么把多步任务串起来。

Supervisor 模式把这个压力拆开了。总控层先只做分工,专门层再各自处理自己的细节。这样每个 Agent 的工具范围会小很多,提示词也会短很多。

还有一个很实际的好处,是后面更容易加角色。今天只有日程和邮件,明天再加一个「知识库助理」,你不需要去重写整个系统,只要:

先新建一个专门 Agent,再把它包成一个总控可调用的工具,最后挂到 supervisor 上,整套结构就能继续往前长。

7. 写这种模式时最容易踩的坑

最常见的问题,是把总控 Agent 写得太勤快。

如果总控层自己也开始直接查日程、直接写邮件、直接做格式整理,那角色边界很快就会乱掉。写着写着,它又变回了一个什么都做的大 Agent,前面拆角色的意义就没了。

第二个常见问题,是让专门 Agent 看到太多不该看到的上下文。比如邮件 Agent 本来只需要知道「帮我写一封改期邮件」,你却把整段长长的历史对话、审批信息、其它工具结果一股脑都塞给它。这样一来,专门 Agent 的上下文又开始膨胀,角色拆分的好处也被吃掉了。

还有一个问题,是把专门 Agent 当成长期记忆主体。当前这类 subagents 结构里,更稳的心智模型是:会话记忆主要留在总控层,专门 Agent 每次按任务被调用,处理完就把结果交回来。这样边界更清楚,也更容易排查问题。

8. 总结

Supervisor 模式本质上是在做一件很朴素的事:把一个开始忙不过来的 Agent,拆成一个总控角色和几个专门角色。

前面这一章写到现在,已经把流程控制、暂停恢复、子图模块化都接起来了。到了这里,视角又往前走了一步,不再只是「一张图怎么组织」,而是「多个角色怎么分工」。

下一篇讲 Handoff 与 Swarm。到那时,重点就不再是「总控负责分发任务」,而是「角色之间能不能直接把对话和控制权交给彼此」。