
一、单代理的极限
1.1 问题从"会不会做"变成"怎么分工"
单代理的小矛盾常靠耐心遮过去,但任务一大,研究、实现、验证就挤在同一条上下文链上抢预算、抢注意力、抢叙事中心。多代理看上去是自然答案,实际并不便宜:不加约束的并行只会把单代理的混乱复制几份。真正困难的是隔离各 agent 的不稳定性,同时把结果组织回来。
Claude Code 的源码在这点上很清醒:它没有把 subagent 当成"另一个会说话的窗口",而是当成一段需要明确缓存边界、状态边界、验证职责和清理责任的受管执行流程。
二、Forked Agent 的第一原则:Cache-Safe
2.1 缓存优先
src/utils/forkedAgent.ts 开头有一段注释,非常能说明 Claude Code 对 subagent 的真实理解。它说 forked agent utility 的职责包括:
与父代理共享 cache-critical params(缓存关键参数),确保 prompt cache hit(提示词缓存命中)
跟踪整个 query loop 的 usage(用量)
记录指标
隔离可变状态,防止干扰主循环
这四条里,最先出现的是"共享 cache-critical params"。这并非偶然。它说明在 Claude Code 眼里,fork 是运行时层面的受控分叉。既然是分叉,就必须非常在意哪些参数必须和父请求保持一致,否则 prompt cache 共享就失效,成本和延迟会立刻变坏。
2.2 CacheSafeParams 清单
CacheSafeParams 里明明白白列了这些要素:
-
systemPrompt(系统提示词) -
userContext(用户上下文) -
systemContext(系统上下文) -
toolUseContext(工具使用上下文) -
forkContextMessages(分叉上下文消息)
还专门提醒:别随便改 maxOutputTokens(最大输出令牌数),因为 thinking config(思考配置)也会受影响,而 thinking config 又是 cache key 的一部分。
2.3 运行时经济学
这段设计说明,多代理首先是运行时经济学问题。一个子代理如果每次都把父上下文重新烧一遍 token,看上去像在并行提效,实际只是把浪费并行化。Claude Code 在这个环节先处理的是:怎么 fork 才不把缓存打烂。
2.4 骨架与不变式
python
// 骨架: forkAgent() 核心骨架
params = CacheSafeParams {
systemPrompt, userContext, systemContext,
toolUseContext, forkContextMessages // 必须与父一致,保 prompt cache hit
}
ctx = createSubagentContext(parent):
readFileState := clone(parent.readFileState) // 默认隔离
abortController := new ChildAbortController(parent) // 子控制器挂在父上
getAppState := wrap(parent.getAppState, suppress_prompt)
setAppState := noop // 默认只读
nestedMemoryAttachmentTriggers := new Set()
loadedNestedMemoryPaths := new Set()
三条不变式:
1. child.CacheSafeParams == parent.CacheSafeParams // fork 不破坏 prompt cache 2. child.mutable_state 默认隔离,除非显式 opt-in 共享 // 共享必须显式声明 3. parent.abort ⇒ propagate(child.abort) // 父死子亡
三、状态隔离:子代理首先要减少污染
3.1 默认隔离
forked agent 的第二个关键,在 createSubagentContext()。源码里对它的默认行为写得很直白:默认情况下,所有 mutable state 都隔离,避免干扰 parent。
它默认会做这些事:
-
readFileState(已读取文件状态)先 clone(克隆) -
abortController(中断控制器)生成 child controller(子级控制器),而不是直接共享 -
getAppState(获取应用状态)做包装,让子代理避免 permission prompt(权限提示) -
setAppState(设置应用状态)默认 no-op(空操作) -
nestedMemoryAttachmentTriggers(嵌套记忆附件触发器)、loadedNestedMemoryPaths(已加载的嵌套记忆路径)等集合都重新建
只有在明确 opt-in(选择加入)的情况下,才会共享某些 callback,例如 shareSetAppState、shareSetResponseLength、shareAbortController。
3.2 隔离作为默认伦理
这套设计特别重要,因为它揭示了一个很多人做多代理时都会忽略的事实:子代理最宝贵的地方,在于它可以避免把自己的局部混乱污染主线程。
研究中的误判、临时读到的文件状态、一次性的推理枝杈、正在进行的工具决策,如果全都直接写回主上下文,你得到的只会是更快的脏化。
Claude Code 在这里的态度是:共享要靠明确同意,隔离才是默认伦理。 这种伦理很像数据库事务设计,不像聊天玩具。它不假定"大家都是自己人,状态可以随便串",而是假定"只要是可变状态,就必须先隔离,再决定共享哪些部分。"
四、协调者模式:Synthesis 才是稀缺能力
4.1 Coordinator 的职责
如果只看 src/coordinator/coordinatorMode.ts,你会发现 Claude Code 对 coordinator 的要求很有分寸。它明确说 coordinator 的工作包括:
-
帮用户达成目标
-
指挥 worker 做 research(研究)、implementation(实现)、verification(验证)
-
综合结果并和用户沟通
-
能直接回答的问题就直接回答,不要滥委派
4.2 最关键的一句:Always Synthesize
最关键的一句,在第 5 节 prompt 里:Always synthesize(始终综合)。当 worker 回报研究结果后,协调者必须先读懂,再写出具体 prompt;不要说"based on your findings"(基于你的发现),不要把理解继续外包给 worker。
这句话几乎是多代理的命门。真正稀缺的不是并行输出,而是有人把 worker 的局部知识重新压成清晰、可执行、可验证的下一步;缺这一层,多代理很快退化成礼貌的任务转发机。
Claude Code 在 prompt 设计上要求 research / synthesis 分开、协调者对结果负责,后续 prompt 必须出现具体文件、具体位置、具体变更------研究可以分布式,但理解必须重新收束。
五、验证必须独立成阶段
5.1 四阶段分工
coordinatorMode.ts 还有一段特别值得抄下来。它把常见任务分成:
| 阶段 | 职责 |
|---|---|
| Research(研究) | 探索和收集信息 |
| Synthesis(综合) | 理解并整合结果 |
| Implementation(实现) | 写代码改代码 |
| Verification(验证) | 证明代码有效 |
5.2 验证的目标
并且专门强调:verification 的目标是证明代码有效 ,而不只是确认代码存在。源码里甚至写得近乎不留情面:
-
run tests with the feature enabled(启用功能后运行测试)
-
investigate errors, don't dismiss as unrelated(调查错误,不要当作无关问题)
-
be skeptical(保持怀疑)
-
test independently, don't rubber-stamp(独立测试,不要走形式盖章)
5.3 实现 ≠ 交付
这说明 Claude Code 把验证当成第二层质量关,而不是实现 worker 顺手带的附属动作:prompt 里明确分层为"implementation 自证 + verification 作为独立 QA"。
为什么重要?"我改了代码"和"代码因此正确"之间隔着一条很宽的河,模型尤其擅长在上面搭纸桥------它会给出改动、解释、甚至像样的测试输出,但这些都不等于功能真的在系统里站住。
把 verification 单列,正是为了防止"会改代码"冒充"能交付结果":实现专注于改,验证专门怀疑这些改动配不配活着。
六、Hooks 和任务生命周期:子代理不是扔出去就算了
6.1 生命周期 Hook
多代理系统还有一个很容易被忽略的地方:spawn(生成)只是开头,收尾同样重要。
src/utils/hooks/hooksConfigManager.ts 里定义了 SubagentStart(子代理启动)和 SubagentStop(子代理停止)两类 hook。前者在 subagent 启动时触发,输入里有 agent_id 和 agent_type;后者在 subagent 即将结束时触发,输入里还带 agent_transcript_path(代理转录路径),并允许 exit code 2 把 stderr 反馈给 subagent,继续让它跑。
这说明子代理在 Claude Code 里是显式暴露生命周期节点的系统对象。启动时可以观测,停止前可以介入,转录路径可追踪。这里的重点在于,"子代理结束"也是需要被管理的事件。
6.2 Cleanup 机制
与此同时,src/tasks/LocalAgentTask/LocalAgentTask.tsx 的 registerAsyncAgent() 又展示了另一个层面:每个 async agent 都会注册 cleanup handler(清理处理器),父 abort 可以自动传播给子 abort controller。任务结束后还要 evict output(驱逐输出)、更新状态、解除 cleanup 注册。
这套机制非常像操作系统,不像聊天面板。它关心的核心问题是:
-
这个 agent 是否仍在运行
-
父任务死了它是否该跟着死
-
它的输出文件是否还要保留
-
它的 cleanup callback 有没有泄漏(没有及时清理导致内存或资源无法释放)
很多多代理 demo 都只做到"我能再起一个 agent",Claude Code 至少多做了一步:它把 agent 当作会泄漏资源、会残留状态、会在父进程结束后变成孤儿的运行实体来看待。 这才像是在把代理当系统组件处理。
6.3 子代理生命周期失败矩阵
| 事件顺序 | 前置状态 | 触发 | 下一步 |
|---|---|---|---|
| parent abort + child in-flight | 子任务未完成 | parent abort | 传播 child.abort,等 cleanup,不可留孤儿 |
| child crash | 子代理异常退出 | SubagentStop exit ≠ 0 |
evict output,fire stop hook |
| child timeout | 子运行超限 | registerAsyncAgent 超时 | abort child,写 synthetic result |
| cache key drift | CacheSafeParams 被改 |
fork | 拒绝 fork 或重算 cache |
| shared state 未声明 | opt-in 未开 | child 试图写 setAppState |
no-op,不污染父 |
| cleanup 泄漏 | task 结束未取消 handler | finalize | 强制 unregister + evict |
七、验证不仅针对代码,也针对记忆和建议
7.1 记忆也会过时
多代理与验证并不只发生在 code change 之后。Claude Code 在 memory 体系里也埋了一条很值得注意的原则。
src/memdir/memoryTypes.ts 里专门提醒:memory records can become stale (记忆记录可能过时);在基于 memory 给用户建议之前,要先 verify current state(验证当前状态);如果记忆与现状冲突,要相信眼下读到的真实状态,并更新或删除 stale memory(过时记忆)。
7.2 验证是抵抗时间漂移的习惯
这句话放在多代理章节里,恰好能说明一个更一般的事实:验证是整个系统用来抵抗时间漂移和上下文漂移的基本习惯。
一个系统如果只验证新写下去的代码,却不验证旧记忆、旧假设、旧索引,那它仍然会被历史信息带偏。
从这个角度看,verify 既是一项 skill,也是一种组织纪律。你可以把工作分出去,可以把信息存起来,可以让其他 agent 先跑在前面,但在用户准备据此行动之前,总要有人回到当前现实,重新确认这些东西还是真的。
八、多代理真正解决的是不确定性的分区
8.1 分区的好处
Claude Code 的多代理设计围绕一个朴素目标:给不确定性分区。
-
research worker 在局部上下文里探索
-
implementation worker 专注修改
-
verification worker 专门怀疑
-
coordinator 在中间收束、综合、接口用户
8.2 职责清晰,错误可定位
这种分区的最大好处是职责清晰,错误可定位:
-
research 漏线索
-
synthesis 没吃透
-
implementation 写错
-
verification 放水
每一种都能被指认。反过来,一锅浓汤式的单代理方案出问题只能整体返工。多代理真正有价值的地方,不是并发,而是把不同种类的不确定性关进不同容器,再由 coordinator 组织回来。
九、总结
9.1 核心原则
这一章可以归纳成一句话:
多代理依赖清晰分工:研究、实现、验证和综合各自处在不同约束容器里,最后由协调者把结果重新缝合成可交付结果。
9.2 六个源码位置指向同一结论
| 源码位置 | 结论 |
|---|---|
forkedAgent.ts |
cache-safe 参数、usage tracking 与状态隔离放在第一位 |
createSubagentContext() |
默认隔离 mutable state,只允许显式 opt-in 共享 |
coordinatorMode.ts |
coordinator 必须 synthesize,综合理解不能外包 |
coordinatorMode.ts |
verification 独立成阶段,实现与验证必须角色分离 |
hooksConfigManager.ts |
SubagentStart / SubagentStop 生命周期 hook |
LocalAgentTask.tsx |
parent abort、cleanup、output eviction,生命周期需要回收机制 |
9.3 最终结论
抽成工程原则:
fork 先看 cache 与状态边界,再谈"人格分工";子代理默认隔离,可共享必须显式;研究可委派、综合不可外包;验证必须与实现解耦,否则系统会奖励自证正确;agent 生命周期必须可观测、可中止、可清理;并行的真正价值在于职责更清楚。