芥末
发布于 2026-05-09 / 0 阅读
0
0

Claude Code 多 Agent 机制解析:Subagent、Fork 与 Coordinator

Claude Code 的多 Agent 设计并不是简单地“主 Agent 再启动几个子 Agent”。它真正解决的是一组工程问题:

  • 子 Agent 能不能访问父 Agent 的所有工具?
  • 子 Agent 读取或修改状态时,会不会污染父 Agent?
  • 父 Agent 和子 Agent 之间是同步函数调用,还是异步消息通信?
  • 子 Agent 的系统提示词很长时,怎么减少 Token 成本和延迟?
  • 多个 Worker 并行执行时,谁负责拆任务、收结果、合成最终答案?

围绕这些问题,Claude Code 里可以抽象出三套机制:

机制解决的问题系统形态
常规 Subagent把局部任务交给独立子 Agent 执行父子结构
Fork Subagent复用父 Agent 的 Prompt 缓存,降低成本和延迟缓存友好的父子结构
Coordinator 模式大任务拆给多个 Worker 并行执行协调者 + Worker

整体关系可以概括成下面这张图:

flowchart TB
    A[Claude Code Multi-Agent 机制] --> B[常规 Subagent]
    A --> C[Fork Subagent]
    A --> D[Coordinator 模式]

    B --> B1[工具隔离]
    B --> B2[上下文隔离]
    B --> B3[异步消息通信]

    C --> C1[复用父 Agent Prompt 前缀]
    C --> C2[命中 Prompt Cache]
    C --> C3[降低输入 Token 成本和首 Token 延迟]

    D --> D1[主 Agent 只做协调]
    D --> D2[Worker 并行执行]
    D --> D3[协调者合成结果]

Multi-Agent 要解决什么问题

一个基础 Agent 通常由三部分组成:

flowchart LR
    U[用户任务] --> L[LLM 大语言模型]
    L --> T[工具调用]
    T --> E[执行环境]
    E --> R[观察结果]
    R --> L
    L --> O[最终回答]

LLM(大语言模型)负责思考,工具负责执行,循环负责持续推进任务。这个模型在小任务里很好用,但在真实软件工程场景里很快会遇到瓶颈。

比如给 Agent 一个任务:

调研 React 18 的新特性,在项目里实现一个 useTransition 示例,并完成代码审查。

这个任务至少包含三类工作:

工作需要的上下文合适角色
调研 React 18官方文档、示例、社区资料研究员
修改项目代码项目结构、组件代码、构建配置工程师
审查实现质量改动 diff、潜在 bug、边界条件Reviewer

如果所有事情都压给一个 Agent,会出现三个典型问题。

问题表现
上下文膨胀调研资料、项目代码、审查记录全塞进同一个上下文,Token 消耗快速增长
职责混乱同一个 Agent 一会儿调研、一会儿写代码、一会儿审查,容易在任务阶段之间跳来跳去
无法并行调研文档时不能同时分析项目代码,写代码时不能同时准备审查清单

Multi-Agent 的核心思路是把一个大任务拆成多个职责清晰的小任务,让不同 Agent 在隔离环境里执行,再通过消息或共享状态把结果汇总回来。

常见的 Multi-Agent 形态可以分成三类:

形态说明适合场景
父子型主 Agent 遇到子问题时启动 Subagent,拿到结果后继续执行局部调研、代码搜索、一次性分析
平级协作型多个 Agent 地位相近,通过共享状态或消息协作需要复杂协商的任务,但工程实现难度较高
Coordinator-Worker 型协调者只负责拆任务、派 Worker、收结果、合成答案大规模并行调研、迁移、验证

Claude Code 的常规 Subagent 对应父子型;Coordinator 模式对应 Coordinator-Worker 型;Fork Subagent 则是父子型里的缓存优化版本。

常规 Subagent:独立执行单元

