Claude Code深度拆解-多Agent协作 1-子Agent生成与生命周期
Hi,大家好,欢迎来到维元码簿。
本文属于《Claude Code 源码 Deep Dive》系列,专注于多 Agent 协作中的子 Agent 生成与生命周期板块。如果你想了解整个系列,可以先看系列开篇 | Claude Code 源码架构概览:51万行代码的模块地图。
本文讲一件事:主 Agent 调用 AgentTool 后,一个子 Agent 是怎么被创建、怎么拥有独立对话循环、怎么把结果回流给主 Agent 的。
读完全文,你将能回答这几个问题:
- 一个子 Agent 是怎么从“无”到“有”被创造出来的?从
createAgentId()生成独立身份,到工具池裁剪、System Prompt 组装、AbortController 隔离、Skills/MCP 初始化,再到进入query()循环——runAgent()的 12 步流程就是一个子 Agent 完整的“出生流程”。 - 主 Agent 调用 AgentTool 后,背后发生了什么?不是简单的函数调用,而是一个完整的
query()循环——子 Agent 拥有自己的 System Prompt、Messages[]、工具池和模型。 - Fork Agent 和普通子 Agent 有什么区别?Fork 继承父 Agent 的上下文和 Prompt Cache,普通子 Agent 从零开始。前者省 token 但受限,后者独立但昂贵。
- Claude Code 内置了几种子 Agent 类型?各自做什么?GeneralPurpose(通用)、Explore(探索)、Plan(规划)、Verification(验证)、Fork(分身)——五种角色覆盖从研究到验证的完整链路。
本篇覆盖的源码范围
| 模块 | 核心文件 | 核心代码行 | 文件总行 | 职责 |
|---|---|---|---|---|
| AgentTool 入口 | src/tools/AgentTool/AgentTool.tsx | L250-577(call 方法) | 1387 行 | 工具入口、spawn 路由、Fork/Regular 分流、worktree 管理 |
| 执行引擎 | src/tools/AgentTool/runAgent.ts | L248-860(runAgent 生成器函数) | 974 行 | 完整 query() 循环、子 Agent 生命周期管理 |
| Fork 子 Agent | src/tools/AgentTool/forkSubagent.ts | L32-56(isForkSubagentEnabled 门控) | 200 行 | Fork 功能门控、Fork 提示词生成 |
| Agent 恢复 | src/tools/AgentTool/resumeAgent.ts | L126-211(resume 参数组装) | 250 行 | 从 transcript 恢复暂停的子 Agent |
| Agent Prompt | src/tools/AgentTool/prompt.ts | L67-287(getPrompt 生成逻辑) | 450 行 | Agent 选择指南、Fork/Regular 提示词差异 |
| 内置 Agent | src/tools/AgentTool/built-in/*.ts | 5 个文件 | 400 行 | 5 种内置 Agent 的角色定义、工具权限、模型选择 |
| Agent 加载 | src/tools/AgentTool/loadAgentsDir.ts | 散布 | 700 行 | 从文件目录加载用户自定义 Agent |
前情提要:从工具到 Agent
在工具系统系列中,我们拆解了 30+ 个工具怎么注册、怎么执行。工具让 Agent 有了"手和脚"——能读写文件、能执行命令、能搜索代码。但单线程的 Agent 面对复杂任务时效率有限:它一次只能想一件事、做一件事。
AgentTool 就是解决这个问题的关键机制。它不是一个普通的工具——它是一个**“工具的工厂”**。调用它,系统就创建一个完整的子 Agent,子 Agent 拥有自己的对话循环,和主 Agent 一样能调用工具、能多轮推理,完成后把结果回流。
更关键的是,部分类型的子 Agent 工具池中也包含 AgentTool(典型如 GeneralPurpose),这意味着它们能再创建子子 Agent,形成递归的 Agent 树。但多数内置角色(Explore、Plan、Verification 等)在工具层面就禁用了 AgentTool——“能不能再 spawn” 是一个每种 Agent 独立决定的权限,而不是全局能力。这就是 Claude Code 从“单线程聊天”进化到“多线程编程助手”的核心机制。
上图展示了多 Agent 系统的三层架构。本文聚焦中间层——AgentTool 的入口逻辑和执行引擎 runAgent()——这是整个多 Agent 系统的"心脏"。
AgentTool:工具调用工具的入口
AgentTool 是区分"普通 Agent"和"能协作的 Agent"的那条线。它不是 Bash、Read、Edit 那样的"操作型工具"——它操作的不是文件系统或 Shell,而是 Agent 本身。
入口参数:一个工具调用,26 个控制参数
AgentTool 的 call 方法接收这些关键参数:
// src/tools/AgentTool/AgentTool.tsx L239-250asynccall({prompt,// 任务描述:"搜索所有 TODO 注释"subagent_type,// Agent 类型:"Explore" | "Plan" | undefined(→Fork)description,// 简短描述(用于 UI 显示)model,// 模型选择(可选)run_in_background,// 同步/异步name,// teammate 名称(触发 Swarm 路径)team_name,// 团队名称(触发 Swarm 路径)isolation,// 隔离模式:"worktree" | "remote"}:AgentToolInput,...)这些参数不是随意组合的——它们决定了下游的三条路由。
三条路由:AgentTool 的分流逻辑
从 AgentTool 的 call 方法进去,系统会根据参数组合走完全不同的三条路径。这是整个多 Agent 系统的第一个关键决策点:
// src/tools/AgentTool/AgentTool.tsx L282-336(简化)// 路径 1:Swarm Teammate —— team_name + name 都存在if(teamName&&name){constresult=awaitspawnTeammate({name,prompt,...},toolUseContext)return{data:spawnResult}}// 路径 2:Fork Agent —— subagent_type 省略,Fork 功能开启consteffectiveType=subagent_type??(isForkSubagentEnabled()?undefined:GENERAL_PURPOSE_AGENT.agentType)if(effectiveType===undefined){// 走 Fork 路径selectedAgent=FORK_AGENT}// 路径 3:Regular Subagent —— 指定了 subagent_typeselectedAgent=allAgents.find(a=>a.agentType===effectiveType)三条路径的本质区别:
| 维度 | Regular Subagent | Fork Agent | Swarm Teammate |
|---|---|---|---|
| 触发条件 | subagent_type指定 | subagent_type省略 | team_name+name |
| 上下文 | 从零开始 | 继承父对话历史 | 通过 mailbox 接收 |
| Prompt Cache | 独立前缀 | 共享前缀(高命中) | 独立进程 |
| 适用场景 | 研究、探索、验证 | 实现、多步编辑 | 多 Agent 团队协作 |
| Feature Gate | 无 | FORK_SUBAGENT | ENABLE_AGENT_SWARMS |
Regular Subagent 和 Fork Agent 最终都走向runAgent()——但带着完全不同的参数。Swarm Teammate 是另一个世界的故事,我们在姊妹篇[团队协作与编排](./05-Claude Code深度拆解-多Agent协作 4-团队协作与编排.md)中展开。
Fork 路径的递归防护
Fork Agent 有一个特别的设计:Fork 的子 Agent 不能再 Fork。这不是功能缺失,而是有意为之的防护:
// src/tools/AgentTool/AgentTool.tsx L326-333// Recursive fork guard: fork children keep the Agent tool in their// pool for cache-identical tool defs, so reject fork attempts at call time.if(toolUseContext.options.querySource===`agent:builtin:${FORK_AGENT.agentType}`||isInForkChild(toolUseContext.messages)){thrownewError('Fork is not available inside a forked worker. Complete your task directly...')}为什么禁止递归 Fork?因为每个 Fork 都继承父的上下文——如果允许嵌套 Fork,会导致上下文无限膨胀,同时失去“共享 Prompt Cache”的优势(子 Fork 的 Cache 前缀和父 Fork 不同)。说人话:Fork 让你“隐身后台干活”,但不能套娃。
把视角放大一点看,“子 Agent 能不能再 spawn 子 Agent”这件事,在 Claude Code 里其实是按角色逐一决定的权限,而不是全局能力。下面这张表是对六种内置角色工具池的梳理(源码位于src/tools/AgentTool/built-in/*.ts):
| Agent 类型 | 工具池声明 | AgentTool 是否可用 | 能否再创建子 Agent |
|---|---|---|---|
| GeneralPurpose | tools: ['*'] | ✅ 包含 | ✅ 可以 |
| Fork | 继承父工具池(useExactTools) | ✅ 包含,但运行时拦截再 Fork | ⚠️ 可调 Regular,不可再 Fork |
| Explore | disallowedTools: [AGENT_TOOL_NAME, ...] | ❌ 禁用 | ❌ 不能 |
| Plan | disallowedTools: [AGENT_TOOL_NAME, ...] | ❌ 禁用 | ❌ 不能 |
| Verification | disallowedTools: [AGENT_TOOL_NAME, ...] | ❌ 禁用 | ❌ 不能 |
| Claude Code Guide | 显式白名单(Bash/Read/WebFetch/WebSearch 等) | ❌ 不在白名单 | ❌ 不能 |
| Statusline Setup | tools: ['Read', 'Edit'] | ❌ 不在白名单 | ❌ 不能 |
再加上运行时的 Recursive Fork Guard,整个“谁能 spawn 谁”的约束就落在了两个层面:工具池裁剪(静态)+call 时拦截(动态)。只有 GeneralPurpose 才是真正意义上可以递归的“通用工具人”,其他角色在设计上就被框在“一次性工作者”的位置上——这正是多 Agent 系统能避免上下文爆炸和成本失控的关键工程取舍。
runAgent():子 Agent 的执行引擎
runAgent() 是整个多 Agent 系统的心脏。它是一个async function*生成器函数——用yield逐条产出子 Agent 的消息。主 Agent 通过这些消息知道子 Agent 的进展。
26 个参数:从 Agent 定义到进度回调的完整控制面
runAgent() 的参数列表是 Claude Code 中最长的函数签名之一——不是因为设计过度,而是因为子 Agent 的每一个行为维度都需要可控:
// src/tools/AgentTool/runAgent.ts L248-329(简化)exportasyncfunction*runAgent({agentDefinition,// 子 Agent 的角色定义(角色、工具、模型、权限)promptMessages,// 初始消息(任务描述)toolUseContext,// 父 Agent 的工具上下文canUseTool,// 权限检查函数isAsync,// 是否异步(决定 Abort 隔离)forkContextMessages,// Fork 路径:父对话历史availableTools,// 预计算的工具池allowedTools,// 白名单工具(父权限不穿透)useExactTools,// Fork 路径:字节级缓存优化maxTurns,// 最大轮次限制// ... 共 26 个参数}):AsyncGenerator<Message,void>其中三个最关键的设计决策:
① availableTools 由调用方预计算——注释写着:“Precomputed by the caller (AgentTool.tsx) to avoid a circular dependency between runAgent and tools.ts”。如果 runAgent() 自己调用assembleToolPool(),会形成循环依赖。把工具池作为参数传入,解耦了执行引擎和工具注册系统。
② allowedTools 替换全部 allow 规则——“When provided, replaces ALL allow rules so the agent only has what’s explicitly listed (parent approvals don’t leak through)”。这是"权限不穿透"的第一道防线。
③ forkContextMessages 决定上下文继承——Fork 路径传入父对话历史,Regular 路径传undefined。这是 Fork 和 Regular 在 runAgent() 内部的第一个分叉。
12 步执行流程
runAgent() 的内部逻辑可以拆为 12 个步骤,分为准备、组装、运行、清理四个阶段:
准备阶段(Step 1-3):
Step 1 创建独立 AgentId。createAgentId()生成 UUID,子 Agent 从此有了独立身份。这个 ID 贯穿整个生命周期——transcript 文件用它命名、遥测事件用它归因、Perfetto trace 用它显示层级。
Step 2 上下文继承决策。Fork 路径复制父的 messages 并过滤不完整的 tool_use block(filterIncompleteToolCalls()),Regular 路径从空数组开始。文件读取缓存也相应处理:Fork 复制父缓存(cloneFileStateCache),Regular 创建新缓存(createFileStateCacheWithSizeLimit)。
Step 3 权限模式解析。这是整个隔离体系的第一道决策——涉及的逻辑在姊妹篇[上下文隔离与权限边界](./05-Claude Code深度拆解-多Agent协作 2-上下文隔离与权限边界.md)中详述。
组装阶段(Step 4-9):
Step 4 工具池组装。resolveAgentTools()根据 Agent 定义的tools允许列表和disallowedTools禁止列表,从父工具池中筛出子 Agent 的工具。例如 Explore Agent 禁止了 Edit、Write、NotebookEdit、AgentTool——它只能是"只读研究 Agent"。
Step 5 构建 Agent 专用 System Prompt。每个 Agent 类型都有自己的 System Prompt。GeneralPurpose 的 Prompt 是"你是一个 Agent,完成任务后回报";Explore 的 Prompt 是"你是一个文件搜索专家,严格只读";Verification 的 Prompt 开篇就写:“Your job is not to confirm the implementation works — it’s to try to break it.”
Step 6 确定 AbortController。异步 Agent 创建全新独立的 AbortController——父 Agent 被取消不影响子 Agent 继续跑。同步 Agent 共享父的 AbortController——父取消则子也停。
Step 7 执行 SubagentStart Hooks。外部系统可以 Hook 子 Agent 的启动事件,注入额外上下文。
Step 8 预加载 Skills。Agent 定义中skills: ['skill-a', 'skill-b']声明的 Skills 在启动时预加载,内容注入到子 Agent 的初始消息中。
Step 9 初始化 Agent 专属 MCP。子 Agent 可以有自己声明的 MCP Server(additive to parent),initializeAgentMcpServers()建立独立连接。
运行与清理阶段(Step 10-12):
Step 10 创建隔离上下文。createSubagentContext()创建子 Agent 的 ToolUseContext——独立的消息队列、独立的文件读取缓存、独立的 AbortController。
Step 11 进入 query() 循环。这是关键:子 Agent 进入的query()和主 Agent 是同一条代码路径!同样的 Agent Loop、同样的工具调用流水线、同样的结果回流机制。唯一的区别是thinkingConfig: { type: 'disabled' }——子 Agent 默认不开启思考模式(控制输出成本)。
Step 12 资源清理。finally 块执行:断开 MCP 连接、注销 Hooks、释放文件缓存、清除 Prompt Cache 追踪、移除 Perfetto 注册、终止后台 Bash 任务。不遗漏任何一个资源——因为子 Agent 的生命周期可能在一个长会话中重复数百次。
Fork Agent:共享缓存的分身术
Fork 是 Prompt Cache 驱动的一个设计。它不是为了"方便"而存在,而是为了"省钱"。
Fork 和 Regular 的本质差异:三个维度
| 维度 | Regular Subagent | Fork Agent |
|---|---|---|
| System Prompt | 独立生成(子 Agent 专用) | 继承父 Agent(useExactTools) |
| Messages[] | 仅任务描述(从零开始) | 复制父对话历史+ 任务指令 |
| Prompt Cache | 独立前缀,首次调用无命中 | 共享前缀,高命中率 |
| 工具池 | resolveAgentTools() 重新解析 | 直接使用父工具池 |
| 模型 | 可指定(如 Explore 用 haiku) | 必须和父相同(不同模型 Cache 不共享) |
useExactTools 是关键开关——设true时,子 Agent 使用和父完全相同的工具池,不做任何过滤。目的是保持 API 请求前缀的字节一致性——只要前缀字节不变,Prompt Cache 就能命中。
Fork 的三条铁律(来自 System Prompt)
Fork Agent 的 System Prompt 中有一节 “When to fork”,定义了三条规则:
铁律一:“中间工具输出不值得保留在上下文时用 Fork。”判断标准是定性的——“我还需要再次看到这些输出吗?”——而不是任务大小。研究型问题如果可拆成独立子问题,可以并行启动多个 Fork。
铁律二:“Fork 是 cheap 的——共享 Prompt Cache。不要给 Fork 设 model 参数——不同模型不能复用父 Cache。”设了不同模型,Prompt Cache 的优势就没了。
铁律三:"不要偷看。tool result 里有一个output_file路径——不要 Read 或 tail 它,除非用户明确要求检查进度。你会收到完成通知;信任它。读 transcript 会把 Fork 的工具噪音拖进你的上下文,违背了 Fork 的初衷。"
这条铁律特别有意思——它说明 Fork 的设计目标不仅是"让子 Agent 干活",更是"让主 Agent 保持上下文干净"。如果主 Agent 去读子 Agent 的对话记录,那些 Bash 输出、文件搜索细节就回到了主上下文——等于 Fork 白做了。
五种内置 Agent:从探索到验证的完整角色谱
Claude Code 不是让用户去设计 Agent 角色——它内置了五个经过工程验证的角色,覆盖了代码工作中最常见的五种场景。
GeneralPurpose Agent:默认万能工具人
// src/tools/AgentTool/built-in/generalPurposeAgent.ts L25-34exportconstGENERAL_PURPOSE_AGENT:BuiltInAgentDefinition={agentType:'general-purpose',tools:['*'],// 全部工具source:'built-in',getSystemPrompt:getGeneralPurposeSystemPrompt,}tools: ['*']意味着它可以使用父 Agent 能用的所有工具。它的 System Prompt 简洁到只有一句话:“完成这个任务,别过度设计,也别半途而废。” 它是当你不确定该用哪个 Agent 时的默认选择。
Explore Agent:快速只读搜索专家
Explore Agent 是设计得最仔细的内置 Agent——因为它被调用的频率最高。它的 System Prompt 第一段就是:
“=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===”
它不是"建议"只读——是把 Edit、Write、NotebookEdit、AgentTool 全部放入disallowedTools,从工具层面杜绝了修改的可能。此外:
- 模型用 haiku(对外部用户)——因为搜索不需要深度推理,haiku 更便宜更快
- omitClaudeMd = true——不加载项目的 CLAUDE.md 规则文件,节省 5-15 Gtok/week
- 不加载 gitStatus——不传 40KB 的 Git 状态信息,要时自己跑
git status
Plan Agent:规划先行
Plan Agent 进入 Plan Mode,只分析不执行。它读取代码、理解结构、输出结构化计划。工具权限和 Explore 类似(只读),但 System Prompt 引导它生成可执行的计划而非搜索结果。
Verification Agent:破坏性验证
Verification Agent 的开篇 System Prompt 是我见过最精彩的 Agent 角色定义之一:
“Your job is not to confirm the implementation works — it’s to try to break it.”
它定义了两种经典失败模式:① 验证逃避——“被要求检查时就找理由不跑,读读代码、叙述’我会测试什么’、写上 PASS,然后走人”;② 被前 80% 迷惑——“看到漂亮的 UI 或通过的测试集就想放行,却没注意到一半按钮没反应、刷新后状态消失、或者后端遇到坏输入就崩溃”。
Verification Agent 按变更类型(前端/后端/CLI/配置/库/移动端/数据管道/数据库迁移/重构)分别定义了验证策略——它是一个真正的"测试工程专家"角色。
Fork Agent:隐身的执行分身
Fork Agent 没有独立的角色定义——它就是一个"带父上下文的异步子 Agent"。它的 System Prompt 实际上就是 Fork 的"三条铁律"。不指定subagent_type且 Fork 功能开启时,系统自动选择它。
本章小结
- AgentTool 不是普通工具,而是"工具的工厂"——三次路由分流(Regular/Fork/Swarm)是多 Agent 系统的第一个决策点
- runAgent() 的 12 步流程覆盖了子 Agent 生命周期的每一个关键决策——从 AgentId 创建到资源清理,每步都有明确的设计意图
- Fork Agent 的核心价值是共享 Prompt Cache——不为方便,为省钱。三条铁律确保这个目标不偏离
- 五种内置 Agent 覆盖从探索到验证的完整角色谱——Explore 的只读防护、Verification 的破坏性思维、Plan 的结构化输出,每个角色都经过工程验证
系列导航:
本文属于《Claude Code 源码 Deep Dive》系列中「多 Agent 协作」命题的子篇章,专注于子 Agent 生成与生命周期。
姊妹篇(可独立阅读):
- Claude Code 深度拆解:多 Agent 协作 2 — 上下文隔离与权限边界
- Claude Code 深度拆解:多 Agent 协作 3 — 任务系统与 Agent 间通信
- Claude Code 深度拆解:多 Agent 协作 4 — 团队协作与编排
如果这篇文章对你有帮助,欢迎点赞收藏支持一下。如果你对 Claude Code 源码感兴趣,欢迎关注本系列后续更新。有任何想法或疑问,欢迎评论区留言讨论👋
