手撕 Claude Code-5:Subagent 与 Agent Teams

第 5 章:Subagent 与 Agent Teams --- 多代理编排协议

源码位置:src/tools/AgentTool/src/utils/swarm/src/tasks/InProcessTeammateTask/


5.1 多代理的三种模式

Claude Code 支持三种不同的多代理模式:

php 复制代码
模式 1:普通 Subagent(指定类型)
  Agent({ subagent_type: 'general-purpose', prompt: '...' })
  → 创建预定义类型的子代理,独立运行

模式 2:Fork Subagent(继承上下文)
  Agent({ prompt: '...' })  // 不指定 subagent_type
  → Fork 当前会话,子代理继承父代理的完整上下文

模式 3:Agent Teams(团队协作)
  TeamCreate → 创建团队
  → 多个 in-process 队友并发运行
  → 通过 mailbox 通信和权限同步

5.2 普通 Subagent:runAgent()

5.2.1 入口

源码位置:src/tools/AgentTool/runAgent.ts:248

typescript 复制代码
// 注意:是 async generator(function*),不是普通 async function
// 逐条 yield Message,调用方以 for await...of 消费
export async function* runAgent({
  agentDefinition,
  promptMessages,    // 初始消息(替代旧的 directive 字符串)
  toolUseContext,
  canUseTool,        // 工具权限检查函数(子代理独立实例)
  isAsync,           // 是否后台异步运行
  forkContextMessages,
  querySource,
  override,          // { systemPrompt?, userContext?, agentId?, abortController? }
  model,
  maxTurns,
  availableTools,    // 调用方预组装的工具池
  allowedTools,      // 显式工具白名单(替换所有父代理 allow 规则)
  useExactTools,     // Fork 专用:跳过 resolveAgentTools(),直接用 availableTools
  worktreePath,
  ...
}: { ... }): AsyncGenerator<Message, void>

为什么是 async generator? 子代理的 query() 也是 async generator,runAgent 直接 yield 转发其事件,使调用方能实时观察子代理进度(用于 TaskOutput 更新),无需等待子代理完成。

5.2.2 代理定义(AgentDefinition)

源码位置:src/tools/AgentTool/loadAgentsDir.ts:106

代理定义有三种来源,共用 BaseAgentDefinition 基础字段:

typescript 复制代码
// 所有代理共用的基础字段
type BaseAgentDefinition = {
  agentType: string
  whenToUse: string
  tools?: string[]           // 工具白名单('*' = 父代理全部工具)
  disallowedTools?: string[]
  model?: string             // 'inherit' = 继承父代理模型
  permissionMode?: PermissionMode
  maxTurns?: number
  skills?: string[]          // 预加载的 skill 名称列表
  mcpServers?: AgentMcpServerSpec[]  // 代理专属 MCP 服务器
  hooks?: HooksSettings
  isolation?: 'worktree' | 'remote' // 隔离模式:git worktree 或远程
  background?: boolean       // 总是以后台任务方式 spawn
  memory?: 'user' | 'project' | 'local'  // 持久记忆 scope
  initialPrompt?: string     // 预置在第一个 user turn 前的提示
  omitClaudeMd?: boolean     // 省略 CLAUDE.md(Explore/Plan 优化,节省 token)
  requiredMcpServers?: string[]  // 必须存在的 MCP 服务器(否则不可用)
  criticalSystemReminder_EXPERIMENTAL?: string  // 每轮注入的关键提醒
}

// 内置代理(源码定义)
type BuiltInAgentDefinition = BaseAgentDefinition & {
  source: 'built-in'
  baseDir: 'built-in'
  // getSystemPrompt 需要 toolUseContext(访问 options 判断环境)
  getSystemPrompt: (params: { toolUseContext: Pick<ToolUseContext, 'options'> }) => string
}

// 自定义代理(从 .claude/agents/*.md 加载)
type CustomAgentDefinition = BaseAgentDefinition & {
  source: 'userSettings' | 'projectSettings' | 'policySettings' | 'flagSettings'
  filename?: string
  getSystemPrompt: () => string  // 内容来自 markdown 文件 body
}

// 插件代理(从插件系统加载)
type PluginAgentDefinition = BaseAgentDefinition & {
  source: 'plugin'
  plugin: string   // 插件名
  getSystemPrompt: () => string
}