在 Claude Code 里,Subagent 可以理解为主 Agent 通过特定工具派出去的独立执行单元。它不是一个普通函数,而是一个拥有自己上下文、工具集合和生命周期的 Agent 实例。

典型流程如下:

flowchart LR
    A[主 Agent] -->|调用 Agent/Task 工具| B[创建 Subagent]
    B --> C[分配工具集合]
    B --> D[创建隔离上下文]
    C --> E[Subagent 独立运行]
    D --> E
    E --> F[生成结果]
    F -->|通知| A

Subagent 机制最重要的不是“能启动另一个 Agent”,而是启动之后如何隔离。隔离做不好,子 Agent 会污染父 Agent 的状态,或者越权调用不该用的工具。

Claude Code 主要做了两层隔离:

  1. 工具隔离:不同类型的 Agent 拿到不同工具集合。
  2. 上下文隔离:运行时状态按字段决定克隆、共享、屏蔽或新建。

工具隔离:给不同 Agent 分配不同工具箱

主 Agent 通常能使用很多工具:读文件、写文件、执行命令、发起子任务、向用户提问、管理任务列表等。不能把这些工具原封不动交给 Subagent。

原因很直接:

  • 如果子 Agent 也能继续派子 Agent,就可能形成无限递归。
  • 如果子 Agent 能向用户提问,会抢走主 Agent 的对话权。
  • 如果子 Agent 能修改主 Agent 的待办列表,会污染主流程状态。
  • 如果后台 Agent 能用交互型工具,执行过程会卡在无人响应的交互上。

Claude Code 的工具过滤可以抽象成三道门:

flowchart TB
    A[父 Agent 的完整工具集合] --> B{MCP 工具?}
    B -->|是| K[保留]
    B -->|否| C{全体 Subagent 黑名单?}
    C -->|是| X[移除]
    C -->|否| D{自定义 Agent 黑名单?}
    D -->|是且非内置 Agent| X
    D -->|否| E{异步后台 Agent?}
    E -->|否| K
    E -->|是| F{异步白名单内?}
    F -->|是| K
    F -->|否| X

    K --> G[Subagent 可用工具集合]

这里的 MCP 指 MCP(Model Context Protocol,模型上下文协议)工具。Claude Code 对 MCP 工具采用了特殊放行策略,其他工具则按黑名单或白名单过滤。

简化后的 TypeScript 逻辑如下:

type Tool = {
  name: string
}

function filterToolsForAgent(options: {
  tools: Tool[]
  isBuiltIn: boolean
  isAsync: boolean
}): Tool[] {
  const { tools, isBuiltIn, isAsync } = options

  return tools.filter(tool => {
    // MCP 工具单独放行
    if (tool.name.startsWith("mcp__")) {
      return true
    }

    // 所有 Subagent 都不能用的工具
    if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) {
      return false
    }

    // 用户自定义 Agent 再加一层限制
    if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) {
      return false
    }

    // 异步后台 Agent 只允许使用白名单里的工具
    if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) {
      return false
    }

    return true
  })
}

这套设计的关键点是:权限不是按“是不是 Agent”粗暴区分,而是按 Agent 类型、运行模式、工具风险分级处理。

上下文隔离:不能全共享,也不能全新建

工具隔离解决“能做什么”,上下文隔离解决“运行时状态怎么处理”。

父 Agent 运行时会维护很多状态,例如:

  • 文件读取缓存:某个文件是否读过、读到第几行。
  • 全局 UI 状态:界面展示、任务列表、进度信息。
  • 中止信号:用户是否按下 Ctrl+C。
  • 权限模式:当前是否允许执行命令或修改文件。
  • 任务注册表:后台进程、异步任务的状态。
  • Agent 身份信息:Agent ID、嵌套深度、链路追踪 ID。

如果把父上下文完整共享给子 Agent,会发生状态污染。比如父 Agent 读过 file.ts 前 100 行,子 Agent 接着读到 200 行,如果两者共享文件读取缓存,父 Agent 之后可能误以为自己也读过前 200 行。

