← 返回文章列表← Back to posts

从串行到并行:Agent Teams 多智能体调度优化实战From Serial to Parallel: Agent Teams Multi-Agent Scheduling Optimization

深入分析 Agent Teams 串行执行的根因(SDK 黑盒 + 缺少编排层),通过 auto-resume 机制和 SubAgent 委派 prompt 两个策略实现并行优化,附完整实验数据验证。Deep analysis of why Agent Teams execute serially (SDK black box + missing orchestration layer), optimized with auto-resume mechanism and SubAgent delegation prompt, with full experimental validation.

·12 分钟阅读min read

从串行到并行: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

核心原则

  1. 不要和 SDK 对着干。 SDK 是黑盒就接受它是黑盒,在它的边界之外做编排。
  2. Prompt 的杠杆率极高。 60 行 prompt 改变了模型的整个行为模式 —— 从"我自己做"变成"我有一个团队"。这比任何代码优化的 ROI 都高。
  3. 先读参考实现再动手。 分析了 Claude Code 源码和同类项目后才确定方案,避免了走弯路(我们最初想在 SDK 和 orchestrator 之间加 runTools() 层,后来发现这条路走不通)。

还能继续优化什么

当前方案让耗时从 72s 降到 54s(-25%),主要收益来自 auto-resume 减少了结果收集的等待时间。要进一步压缩到真正的并行(~20s),需要:

  • SDK 层面的改进:等 Anthropic 在 SDK 内部实现 Task 工具的并行调度
  • 或者换架构:像 Claude Code 那样自己实现 query() + runTools() 层,完全控制工具执行顺序

但对于大多数基于 SDK 的应用来说,auto-resume + prompt 指引已经是最佳的投入产出比。