EN

第 9 章:分叉智能体与 Prompt 缓存

95% 的洞见

当父智能体并发衍生五个子智能体时,每个子智能体的 API 请求中绝大多数内容都是完全相同的。系统 Prompt 是相同的,工具定义是相同的,对话历史是相同的,触发此次衍生的助手消息(assistant message)也是相同的。唯一不同的仅仅是最终分配的具体指令:“你负责数据库迁移”,“你负责编写测试”,“你负责更新文档”。

在一次典型的已建立充分对话上下文的分叉(Fork)调用中,共享前缀可能高达 80,000 个 Token。而分配给每个子智能体的指令往往只有区区 200 个 Token。这意味着重合度高达 99.75%。Anthropic 的 Prompt 缓存机制为命中缓存的输入 Token 提供了高达 90% 的折扣。如果你能让第 2 至第 5 个子智能体的这 80,000 个 Token 全部稳定命中缓存,你就能将这四次请求的输入成本直接砍掉 90%。对于父智能体而言,在执行完全相同的并行分发任务时,这就是耗费 4 美元与仅耗费 0.50 美元的云泥之别。

但其中的难点在于:Prompt 缓存要求字节级别绝对精确一致(byte-exact)。不是“大致相似”,不是“语义等价”。从系统 Prompt 的第一个字节开始,直到每个子智能体的特有内容发生分歧的前一个字节为止,每一个字符都必须分毫不差地完全匹配。多一个额外的空格、工具定义的顺序调换、一个过期的特性开关改变了系统 Prompt 的某个片段——缓存就会立刻脱靶失效。整个前缀都将被迫按照全额价格重新计算处理。

分叉智能体(Fork Agent)正是 Claude Code 针对这一硬性技术约束给出的精妙答卷。它们绝非仅仅为了“衍生一个携带上下文的子智能体”而提供的普通便利特性——它们是伪装成编排功能的 Prompt 缓存压榨利器。分叉系统中的每一项设计抉择,归根结底都在回答同一个问题:我们如何才能在并行的子智能体之间,绝对保证前缀字节的一致性?


分叉子智能体继承了什么

分叉智能体从其父智能体继承了四样东西,并且是通过引用或字节完全一致的复制来继承的,绝非重新计算派生:

1. 系统 Prompt。 绝非重新生成——而是直接透传(threaded)。父智能体已经渲染好的系统 Prompt 字节串通过 override.systemPrompt 直接注入,其数据来源于 toolUseContext.renderedSystemPrompt。这正是父智能体在最近一次 API 调用中所发送的完全相同的字符串。

2. 工具定义。 分叉智能体定义虽然声明了 tools: ['*'],但在 useExactTools 标志设为 true 的情况下,子智能体直接接收父级组装好的工具数组。没有二次过滤,没有重新排序,也没有重新序列化。

3. 对话历史。 父智能体与 API 交互的每一条消息——用户轮次、助手轮次、工具调用、工具结果——均通过 forkContextMessages 完整克隆至子智能体的上下文中。

4. 思考配置与模型。 分叉定义指定了 model: 'inherit',这会严格解析为父智能体所用的完全相同的模型。相同的模型意味着相同的分词器(Tokenizer)、相同的上下文窗口,以及相同的缓存命名空间。

分叉智能体定义本身极其精简——近乎于一个空操作(no-op):

分叉智能体定义在设计上极尽克制——它从父级继承了一切。它指定了全部工具('*'),继承了父级的模型,采用冒泡模式处理权限(以便授权提示能够直接浮现在父级终端中),并提供了一个根本不会被实际调用的空系统 Prompt 函数——真正的 Prompt 早已通过 override 通道送达,且已预先渲染就绪、字节稳定。


字节完全一致的前缀技巧

向 Claude 发送的 API 请求具备特定的标准结构:系统 Prompt,随后是工具定义,最后是消息列表。为了让 Prompt 缓存生效,从请求起始处直至某个前缀边界为止的每一个字节,在不同请求之间都必须完全相同。