如果给子 Agent 一个完全空的新上下文,也会出问题。用户中止任务时,子 Agent 可能收不到中止信号;子 Agent 启动的后台进程也可能无法登记到全局任务表,最后变成没人管理的孤儿任务。

Claude Code 的做法是按字段语义分别决策:

状态字段处理方式原因
文件读取缓存克隆一份防止子 Agent 改变父 Agent 对文件读取进度的认知
全局 UI 写入屏蔽为空操作防止异步 Agent 与主线程同时修改 UI 状态
任务注册通路保留子 Agent 启动的后台任务需要登记和回收
Agent ID新建每个 Agent 必须可追踪
嵌套深度父深度 + 1防止递归嵌套失控
中止信号、权限信息按需共享子 Agent 必须感知外部控制和权限边界

可以用一张流程图理解:

flowchart LR
    A[父 Agent 上下文] --> B{字段语义判断}

    B --> C[克隆]
    B --> D[共享]
    B --> E[屏蔽]
    B --> F[新建]

    C --> C1[文件读取缓存]
    D --> D1[中止信号 / 权限状态 / 任务注册通路]
    E --> E1[全局 UI 写入]
    F --> F1[Agent ID / Trace ID / 深度计数]

    C1 --> G[Subagent 上下文]
    D1 --> G
    E1 --> G
    F1 --> G

简化后的上下文创建逻辑如下:

function createSubagentContext(
  parentContext: ToolUseContext,
  overrides?: Partial<ToolUseContext>
): ToolUseContext {
  return {
    ...parentContext,

    // 文件读取状态克隆,避免污染父 Agent
    readFileState: cloneFileStateCache(parentContext.readFileState),

    // 子 Agent 不能直接修改全局 UI 状态
    setAppState: () => {},

    // 但任务注册通路要保留,否则后台任务无法被管理
    setAppStateForTasks:
      parentContext.setAppStateForTasks ?? parentContext.setAppState,

    // 每个 Agent 都有独立身份
    agentId: overrides?.agentId ?? createAgentId(),

    // 嵌套深度递增,方便限制递归
    queryTracking: {
      chainId: randomUUID(),
      depth: (parentContext.queryTracking?.depth ?? -1) + 1,
    },
  }
}

真正重要的原则是:上下文隔离不是“全部共享”或“全部隔离”的二选一,而是逐字段判断这个状态对子 Agent 是否必要、是否会反向影响父 Agent。

父子 Agent 通信:用消息队列,不用同步函数调用

Subagent 启动之后,父子之间需要通信。最直觉的设计是父 Agent 调一个函数,然后等待子 Agent 返回结果:

const result = await runSubagent(prompt)

这个模型简单,但很快会遇到问题:

  • 子 Agent 如果跑 5 分钟,父 Agent 就会被阻塞 5 分钟。
  • 父 Agent 想同时派 5 个子 Agent,需要额外管理并发、取消、超时、错误恢复。
  • 子 Agent 已经完成后,如果父 Agent 想继续给它补充指令,函数返回模型很难处理。

Claude Code 采用消息驱动模型。每个 Subagent 都有一份任务状态,里面包含 Agent ID、状态、结果、进度和待处理消息队列。

简化类型如下:

type TaskStatus =
  | "pending"
  | "running"
  | "completed"
  | "failed"
  | "killed"

type LocalAgentTaskState = {
  type: "local_agent"
  agentId: string
  prompt: string
  agentType: string
  status: TaskStatus
  result?: AgentToolResult
  progress?: AgentProgress
  isBackgrounded: boolean

  // 父 Agent 发给子 Agent 的消息会先进入这里
  pendingMessages: string[]

  // 子 Agent 的对话历史
  messages?: Message[]
}

pendingMessages 就是子 Agent 的收件箱。

父 Agent 给子 Agent 发消息