// 联合类型
type AgentDefinition = BuiltInAgentDefinition | CustomAgentDefinition | PluginAgentDefinition

代理优先级与覆盖顺序(src/tools/AgentTool/loadAgentsDir.ts:196):

复制代码
built-in → plugin → userSettings → projectSettings → flagSettings → policySettings

相同 agentType 时,后者覆盖前者。这意味着 managed(policySettings)代理可以覆盖所有用户自定义代理,plugin 代理可以覆盖 built-in。

omitClaudeMd 设计背景:Explore、Plan 等只读代理不需要提交/PR/lint 规范(来自 CLAUDE.md),主代理会解读它们的输出。关闭后每次 spawn 节省 5-15 Gtok,跨 3400万+ Explore spawn 有显著效果。

5.2.3 AsyncLocalStorage 上下文隔离

子代理通过 AsyncLocalStorage 实现进程内隔离:

typescript 复制代码
// src/utils/agentContext.ts
type SubagentContext = {
  agentId: string
  parentAgentId?: string
  // ...
}

// 子代理运行时,所有在此 async 调用链内的代码
// 都能通过 getAgentContext() 读到正确的 agentId
function runWithAgentContext<T>(
  context: AgentContext,
  fn: () => Promise<T>
): Promise<T>

这意味着:

  • 子代理读取 getSessionId() 会得到自己的 ID
  • 子代理的 Todo 列表与父代理隔离
  • 子代理的工具调用权限检查使用子代理自己的上下文

5.3 Fork Subagent:继承父代理上下文

这是最精妙的设计之一,也是 Prompt Cache 优化的关键。

5.3.1 什么是 Fork?

源码位置:src/tools/AgentTool/forkSubagent.ts:32

