第4章:子代理与多 Agent——Claude Code 如何将自己"一分为多"
核心问题:当一个任务需要并行推进、需要保护主上下文不被淹没、或者需要一个独立视角来验证自己的工作时,Agent 是如何从一个单一进程变成一套协作体系的?Claude Code 和 OpenClaw 在这里给出了截然不同的答案。
0. 起点:两套不同的哲学
在开始看代码之前,先把两个系统的核心理念说清楚:
Claude Code:一切皆子代理(subagent-centric)。CC 的多 Agent 策略建立在一个统一原语上:AgentTool(即 Task 工具)。无论是并行搜索、自动验证还是多工作流并行,都是这个 Tool 的不同路径。系统提示、工具池、权限模式——子代理启动时按需组装,互相隔离。
OpenClaw:Coordinator-Worker 模型(coordinator-centric)。OC 引入了一个明确的角色分工:有一个专门的 Coordinator 实例,负责任务拆解、派发指令、汇总结果;Worker 实例只负责执行具体工作。两者通过消息通道通信,Worker 完成后发来 <task-notification> XML,Coordinator 收到后决定下一步。
这两种哲学的分歧,体现在架构的每一个细节里。
1. CC 的三种 Agent 创建模式
在 CC 里,"派 Agent"这一个动作,对应三条完全不同的执行路径(AgentTool.tsx#L318-L356):
graph TD
A["调用 AgentTool"] --> B{subagent_type?}
B -- "省略 + Fork gate 开启" --> C["🍴 Fork 路径<br/>继承父 Agent 全部上下文"]
B -- "指定类型" --> D{name + team_name?}
D -- "是" --> E["👥 多代理团队路径<br/>spawnTeammate() 起新进程"]
D -- "否" --> F["🎯 显性功能子代理路径<br/>独立系统提示 + 裁剪工具池"]
style C fill:#fce4ec,stroke:#e91e63
style E fill:#e3f2fd,stroke:#1565c0
style F fill:#e8f5e9,stroke:#2e7d32
| 模式 | 触发条件 | Context 来源 | AbortController |
|---|---|---|---|
| Fork 子代理 | subagent_type 省略 + gate 开启 |
继承父代理完整对话 + 占位 tool_result | 独立(异步) |
| 功能子代理 | 指定 subagent_type |
独立系统提示 + 新的 user message | 继承(同步)或独立(异步) |
| 多代理团队 | name + team_name 都存在 |
新进程,完全独立 | 进程级隔离 |
2. Fork 子代理:Prompt Cache 共享的工程极致
Fork 是三条路径里工程最精妙的一条。
2.1 为什么要 Fork?
当主 Agent 在一轮里需要并行执行多个独立任务,最直观的做法是 spawn 多个功能子代理,每个拿一个全新的系统提示、从零开始对话。但这意味着每个子代理都会独占一个 Prompt Cache 槽位——60KB+ 的工具定义在每个子代理身上各缓存一份。
Fork 的设计目标正是为了消灭这种浪费:让同时 fork 出的多个子代理,共享同一个 Prompt Cache 槽位。
2.2 消息构造:占位符是整个设计的关键
buildForkedMessages() 函数(forkSubagent.ts#L107)构造的消息结构是:
[...父代理历史]
→ [父代理当前的 AssistantMessage(含所有 tool_use 块)]
→ [User message:占位 tool_result × N + 末尾的任务指令 text block]
关键约束:父代理在一轮里可能同时 fork 出 A、B、C 三个子代理,每个的任务指令不同。如果每个子代理产生的 API 请求有任何字节差异,它们就无法共享缓存。
解决方案:
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
所有 tool_use 块都用完全相同的占位文字作为 tool_result。所有子代理看到的 toolResultBlocks 都是字节相同的。只有最末尾那个额外的 text block(即具体的任务指令)是不同的。
结果:[通用历史 + 相同 placeholder results] 这部分所有 Fork 子代理全部命中缓存,只有 directive 那一块不缓存。
2.3 系统提示的字节级继承
Fork 子代理还有另一个反直觉的设计(runAgent.ts#L508-L518):
// Fork 路径:直接传递父代理已渲染的系统提示字节
const agentSystemPrompt = override?.systemPrompt
? override.systemPrompt // ← Fork 路径走这里
: asSystemPrompt(await getAgentSystemPrompt(...)) // ← 正常路径
Fork 子代理不重新调用 getSystemPrompt() 重建系统提示,而是直接传递父代理 renderedSystemPrompt 的字节副本。
原因:getSystemPrompt() 内部有 GrowthBook feature gate 的读取,它的"冷"和"暖"状态可能不同(Session 开始时读磁盘缓存,一段时间后从远程获取新值)。如果重新构建,子代理的系统提示字节可能与父代理的微小不同,导致 Prompt Cache miss。
2.4 防递归的两道防护
Fork 子代理保留了父代理的完整工具列表(包括 AgentTool 本身),以保证工具定义字节相同。但这带来了无限递归的风险。
CC 用两道防护阻止(AgentTool.tsx#L326-L334):
querySource 检查(主要防护,AutoCompact 安全):Fork 子代理的
toolUseContext.options.querySource被设置为agent:builtin:fork。这个值存在 context.options 里,即使 AutoCompact 把消息历史全部重写,这个标识也不会消失。消息扫描(兜底防护):扫描历史消息里是否包含
<fork-boilerplate>标签。Fork 子代理任务开头会注入这段文字,因此可以通过历史消息检测。
这也是 buildChildMessage() 的开头为什么有那段"戏剧化"的告知:
STOP. READ THIS FIRST.
You are a forked worker process. You are NOT the main agent.
RULES (non-negotiable):
1. Your system prompt says "default to forking." IGNORE IT — that's for the parent.
You ARE the fork. Do NOT spawn sub-agents; execute directly.
这不是在对模型说废话——这是提示词层面的防护,配合 querySource 和标签扫描,构成三层递归防卫。
3. 功能子代理:精心裁剪的独立世界
显性功能子代理拥有完全独立的上下文,从 CC 的内置 Agent 库里选取。
3.1 工具池的权限裁剪
每个内置 Agent 的工具权限在定义里写死,体现了"按最小特权原则"设计子 Agent:
| Agent | 禁用的工具 | 开放的工具 | 设计意图 |
|---|---|---|---|
| Explore | AgentTool, FileEdit, FileWrite, NotebookEdit | FileRead, Glob, Grep, Bash(只读) | 只搜索,禁修改 |
| Verification | AgentTool, FileEdit, FileWrite, NotebookEdit | Bash(可写 /tmp) | 只验证,禁改项目文件 |
| Fork | 无禁用 | ['*'](继承全部) |
字节相同前缀 + cache 命中 |
| Plan | AgentTool, FileEdit, FileWrite | FileRead, Bash(只读) | 只制定计划 |
Verification Agent 允许写 /tmp 的权限是有意为之——验证者可能需要写测试脚本、编译临时文件,但绝不应该修改项目本身。
3.2 Context 精细裁剪
功能子代理的 Context 裁剪是 Token 成本的核心杠杆(runAgent.ts#L385-L410):
CLAUDE.md 剥离(Explore 和 Plan Agent):
const shouldOmitClaudeMd =
agentDefinition.omitClaudeMd &&
!override?.userContext &&
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slim_subagent_claudemd', true)
注释里写得很直接:"Dropping claudeMd here saves ~5-15 Gtok/week across 34M+ Explore spawns."
CLAUDE.md 包含项目规约、提交规范、lint 规则——这些对只做搜索的 Explore Agent 毫无意义,但会消耗宝贵的 Token 预算。
git status 剥离(Explore 和 Plan Agent):
const resolvedSystemContext =
agentDefinition.agentType === 'Explore' || agentDefinition.agentType === 'Plan'
? systemContextNoGit
: baseSystemContext
Session 启动时采集的 git status 快照可达 40KB,且明确标注是 stale 的。搜索型 Agent 如果需要 git 信息,直接运行 git status 拿新鲜数据即可。
3.3 Verification Agent:对抗自身 LLM 倾向的系统设计
Verification Agent 的系统提示是整个 CC 代码库里最值得研读的文字之一,它直接命名了两个 LLM 在自我验证时的失败模式:
验证回避:无论面对什么检查,都能找到不运行的理由——读代码、描述应该测试什么、写 "PASS",然后继续。
被前 80% 诱惑:看到精美的 UI 或通过的测试套件就觉得可以放行,没注意到有一半按钮什么也不做,刷新后状态消失,或者后端在边界输入时崩溃。
解决方案是强制证据化输出格式:
### Check: [验证点]
**Command run:** [实际执行的命令]
**Output observed:** [终端输出原文,不要解释]
**Result: PASS** (或 FAIL — 附 Expected vs Actual)
没有 Command run 的 PASS 会被拒绝。
// Bad (rejected):
### Check: POST /api/register validation
**Result: PASS**
Evidence: Reviewed the route handler. The logic correctly validates email format...
(没有命令输出,读代码不算验证)
主代理在收到报告后还会抽查:随机 re-run 报告里 2-3 条命令,检查输出是否匹配。这是更高层面的验证——验证"验证报告"本身是否诚实。
4. CC 多 Agent 体系:Coordinator 模式与 Agent 团队
4.1 Coordinator 模式
CC 提供了一个企业级的多 Agent 编排模式:通过环境变量 CLAUDE_CODE_COORDINATOR_MODE=1 激活。在这个模式下,主 Agent 的角色完全改变(coordinatorMode.ts#L111):
Coordinator 的系统提示开头:
You are Claude Code, an AI assistant that orchestrates software engineering tasks
across multiple workers.
## 1. Your Role
You are a coordinator. Your job is to:
- Help the user achieve their goal
- Direct workers to research, implement and verify code changes
- Synthesize results and communicate with the user
Coordinator 的工具集被专门定制:
- AgentTool — 派发新 Worker
- SendMessageTool — 继续既有的 Worker(发送后续指令)
- TaskStopTool — 中止运行中的 Worker
- 没有 FileEdit, FileWrite, Bash——Coordinator 不直接修改文件
Worker 结果以 <task-notification> XML 的形式作为 user 消息到达 Coordinator:
<task-notification>
<task-id>agent-a1b</task-id>
<status>completed</status>
<summary>Agent "Investigate auth bug" completed</summary>
<result>Found null pointer in src/auth/validate.ts:42...</result>
<usage>
<total_tokens>4521</total_tokens>
<tool_uses>8</tool_uses>
<duration_ms>12480</duration_ms>
</usage>
</task-notification>
4.2 Coordinator 内置的并发哲学
Coordinator 系统提示里有一段非常明确的并发指导原则:
Parallelism is your superpower. Workers are async. Launch independent workers concurrently whenever possible — don't serialize work that can run simultaneously and look for opportunities to fan out.
并发管理规则:
- 只读任务(研究):自由并行
- 写入任务(实现):同一批文件一次只有一个 Worker
- 验证任务:可与不同文件区域的实现并行
"Continue vs Spawn" 的决策框架也被明确写入 Coordinator 提示:
| 情况 | 策略 | 原因 |
|---|---|---|
| 研究覆盖了将要编辑的文件 | Continue(SendMessage) | Worker 已加载文件,现在给它明确计划 |
| 研究范围宽泛但实现很聚焦 | Spawn fresh(AgentTool) | 避免拖走探索阶段的噪音;聚焦的 Context 更干净 |
| 纠正失败或扩展最近的工作 | Continue | Worker 有错误 Context,知道刚才尝试了什么 |
| 验证另一个 Worker 刚写的代码 | Spawn fresh | 验证者应该以新鲜视角看代码,不带实现阶段的假设 |
4.3 Agent 团队(Swarms):进程级并行
当 name 和 team_name 都存在时,CC 走的是进程级多 Agent 路径,通过 spawnTeammate() 在 tmux 新窗口起一个独立的 CC 进程。
团队中的 Agent 通过文件系统邮箱(mailbox)系统通信:
~/.claude/teams/<teamName>/mailboxes/<agentName>.json
每个 Agent 持续 poll 自己的邮箱(500ms 间隔)。权限确认请求通过邮箱发给 Leader,Leader 通过 UI 按钮回复 allow/reject。响应也写回邮箱,Worker 拿到响应后继续。
graph LR
L["👑 Leader (主进程)"]
W1["👷 Worker A (tmux)"]
W2["👷 Worker B (tmux)"]
L -->|"派任务 via AgentTool"| W1
L -->|"派任务 via AgentTool"| W2
W1 -->|"idle notification 写入 Leader 邮箱"| L
W2 -->|"idle notification"| L
W1 <-->|"권限 请求/响应 via 邮箱"| L
进程级隔离的关键特性:
- Worker 的 AbortController 独立于 Leader——Leader 按 ESC 不会杀 Worker
- Worker 有独立的缓冲区,AutoCompact 各自触发
- 但文件系统是共享的(默认情况下),写冲突风险真实存在
4.4 Worktree 隔离:文件系统级隔离
为了解决并行 Worker 的写冲突,CC 提供了 isolation: 'worktree' 参数:
if (effectiveIsolation === 'worktree') {
const slug = `agent-${earlyAgentId.slice(0, 8)}`
worktreeInfo = await createAgentWorktree(slug)
}
每个 Worker 在独立的 git worktree 里操作,拥有相同代码库的不同工作副本。完成后,如果没有任何变更,worktree 自动删除;如果有变更,保留供用户合并。
Fork + Worktree 组合时,系统还会注入路径翻译提示:
You've inherited the conversation context above from a parent agent working in /Users/xxx/project.
You are operating in an isolated git worktree at /Users/xxx/project-agent-a1b3c5de —
same repository, same relative file structure, separate working copy.
Paths in the inherited context refer to the parent's working directory;
translate them to your worktree root.
5. OC 对比:Interface Dispatch vs 内联管道
OpenClaw 在子代理这个维度上,最显著的工程差异体现在上下文隔离的方式和Agent 定义的扩展方式:
5.1 Agent 定义:YAML Frontmatter vs TypeScript 定义
CC(TypeScript 定义,编译时固化):
export const EXPLORE_AGENT: BuiltInAgentDefinition = {
agentType: 'Explore',
disallowedTools: [AGENT_TOOL_NAME, FILE_EDIT_TOOL_NAME, ...],
model: process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku',
omitClaudeMd: true,
getSystemPrompt: () => getExploreSystemPrompt(),
}
内置 Agent 的能力边界在编译时就固化了,用户无法在运行时修改。
OC(Markdown + YAML Frontmatter,运行时加载):
---
agentType: my-reviewer
model: sonnet
tools: [Read, Grep, Glob]
permissionMode: readonly
disallowedTools: [Bash, FileWrite]
mcpServers:
- github-mcp
hooks:
SubagentStart: check-context.sh
---
You are a code reviewer specializing in security vulnerabilities...
OC 的 Agent 是文件,放在 .agents/ 目录下,运行时扫描和加载。用户可以添加自己的 Agent、修改内置 Agent、通过 Plugin 分发 Agent。这是一个开放的 Agent Registry,CC 的相应机制是封闭的。
5.2 Coordinator vs 无中心化
CC 的 Coordinator 模式有一个明确的中心节点:一个专门的 Coordinator 实例,只负责编排,不干活。这种架构在大型任务上有明显优势——Coordinator 的上下文不会被实现细节污染,始终保持对全局状态的清晰把握。
OC 的多 Agent 场景更多是去中心化的对等协作:多个 Agent 可以通过 Task 系统相互派发任务,没有固定的"谁是 Boss"。这在灵活性上更强,但在任务依赖追踪和失败恢复上更复杂。
5.3 消息传递:task-notification vs 直接 tool_result
CC Coordinator 模式下,Worker 的结果通过 <task-notification> XML 以 user 消息的形式注入 Coordinator 的对话历史。这意味着:
- Coordinator 可以在多个 Worker 同时运行时,按完成顺序处理结果
- 等待中的结果不阻塞其他工作
- Coordinator 的对话历史里是"用户在告知我 Worker 完成了",而不是"工具执行完返回了结果"
OC 更接近传统的同步 tool_result 模型:当 OC 调用子代理时,主循环等待子代理完成,结果作为 tool_result 返回。这在简单场景下更直观,但失去了 Coordinator 那种异步感知多个并行任务的能力。
graph LR
subgraph "CC Coordinator 模型"
C["Coordinator"]
W1c["Worker A"]
W2c["Worker B"]
C -->|"异步派发"| W1c
C -->|"异步派发"| W2c
W1c -->|"task-notification (user msg)"| C
W2c -->|"task-notification (user msg)"| C
C -->|"继续处理,不阻塞"| C
end
subgraph "OC 同步模型"
M["主 Agent"]
S1["子 Agent 1"]
S2["子 Agent 2"]
M -->|"同步等待"| S1
S1 -->|"tool_result"| M
M -->|"串行执行"| S2
end
6. 生命周期:同步 vs 异步
一个子代理究竟运行在父代理的时间线上,还是独立于父代理之外?这个问题决定了用户体验的根本差异。
同步路径(传统方式,isAsync: false):
- 父代理的主循环进入阻塞等待
- 子代理完成后,结果作为
tool_result返回 - 父代理继续下一轮推理
- 用户看到的是:一次工具调用,等待,返回结果
异步路径(现代方式,isAsync: true):
// 父代理不等待,立即返回
void runWithAgentContext(asyncAgentContext, () => runAsyncAgentLifecycle({...}))
return {
status: 'async_launched',
agentId: agentBackgroundTask.agentId,
outputFile: getTaskOutputPath(...)
}
- 子代理独立开一个
AbortController,脱离父代理生命周期 - 用户按 ESC 取消主代理时,后台子代理不受影响
- 子代理的 transcript 写入
outputFile,有结果时再通知
关键阈值:isForkSubagentEnabled() 激活时,所有 subagent 调用都被强制走异步路径(forceAsync = true)。还有 getAutoBackgroundMs() = 120_000:运行时间超过 2 分钟的子代理会被自动转入后台,即使原来是同步派发的。
核心洞察对比
| 设计维度 | Claude Code | OpenClaw |
|---|---|---|
| 子代理定义方式 | TypeScript 硬编码,编译时固化 | YAML Frontmatter,运行时加载 |
| Context 共享策略 | Fork 路径字节级继承 + Prompt Cache 共享 | 每次调用独立系统提示,重新构建 |
| 多 Agent 协作 | Coordinator 模式(中心化编排)+ Agent 团队(进程级) | 对等协作,Task 系统分发 |
| 结果传递 | 异步 <task-notification> user message 注入 |
同步 tool_result 返回 |
| 文件系统隔离 | Worktree 隔离(可选) | 无内置隔离(冲突需用户管理) |
| Token 成本意识 | CLAUDE.md 剥离、git status 剥离、占位符缓存共享 | 各子 Agent 独立 Context,无专项优化 |
| 扩展性 | 封闭内置 Agent 库(Plugin 可扩展) | 开放 Agent Registry(.agents/ 目录) |
源码定位
| 功能 | 文件 | 关键行 |
|---|---|---|
| Fork Agent 定义与递归防护 | AgentTool/forkSubagent.ts |
L32, L78, L107, L171 |
| Fork vs 显性 subagent 路由分支 | AgentTool/AgentTool.tsx |
L318-L356, L483-L541 |
| Coordinator 模式系统提示 | coordinator/coordinatorMode.ts |
L111-L368 |
| CLAUDE.md / gitStatus 裁剪 | AgentTool/runAgent.ts |
L385-L410 |
| 同步/异步生命周期分叉 | AgentTool/AgentTool.tsx |
L686-L800 |
| Agent 团队邮箱通信 | utils/swarm/inProcessRunner.ts |
L689-L800 |
| Explore Agent 定义 | AgentTool/built-in/exploreAgent.ts |
全文 |
| Verification Agent 系统提示 | AgentTool/built-in/verificationAgent.ts |
L10-L129 |
| Worktree 隔离创建 | AgentTool/AgentTool.tsx |
L590-L593 |