Skip to content

Latest commit

 

History

History
160 lines (104 loc) · 6.98 KB

File metadata and controls

160 lines (104 loc) · 6.98 KB

39. Forking、子代理与上下文经济学

所属分卷:卷五「Anthropic Agent 设计研究」

建议前读:11. 子代理与任务系统15. Agent 设计理念研究

建议后读:40. Agent 运行时生命周期:Background 与 Resume

研究问题

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 的中途输出,因为那会把工具噪声重新拉回主上下文。

源码依据

Mermaid 图:fork vs fresh subagent 路径图

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"]
Loading

Anthropic 在这里想解决什么

普通 subagent 很适合做角色分工,比如 ExplorePlanverification。但当父代理已经拿到了大量上下文、又只想把某一段工作静默分叉出去时,重新创建一个 fresh worker 会有两个问题:

  • 需要重新把背景说明一遍,提示词会变长。
  • 新 worker 的 prompt 前缀不再和父线程一致,prompt cache 很难复用。

fork 的设计就是为了解决这个问题。它更像“把当前线程克隆出一个受限工作副本”,而不是“再叫来一个刚进门的同事”。

机制直觉

从实现上看,fork 的关键不是“复制消息数组”,而是尽量让多个 fork child 共享完全相同的请求前缀。这样 Anthropic 可以把 fork 做成一种非常便宜的上下文侧链。

这也是为什么源码反复强调:

  • fork child 要继承父代理的已渲染 system prompt 字节
  • tool result 使用统一 placeholder
  • 只有最后的 directive 文本才允许变化

fork path 和 fresh subagent 的结构性差别

fresh subagent

  • 有明确 agentType
  • 按 agent definition 解析工具、memory、permissionMode、maxTurns
  • 默认从零上下文启动
  • prompt 必须是完整 briefing

fork path

  • subagent_type 省略时触发
  • 使用特殊的 FORK_AGENT
  • tools: ['*'] + useExactTools
  • model: inherit
  • permissionMode: bubble
  • system prompt 不重新生成,而是直接透传父线程的已渲染 prompt

这说明 fork 在设计上更像“同线程分叉”,而不是“角色型 worker”。

buildForkedMessages 到底在做什么

src/tools/AgentTool/forkSubagent.ts 里的 buildForkedMessages() 暴露了一个很关键的设计点:

  1. 保留父 assistant message 的全部内容,包括 tool use、thinking、text。
  2. 为所有 tool use block 统一生成相同的 placeholder tool_result
  3. 再把每个 child 的具体 directive 作为最后一个 text block 追加进去。

这样做的直接效果是:

  • fork child 之间的大部分 API request prefix 完全相同
  • 真正变化的只有最后一段任务指令

这就是 Anthropic 在源码里说的 cache-identical API prefixes

为什么 Anthropic 反复强调“不要偷看”

src/tools/AgentTool/prompt.ts 对 fork 的提示写得很直白:

  • output_file 也不要主动去读
  • 等待完成通知
  • 不要预测 fork 的结果

这不是单纯的 UI 习惯,而是上下文治理策略:

  • 主线程如果频繁读 fork transcript,就会把 fork 的工具噪声重新带回主上下文
  • 这样一来,fork 的意义就被破坏了

换句话说,fork 便宜的前提不是“它自己便宜”,而是“父线程不要把它的中间过程重新吞回来”。

fork child 为什么要禁止再 fork

isInForkChild() 通过检测历史里的 FORK_BOILERPLATE_TAG 来阻止递归 fork。原因不是功能做不到,而是递归 fork 会迅速破坏这条路径的语义:

  • cache-identical prefix 不再稳定
  • 上下文继承链会变复杂
  • 父子边界会变模糊

因此 Anthropic 在这里宁可做硬限制,也不把 fork 变成无限扩展的分形 delegation。

worktree notice 暴露了什么设计假设

buildWorktreeNotice() 会在 fork child 跑在隔离 worktree 时额外注入提醒:

  • 继承的是父代理上下文,不是当前 worktree 的实时文件状态
  • inherited context 中的路径指向父目录
  • child 需要重新读取文件再做修改

这说明 Anthropic 明确认识到“继承上下文”和“继承工作副本”是两件不同的事。fork 不等于共享 live filesystem view。

对 Anthropic agent 设计研究意味着什么

这一套设计非常能说明 Anthropic 对 agent 的看法:

  • delegation 不只是角色问题,也是上下文经济问题
  • 不是所有子代理都应该 fresh start
  • 当上下文已经很贵时,最好的子代理不是“再解释一遍背景”,而是“克隆一个上下文受控的分叉”

这篇的工作结论

fork path 是 Claude Code 里非常有代表性的 agent 设计:它把“子代理”从角色分工问题,推进成上下文成本治理问题。Anthropic 在这里真正优化的不是语法,而是长期会话下的 prompt cache、上下文卫生和任务噪声隔离。

延伸阅读