分叉智能体通过确保以下三层完全“冻结”,实现了这一严苛目标:

第 1 层:通过透传而非重算锁定系统 Prompt。

当父智能体的系统 Prompt 为其上一次 API 调用完成渲染时,最终结果被完整捕获在 toolUseContext.renderedSystemPrompt 中。这是历经了所有动态插值之后的最终字符串——包括 GrowthBook 特性开关、环境运行细节、MCP 服务器描述、技能内容以及 CLAUDE.md 文件。分叉子智能体直接原样接收这一完全相同的字符串。

为什么不能直接再次调用 getSystemPrompt()?因为系统 Prompt 的生成过程并不是纯函数。随着 SDK 在后台拉取远程配置,GrowthBook 特性开关会从冷启动状态向热状态渐变。一个在父智能体第一轮返回 false 的开关,到了分叉子智能体启动时可能已经变成了 true。如果系统 Prompt 中包含受该开关控制的条件逻辑块,重新渲染出的 Prompt 只要产生哪怕一个字符的分歧,缓存就会瞬间失效。80,000 个 Token 的全额计算开销,乘以五个子智能体,代价是灾难性的。

透传已渲染的字节串彻底杜绝了此类差异的发生。

第 2 层:通过原样透传锁定工具定义。

常规的子智能体会经过 resolveAgentTools() 处理,该函数会根据智能体定义的 tools 和 disallowedTools 数组对工具池进行过滤,应用权限模式的差异,甚至可能对工具进行重排。由此序列化生成的工具数组将不可避免地与父级产生分歧——不同的子集、不同的顺序、不同的权限元数据注解。

分叉智能体彻底跳过了这一步骤:

const resolvedTools = useExactTools
  ? availableTools  // 父智能体完全一致的数组
  : resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools

useExactTools 标志仅在分叉路径下才会被置为 true。子智能体原汁原味地拿到父智能体的工具池。相同的工具、相同的顺序、相同的序列化输出。这甚至包括在子智能体的工具池中保留 Agent 工具本身,尽管子智能体被明令禁止调用它——如果把它从数组中剔除,就会改变工具数组的序列化结果,从而毁掉缓存。

第 3 层:消息数组的精密构造。

这正是 buildForkedMessages() 发挥其精湛技艺的地方。该函数负责构建位于共享历史记录与每个子智能体特有指令之间的最后两条关键消息:

buildForkedMessages() 函数负责构造介于共享历史与子级独立指令之间的最后两条消息。其算法逻辑为:

  1. 克隆父智能体的助手消息(完整保留所有 tool_use 块及其原始 ID)。
  2. 为每个 tool_use 块创建一个带有固定常量占位字符串的 tool_result(在所有子智能体之间字节完全一致)。
  3. 构建一条包含所有占位结果的单一用户消息,随后附上包裹在样板标签中的子智能体特有指令。
  4. 返回 [clonedAssistantMessage, userMessageWithPlaceholdersAndDirective]。
// 伪代码 — 说明消息构造逻辑
function buildChildMessages(directive, parentAssistant) {
  const cloned = cloneMessage(parentAssistant)
  const placeholders = parentAssistant.toolUseBlocks.map(b =>
    toolResult(b.id, CONSTANT_PLACEHOLDER)  // 在所有子智能体间字节完全一致
  )
  const userMsg = createUserMessage([...placeholders, wrapDirective(directive)])
  return [cloned, userMsg]
}

由此为每个子智能体组装出的消息数组结构如下:

[...共享历史记录, 助手消息(包含全部tool_use), 用户消息(占位tool_result..., 专项指令)]

在专项指令之前的所有元素,在各个子智能体之间都是绝对一致的。FORK_PLACEHOLDER_RESULT——一个常量字符串 'Fork started -- processing in background'(分叉已启动——正在后台处理)——确保了即便工具结果块也能保持字节级别的完全一致。tool_use_id 的值完全相同,因为它们引用自同一个父级助手消息。唯有最后那个包含每个子智能体专项指令的文本块各不相同。