subagent_type 省略时,且 isForkSubagentEnabled() 返回 true 时触发 Fork 模式。子代理继承父代理的:

  • 完整对话历史(字节级相同的消息前缀)
  • 系统提示(通过 override.systemPrompt 传递已渲染字节,不重新构建)
  • 工具列表(tools: ['*'] + useExactTools: true,跳过 resolveAgentTools()
  • 权限模式(permissionMode: 'bubble'

isForkSubagentEnabled() 的三个条件(需同时满足):

  1. feature('FORK_SUBAGENT') 编译时 gate 开启
  2. !isCoordinatorMode() --- 与 Coordinator 模式互斥(coordinator 有自己的编排模型)
  3. !getIsNonInteractiveSession() --- 仅在交互式会话中启用

5.3.2 FORK_AGENT 定义

源码位置:src/tools/AgentTool/forkSubagent.ts:60

typescript 复制代码
export const FORK_AGENT = {
  agentType: 'fork',  // analytics 标识
  whenToUse: 'Implicit fork --- inherits full conversation context.',
  tools: ['*'],              // 继承父代理所有工具
  maxTurns: 200,
  model: 'inherit',          // 继承父代理模型
  permissionMode: 'bubble',  // 权限请求冒泡给父代理
  source: 'built-in',
  getSystemPrompt: () => '',  // 不使用!由 override 传递已渲染的提示
}

为什么 getSystemPrompt 返回空字符串?

因为 Fork 子代理通过 override.systemPrompt 传递父代理已渲染 的系统提示字节。重新调用 getSystemPrompt() 可能因 GrowthBook(功能开关服务)状态变化而产生不同结果,破坏 Prompt Cache。

5.3.3 buildForkedMessages():Prompt Cache 最大化

源码位置:src/tools/AgentTool/forkSubagent.ts:107

typescript 复制代码
export function buildForkedMessages(
  directive: string,
  assistantMessage: AssistantMessage,
): MessageType[]  // 只返回 2 条新消息!不含父代理历史(历史已在上下文中)

关键设计:函数只返回追加到父代理历史末尾的 2 条消息:

typescript 复制代码
// Step 1:克隆父代理 assistant 消息(含所有 tool_use 块,包括 thinking 和 text)
const fullAssistantMessage = {
  ...assistantMessage,
  uuid: randomUUID(),   // 新 UUID(避免与父代理冲突)
  message: { ...assistantMessage.message, content: [...assistantMessage.message.content] },
}

// Step 2:为每个 tool_use 块创建相同占位符 tool_result
const FORK_PLACEHOLDER_RESULT = 'Fork started --- processing in background'
const toolResultBlocks = toolUseBlocks.map(block => ({
  type: 'tool_result',
  tool_use_id: block.id,
  content: [{ type: 'text', text: FORK_PLACEHOLDER_RESULT }],
  // 所有子代理占位符文本完全相同 → prompt cache 命中
}))

// Step 3:构建最终用户消息([...tool_results, 指令文本])
const toolResultMessage = createUserMessage({
  content: [...toolResultBlocks, { type: 'text', text: buildChildMessage(directive) }],
})

return [fullAssistantMessage, toolResultMessage]  // 只有 2 条
// 完整 API 序列 = parentHistory + [fullAssistantMessage, toolResultMessage]

边界情况 :若 assistantMessage 中没有 tool_use 块(不应发生),直接返回单条包含指令文本的 user message。

5.3.4 buildChildMessage():Fork 工作者指令

源码位置:src/tools/AgentTool/forkSubagent.ts:171

buildChildMessage() 生成 fork 子代理的操作规则,包裹在 <fork-boilerplate> 标签内:

markdown 复制代码
<fork-boilerplate>
STOP. READ THIS FIRST.
你是一个 forked worker process,你不是主代理。

规则(不可违反):
1. 系统提示说"默认 fork"------忽略它,你已经是 fork,不要再 spawn 子代理
2. 不要闲聊、问问题、或建议后续步骤
3. 直接使用工具:Bash、Read、Write 等
4. 修改文件后提交,报告中包含 commit hash
5. 不在工具调用之间输出文本,最后统一报告
6. 严格在指令 scope 内工作
7. 报告不超过 500 字

输出格式(纯文本标签,不用 markdown 标题):
  Scope: <你的工作范围,一句话>
  Result: <关键发现/答案>
  Key files: <相关文件路径>
  Files changed: <修改的文件 + commit hash>
  Issues: <发现的问题(仅有问题时列出)>
</fork-boilerplate>
{FORK_DIRECTIVE_PREFIX}{directive}

这段强制输出格式的设计防止了 fork 子代理将父代理的系统提示("默认 fork 以并行执行")错误地应用到自身。

5.3.5 防止递归 Fork

源码位置:src/tools/AgentTool/forkSubagent.ts:78

typescript 复制代码
// FORK_BOILERPLATE_TAG 从 constants/xml.ts 导入,值为 'fork-boilerplate'
import { FORK_BOILERPLATE_TAG } from '../../constants/xml.js'

export function isInForkChild(messages: MessageType[]): boolean {
  return messages.some(m => {
    if (m.type !== 'user') return false
    const content = m.message.content    // 注意:需要先声明 content
    if (!Array.isArray(content)) return false
    return content.some(
      block => block.type === 'text' && block.text.includes(`<${FORK_BOILERPLATE_TAG}>`)
    )
  })
}

为什么 Fork 子代理仍保留 Agent 工具? 为保证 API 请求前缀中工具定义字节完全一致(prompt cache 需要)。但在调用时通过 isInForkChild() 拒绝递归 fork,而不是从工具池中移除 Agent 工具。

5.3.6 Worktree 隔离的 Fork

源码位置:src/tools/AgentTool/forkSubagent.ts:205

当代理定义中有 isolation: 'worktree' 时,fork 子代理在独立的 git worktree 中运行。此时会在指令中注入 buildWorktreeNotice(),告知子代理:

go 复制代码
你继承了工作在 {parentCwd} 的父代理的对话上下文。
你在 {worktreeCwd} 的隔离 git worktree 中运行------
同一仓库、相同相对文件结构、独立 working copy。
上下文中的路径指向父代理目录;请翻译到你的 worktree 根目录。
如果父代理可能修改了文件,请在编辑前重新读取。
你的修改仅在此 worktree 中,不影响父代理文件。

5.4 Agent Teams:in-process 并发队友

5.4.1 架构概览

css 复制代码
Leader(主代理)
    │
    ├─ TeamCreate({ teammates: ['worker-a', 'worker-b'] })
    │
    ├─ worker-a 在 in-process 中运行(InProcessTeammateTask)
    │    ├─ 独立的 AgentContext(AsyncLocalStorage)
    │    ├─ 独立的消息历史
    │    └─ 通过 mailbox 与 leader 通信
    │
    └─ worker-b 在 in-process 中运行(InProcessTeammateTask)
         ├─ 独立的 AgentContext
         ├─ 独立的消息历史
         └─ 通过 mailbox 与 leader 通信

5.4.2 In-process Runner(核心)

源码位置:src/utils/swarm/inProcessRunner.ts

typescript 复制代码
// 注意:上下文是 runWithTeammateContext(不是 runWithAgentContext)
// TeammateContext 比 AgentContext 多了 teamName、mailbox、permissionMode 等字段

async function runInProcessTeammate(
  identity: TeammateIdentity,
  toolUseContext: ToolUseContext,
) {
  // 1. 克隆文件状态缓存(隔离文件读写状态,防止并发队友互相影响)
  cloneFileStateCache()

  // 2. 为队友创建独立的 AbortController(2 个!)
  const abortController = createAbortController()          // kill 整个队友
  const currentWorkAbortController = createAbortController() // 仅 abort 当前轮次

  // 3. 创建队友专用的 canUseTool 函数(权限路由,见下文)
  const canUseTool = createInProcessCanUseTool(identity, abortController)

  // 4. 在独立的 TeammateContext 中运行(AsyncLocalStorage 隔离)
  return runWithTeammateContext(teammateContext, async () => {
    // 5. 启动队友的 Agent Loop(runAgent 是 async generator)
    for await (const message of runAgent({ agentDefinition: teammate, ... })) {
      // 更新 AppState、写 sidechain 转录、检查 mailbox 消息等
    }
  })
}

5.4.3 Mailbox 通信机制

队友之间(包括与 leader)通过 "邮箱"(mailbox)进行异步通信:

typescript 复制代码
// src/utils/teammateMailbox.ts
readMailbox(teammateId: string): Message[]     // 读取收到的消息
writeToMailbox(teammateId: string, msg): void  // 发送消息

// 消息类型识别
isPermissionResponse(msg): boolean            // 权限批准/拒绝
isShutdownRequest(msg): boolean               // 关闭请求
createIdleNotification(): Message             // 队友完成通知

5.4.4 权限同步(Permission Sync)

源码位置:src/utils/swarm/inProcessRunner.ts:128

当队友的工具调用需要用户确认(behavior === 'ask')时,走以下路由:

arduino 复制代码
Worker 调用工具
  │
  ├─ hasPermissionsToUseTool() 返回 'allow'/'deny' → 直接通过/拒绝
  │
  └─ 返回 'ask'(需要用户确认)
       │
       ├─【主路径】setToolUseConfirmQueue 可用(leader 的 TUI 在线)
       │    → 将权限请求推入 leader 的 ToolUseConfirm 队列
       │    → 显示带 worker badge(名称 + 颜色)的确认对话框
       │    → 等待用户操作,与 leader 自己的工具确认使用相同 UI
       │
       └─【备用路径】TUI bridge 不可用(headless / 后台模式)
            → 通过 mailbox 向 leader 发送权限请求
            → 以 500ms 间隔轮询(PERMISSION_POLL_INTERVAL_MS)
            → 等待 leader 响应后继续

Bash 命令的预处理 :在发送给 leader 前,先用 classifier(BASH_CLASSIFIER feature gate)自动审批,若分类器批准则跳过 leader 确认。这与 leader 的处理不同(leader 是竞速,worker 是串行等待)。

typescript 复制代码
// leaderPermissionBridge.ts 提供的关键函数
getLeaderToolUseConfirmQueue()   // 获取 leader 的 ToolUseConfirm 状态设置函数
getLeaderSetToolPermissionContext() // 获取 leader 的权限上下文

5.5 Coordinator 模式:自主代理编排

源码位置:src/coordinator/coordinatorMode.ts

Coordinator 模式专用于长时间自主运行的任务。通过环境变量开启,与 Fork Subagent 互斥。

typescript 复制代码
// 判断当前是否处于 coordinator 模式
export function isCoordinatorMode(): boolean {
  if (feature('COORDINATOR_MODE')) {
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
  }
  return false
}

// 恢复会话时,自动匹配会话存储的模式(必要时翻转 env var)
export function matchSessionMode(sessionMode: 'coordinator' | 'normal' | undefined): string | undefined

// 向 coordinator 注入 worker 的可用工具列表(coordinator 据此决定分配什么能力给 worker)
export function getCoordinatorUserContext(scratchpadDir?: string): string

注意 :源码中没有 getCoordinatorSystemPrompt() 函数。Coordinator 的系统提示来自 buildEffectiveSystemPrompt() 中的标准路径(coordinator 模式通过 userContext 注入工具列表,不是独立的 prompt 函数)。

与 Fork 的互斥关系

typescript 复制代码
// forkSubagent.ts
export function isForkSubagentEnabled(): boolean {
  if (feature('FORK_SUBAGENT')) {
    if (isCoordinatorMode()) return false  // ← 互斥
    ...
  }
}

Worker 工具集限制src/constants/tools.ts):

typescript 复制代码
// ASYNC_AGENT_ALLOWED_TOOLS 限制 worker 的工具池
// 不包含:TeamCreate/TeamDelete(高级编排工具)、SendMessage、SyntheticOutput
const INTERNAL_WORKER_TOOLS = new Set([
  TEAM_CREATE_TOOL_NAME,
  TEAM_DELETE_TOOL_NAME,
  SEND_MESSAGE_TOOL_NAME,
  SYNTHETIC_OUTPUT_TOOL_NAME,
])
// Worker = ASYNC_AGENT_ALLOWED_TOOLS - INTERNAL_WORKER_TOOLS

5.6 时序图:Fork Subagent 的 Prompt Cache 优化

ini 复制代码
时间 →

父代理历史消息(共享 Prompt Cache):
┌──────────────────────────────────────────┐
│  user: "完成以下三个任务..."              │
│  assistant: [tool_use: agent(task1)]      │
│             [tool_use: agent(task2)]      │
│             [tool_use: agent(task3)]      │
└──────────────────────────────────────────┘
                    │
    buildForkedMessages() 为每个任务创建子代理消息:
                    │
        ┌───────────┼───────────┐
        ▼           ▼           ▼
  子代理1消息:   子代理2消息:  子代理3消息:
  [...共享历史]  [...共享历史]  [...共享历史]
  [assistant]   [assistant]   [assistant]   ← 完全相同
  [user:        [user:        [user:
    placeholder]  placeholder]  placeholder] ← 完全相同
    task1指令]    task2指令]    task3指令]   ← 只有这行不同

  ↑ Prompt Cache 命中 ↑  ↑ 命中 ↑          ↑ 命中 ↑
  只有末尾指令行不同,前缀字节完全相同,最大化缓存命中