父 Agent 向子 Agent 发送消息时,并不会直接打断子 Agent 的执行,而是把消息追加到目标任务状态的 pendingMessages 数组里。

function queuePendingMessage(
  taskId: string,
  message: string,
  setAppState: SetAppState
): void {
  updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => ({
    ...task,
    pendingMessages: [...task.pendingMessages, message],
  }))
}

子 Agent 每轮工具调用结束后,会检查自己的收件箱。如果有新消息,就把这些消息注入自己的对话历史,再进入下一轮 LLM 调用。

时序如下:

sequenceDiagram
    participant P as 父 Agent
    participant S as SendMessage 工具
    participant T as 任务状态表
    participant C as 子 Agent

    P->>S: 给 agent-a1b 发送补充指令
    S->>T: pendingMessages 追加消息
    S-->>P: 发送成功,立即返回

    C->>T: 当前工具调用结束,检查收件箱
    T-->>C: 返回 pendingMessages
    C->>C: 将消息作为用户输入注入对话历史
    C->>C: 进入下一轮 LLM 调用

如果子 Agent 已经完成或被停止,Claude Code 还可以从保存的 transcript 恢复它的对话历史,再把新消息拼进去重新启动。这样子 Agent 不是一次性对象,而是可以被唤醒继续工作的执行单元。

简化逻辑如下:

async function sendMessageToAgent(input: {
  agentId: string
  message: string
}) {
  const task = appState.tasks[input.agentId]

  if (isLocalAgentTask(task) && task.status === "running") {
    queuePendingMessage(
      input.agentId,
      input.message,
      context.setAppStateForTasks
    )

    return {
      success: true,
      message: "message queued",
    }
  }

  // 已停止的 Agent 可以从 transcript 恢复后继续运行
  return resumeAgentBackground({
    agentId: input.agentId,
    prompt: input.message,
  })
}

子 Agent 给父 Agent 发通知

子 Agent 完成任务后,需要把结果交给父 Agent。Claude Code 没有把完成事件设计成复杂的内部对象,而是把结果包装成一段 XML 文本,再作为一条消息注入父 Agent 的对话历史。

通知格式大致如下:

<task-notification>
  <task-id>agent-a1b</task-id>
  <output-file>/tmp/agent-a1b-output.txt</output-file>
  <status>completed</status>
  <summary>认证模块调研已完成</summary>
  <result>
    发现 token 校验逻辑在 src/auth/validate.ts:42
    没有处理 null session,可能导致异常。
  </result>
  <usage>
    <total_tokens>12345</total_tokens>
    <tool_uses>8</tool_uses>
    <duration_ms>34567</duration_ms>
  </usage>
</task-notification>

这个设计有三个好处:

设计点好处
XML 是纯文本可以直接进入对话历史,不需要额外定义复杂事件协议
XML 有清晰结构LLM 能稳定识别 task-id、status、result 等字段
通知被包装成消息父 Agent 可以复用现有 agentic loop,不需要额外状态机

生成通知的逻辑本质上是拼接字符串,然后把它放入父 Agent 的待处理消息队列:

function buildTaskNotification(task: LocalAgentTaskState): string {
  return `
<task-notification>
  <task-id>${task.agentId}</task-id>
  <status>${task.status}</status>
  <summary>${escapeXml(task.progress?.summary ?? "")}</summary>
  <result>${escapeXml(task.result?.content ?? "")}</result>
</task-notification>`
}

enqueuePendingNotification({
  value: buildTaskNotification(task),
  mode: "task-notification",
})

父子通信整体可以概括为:

flowchart LR
    P[父 Agent] -->|SendMessage| Q1[子 Agent pendingMessages]
    Q1 -->|循环边界读取| C[子 Agent]

    C -->|完成任务| X[task-notification XML]
    X --> Q2[父 Agent 待处理消息队列]
    Q2 -->|下一轮循环处理| P

Auto-background:长任务自动转后台