Prompt 缓存的边界恰好卡在最后这个文本块之前。位于其上方的一切内容——涵盖多达数万 Token 的系统 Prompt、工具定义、对话历史以及占位工具结果——在第 2 个及后续所有子智能体请求中,全部能以 90% 的折扣稳稳命中缓存。


分叉样板标签 (The Fork Boilerplate Tag)

每个子智能体的独立指令都被包裹在一个专用的 XML 样板标签中,它承担着双重使命:一方面指导子智能体如何开展行为,另一方面作为检测递归分叉的特征标记。

该样板规范大约包含 10 条铁律,其中最核心的几条包括:

  • 覆盖父智能体的分叉指令。 父智能体的系统 Prompt 中写着“默认优先采用分叉”——而样板标签明确告诫子智能体:“那条指令是给父智能体看的。你自己就是分叉出来的子智能体。绝对禁止再次衍生子智能体。”
  • 静默执行,单次汇报。 在各次工具调用之间不得输出闲聊对话文本。直接调用工具,并在全部完成后输出结构化摘要。
  • 严守任务边界。 子智能体绝不得擅自将工作范围扩大到指令之外。
  • 结构化输出格式。 最终产出必须严格遵循 Scope(范围)/ Result(结果)/ Key files(核心文件)/ Files changed(变更文件)/ Issues(潜在问题)的标准模板,以便父智能体在多个子智能体同时并发汇报成果时能够高效解析。

第 1 条规则尤为耐人寻味。由于缓存命中的刚性需求,分叉子智能体逐字逐句继承了父智能体的系统 Prompt,而其中赫然包含“遇到并行工作时默认优先分叉”的指令。如果子智能体也机械遵循该指令,它就会试图衍生出属于它自己的子智能体,从而引发智能体的无限递归衍生。样板标签以极高的优先级显式进行了纠偏:“那条指令是给父智能体看的。你自己就是分叉智能体。”

结构化输出模板(Scope/Result/Key files/Files changed/Issues)绝非用于美观装饰。它将子智能体的输出严格收束在客观的事实汇报框架内,使得父智能体在五路子智能体同时交卷时,能够以极高的确定性迅速进行结果提取与归纳汇总。


递归分叉预防

分叉子智能体在其工具池中保留了 Agent 工具。它必须这么做——若将其剔除,序列化后的工具数组就会发生变动,进而击穿 Prompt 缓存。但如果子智能体在没有指定 subagent_type 的情况下实际调用了 Agent 工具,分叉逻辑就会被再次触发,从而诞生孙智能体分叉。这个孙智能体将继承更加庞大的上下文(父级 + 子级的对话历史),进而可能派生出重孙智能体,陷入灾难性深渊。

系统设立了两道严密的防线以杜绝此类事件:

首要防线:querySource 检查。 当分叉子智能体衍生时,其 context.options.querySource 被设定为 'agent:builtin:fork'。call() 方法在准许走分叉路径之前会对此进行前置断言:

// 在 AgentTool.call() 中:
if (effectiveType === undefined) {
  // 分叉路径 -- 但我们当前是否已经处于分叉子智能体中了?
  if (querySource === 'agent:builtin:fork') {
    // 拦截拒绝:当前已经是分叉子智能体
  }
}

这是极速生效的快速通道,仅仅涉及针对 options 对象中单个字符串的快速比较。

后备防线:消息扫描。 防范递归分叉采用了双重守卫机制:在衍生时植入的 querySource 标记(快速路径——单次字符串比对),以及作为兜底方案扫描历史消息中是否存在样板 XML 标签。之所以需要后备防线,是因为尽管 querySource 能在 autocompact 自动压缩后存活,但在某些由于调用链路未规范透传 querySource 的极端边缘场景下,基于消息扫描的后备机制能够死守递归红线。这是一种典型的“双保险”策略,相较于意外触发递归分叉导致 API 账单失控爆炸的惨重代价,检查(扫描消息)所付出的微小性能开销完全可以忽略不计。