关键设计约定总结

约定 说明
runAgentasync function* 不是普通 async function;以 async generator yield Message
buildForkedMessages 返回 2 条 只返回追加的 2 条;完整序列 = parentHistory + 2条
Fork 子代理保留 Agent 工具 为保证 prompt cache,不移除工具;通过 isInForkChild() 在调用时阻断
getCoordinatorSystemPrompt() 不存在 Coordinator 通过 getCoordinatorUserContext() 注入工具列表
权限路由主路径是 leader TUI mailbox 是 headless/后台的 fallback,不是主路径
Teammate 用 runWithTeammateContext 不是 runWithAgentContext;TeammateContext 含更多字段
相关推荐
柯西劝我别收敛2 小时前
K8s Scheduling Framework 解析
后端
于慨2 小时前
mac安装flutter
javascript·flutter·macos
金銀銅鐵2 小时前
[Java] 从 class 文件看 cglib 对 MethodInterceptor 的处理 (下)
java·后端
Walter先生2 小时前
WebSocket 连接池生产级实现:实时行情高可用与负载均衡
后端·websocket·架构
踩着两条虫2 小时前
VTJ.PRO的平台介绍与特性
前端·架构·ai编程
光影少年3 小时前
前端工程化升级
前端·javascript·react.js·前端框架
Hello--_--World3 小时前
节流 VS 防抖 相关知识点与面试题
前端·javascript
We་ct3 小时前
AI辅助开发术语体系深度剖析
开发语言·前端·人工智能·ai·ai编程
去伪存真3 小时前
Superpowers 从“调教提示词”转向“构建工程规范”
前端·agent