从串行到并行:Agent Teams 多智能体调度优化实战
当你的 AI Agent 平台支持多智能体协作,但两个做同样事情的 Agent 一个要 11 秒、另一个要 51 秒时,问题出在哪里?
问题发现
我们在对 AgentZero(一个基于 Claude Agent SDK 的桌面 AI 工作台)做端到端测试时,发现了一个反直觉的现象:
测试任务:派遣 2 个子 Agent 并行执行 echo 命令
子 Agent 1: echo "agent1_test" → 51.4s
子 Agent 2: echo "agent2_test" → 10.9s
两个完全一样的 echo 命令,耗时差了 5 倍。这不是 API 延迟能解释的。
根因分析
第一层:表象 —— 串行执行
通过 trace 分析,我们发现两个子 Agent 是串行执行的,不是并行:
实际执行顺序:
Agent 1 ████████████ 11s(启动 + 执行)
Agent 2 ████████████ 11s(启动 + 执行)
主 Agent 汇总 ~10s
─────────────────────────────────────────────────────────
总耗时 ≈ 51s
第二层:架构 —— SDK 是黑盒
AgentZero 使用 Claude Agent SDK 的 sdk.query() 接口。这个接口返回一个 AsyncGenerator,调用方通过 for await 逐条消费消息:
const queryResult = sdk.query({ prompt, options })
for await (const message of queryResult) {
// 被动接收 —— 无法控制工具执行顺序
}
问题在于:当模型决定调用 Agent 工具时,工具执行发生在 SDK 内部。调用方只有一个 canUseTool 权限回调,没有工具执行回调。这意味着:
- SDK 内部串行执行工具调用
- 调用方无法拦截 tool_use 做并行编排
- 调用方只能被动等待所有工具完成
第三层:对比 —— Claude Code 怎么做的
分析 Claude Code 的源码后,我们发现它的并行能力来自完全不同的架构层级:
| 层级 | Claude Code | AgentZero |
|---|---|---|
| SDK 使用 | 自己就是 SDK 宿主,自己实现 query() |
调用 sdk.query() 作为消费者 |
| 工具执行 | runTools() 编排层,Promise.all() 并发 |
SDK 内部执行,无法干预 |
| Agent 生命周期 | void runAsyncAgentLifecycle() fire-and-forget |
阻塞等待完成 |
Claude Code 能做并行是因为它控制了工具执行层。但对于基于 SDK 的应用,这条路走不通。
解决方案
既然不能在工具执行层做并行,我们换一个思路:让 SDK 内部去处理并行,我们只负责结果收集。
策略一:Auto-Resume 机制
核心思路:主 Agent 的 query() 流结束后,检测是否有 teammate 任务在跑,如果有就轮询收件箱收集结果,再次调用 query() 让主 Agent 汇总。
主 Agent query() 结束
↓
检测到 startedTaskIds.size > 0(有子任务)
↓
轮询 ~/.claude/teams/{name}/inboxes/team-lead.json
↓
收到 worker 结果
↓
构建 resume prompt → 再次 query({ resume: sdkSessionId })
↓
主 Agent 汇总所有 worker 结果 → 输出最终回复
实现分三个模块:
1. Inbox 读取器(agent-team-reader.ts)
// 通过 SDK session ID 找到 team 的 inbox 路径
export async function findTeamLeadInboxPath(sdkSessionId: string)
// 带重试的 inbox 轮询(5 次,每次间隔 2 秒)
export async function pollInboxWithRetry(inboxPath: string, config)
// 格式化 inbox 消息为 resume prompt
export function formatInboxPrompt(messages: InboxMessage[]): string
// 当 inbox 为空时,用 task_notification 的 summary 作为 fallback
export function formatSummaryFallbackPrompt(summaries): string
2. SDK Session ID 捕获
每条 SDK 消息都带有 session_id 字段。从第一条消息中提取并持久化,用于后续 resume:
if (!accumulated.sdkSessionId) {
const msgRecord = message as Record<string, unknown>
if (typeof msgRecord.session_id === 'string') {
accumulated.sdkSessionId = msgRecord.session_id
}
}
3. Auto-Resume 触发
在主 Agent 的 query 循环结束后,检测是否有未完成的 teammate 任务:
if (accumulated.startedTaskIds.size > 0 && accumulated.sdkSessionId) {
// 通知前端:正在收集结果
callbacks.onEvent({ type: 'waiting_resume', message: '...' })
// 轮询 inbox
const inboxInfo = await findTeamLeadInboxPath(accumulated.sdkSessionId)
const messages = await pollInboxWithRetry(inboxInfo.inboxPath)
// 构建 resume prompt 并再次调用 SDK
const resumeResult = await callAgentSDK({
...params,
prompt: formatInboxPrompt(messages),
resumeSessionId: accumulated.sdkSessionId,
})
}
策略二:SubAgent 委派指引
代码层面的 auto-resume 解决了结果收集问题,但还有一个问题:模型不知道什么时候该委派。
没有指引时,模型倾向于自己做所有事情 —— 自己 Bash、自己 Read、自己 Grep,串行完成所有工作。即使我们注册了 explorer、researcher、security-auditor 等子代理,模型也不会主动使用它们。
解决方案是在 system prompt 中加入一段 SubAgent 委派策略(约 60 行),教模型:
## SubAgent 委派策略
### 何时委派
- 动手修改前 → 先派 explorer 收集上下文
- 技术决策前 → 派 researcher 对比方案
- 代码写完后 → 派 code-reviewer 审查质量
- 涉及安全时 → 派 security-auditor 扫描风险
### 模型选择
- Haiku(默认):信息收集、简单搜索、常规审查
- Sonnet:需要推理的分析、跨模块关联
- Opus:高难度架构决策、复杂系统设计
### 并行执行
当有多个独立任务时,在同一条消息中发出多个 Agent 工具调用:
// 好:一条消息中多个 Agent 调用 → 并行
Agent(subagent_type="explorer", prompt="查找认证相关代码")
Agent(subagent_type="explorer", prompt="查找数据库访问层")
这段 prompt 的核心价值不是教模型"怎么调 API",而是建立一种认知框架 —— 让模型意识到"我不需要自己做所有事,我有一个团队"。
效果验证
实验 1:多 Agent 调度耗时对比
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| J2B 测试耗时 | 72s | 54s | -25% |
| 结果完整性 | ✓ 两个 echo 结果 | ✓ 两个 echo 结果 | 不变 |
| Team 面板 | ✓ 出现 | ✓ 出现 | 不变 |
实验 2:委派策略有效性验证
发送一个不显式要求 Agent 的复杂任务:
请对这个项目做两件事:
1. 分析项目的整体架构:目录结构、核心模块、模块间依赖关系
2. 检查项目中是否存在安全隐患:硬编码密钥、注入风险、敏感信息泄露
| 指标 | 结果 |
|---|---|
| 模型主动委派 | ✓ — 无需显式要求,自主决定使用 Agent 工具 |
| 选择正确的 Agent | ✓ — explorer(架构)+ security-auditor(安全) |
| 并行执行 | ✓ — Team 面板同时显示 2 个 Agent 运行 |
| 输出质量 | 完整的安全报告,含 P0-P3 修复优先级 |
| 工具调用总数 | 77 次(密集的文件分析和安全扫描) |
模型的实际行为:
glm-5: "我将并行派出两个子代理来完成这两项分析任务。"
→ Agent(explorer): 分析 AgentZero 项目架构
→ Agent(security-auditor): 审计 AgentZero 安全风险
→ [两个 Agent 并行执行]
→ [auto-resume 收集结果]
→ 输出完整报告(架构分析 + 安全审计 + 修复优先级)
技术总结
改动量
| 组件 | 改动 |
|---|---|
| agent-team-reader.ts | 新增 180 行(inbox 读取/轮询/格式化) |
| agent-orchestrator.ts | 修改 ~100 行(session 捕获 + task 追踪 + auto-resume) |
| agent-prompt-builder.ts | 新增 60 行(SubAgent 委派策略 prompt) |
| SDK 版本 | 0.2.76 → 0.2.91 |
核心原则
- 不要和 SDK 对着干。 SDK 是黑盒就接受它是黑盒,在它的边界之外做编排。
- Prompt 的杠杆率极高。 60 行 prompt 改变了模型的整个行为模式 —— 从"我自己做"变成"我有一个团队"。这比任何代码优化的 ROI 都高。
- 先读参考实现再动手。 分析了 Claude Code 源码和同类项目后才确定方案,避免了走弯路(我们最初想在 SDK 和 orchestrator 之间加
runTools()层,后来发现这条路走不通)。
还能继续优化什么
当前方案让耗时从 72s 降到 54s(-25%),主要收益来自 auto-resume 减少了结果收集的等待时间。要进一步压缩到真正的并行(~20s),需要:
- SDK 层面的改进:等 Anthropic 在 SDK 内部实现 Task 工具的并行调度
- 或者换架构:像 Claude Code 那样自己实现
query()+runTools()层,完全控制工具执行顺序
但对于大多数基于 SDK 的应用来说,auto-resume + prompt 指引已经是最佳的投入产出比。