子 Agent 设计:分身术与团队协作

一句话理解

想象你是一个团队 leader。遇到一个大项目,你不会自己一个人干——你会派出几个队员分头去查资料、写代码、做测试。每个队员带着你给的指令出发,完成后向你汇报结果。

Claude Code 的子 Agent 就是这样的"分身"机制。主 Agent 可以派出多个子 Agent,它们各自独立工作,完成后把结果交回来。

整体架构

┌─────────────────────────────────────────────┐ │ 主 Agent (Main Session) │ │ │ │ 用户: "调研这个项目的架构,然后写个新模块" │ │ │ │ 主 Agent 决定:拆成两步 │ │ │ │ ┌──────────────┐ ┌───────────────────┐ │ │ │ 子Agent A │ │ 子Agent B │ │ │ │ (Explore) │ │ (General Purpose) │ │ │ │ │ │ │ │ │ │ 只读工具: │ │ 全部工具: │ │ │ │ Read,Grep, │ │ Read,Write,Edit, │ │ │ │ Glob,Bash │ │ Bash,Grep... │ │ │ │ │ │ │ │ │ │ 结果:"项目用 │ │ 结果:"已创建新 │ │ │ │ React+Node" │ │ 模块 src/new/" │ │ │ └──────┬───────┘ └────────┬──────────┘ │ │ │ │ │ │ └──────┬─────────────┘ │ │ ▼ │ │ 汇总结果,回复用户 │ └─────────────────────────────────────────────┘

Agent 类型系统

Claude Code 内置了多种"专业角色"的 Agent:

类型职责可用工具模型
General Purpose通用任务全部工具继承父级
Explore快速搜代码Read, Grep, Glob, Bash(只读)Haiku(更快更省)
Plan制定计划只读工具继承父级
Fork分身(隐式)父级完全相同的工具继承父级

Agent 的定义结构

每个 Agent 类型都是一个定义对象:

// src/tools/AgentTool/loadAgentsDir.ts (lines 106-165)
type AgentDefinition = {
  agentType: string          // "Explore", "Plan" 等
  whenToUse: string          // 什么时候该用这个 Agent
  tools?: string[]           // 允许使用的工具列表,['*'] 代表全部
  disallowedTools?: string[] // 明确禁止的工具
  model?: string             // 'haiku', 'sonnet', 'opus', 'inherit'
  maxTurns?: number          // 最大执行轮数
  permissionMode?: string    // 'bubble' = 权限弹窗转发给父级
  omitClaudeMd?: boolean     // 是否跳过加载 CLAUDE.md(省 token)
}

Explore Agent 的定义示例:

// src/tools/AgentTool/built-in/exploreAgent.ts (lines 13-83)
{
  agentType: 'Explore',
  tools: ['Read', 'Glob', 'Grep', 'Bash'],   // 只给只读工具
  disallowedTools: ['Agent', 'Write', 'Edit'], // 禁止写和嵌套
  model: 'haiku',                              // 用最快的小模型
  omitClaudeMd: true,                         // 省掉项目说明(省 token)
}

比喻:Explore Agent 就像一个"侦察兵"——只带望远镜(只读工具),跑得快(Haiku 模型),轻装上阵(不带 CLAUDE.md 背包)。

子 Agent 的生命周期

创建与执行

当主 Agent 决定派出子 Agent 时,调用 AgentTool

// 主 Agent 的视角:调用 AgentTool
Agent({
  description: "搜索数据库相关代码",
  prompt: "在项目中找到所有数据库相关的文件和函数",
  subagent_type: "Explore"  // 指定类型
})

内部执行流程:

AgentTool.call() │ ├─ 解析 agent 定义 ├─ 筛选可用工具 ├─ 拼装 system prompt │ ▼ ┌──────────────────────────────┐ │ 同步执行?还是后台执行? │ ├──────────┬───────────────────┤ │ 同步 │ 后台 │ │ │ │ │ runAgent() 直接执行 │ │ 阻塞等待结果 registerAsyncAgent() │ 立即返回结果 │ 创建后台任务 │ │ │ 返回 task ID │ │ │ 结果通过通知送达 │ └──────────┴───────────────────┘

runAgent() 详解

