|
1 | 1 | --- |
2 | | -title: "子 Agent 机制 - AI 分身术与任务委派" |
3 | | -description: "深入解析 Claude Code 子 Agent 机制:主 Agent 如何通过 AgentTool 委派子任务,子 Agent 的生命周期管理、工具继承和结果回传。" |
4 | | -keywords: ["子 Agent", "Agent 分身", "任务委派", "AgentTool", "多 Agent"] |
| 2 | +title: "子 Agent 机制 - AgentTool 的执行链路与隔离架构" |
| 3 | +description: "从源码角度解析 Claude Code 子 Agent:AgentTool.call() 的完整执行链路、Fork 子进程的 Prompt Cache 共享、Worktree 隔离、工具池独立组装、以及结果回传的数据格式。" |
| 4 | +keywords: ["子 Agent", "AgentTool", "任务委派", "forkSubagent", "子进程隔离"] |
5 | 5 | --- |
6 | 6 |
|
7 | | -{/* 本章目标:解释子 Agent 机制的设计和应用场景 */} |
| 7 | +{/* 本章目标:从源码角度揭示子 Agent 的完整执行链路、工具隔离、通信协议和生命周期管理 */} |
8 | 8 |
|
9 | | -## 为什么需要子 Agent |
| 9 | +## 执行链路总览 |
10 | 10 |
|
11 | | -有些任务太大,一个 AI 实例忙不过来: |
| 11 | +一条 `Agent(prompt="修复 bug")` 调用的完整路径: |
12 | 12 |
|
13 | | -- "在 5 个不同的文件中分别找到并修复同类 bug" |
14 | | -- "一边重构后端 API,一边更新前端调用" |
15 | | -- "研究这个库的用法,同时修改我们的代码" |
| 13 | +``` |
| 14 | +AI 生成 tool_use: { prompt: "修复 bug", subagent_type: "Explore" } |
| 15 | + ↓ |
| 16 | +AgentTool.call() ← 入口(AgentTool.tsx:239) |
| 17 | + ├── 解析 effectiveType(fork vs 命名 agent) |
| 18 | + ├── filterDeniedAgents() ← 权限过滤 |
| 19 | + ├── 检查 requiredMcpServers ← MCP 依赖验证(最长等 30s) |
| 20 | + ├── assembleToolPool(workerPermissionContext) ← 独立组装工具池 |
| 21 | + ├── createAgentWorktree() ← 可选 worktree 隔离 |
| 22 | + ↓ |
| 23 | +runAgent() ← 核心执行(runAgent.ts:248) |
| 24 | + ├── getAgentSystemPrompt() ← 构建 agent 专属 system prompt |
| 25 | + ├── initializeAgentMcpServers() ← agent 级 MCP 服务器 |
| 26 | + ├── executeSubagentStartHooks() ← Hook 注入 |
| 27 | + ├── query() ← 进入标准 agentic loop |
| 28 | + │ ├── 消息流逐条 yield |
| 29 | + │ └── recordSidechainTranscript() ← JSONL 持久化 |
| 30 | + ↓ |
| 31 | +finalizeAgentTool() ← 结果汇总 |
| 32 | + ├── 提取文本内容 + usage 统计 |
| 33 | + └── mapToolResultToToolResultBlockParam() ← 格式化为 tool_result |
| 34 | +``` |
16 | 35 |
|
17 | | -## 分身术的运作方式 |
| 36 | +## 两种子 Agent 路径:命名 Agent vs Fork |
18 | 37 |
|
19 | | -Claude Code 中的 Agent 工具让 AI 能够**启动另一个 AI 实例**来处理子任务: |
| 38 | +`AgentTool.call()` 根据是否提供 `subagent_type` 走两条完全不同的路径(`AgentTool.tsx:322-356`): |
20 | 39 |
|
21 | | -<Steps> |
22 | | - <Step title="主 Agent 分析任务"> |
23 | | - 主 Agent 判断任务可以被拆解为独立的子任务 |
24 | | - </Step> |
25 | | - <Step title="启动子 Agent"> |
26 | | - 通过 Agent 工具创建一个或多个子 Agent,每个子 Agent 收到一个清晰的子任务描述 |
27 | | - </Step> |
28 | | - <Step title="并行执行"> |
29 | | - 多个子 Agent 可以同时工作,互不干扰 |
30 | | - </Step> |
31 | | - <Step title="结果汇总"> |
32 | | - 子 Agent 完成后,结果返回给主 Agent,主 Agent 汇总并呈现给用户 |
33 | | - </Step> |
34 | | -</Steps> |
| 40 | +| 维度 | 命名 Agent(`subagent_type` 指定) | Fork 子进程(`subagent_type` 省略) | |
| 41 | +|------|-------------------------------------|--------------------------------------| |
| 42 | +| **触发条件** | `subagent_type` 有值 | `isForkSubagentEnabled()` && 未指定类型 | |
| 43 | +| **System Prompt** | Agent 自身的 `getSystemPrompt()` | 继承父 Agent 的完整 System Prompt | |
| 44 | +| **工具池** | `assembleToolPool()` 独立组装 | 父 Agent 的原始工具池(`useExactTools: true`) | |
| 45 | +| **上下文** | 仅任务描述 | 父 Agent 的完整对话历史(`forkContextMessages`) | |
| 46 | +| **模型** | 可独立指定 | 继承父模型(`model: 'inherit'`) | |
| 47 | +| **权限模式** | Agent 定义的 `permissionMode` | `'bubble'`(上浮到父终端) | |
| 48 | +| **目的** | 专业任务委派 | Prompt Cache 命中率优化 | |
35 | 49 |
|
36 | | -## 子 Agent 的边界 |
| 50 | +Fork 路径的设计核心是 **Prompt Cache 共享**:所有 fork 子进程共享父 Agent 的完整 `assistant` 消息(所有 `tool_use` 块),用相同的占位符 `tool_result` 填充,只有最后一个 `text` 块包含各自的指令。这使得 API 请求前缀字节完全一致,最大化缓存命中。 |
37 | 51 |
|
38 | | -子 Agent 不是和主 Agent 完全一样的——它有明确的能力边界: |
| 52 | +```typescript |
| 53 | +// forkSubagent.ts:142 — 所有 fork 子进程的占位结果 |
| 54 | +const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background' |
39 | 55 |
|
40 | | -| 特性 | 主 Agent | 子 Agent | |
41 | | -|------|---------|---------| |
42 | | -| 可用工具 | 全部工具 | 受限子集(不能再启动子 Agent 等) | |
43 | | -| 上下文 | 完整的会话历史 | 只有主 Agent 给的任务描述 | |
44 | | -| 权限 | 用户设定 | 继承主 Agent 的权限,或更严格 | |
45 | | -| 状态 | 可修改全局状态 | 隔离的状态空间 | |
| 56 | +// buildForkedMessages() 构建: |
| 57 | +// [assistant(全量 tool_use), user(placeholder_results..., 子进程指令)] |
| 58 | +``` |
46 | 59 |
|
47 | | -## 通信方式 |
| 60 | +### Fork 递归防护 |
48 | 61 |
|
49 | | -主 Agent 和子 Agent 之间通过**消息邮箱**通信: |
| 62 | +Fork 子进程保留 Agent 工具(为了 cache-identical tool defs),但通过两道防线防止递归 fork(`AgentTool.tsx:332`): |
50 | 63 |
|
51 | | -- 主 Agent 通过 `Agent` 工具启动子 Agent |
52 | | -- 子 Agent 通过 `SendMessage` 工具向主 Agent 报告进度 |
53 | | -- 这种松耦合的通信方式让 Agent 可以异步协作 |
| 64 | +1. **`querySource` 检查**(压缩安全):`context.options.querySource === 'agent:builtin:fork'` |
| 65 | +2. **消息扫描**(降级兜底):检测 `<fork-boilerplate>` 标签 |
| 66 | + |
| 67 | +## 工具池的独立组装 |
| 68 | + |
| 69 | +子 Agent 不继承父 Agent 的工具限制——它的工具池完全独立组装(`AgentTool.tsx:573-577`): |
| 70 | + |
| 71 | +```typescript |
| 72 | +const workerPermissionContext = { |
| 73 | + ...appState.toolPermissionContext, |
| 74 | + mode: selectedAgent.permissionMode ?? 'acceptEdits' |
| 75 | +} |
| 76 | +const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools) |
| 77 | +``` |
| 78 | + |
| 79 | +关键设计决策: |
| 80 | +- **权限模式独立**:子 Agent 使用 `selectedAgent.permissionMode`(默认 `acceptEdits`),不受父 Agent 当前模式的限制 |
| 81 | +- **MCP 工具继承**:`appState.mcp.tools` 包含所有已连接的 MCP 工具,子 Agent 自动获得 |
| 82 | +- **Agent 级 MCP 服务器**:`runAgent()` 中的 `initializeAgentMcpServers()` 可以为特定 Agent 额外连接专属 MCP 服务器 |
| 83 | + |
| 84 | +### 工具过滤的 resolveAgentTools |
| 85 | + |
| 86 | +`runAgent.ts:500-502` 在工具组装后进一步过滤: |
| 87 | + |
| 88 | +```typescript |
| 89 | +const resolvedTools = useExactTools |
| 90 | + ? availableTools // Fork: 直接使用父工具 |
| 91 | + : resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools |
| 92 | +``` |
| 93 | + |
| 94 | +`resolveAgentTools()` 会根据 Agent 定义中的 `tools` 字段过滤可用工具,将 `['*']` 映射为全量工具。 |
| 95 | + |
| 96 | +## Worktree 隔离机制 |
| 97 | + |
| 98 | +`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:590-593`): |
| 99 | + |
| 100 | +```typescript |
| 101 | +const slug = `agent-${earlyAgentId.slice(0, 8)}` |
| 102 | +worktreeInfo = await createAgentWorktree(slug) |
| 103 | +``` |
| 104 | + |
| 105 | +Worktree 生命周期: |
| 106 | +1. **创建**:在 `.git/worktrees/` 下创建独立工作副本 |
| 107 | +2. **CWD 覆盖**:`runWithCwdOverride(worktreePath, fn)` 让所有文件操作在 worktree 中执行 |
| 108 | +3. **路径翻译**:Fork + worktree 时注入路径翻译通知(`buildWorktreeNotice`) |
| 109 | +4. **清理**(`cleanupWorktreeIfNeeded`): |
| 110 | + - Hook-based worktree → 始终保留 |
| 111 | + - 有变更 → 保留,返回 `worktreePath` |
| 112 | + - 无变更 → 自动删除 |
| 113 | + |
| 114 | +## 生命周期管理:同步 vs 异步 |
| 115 | + |
| 116 | +### 异步 Agent(后台运行) |
| 117 | + |
| 118 | +当 `run_in_background=true` 或 `selectedAgent.background=true` 时,Agent 立即返回 `async_launched` 状态(`AgentTool.tsx:686-764`): |
| 119 | + |
| 120 | +``` |
| 121 | +registerAsyncAgent(agentId, ...) ← 注册到 AppState.tasks |
| 122 | + ↓ (void — 火后不管) |
| 123 | +runAsyncAgentLifecycle() ← 后台执行 |
| 124 | + ├── runAgent().onCacheSafeParams ← 进度摘要初始化 |
| 125 | + ├── 消息流迭代 |
| 126 | + ├── completeAsyncAgent() ← 标记完成 |
| 127 | + ├── classifyHandoffIfNeeded() ← 安全检查 |
| 128 | + └── enqueueAgentNotification() ← 通知主 Agent |
| 129 | +``` |
| 130 | + |
| 131 | +异步 Agent 获得独立的 `AbortController`,不与父 Agent 共享——用户按 ESC 取消主线程不会杀掉后台 Agent。 |
| 132 | + |
| 133 | +### 同步 Agent(前台运行) |
| 134 | + |
| 135 | +同步 Agent 的关键特性是 **可后台化**(`AgentTool.tsx:818-833`): |
| 136 | + |
| 137 | +```typescript |
| 138 | +const registration = registerAgentForeground({ |
| 139 | + autoBackgroundMs: getAutoBackgroundMs() || undefined // 默认 120s |
| 140 | +}) |
| 141 | +backgroundPromise = registration.backgroundSignal.then(...) |
| 142 | +``` |
| 143 | + |
| 144 | +在 agentic loop 的每次迭代中,系统用 `Promise.race` 竞争下一条消息和后台化信号: |
| 145 | + |
| 146 | +```typescript |
| 147 | +const raceResult = await Promise.race([ |
| 148 | + nextMessagePromise.then(r => ({ type: 'message', result: r })), |
| 149 | + backgroundPromise // 超过 autoBackgroundMs 触发 |
| 150 | +]) |
| 151 | +``` |
| 152 | + |
| 153 | +后台化后,前台迭代器被终止(`agentIterator.return()`),新的 `runAgent()` 以 `isAsync: true` 重新启动,当前台的输出文件继续写入。 |
| 154 | + |
| 155 | +## 结果回传格式 |
| 156 | + |
| 157 | +`mapToolResultToToolResultBlockParam()` 根据状态返回不同格式(`AgentTool.tsx:1298-1375`): |
| 158 | + |
| 159 | +| 状态 | 返回内容 | |
| 160 | +|------|---------| |
| 161 | +| `completed` | 内容 + `<usage>` 块(token/tool_calls/duration) | |
| 162 | +| `async_launched` | agentId + outputFile 路径 + 操作指引 | |
| 163 | +| `teammate_spawned` | agent_id + name + team_name | |
| 164 | +| `remote_launched` | taskId + sessionUrl + outputFile | |
| 165 | + |
| 166 | +对于一次性内置 Agent(Explore、Plan),`<usage>` 块被省略——每周节省约 1-2 Gtok 的上下文窗口。 |
| 167 | + |
| 168 | +## MCP 依赖的等待机制 |
| 169 | + |
| 170 | +如果 Agent 声明了 `requiredMcpServers`,`call()` 会等待这些服务器连接完成(`AgentTool.tsx:371-410`): |
| 171 | + |
| 172 | +```typescript |
| 173 | +const MAX_WAIT_MS = 30_000 // 最长等 30 秒 |
| 174 | +const POLL_INTERVAL_MS = 500 // 每 500ms 轮询 |
| 175 | +``` |
| 176 | + |
| 177 | +早期退出条件:任何必需服务器进入 `failed` 状态时立即停止等待。工具可用性通过 `mcp__` 前缀工具名解析(`mcp__serverName__toolName`)判断。 |
54 | 178 |
|
55 | 179 | ## 适用场景 |
56 | 180 |
|
57 | 181 | <CardGroup cols={2}> |
58 | 182 | <Card title="并行研究" icon="magnifying-glass"> |
59 | | - 多个子 Agent 同时搜索不同方向的信息 |
| 183 | + 多个 fork 子进程并行搜索不同方向,共享 Prompt Cache 前缀,只有指令不同 |
60 | 184 | </Card> |
61 | | - <Card title="分治修改" icon="code-branch"> |
62 | | - 把大规模修改拆分到多个子 Agent 并行执行 |
63 | | - </Card> |
64 | | - <Card title="前后台配合" icon="layer-group"> |
65 | | - 一个子 Agent 在后台运行测试,主 Agent 继续写代码 |
| 185 | + <Card title="专业委派" icon="code-branch"> |
| 186 | + 使用命名 Agent(Explore/Plan/verification)执行专业任务,受限工具集 + 独立权限 |
66 | 187 | </Card> |
67 | 188 | <Card title="隔离实验" icon="flask"> |
68 | | - 在 worktree 中启动子 Agent 尝试一个方案,不影响主分支 |
| 189 | + `isolation: "worktree"` 在独立工作副本中尝试方案,不影响主分支 |
| 190 | + </Card> |
| 191 | + <Card title="后台构建" icon="layer-group"> |
| 192 | + `run_in_background: true` 启动长时间构建/测试任务,主 Agent 继续工作 |
69 | 193 | </Card> |
70 | 194 | </CardGroup> |
0 commit comments