常规 Subagent 刚启动时,可以像一次普通工具调用一样在前台运行。问题是有些任务会跑很久,例如大型代码搜索、迁移分析、测试修复等。

Claude Code 的处理方式是设置自动后台化阈值:

  • 如果 Subagent 很快完成,父 Agent 直接拿结果继续执行。
  • 如果 Subagent 超过阈值仍未完成,就把它转成后台任务。
  • 后台任务完成后,通过 task-notification 通知父 Agent。

阈值逻辑可以抽象成:

function getAutoBackgroundMs(): number {
  if (
    isEnvTruthy(process.env.CLAUDE_AUTO_BACKGROUND_TASKS) ||
    getFeatureValue("tengu_auto_background_agents", false)
  ) {
    return 120_000 // 2 分钟
  }

  return 0
}

这个机制把“同步等待”自动降级为“异步通知”,让父 Agent 不必一直被长任务占住。

flowchart TB
    A[启动 Subagent] --> B{是否在阈值内完成?}
    B -->|是| C[前台返回结果]
    B -->|否| D[转后台执行]
    D --> E[父 Agent 继续处理其他任务]
    D --> F[Subagent 完成]
    F --> G[发送 task-notification]
    G --> H[父 Agent 处理结果]

Fork Subagent:为 Prompt Cache 设计的分身

常规 Subagent 能解决任务拆分问题,但还有一个隐藏成本:系统提示词很长。

Claude Code 的系统提示词可能包含大量内容:

  • 工具说明。
  • 行为规范。
  • 当前项目上下文。
  • 用户配置。
  • 环境信息。
  • 历史对话前缀。

每次启动一个拥有独立系统提示词的 Subagent,API 都需要处理一大段输入 Token。Subagent 调用越频繁,成本和延迟越明显。

Anthropic 的 Prompt Cache 可以缓解这个问题。它的核心规则是:如果请求前缀和之前请求的前缀完全一致,就可以命中缓存,减少输入 Token 成本并降低首 Token 延迟。

关键在于“完全一致”。这里不是语义相同,也不是字符串大致相似,而是字节级一致。一个空格、工具顺序、动态字段差异,都可能导致缓存失效。

Fork Subagent 就是为这个规则设计的。

Fork 的核心思路

Fork Subagent 不是创建一个拥有独立提示词的专业 Agent,而是创建一个“继承父 Agent 请求前缀的分身”。它要尽量保证 API 请求前缀和父 Agent 已经缓存过的前缀一致。

需要对齐的内容包括:

内容为什么影响缓存
system prompt请求前缀的主体
user context会拼进消息前缀
system context会拼进系统上下文
工具池定义和顺序工具 schema 会被序列化进请求
对话历史前缀Fork 从哪一轮消息分叉,会影响请求字节

Claude Code 用一个“缓存安全参数”对象把这些字段打包:

type CacheSafeParams = {
  // 必须直接复用父 Agent 已渲染好的系统提示词
  systemPrompt: SystemPrompt

  // 拼在消息前的用户上下文
  userContext: Record<string, string>

  // 系统环境上下文
  systemContext: Record<string, string>

  // 工具集合、模型、权限等运行时信息
  toolUseContext: ToolUseContext

  // 父 Agent 的消息前缀
  forkContextMessages: Message[]
}

流程如下:

flowchart LR
    A[父 Agent 请求前缀] --> B[Prompt Cache]
    A --> C[Fork Subagent]
    C --> D[复用相同 system prompt]
    C --> E[复用相同工具定义]
    C --> F[复用相同对话前缀]
    D --> G[字节级一致请求前缀]
    E --> G
    F --> G
    G --> B
    B --> H[缓存命中]

Fork 的 system prompt 为什么返回空字符串

Fork Subagent 的定义里有一个容易误解的点:它自己的 getSystemPrompt 可以返回空字符串。

简化定义如下:

const FORK_AGENT = {
  agentType: "fork",
  tools: ["*"],
  maxTurns: 200,
  model: "inherit",
  permissionMode: "bubble",
  source: "built-in",

  // Fork 不靠这个函数生成系统提示词
  getSystemPrompt: () => "",
}

这不是说 Fork Subagent 没有系统提示词,而是它不重新生成系统提示词。它直接复用父 Agent 已经渲染好的那份字节。

如果重新调用生成函数,哪怕逻辑相同,也可能因为动态配置、功能开关、上下文顺序等原因产生细微差异。一旦字节不同,缓存就无法命中。复用父 Agent 已渲染结果是更稳的做法。

Fork 适合什么任务

Fork 适合“需要父 Agent 完整上下文,但又不希望污染主循环”的任务。

例如:

  • 基于当前完整对话生成 PR 描述。
  • 基于刚完成的一轮操作做总结。
  • 尝试某个分支思路,但不想影响父 Agent 的主对话状态。
  • 利用当前上下文做轻量分析。

不适合 Fork 的场景也很明确:

场景更适合的机制原因
专门做代码搜索常规 Subagent只需要只读工具和搜索提示词,不需要继承父 Agent 全量上下文
专门做规划常规 Subagent需要定制角色和系统提示词
大规模并行迁移Coordinator 模式需要协调者统一拆分和合成
Worker 之间要被统一调度Coordinator 模式Fork 不是团队调度机制

Fork 与 Coordinator 通常互斥。Coordinator 模式下,主 Agent 已经是协调者,Worker 默认异步执行,不需要再用 Fork 做“轻量分身”。

function isForkSubagentEnabled(): boolean {
  if (!feature("FORK_SUBAGENT")) {
    return false
  }

  if (isCoordinatorMode()) {
    return false
  }

  if (getIsNonInteractiveSession()) {
    return false
  }

  return true
}

Coordinator 模式:主 Agent 退化成协调者

常规 Subagent 适合“主 Agent 干活,子 Agent 帮忙”。如果任务天然可以拆成很多独立部分,比如并行调研十个模块、批量迁移多个包、同时验证多个测试场景,父子型就不够了。

Coordinator 模式把主 Agent 的职责改成:

  1. 拆任务。
  2. 派 Worker。
  3. 收集 Worker 结果。
  4. 理解结果并做决策。
  5. 合成最终输出。

主 Agent 不再亲自读代码、改代码、跑测试,而是通过 Worker 完成这些工作。

flowchart TB
    U[用户目标] --> C[Coordinator Agent]

    C --> W1[Worker 1: 调研 auth 模块]
    C --> W2[Worker 2: 调研 session 模块]
    C --> W3[Worker 3: 调研 token 模块]

    W1 --> R1[调研结果 1]
    W2 --> R2[调研结果 2]
    W3 --> R3[调研结果 3]

    R1 --> C
    R2 --> C
    R3 --> C

    C --> S[合成实现规格]
    S --> W4[Worker 4: 实现修改]
    S --> W5[Worker 5: 独立验证]

    W4 --> C
    W5 --> C
    C --> O[最终答复]

Coordinator 模式不是默认开启,通常需要功能开关和环境变量同时满足:

function isCoordinatorMode(): boolean {
  if (feature("COORDINATOR_MODE")) {
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
  }

  return false
}

Coordinator 的内部工具

协调者需要一组专门管理 Worker 的工具。典型能力包括:

工具能力作用
创建 Worker 或团队批量启动执行单元
删除团队清理不再需要的 Worker 组
发送消息给已有 Worker 补充指令
合成输出把最终结果交给用户
停止 Worker发现方向错误时及时止损

简化定义如下:

const INTERNAL_WORKER_TOOLS = new Set([
  "team_create",
  "team_delete",
  "send_message",
  "synthetic_output",
])

这些工具只给协调者使用,Worker 不应该继续使用团队管理工具。否则 Worker 还能再派 Worker,系统会变成递归树,难以控制成本和执行边界。