// src/tools/AgentTool/runAgent.ts (lines 248-329)
async function* runAgent({
  agentDefinition,    // Agent 类型定义
  promptMessages,     // 初始消息
  toolUseContext,     // 父级的上下文
  canUseTool,         // 权限检查函数
  isAsync,            // 是否后台执行
  availableTools,     // 预计算的工具池
  worktreePath?,      // Git worktree 隔离路径
}) {
  // 1. 生成唯一 Agent ID
  const agentId = generateAgentId()

  // 2. 准备上下文(克隆文件缓存、设置工作目录)
  const agentContext = createSubagentContext(parentContext, {
    agentId,
    agentType: agentDefinition.agentType,
    // 同步 Agent 共享父级的 AbortController
    // 异步 Agent 使用独立的 AbortController
    abortController: isAsync ? new AbortController() : parentAbortController,
  })

  // 3. 进入 query() 循环(和主 Agent 一样的循环!)
  for await (const message of query({
    messages: promptMessages,
    systemPrompt: agentSystemPrompt,
    tools: filteredTools,
    // ...
  })) {
    yield message  // 把每条消息传回给调用者
  }
}

关键发现:子 Agent 内部跑的也是同一个 query() 循环!这是一个递归结构——Agent 调用 Agent Tool,Agent Tool 里面又跑一个完整的 Agent Loop。

同步 vs 异步 vs Fork

三种执行模式的对比:

同步执行 异步执行 Fork(分身) ──────── ──────── ──────── 主Agent 主Agent 主Agent │ │ │ ├─ 派出子Agent ├─ 派出子Agent ├─ fork 分身 │ │ │ │ │ │ │ │ │ 执行中... │ ▼ │ ▼ ▼ │ │ │ 继续干别的事 │ 分身1 分身2 │ │ │ │ │ │ │ ◄─ 结果返回 │ │ │ │ │ │ ◄─ 收到通知 │ │ │ ▼ ▼ ▼ ▼ ▼ 继续后续工作 处理通知中的结果 汇总所有分身的结果

Fork 分身机制

当你调用 Agent() 时不指定 subagent_type,系统会创建一个 Fork(分身)

// src/tools/AgentTool/forkSubagent.ts (lines 42-71)
const FORK_AGENT = {
  agentType: 'fork',
  tools: ['*'],           // 继承父级完全相同的工具
  permissionMode: 'bubble', // 权限弹窗转发给父级终端
  model: 'inherit',        // 使用和父级一样的模型
}

Fork 的最大特点是共享上下文 + 缓存优化

父 Agent 的消息历史: [系统提示] [用户消息1] [助手回复1] [工具结果1] ... ───────────────────────────────────────────── ↑ 这一段完全相同 Fork A: [相同前缀...] [占位符...] [Fork A 的指令] Fork B: [相同前缀...] [占位符...] [Fork B 的指令] Fork C: [相同前缀...] [占位符...] [Fork C 的指令] ↑ 只有这里不同

因为前缀完全一样,API 的提示词缓存可以被所有 Fork 共享,大幅节省费用。

// src/tools/AgentTool/forkSubagent.ts (lines 107-169)
function buildForkedMessages(directive, parentAssistantMessage) {
  // 为父消息中的每个工具调用创建占位结果
  const placeholders = parentMessage.content
    .filter(block => block.type === 'tool_use')
    .map(block => ({
      type: 'tool_result',
      tool_use_id: block.id,
      content: 'Fork started — processing in background'
    }))

  // 所有 Fork 使用相同的占位符(→ 缓存命中)
  // 只有最后的指令文本不同
  return [parentAssistantMessage, [...placeholders, directive]]
}

Agent 间通信:SendMessage

子 Agent 之间可以通过 SendMessage 工具通信:

// src/tools/SendMessageTool/SendMessageTool.ts (lines 67-87)
{
  to: string,       // 接收者:"agent-名字", "*"(广播)
  summary: string,   // 5-10 字摘要
  message: string,   // 消息内容
}

消息路由方式:

SendMessage({ to: "researcher", message: "检查 auth 模块" }) │ ▼ ┌─────────────────────────────────┐ │ to = "agent名字" │──▶ 写入该 Agent 的邮箱文件 │ to = "*" │──▶ 广播给所有团队成员 │ to = "uds:路径" │──▶ Unix Socket 直接通信 └─────────────────────────────────┘

