Fork subagent 机制
目标
这页回答:
Claude Code 里的 fork subagent 到底是什么?它和普通 subagent 有什么本质区别?
关键文件:
src/tools/AgentTool/forkSubagent.tssrc/tools/AgentTool/AgentTool.tsxsrc/tools/AgentTool/runAgent.tssrc/utils/forkedAgent.ts
一句话结论
fork subagent 不是“选了另一种 agent 类型”的普通子 agent, 而是:
从父 agent 当前上下文里劈出一个分叉 worker,尽可能复用父请求的 cache-critical 前缀,让多个子任务在共享上下文的同时还能吃到 prompt cache。
也就是说,fork 的重点不是“再起一个 agent”,而是:
- 继承父上下文
- 保持 API 前缀尽量字节级一致
- 只在最后一小段 directive 上分叉
1. 什么时候会走 fork?
在 AgentTool.call() 里:
const effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType)
const isForkPath = effectiveType === undefined含义
如果:
- fork feature gate 开启
subagent_type没填
那系统不会默认走 general-purpose,而是进入 fork path。
gate 条件
isForkSubagentEnabled() 里还能看到:
- coordinator mode 下禁用
- non-interactive session 下禁用
所以 fork 不是全局永远开的能力,而是特定交互模式下的实验特性。
2. fork 用的不是普通 agent definition
forkSubagent.ts 定义了一个 synthetic agent:
export const FORK_AGENT = {
agentType: 'fork',
tools: ['*'],
maxTurns: 200,
model: 'inherit',
permissionMode: 'bubble',
...
}这几个字段都很关键
tools: ['*']
表示 fork child 原则上拿全工具面。
但真正关键不是这个,而是后面配合:
useExactTools: true让它直接继承父 agent 的 exact tool pool。
model: 'inherit'
尽量保持和父 agent 一样的模型 / 上下文特性。
permissionMode: 'bubble'
权限提示冒泡到父终端,而不是让 fork child 自己弹 UI。
3. 为什么 fork 要继承父 system prompt?
AgentTool.tsx 里写得很白:
- fork path 不使用
FORK_AGENT.getSystemPrompt() - 而是优先使用
toolUseContext.renderedSystemPrompt - 如果没有,才 fallback 重新计算
原因
因为 fork 的目标之一是:
保持与父请求的 prompt prefix 尽量 byte-identical
如果重新渲染 system prompt,哪怕语义一样, 只要因为 feature flag / GrowthBook 状态变化多了一个字节,cache 就可能 miss。
所以这里直接传父级已经渲染好的 system prompt bytes。
4. buildForkedMessages() 是 fork 的灵魂
这个函数非常关键。
它做的不是简单地给子 agent 一句新 prompt, 而是构造一段特殊消息前缀:
步骤
- 克隆父 assistant message
- 保留其中的全部 content:
- thinking
- text
- 每个
tool_use
- 为每个
tool_use生成一个占位tool_result - 所有占位结果都使用同样的 placeholder 文本:
Fork started — processing in background
- 最后再加一段 per-child directive
结果形态
大致像:
[父 assistant 完整消息]
[一个 user message,其中包含:
tool_result(tool1=placeholder)
tool_result(tool2=placeholder)
...
text(你这次 fork 的具体指令)
]这为什么重要?
因为这样做后:
- 不同 fork child 之间,大部分请求前缀完全一样
- 只有最后 directive 那块不同
这正是 prompt cache 最喜欢的结构。
5. 为什么要给每个 tool_use 补 placeholder tool_result?
这是协议层需要。
Claude 的消息协议要求:
- assistant 发了
tool_use - 后面必须有对应
tool_result
父 assistant message 里保留了全部 tool_use,
那 fork child 这边就必须把这些 tool_result 也补出来。
但这些结果不是要真的重跑父工具, 所以用统一 placeholder 顶上。
这是一举两得
- 协议合法
- 前缀稳定
6. fork child 会收到一段非常强硬的 boilerplate 指令
buildChildMessage() 里写得很凶,核心意思是:
- 你不是主 agent
- 你不要继续 fork
- 你不要聊天,不要解释,不要发散
- 直接用工具做事
- 最后按固定格式简洁汇报
这段指令的意义
fork child 继承了父上下文,甚至可能继承父的“默认倾向继续 fork”。
所以这里必须加一段非常强的 guard rail,明确告诉它:
你就是 worker,不是 orchestrator。
这不只是行为约束,也是防止 fork 递归膨胀的手段。
7. fork 还专门有递归保护
isInForkChild(messages) 会检查消息里有没有 fork boilerplate tag。
而 AgentTool.call() 里也会判断:
toolUseContext.options.querySource === agent:builtin:fork- 或者消息历史里已经出现 fork boilerplate
一旦命中,就拒绝再 fork。
原因
fork child 依然保留 AgentTool,是为了维持工具定义和 prompt prefix 的一致性。
但保留 AgentTool 不代表允许无限递归 fork。
所以要在 call-time 再做守门。
8. fork path 为什么强制 async?
AgentTool.tsx 里有这一句:
const forceAsync = isForkSubagentEnabled();也就是 fork 开启后,agent spawn 会被推向统一的 async / task-notification 模型。
为什么
因为 fork 更像分叉 worker,不适合像同步 subagent 一样卡住主线程等待。
统一 async 的好处是:
- 主 agent 可以继续协调其它工作
- worker 完成后用 task notification 回来
- 交互模型更一致
9. fork path 在 runAgent() 里会启用 useExactTools
AgentTool.tsx 传给 runAgent():
availableTools: isForkPath ? toolUseContext.options.tools : workerTools,
forkContextMessages: isForkPath ? toolUseContext.messages : undefined,
useExactTools: true这三个点要一起看
availableTools = parent exact tools
不是重新 assemble 一套 worker tools,而是直接用父的 exact tool array。
forkContextMessages = parent messages
子 agent 带着父上下文消息一起进来。
useExactTools = true
告诉 runAgent():
- 不要再过滤 / 重建工具池
- thinkingConfig 也尽量继承父级
isNonInteractiveSession也沿用父级
核心目的
还是那个词: cache-identical prefix。
10. fork 与 worktree 也能组合
如果 fork child 还启用了 worktree,系统会额外注入一段 notice:
- 父上下文里的路径是父 cwd
- 你现在在隔离 worktree 里
- 要把路径翻译到自己的 worktree root
- 编辑前最好重新读文件
这说明什么
作者已经意识到 fork child 继承父上下文时, “上下文里的文件路径” 与 “当前实际工作目录” 可能不一致。
所以这里再补一层工作目录认知修正。
11. fork 本质上解决什么问题?
普通 subagent 的特点
- 有自己的 agent type
- 有自己的 system prompt
- 有自己的工具筛选
- 更像独立角色
fork subagent 的特点
- 直接从父 agent 当前上下文分叉
- 尽量共享请求前缀
- 尽量复用 prompt cache
- 更像临时 worker
所以你可以粗暴地记成:
普通 subagent 像“叫来一个专家角色”
fork subagent 像“把当前自己劈成几个并行干活的分身”
12. 这套设计最妙的地方
最妙的不是“能 fork”,而是它把以下几件事绑在了一起:
- 消息协议合法性
- prompt cache 命中率
- 子 worker 行为约束
- 递归保护
- async task 生命周期
也就是说,fork 不是一个孤零零的小 feature, 而是 deeply integrated 到:
- AgentTool
- runAgent
- createSubagentContext
- query / tool protocol
- cache / transcript / task 系统
当前结论
fork subagent 机制的核心不是“省几行 prompt”,而是:
在不破坏消息协议的前提下,把父 agent 当前轨迹尽可能完整地共享给多个 worker,并把分叉成本压到最小、把 prompt cache 命中率拉到最高。
这很像一个为高频并发委派专门优化过的子 agent 模式。
不是花架子,是真有工程味。