flowchart TB
    C[Coordinator] -->|允许| T1[创建 Worker]
    C -->|允许| T2[给 Worker 发消息]
    C -->|允许| T3[合成最终输出]
    C -->|允许| T4[停止 Worker]

    W[Worker] -->|禁止| T1
    W -->|禁止| T3
    W -->|允许| A[读文件 / 搜索 / 编辑 / 执行命令]

Coordinator 的并行执行模型

Coordinator 模式真正有价值的地方在并行。

如果一个任务能拆成三个互不依赖的模块调研,串行执行会是:

sequenceDiagram
    participant C as Coordinator
    participant W1 as Worker 1
    participant W2 as Worker 2
    participant W3 as Worker 3

    C->>W1: 调研 auth
    W1-->>C: 返回结果
    C->>W2: 调研 session
    W2-->>C: 返回结果
    C->>W3: 调研 token
    W3-->>C: 返回结果

并行执行则是:

sequenceDiagram
    participant C as Coordinator
    participant W1 as Worker 1
    participant W2 as Worker 2
    participant W3 as Worker 3

    C->>W1: 调研 auth
    C->>W2: 调研 session
    C->>W3: 调研 token

    W2-->>C: 返回 session 结果
    W1-->>C: 返回 auth 结果
    W3-->>C: 返回 token 结果

    C->>C: 汇总、判断、制定后续方案

Coordinator 要避免把可以并行的工作串行化。一次 LLM 回复里可以生成多个工具调用,底层并发启动多个 Worker。这样总耗时接近最慢的那个 Worker,而不是所有 Worker 时间之和。

Coordinator 的任务流水线

一个软件工程任务在 Coordinator 模式下通常会被拆成四个阶段:

阶段执行者目标
调研多个 Worker 并行找相关文件、理解代码路径、定位风险点
合成Coordinator阅读所有结果,形成统一判断和实现规格
实现Worker按协调者给出的明确规格修改代码
验证新 Worker用相对独立的视角检查改动和运行测试

中间的“合成”必须由 Coordinator 亲自完成。协调者如果只是把 Worker A 的发现转发给 Worker B,就会退化成消息中转站。合格的 Coordinator 要读懂多个 Worker 的输出,识别冲突、过滤无关信息,再把实现要求整理成明确规格。

正确方式:

flowchart LR
    A[Worker 调研结果] --> C[Coordinator 理解和合成]
    B[另一个 Worker 调研结果] --> C
    C --> D[明确实现规格]
    D --> E[Worker 按规格实现]

错误方式:

flowchart LR
    A[Worker 调研结果] --> C[Coordinator]
    C -->|原样转发| E[Worker 实现]

这一区别很关键。Multi-Agent 系统不是“多个 Agent 聊天”,而是“拆分、执行、理解、合成”的工程流水线。

Continue 还是 Spawn:复用旧 Worker,还是新建 Worker

Coordinator 会频繁遇到一个决策:新任务应该交给已有 Worker 继续做,还是启动一个新 Worker?

可以按下面的规则判断:

情况选择原因
新任务和旧 Worker 上下文强相关Continue旧 Worker 已经读过相关文件,继续执行成本更低
新任务与旧上下文无关Spawn避免无关上下文干扰判断
旧 Worker 方向明显走偏Spawn继续复用会放大错误
需要独立验证Spawn写代码的人不适合完全验证自己的改动
只是补充一个小问题Continue发送消息比重新启动更省成本

这个规则和人类团队协作类似:熟悉上下文的人适合继续推进同一块工作,但验证和审查最好换一双新眼睛。

三套机制的对比

常规 Subagent、Fork Subagent、Coordinator 模式各自解决的问题不同,不能混用成一个万能方案。