后台 Agent 的结果通知

后台 Agent 完成任务后,会生成一个 XML 格式的通知:

<task-notification>
  <task-id>a8f2x9kq</task-id>
  <status>completed</status>
  <summary>找到了 15 个数据库相关文件</summary>
  <result>数据库层使用 Prisma ORM...</result>
  <usage>
    <total_tokens>12345</total_tokens>
    <tool_uses>8</tool_uses>
    <duration_ms>5200</duration_ms>
  </usage>
</task-notification>

这个通知会被插入到主 Agent 的消息队列中,在下一轮循环时被处理。

工具过滤:每种 Agent 能用什么

不同类型的 Agent 有不同的工具访问权限:

// src/tools/AgentTool/agentToolUtils.ts (lines 70-116)
function filterToolsForAgent({ tools, isAsync, permissionMode }) {
  // 所有 Agent 都不能用的工具
  const ALWAYS_BLOCKED = ['TaskStop', 'TeamCreate', 'TeamDelete']

  // 后台异步 Agent 只能用这些
  const ASYNC_ALLOWED = [
    'Bash', 'Read', 'Write', 'Glob', 'Grep',
    'FileEdit', 'Agent', 'SendMessage',
    'Skill', 'WebFetch', 'WebSearch',
  ]

  if (isAsync) {
    return tools.filter(t => ASYNC_ALLOWED.includes(t.name))
  }
  return tools.filter(t => !ALWAYS_BLOCKED.includes(t.name))
}

比喻:就像一个公司的权限体系。实习生(Explore Agent)只能看文档;正式员工(General Purpose)能编辑文件;但谁都不能删除团队(TeamDelete)。

Coordinator 模式:多 Agent 编排

当开启 Coordinator 模式时,主 Agent 变成了纯粹的"指挥官":

┌──────────────────────────────────────────┐ │ Coordinator(指挥官) │ │ │ │ 不亲自执行任务,只负责: │ │ 1. 分析用户需求 │ │ 2. 拆分成子任务 │ │ 3. 派出 Worker(工人) │ │ 4. 汇总 Worker 的结果 │ │ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │Worker│ │Worker│ │Worker│ │ │ │ A │ │ B │ │ C │ │ │ │ │ │ │ │ │ │ │ │搜代码 │ │改文件 │ │跑测试 │ │ │ └──┬──┘ └──┬──┘ └──┬──┘ │ │ │ │ │ │ │ └────────┼────────┘ │ │ ▼ │ │ 汇总+综合 │ └──────────────────────────────────────────┘

Agent Memory:子 Agent 的记忆

子 Agent 也有自己的持久化记忆系统:

// src/tools/AgentTool/agentMemory.ts

// 三个记忆范围
User 级别:   ~/.claude/agent-memory/       // 跨项目通用
Project 级别: .claude/agent-memory/         // 项目共享(可提交到 Git)
Local 级别:   .claude/agent-memory-local/   // 本地私有

防止递归炸弹

子 Agent 可以再调用 Agent Tool 来创建孙 Agent。为了防止无限递归,系统有几道限制:

  1. Fork 子代不能再 Fork:检测消息中是否有 Fork 标记
  2. 异步 Agent 工具列表受限:后台 Agent 的工具池更小
  3. maxTurns 限制:每个 Agent 有最大轮数限制
  4. Token Budget 限制:每个 Agent 有 token 预算
// src/tools/AgentTool/forkSubagent.ts (lines 78-89)
function isInForkChild(messages) {
  // 检查消息中是否有 Fork 标记
  // 如果是 Fork 子代,禁止再次 Fork
  return messages.some(m => hasForkBoilerplateTag(m))
}

小结

子 Agent 设计的核心思想是分治 + 隔离

  1. 分治:大任务拆成小任务,每个子 Agent 专注一件事
  2. 类型化:不同类型的 Agent 有不同的能力(工具集、模型、权限)
  3. 隔离:每个子 Agent 有独立的消息历史和 AbortController
  4. 通信:通过 SendMessage 和 task-notification 机制传递结果
  5. 缓存共享:Fork 机制通过共享消息前缀来最大化缓存命中
  6. 递归保护:多层限制防止无限嵌套