为什么需要兜底方案?因为 Claude Code 具备 autocompact 自动压缩特性,当上下文过长时它会重写消息数组。虽然 autocompact 会重写消息内容,但在 options 中保留了 querySource。在理论上,仅靠 querySource 就足够严密。但在工程实践中,消息扫描作为后备防线能够捕捉到 querySource 未被严密透传的罕见边缘情况——这种双保险设计极其明智,因为相较于递归失控引发的天价 API 消耗,做一次消息扫描的代价微不足道。


同步到异步的无缝切换

分叉子智能体最初是在前台开始运行的:其生成的消息会实时流式输出到父智能体的终端,而父智能体则处于阻塞等待完成的状态。但如果子智能体执行耗时远超预期怎么办?Claude Code 支持执行中途无缝切换至后台——用户(或自动超时机制)可以将正在前台运行的智能体推入后台,而绝不丢失任何已完成的工作进度。

其内部流转机制异常清晰洗练:

  1. 当通过 registerAgentForeground() 注册前台智能体时,系统会为其同步创建一个后台转移信号的 Promise(background signal promise)。

  2. 父智能体的同步循环会在智能体的消息流与该后台信号之间展开竞速(Promise.race):

while (true) {
  const result = await Promise.race([
    iterator.next(),         // 来自智能体的下一条消息
    backgroundSignal,        // “转入后台”触发信号
  ])
  if (result === BACKGROUND_SIGNAL) break
  // ... 正常处理消息
}
  1. 一旦后台信号被触发,前台迭代器会通过 iterator.return() 优雅终止。这会立刻激活生成器的 finally 块,执行规范的资源清理。

  2. 系统随即以 isAsync: true 衍生出一个全新的 runAgent() 实例,复用相同的 Agent ID 以及截至目前已经累积的全部消息历史。该智能体直接从中断处无缝衔接,继续在后台推进。

  3. 原始的同步 call() 方法随即返回 { status: 'async_launched' },父智能体得以从阻塞中解脱,继续推进主线对话。

没有任何工作会被白白浪费,因为消息历史本身就是智能体的全部状态机。磁盘上的侧链转录本(sidechain transcript)完整记录了该智能体所生成的每一条消息。全新的异步实例直接从该转录本重放状态,并严丝合缝地从同步实例停下的地方接着往下跑。


自动后台化

当环境变量 CLAUDE_AUTO_BACKGROUND_TASKS 或 GrowthBook 标志 tengu_auto_background_agents 处于启用状态时,前台智能体在运行满 120 秒后将被自动推入后台:

在环境变量或特性开关启用时,前台智能体在达到 120 秒阈值后会被自动转入后台。禁用时,判定函数返回 0(不启用自动后台化)。

这是一项兼具交互体验考量与成本权衡的精巧决策。前台智能体会彻底霸占父级终端——在此期间用户无法打字、无法发出新指令,也无法调度其他智能体。两分钟的时间窗口,既足够让智能体同步搞定绝大多数轻量级任务(此时流式输出是极有价值的实时反馈),又能在任务耗时失控前及时释放终端,避免终端沦为人质。

而在分叉实验全面激活的分支下,自动后台化的问题甚至直接被降维解决了:所有分叉衍生在启动的第一秒就直接强制设为异步模式。run_in_background 参数从 Schema 中被彻底抹去。每个分叉子智能体生来就在后台默默运转,在完工时通过 <task-notification> 向上汇报,父智能体自始至终绝不阻塞。


何时不使用分叉

分叉只是系统支持的若干种编排模式之一,并且在以下三种场景下被刻意明确排除:

协调者模式(Coordinator mode)。 协调者模式与分叉模式互斥。协调者拥有一套严整的结构化委托范式:它维护宏观规划,通过精心编排的 Prompt 为 Worker 分配任务,并全程监控实施进度。分叉模式“全盘继承一切”的做法会彻底破坏这套秩序。一个被分叉的协调者会继承父级协调者的系统 Prompt(其内容为“你是协调者,负责指派工作”),导致子智能体也试图指点江山而非挽起袖子写代码。isForkSubagentEnabled() 函数在运行时会优先校验 isCoordinatorMode(),若协调者模式处于激活状态则断然返回 false。

