多 Agent 协同模式:Router 路由分发、Supervisor 主从编排与 Handoff 交接
本节目标
- 理解为什么当一个单体 Agent 挂载超过 20 个工具时,工具选错率和 Token 开销会急剧恶化
- 掌握三种核心多 Agent 协同拓扑:语义路由(Router)、主从监督者(Supervisor-Worker)与去中心化交接(Handoff)
- 用原生 JavaScript 实现支持**上下文最小化交接(Context Scoping)**的多 Agent 协同编排器
为什么需要把一个大 Agent 拆成多个专职子 Agent?
如果你往单个 Agent 里塞入 35 个涵盖前端、数据库、运维、财务的工具,再配上一份 4,000 字的巨型 System Prompt:
- 工具选择准确率雪崩:当候选工具超过 15~20 个且存在职责重叠时,模型极易调错工具或编造参数。
- 子任务过程污染主上下文:让主 Agent 亲自去翻阅 10 个日志文件排查某行报错,这 10 个文件的原始文本会永远留在主对话历史里,把后续任务的上下文空间挤爆。
多 Agent 分治(Multi-Agent Orchestration)的核心收益在于工具隔离(Tool Scoping)与上下文隔离(Context Isolation):每个专职子 Agent(Sub-agent)只挂载 3~5 个本领域的工具,并在自己独立的临时消息栈里跑完 10 步复杂排查,最后只把 200 字的精炼结论(Summary Artifact)交还给主 Agent!
┌──► [Coder Agent] (独立上下文 + 文件/AST 工具) ──► 仅返回补丁摘要
[Supervisor 主控 Agent] ┼──► [Tester Agent] (独立上下文 + 单测沙箱工具) ──► 仅返回测试报告
└──► [SecOps Agent] (独立上下文 + 漏洞扫描工具) ──► 仅返回审计结论手写支持 Handoff 交接与子上下文隔离的多 Agent 编排器
export class MultiAgentSwarm {
constructor({ llm, maxHandoffs = 6 }) {
this.llm = llm
this.agents = new Map() // agentName -> { name, rolePrompt, registry }
this.maxHandoffs = maxHandoffs
}
registerAgent({ name, description, rolePrompt, registry }) {
this.agents.set(name, { name, description, rolePrompt, registry })
return this
}
// 为当前激活的 Agent 动态注入「交接给其他专职 Agent」的虚拟 Handoff 工具
#buildToolsWithHandoffs(currentAgentName) {
const current = this.agents.get(currentAgentName)
const baseTools = current.registry ? current.registry.toOpenAiTools() : []
const peerNames = [...this.agents.keys()].filter((n) => n !== currentAgentName)
if (peerNames.length === 0) return baseTools
const peerDesc = peerNames
.map((n) => `${n}: ${this.agents.get(n).description}`)
.join(';')
return [
...baseTools,
{
type: 'function',
function: {
name: 'delegate_to_agent',
description: `将子任务委派或交接给更专业的同伴 Agent。可选目标:${peerDesc}`,
parameters: {
type: 'object',
properties: {
targetAgent: { type: 'string', enum: peerNames },
subtaskBrief: { type: 'string', description: '清晰、自包含的子任务说明与已掌握的关键上下文' }
},
required: ['targetAgent', 'subtaskBrief']
}
}
}
]
}
// 在独立隔离的临时上下文中运行指定子 Agent,仅向上游返回最终摘要结论
async runIsolatedSubagent(agentName, subtaskBrief, depth = 0) {
if (depth > this.maxHandoffs) {
return `委派深度超出上限 (${this.maxHandoffs}),已终止交接。`
}
const agent = this.agents.get(agentName)
if (!agent) throw new Error(`未找到目标 Agent: ${agentName}`)
const subMessages = [
{ role: 'system', content: agent.rolePrompt },
{ role: 'user', content: subtaskBrief }
]
for (let step = 1; step <= 6; step++) {
const res = await this.llm.chat({
messages: subMessages,
tools: this.#buildToolsWithHandoffs(agentName)
})
const msg = res.message
subMessages.push(msg)
if (!msg.tool_calls || msg.tool_calls.length === 0) {
return msg.content || ''
}
for (const tc of msg.tool_calls) {
if (tc.function.name === 'delegate_to_agent') {
const { targetAgent, subtaskBrief: nextBrief } = JSON.parse(tc.function.arguments || '{}')
const subResult = await this.runIsolatedSubagent(targetAgent, nextBrief, depth + 1)
subMessages.push({
role: 'tool',
tool_call_id: tc.id,
name: 'delegate_to_agent',
content: JSON.stringify({ delegatedTo: targetAgent, summary: subResult })
})
} else {
const toolMsg = await agent.registry.invokeOne(tc)
subMessages.push(toolMsg)
}
}
}
return subMessages.at(-1)?.content || '子任务已执行完毕'
}
}课后练习
练习 1:在 runIsolatedSubagent 中,为什么每次委派子 Agent 时都要新建一个独立的 subMessages 数组,而不是把主 Agent 的全部历史 messages 原封不动传过去?
答案:这就是多 Agent 架构中最关键的「上下文作用域隔离(Context Scoping)」。子 Agent 在执行子任务时可能调用十几次底层搜索和文件读取工具,产生上万 Token 的中间过程;在独立 subMessages 中运行并只把最终的 summary 返回给主 Agent,能彻底避免中间噪声污染主会话上下文。
练习 2:为什么 delegate_to_agent 工具的参数设计中,要求传入自包含的 subtaskBrief,且必须对递归委派深度 depth > this.maxHandoffs 做限制?
答案:因为子 Agent 看不到主 Agent 的完整对话历史,主 Agent 必须在 subtaskBrief 中把完成该子任务所需的背景参数交待清楚;同时限制 maxHandoffs 可以防止 Agent A 与 Agent B 因为职责边界模糊而互相无限踢皮球(Ping-Pong Loop)。