维度常规 SubagentFork SubagentCoordinator 模式
主 Agent 角色主流程执行者主流程执行者纯协调者
子 Agent 定位专门任务执行者父 Agent 的缓存友好分身Worker
系统提示词通常独立定制复用父 Agent 已渲染前缀Worker 可按任务配置
成本优化常规 API 调用成本尽量命中 Prompt Cache通过并行缩短总时长
通信方式消息队列 + XML 通知类似父子通信协调者统一收发消息
适合场景局部调研、搜索、规划总结、PR 描述、分支尝试大规模并行调研、实现、验证
主要风险上下文隔离和工具权限缓存前缀稍变就失效协调者如果不合成会变成转发器

Multi-Agent 系统的工程原则

Claude Code 的设计可以沉淀成几条通用原则,适合迁移到自建 Agent 系统里。

1. 上下文隔离要按字段粒度做

不要简单地把上下文全共享,也不要给子 Agent 一个完全空上下文。每个状态字段都要单独判断:

  • 子 Agent 是否需要读它?
  • 子 Agent 修改它会不会影响父 Agent?
  • 它是否必须共享才能响应中止、权限、任务回收?
  • 它是否应该新建以便追踪链路?

字段级隔离比整体隔离麻烦,但能避免很多隐蔽 bug。

2. 通信优先走消息模型

同步函数调用适合短任务,不适合长时间运行、可恢复、可并发的 Agent 系统。

更稳的模型是:

  • 父到子:写入子 Agent 的消息队列。
  • 子到父:把完成结果包装成结构化文本消息。
  • 状态变化:落在任务状态表里。
  • 长任务:后台运行,完成后通知。

消息模型天然支持并发、恢复、持久化和异步调度。

3. 工具权限要分级控制

不同 Agent 不应该拿到同一套工具。至少要区分:

Agent 类型工具策略
主 Agent可使用完整工具集合
内置 Subagent去掉高风险工具
自定义 Agent比内置 Agent 更严格
后台异步 Agent只允许白名单工具
Worker禁止团队管理类工具

权限控制不仅是安全问题,也是防递归、防状态污染、防成本失控的基础。

4. 缓存友好也是架构能力

Agent 系统的成本和延迟会直接影响可用性。Prompt Cache 命中条件越严格,架构层越要保证稳定前缀。

Fork Subagent 的思路可以概括为:

  • 不重新生成能复用的系统提示词。
  • 保持工具定义和顺序稳定。
  • 明确哪些字段会影响缓存。
  • 对需要父上下文的轻量任务,优先复用缓存前缀。

能便宜、快速地启动更多执行单元,系统能力边界也会随之扩大。

5. 协调者必须合成,而不是转发

Coordinator 的价值不在于“把消息从 A 传给 B”,而在于理解多个 Worker 的结果,并做出统一判断。

好的协调者会:

  • 发现 Worker 结果之间的冲突。
  • 过滤无关信息。
  • 把模糊发现整理成明确实现规格。
  • 决定哪些任务继续复用旧 Worker,哪些任务新建 Worker。
  • 安排独立验证,而不是让实现者自证正确。

Multi-Agent 系统真正的难点不是“启动多个 Agent”,而是“让多个 Agent 的输出形成一个可控、可验证、可收敛的工程流程”。

总结

Claude Code 的多 Agent 机制可以拆成五个核心设计点:

  • Subagent 不是普通函数,而是拥有独立上下文、工具集合和生命周期的执行单元。
  • 工具隔离通过黑名单、白名单和 Agent 类型控制权限,避免递归派生和越权操作。
  • 上下文隔离按字段粒度处理,既防止状态污染,又保留中止、权限、任务注册等必要通路。
  • 父子通信采用异步消息模型,父到子写消息队列,子到父发送 XML 通知。
  • Fork Subagent 通过复用父 Agent 的字节级请求前缀命中 Prompt Cache,Coordinator 模式通过协调者和 Worker 实现大任务并行。

这套设计的价值在于把 Multi-Agent 从“概念上能跑”推进到“工程上可控”。隔离、权限、通信、缓存和调度都处理好,多 Agent 才能真正承担复杂软件工程任务。


评论