所属分卷:卷五「Anthropic Agent 设计研究」
建议前读:11. 子代理与任务系统 或 15. Agent 设计理念研究
Anthropic 为什么把 fork 设计成一条和普通 subagent_type 完全不同的子代理路径?
fork 不是“少写一个参数”的语法糖,而是 Claude Code 为了节省上下文、复用 prompt cache、把中间噪声隔离到侧链而设计的一条特殊执行协议。
这一章专门解释 AgentTool 里两种完全不同的 delegation 路径:
subagent_type明确指定的 fresh subagent- 省略
subagent_type时触发的 fork path
重点不在“怎么调用”,而在“为什么 Anthropic 要把它们建成两种经济模型”。
- fresh subagent 的核心是角色分工;fork 的核心是上下文继承与缓存节约。
- fork child 继承父代理的上下文和已渲染 system prompt,因此 prompt 写法应是 directive,而不是重新讲背景。
- Anthropic 明确不希望主线程频繁读取 fork 的中途输出,因为那会把工具噪声重新拉回主上下文。
- fork feature gate:src/tools/AgentTool/forkSubagent.ts
- AgentTool 路由与执行:src/tools/AgentTool/AgentTool.tsx
- agent tool prompt:src/tools/AgentTool/prompt.ts
- subagent context / cache-safe params:src/utils/forkedAgent.ts
flowchart TD
A["AgentTool"] --> B{"是否提供 subagent_type"}
B -- 是 --> C["fresh subagent"]
B -- 否且 gate 开启 --> D["fork path"]
C --> E["新角色定义 + 新 system prompt"]
C --> F["需要完整背景 briefing"]
D --> G["继承父上下文"]
D --> H["继承已渲染 system prompt"]
D --> I["保留 cache-identical prefix"]
D --> J["placeholder tool_result + directive"]
普通 subagent 很适合做角色分工,比如 Explore、Plan、verification。但当父代理已经拿到了大量上下文、又只想把某一段工作静默分叉出去时,重新创建一个 fresh worker 会有两个问题:
- 需要重新把背景说明一遍,提示词会变长。
- 新 worker 的 prompt 前缀不再和父线程一致,prompt cache 很难复用。
fork 的设计就是为了解决这个问题。它更像“把当前线程克隆出一个受限工作副本”,而不是“再叫来一个刚进门的同事”。
从实现上看,fork 的关键不是“复制消息数组”,而是尽量让多个 fork child 共享完全相同的请求前缀。这样 Anthropic 可以把 fork 做成一种非常便宜的上下文侧链。
这也是为什么源码反复强调:
- fork child 要继承父代理的已渲染 system prompt 字节
- tool result 使用统一 placeholder
- 只有最后的 directive 文本才允许变化
- 有明确
agentType - 按 agent definition 解析工具、memory、permissionMode、maxTurns
- 默认从零上下文启动
- prompt 必须是完整 briefing
subagent_type省略时触发- 使用特殊的
FORK_AGENT tools: ['*'] + useExactToolsmodel: inheritpermissionMode: bubble- system prompt 不重新生成,而是直接透传父线程的已渲染 prompt
这说明 fork 在设计上更像“同线程分叉”,而不是“角色型 worker”。
src/tools/AgentTool/forkSubagent.ts 里的 buildForkedMessages() 暴露了一个很关键的设计点:
- 保留父 assistant message 的全部内容,包括 tool use、thinking、text。
- 为所有 tool use block 统一生成相同的 placeholder
tool_result。 - 再把每个 child 的具体 directive 作为最后一个 text block 追加进去。
这样做的直接效果是:
- fork child 之间的大部分 API request prefix 完全相同
- 真正变化的只有最后一段任务指令
这就是 Anthropic 在源码里说的 cache-identical API prefixes。
src/tools/AgentTool/prompt.ts 对 fork 的提示写得很直白:
- 有
output_file也不要主动去读 - 等待完成通知
- 不要预测 fork 的结果
这不是单纯的 UI 习惯,而是上下文治理策略:
- 主线程如果频繁读 fork transcript,就会把 fork 的工具噪声重新带回主上下文
- 这样一来,fork 的意义就被破坏了
换句话说,fork 便宜的前提不是“它自己便宜”,而是“父线程不要把它的中间过程重新吞回来”。
isInForkChild() 通过检测历史里的 FORK_BOILERPLATE_TAG 来阻止递归 fork。原因不是功能做不到,而是递归 fork 会迅速破坏这条路径的语义:
- cache-identical prefix 不再稳定
- 上下文继承链会变复杂
- 父子边界会变模糊
因此 Anthropic 在这里宁可做硬限制,也不把 fork 变成无限扩展的分形 delegation。
buildWorktreeNotice() 会在 fork child 跑在隔离 worktree 时额外注入提醒:
- 继承的是父代理上下文,不是当前 worktree 的实时文件状态
- inherited context 中的路径指向父目录
- child 需要重新读取文件再做修改
这说明 Anthropic 明确认识到“继承上下文”和“继承工作副本”是两件不同的事。fork 不等于共享 live filesystem view。
这一套设计非常能说明 Anthropic 对 agent 的看法:
- delegation 不只是角色问题,也是上下文经济问题
- 不是所有子代理都应该 fresh start
- 当上下文已经很贵时,最好的子代理不是“再解释一遍背景”,而是“克隆一个上下文受控的分叉”
fork path 是 Claude Code 里非常有代表性的 agent 设计:它把“子代理”从角色分工问题,推进成上下文成本治理问题。Anthropic 在这里真正优化的不是语法,而是长期会话下的 prompt cache、上下文卫生和任务噪声隔离。