数据截至 (上游 commit 35efe178d76b)
子代理与 Supervisor:多 agent 协作与任务交接
30 秒导读: 一个 Agent 太"全能"就什么都做不精。VoltAgent 让你把一个 supervisor(主管)Agent 和若干 专精子 Agent 组队——主管只负责"拆任务、点名、汇总",真正干活交给子代理。本章讲清楚:这个委派是怎么在运行时发生的、任务怎么交接过去、父子怎么共享上下文与追踪、结果又怎么流回来。
本章只讲运行时委派(supervisor/sub-agent)。它和第 5 章的 Workflow 引擎是两种编排:
| 子代理委派(本章) | Workflow(第 5 章) | |
|---|---|---|
| 谁决定下一步 | 模型在运行时临时决定点名谁 | 你写死的声明式步骤图 |
| 形态 | 一个工具调用(delegate_task) | .andThen / .andAgent / .andWhen 步骤链 |
| 适合 | 开放式、需要模型判断该找谁 | 固定流程、要能暂停/恢复 |
如果你还不了解单个 Agent 一次生成的生命周期,先读 01 · Agent 运行时;委派本质上就是"在父 Agent 的一次生成里,调了一个特殊工具,而这个工具会去跑另一个 Agent 的完整生成"。
1. 这是什么(零基础也能懂)
一句话定义: 把一个 Agent 声明为另一个 Agent 的 subAgents,父 Agent 就变成 supervisor,自动获得一个 delegate_task 工具,能在对话中途把子任务甩给专精的子代理去做。
解决什么问题: 假设你在做一个客服机器人。用户既可能问"我的订单到哪了",又可能问"帮我算一下退款金额",还可能要"用英文重写这封投诉"。你可以塞一个巨型 prompt 让单个 Agent 全包——但它会顾此失彼。更好的做法:
- 一个 物流查询 Agent(接了物流 API)
- 一个 计算 Agent(擅长数值)
- 一个 翻译 Agent
再放一个 supervisor 在最前面。它读懂用户意图,把子问题分发给对应的专家,收齐答案后拼成最终答复。
用起来什么样: 声明子代理只要一个 subAgents 数组(packages/core/src/agent/agent.ts:1183 构造 SubAgentManager):
// 示意,非源码:声明一个 supervisor
const supervisor = new Agent({
name: "Supervisor",
instructions: "你负责把用户问题分派给合适的专家,并汇总答复。",
model: myModel,
subAgents: [logisticsAgent, mathAgent, translatorAgent], // ← 关键
});
// 之后照常调用,委派对模型是透明的
await supervisor.streamText("我的订单到哪了?顺便把这句翻成英文:'请尽快发货'");
你不需要手写"先调物流、再调翻译"的逻辑——一旦 subAgents 非空,VoltAgent 自动:
- 把 supervisor 的 system prompt 改写成"你是主管,手下有这些专家……"(见 §8)。
- 给它挂上一个
delegate_task工具。
一句话直觉: 把 supervisor 想成一个项目经理。它自己不写代码,但知道手下每个人擅长什么;delegate_task 就是它派活的"工单系统",可以一次给多个人派活,然后等大家交活。
本节不出现底层细节。记住一件事:委派 = 父 Agent 调了一个会去跑子 Agent 的工具。
2. 顶层全景(它大概怎么转)
怎么读这张图: 从上往下是一次委派的控制流。左边竖线是"用户 ↔ supervisor"的主循环,右边是委派工具触发后 SubAgentManager 把任务 fan-out(扇出)给多个子代理。
用户请求
│
▼
┌──────────────────┐ subAgents 非空 → 自动挂上
│ Supervisor │─────────────────────────┐
│ (父 Agent) │ ▼
└──────────────────┘ [ delegate_task 工具 ]
▲ │ 模型决定调用
│ 汇总子代理结果, │ 传入 {task, targetAgents[]}
│ 生成最终答复 ▼
│ ┌──────────────────────┐
│ │ SubAgentManager │
└──────────────────────────────────│ handoffToMultiple │
└──────────────────────┘
│ Promise.all 并行
┌───────────────┼───────────────┐
▼ ▼ ▼
子Agent A 子Agent B 子Agent C
(跑完整生成) (跑完 整生成) (跑完整生成)
│ │ │
└───── fullStream 打元数据转发回父流 ──────┘
部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
SubAgentManager | 管理子代理列表、生成委派工具、执行交接 | packages/core/src/agent/subagent/index.ts:55 |
delegate_task 工具 | 模型调用它来点名子代理派活 | subagent/index.ts:758 createDelegateTool |
handoffTask | 把一个任务交接给一个子代理并跑它 | subagent/index.ts:319 |
handoffToMultiple | 把任务并行交接给多个子代理 | subagent/index.ts:720 |
| stream-metadata-enricher | 给子代理的流事件贴上"这是谁产的"元数据 | subagent/stream-metadata-enricher.ts:38 |
AgentRegistry | 全局登记父子关系(算委派深度用) | packages/core/src/registries/agent-registry.ts:91 |
主线走一遍(高层):
- 用户消息进 supervisor,它的一次生成开始(见 01 章)。
- 因为有子代理,工具集里多了
delegate_task(agent.ts:6312)。 - 模型输出一个
delegate_task工具调用,参数是{ task, targetAgents: ["物流查询", "翻译"] }。 - 工具
execute按名字查到子代理配置,调handoffToMultiple并行跑它们(subagent/index.ts:837)。 - 每个子代理跑一次自己的完整生成,结果、用量、消息回收成结构化数组返给模型。
- supervisor 拿到各家答复,继续它的循环,最终拼出答复给用户。
3. 核心原理
3.1 delegate_task 工具是怎么"长出来"的
要解决的小问题: 委派对模型必须表现成一个普通工具——模型只会调工具,不懂"子代理"这种概念。所以框架要把"派活"这件事包装成一个 schema 清晰的工具。
思路: 只要 Agent 有子代理,就在每次准备工具时动态塞进一个 delegate_task。它的参数很简单:任务文本 + 目标代理名字列表 + 可选上下文。
真实实现: 工具在 createDelegateTool 里用 createTool 造出来,参数 schema 见 subagent/index.ts:780:
parameters: z.object({
task: z.string().describe("The task to delegate"),
targetAgents: z.array(z.string()).describe("List of agent names to delegate the task to"),
context: z.record(z.string(), z.any()).optional().describe("Additional context for the task"),
}),
它按名字匹配子代理(subagent/index.ts:810)——找不到的名字会 warn 并过滤掉,一个都没匹配上才抛错。这段是容错的关键:模型偶尔会拼错代理名,框架不让整次委派崩掉。
挂载时机有两处:
- 每次生成前:
agent.ts:6312在hasSubAgents()为真时把 delegate 工具加进本次运行的工具集。 - 动态加子代理:
agent.ts:8287的addSubAgent——如果这是第一个子代理,顺手把工具挂上静态工具管理器;removeSubAgent(agent.ts:8305)在子代理清空时把工具摘掉。
关键细节: 工具的 execute 结果永远返回数组(subagent/index.ts:869 "Always return array for consistent API"),即使只派给一个代理。每个元素是 { agentName, response, usage, bailed },让模型能分辨哪段答复来自谁。