非交互式会话(Non-interactive sessions)。 SDK 与 API 消费者(如 --print 命令行模式、Claude Agent SDK)脱离终端界面运行。分叉模式的 permissionMode: 'bubble' 会将权限确认弹窗冒泡推送到父级终端——而在非交互式模式下根本不存在这个终端。系统并未为此单独开发一套复杂的权限补偿链路,而是简单利落地直接禁用了分叉路径。SDK 消费者转而通过显式声明 subagent_type 来实现子任务调度。

显式声明了 subagent_type。 当模型明确指定了 subagent_type(例如 "Explore"、"Plan"、"general-purpose")时,分叉路径绝不会被触发。分叉机制当且仅当模型省略了 subagent_type 时才会激活。这赋予了模型明确的主动权,能够在“我需要一个拥有专属系统 Prompt 和定制工具集的特化专家”(显式指定类型)与“我需要一个继承全部上下文的克隆体来与我并行协同”(省略类型)之间做出清晰决断。


经济效益

让我们来看一个真实的工程推演场景。一位开发者让 Claude Code 对某个大型模块进行重构。父智能体深度分析了代码库,制定了重构蓝图,并并发分发了五个分叉子智能体:一个更新数据库 Schema,一个重写服务层业务逻辑,一个改造路由层,一个修复破损的单元测试,最后一个负责同步更新类型定义。

在对话推进到这一时刻,共享上下文的规模已经相当可观:

  • 系统 Prompt:约 4,000 Token
  • 工具定义(40+ 个工具):约 12,000 Token
  • 对话历史(先前的深度分析与规划细节):约 30,000 Token
  • 包含五个 tool_use 块的助手消息:约 2,000 Token
  • 占位工具执行结果:约 500 Token

共享前缀总计:约 48,500 Token。分派给各个子智能体的专属指令:约 200 Token。

如果不使用分叉(启动五个独立的智能体,各自带着冷启动上下文和专属的独立系统 Prompt):

  • 每个子智能体都需要完整处理自己的系统 Prompt + 工具列表 + 任务 Prompt;
  • 无法实现缓存共享(系统 Prompt 各异,工具集各异);
  • 综合成本:5 次全额输入 Token 计算开销。

如果采用分叉机制(构建字节完全一致的前缀):

  • 子智能体 1:48,700 Token 按全额计费(首个请求构建缓存);
  • 子智能体 2 至 5:各自的 48,500 Token 享受 90% 缓存折扣(命中缓存)+ 仅对 200 Token 的增量指令支付全额;
  • 子智能体 2 至 5 的等效计费开销:每个仅相当于约 4,850 + 200 = 5,050 Token。

节约的规模随着上下文尺寸的增长以及子智能体数量的增加呈爆发式放大。对于一个积累了 10 万 Token 对话历史的深度会话,若并发衍生 8 个分叉子智能体,通过 Prompt 缓存节省下的费用,相较于无共享模式可以轻松突破 90%。

这正是为什么分叉系统中的每一项技术决策——通过透传取代重新计算、工具列表完全原样透传、采用常量占位结果,甚至在明知子智能体禁止调用的情况下依然将其工具池中的 Agent 保留——都在狂热地追求同一件事:字节完全一致的前缀。每一项决策都在不惜牺牲少许代码纯粹性或静态约束的前提下,换取了 API 实际消耗成本在数量级上的断崖式下降。


架构权衡张力 (Design Tensions)

分叉系统做出了非常鲜明且极端的架构取舍,深入理解这些权衡极具启发意义:

隔离性 vs. 缓存效率。 分叉子智能体继承了一切,包括大量对其当前任务完全无关的陈旧对话历史。一个负责重写测试的子智能体,根本不需要父智能体之前讨论数据库 Schema 细节的 15 轮长篇大论。然而,保留这些消息正是让前缀维持绝对一致的唯一途径。剥离无关历史虽然能腾出宝贵的上下文窗口空间,但代价是彻底击穿缓存。系统的架构赌注是:缓存带来的经济效益,远远凌驾于背负冗余上下文的微小代价之上。

安全性 vs. 缓存效率。 尽管子智能体绝不应该使用 Agent 工具,但该工具依然堂而皇之地留在其工具池中。直接将其剔除在静态安全性上无疑更高(子智能体甚至从物理上连尝试分叉的机会都没有),但这样会直接打破工具数组的序列化特征。样板标签中的铁律警示与运行时的递归守卫,正是为了弥补这一静态缺陷而设立的补偿性控制机制——用运行时的主动防护换取缓存红利。

简洁性 vs. 缓存效率。 占位的工具结果在语义上纯属善意的“谎言”。对于父级助手消息中的每一个 tool_use 块,子智能体看到的统一都是 'Fork started -- processing in background',而完全不顾那些工具调用在物理现实中究竟干了什么。这在工程上是完全行得通的,因为子智能体的独立指令已经明确交代了它的行动目标——它根本不需要回溯父级分发轮次中那些工具调用的真实响应。但这确实意味着子智能体的对话历史在逻辑上是略带虚构的。常量占位符的设计是纯粹为了简洁统一定长以利于缓存,而非追求历史的绝对写实。

上述每一项权衡都清晰地折射出同一种价值观优先级:当你在以数亿计的规模按 Token 为 API 账单付费时,为了博取字节完全一致的前缀,哪怕让软件架构做一些战术性扭曲也是绝对物超所值的。


实践应用:面向 Prompt 缓存效率的设计

分叉智能体的核心设计范式不仅局限于 Claude Code。任何在同一上下文下需要并发分发多个大语言模型(LLM)调用的系统,都可以通过面向缓存友好的请求构造中汲取巨大的经济效益。核心设计原则总结如下:

1. 透传已渲染的 Prompt,切勿重新计算。 如果你的系统 Prompt 中包含任何动态属性——特性开关、时间戳、用户偏好标签、A/B 测试分组——务必在首次渲染后捕获其最终结果,并以传值方式透传给所有子调用。任何重新计算都有可能引入哪怕单字节的分歧风险。

2. 坚决冻结工具定义数组。 如果不同的子任务需要不同的工具子集,意味着你直接放弃了工具定义块的缓存共享红利。更优的架构策略往往是:在向模型暴露时维持完整的全量工具集,转而通过运行时的提示守卫(类似分叉样板中“不要调用 Agent”)或权限拦截层来进行权限收敛,而非在工具声明层面物理剔除。

3. 最大化共享前缀,最小化每个子级的独立后缀。 科学编排你的消息数组,让所有共享的内容全部靠前排列,将每个子任务特有的独立内容严密收拢在最尾端追加。将共享内容与专属内容穿插交织,会彻底粉碎缓存边界。

4. 针对可变内容使用固定的常量占位符。 当请求结构必须对历史工具调用做出回复时,在所有并发子请求间统一采用完全一致的常量占位字符串,而不是填充彼此各异的真实历史数据。

5. 精确测算盈亏平衡点。 缓存共享并非绝对没有代价:子请求背负了更庞大的上下文窗口负担(承载了与自身无关的历史)、需要建立运行时防御机制来替代编译期静态安全,以及系统复杂度有所上升。务必建立精密的量化模型,测算在你的具体并发模式下(衍生多少个子任务、共享前缀的体积究竟有多大),扣除冗余上下文 Token 开销之后,最终是否真正实现了资金的净节省。

分叉智能体系统,本质上是一台无所不用其极的 Prompt 缓存榨取引擎。它直面了每一位构建多智能体系统的工程师迟早都要回答的灵魂拷问:当底层 API 愿意为完全复用的前缀提供高达 90% 的惊人折扣时,你愿意将自己的系统架构重塑到何种极致的程度来揽下这份红利?Claude Code 给出的答案斩钉截铁:不惜一切代价,重塑到底。

Prompt Cache Calculator

Calculate fork agent cache sharing savings.