托管推理提供商按前缀哈希缓存请求,一旦token流分歧,缓存立即终止且不会重新同步,这是结构性问题而非配置错误。
我的多 Agent 系统没有命中 Prompt Cache。问题出在系统提示词上。
我有一套多 Agent 设置,十个 Agent 同时分析同一份输入。同一个文档,同一份市场数据,一切相同。唯一的区别在于角色设定:每个 Agent 被要求从不同视角来审视这份材料。
十个 Agent,一个共享上下文。这本应是 Prompt Cache 的理想场景——把昂贵的上下文发送一次,全额支付一次,其余九个 Agent 以极低的成本读取。
但我在三成的模型上缓存命中率为零,另外两个模型的命中率也不到百分之七。
我一直在看账单,一直在优化错误的东西。以下是实际发生的情况,因为这个错误是结构性的,我怀疑并非只有我一个人犯。
托管推理提供商会按前缀进行缓存。提供商会从第一个 Token 开始对你的请求进行哈希,然后在已计算过的请求中寻找最长匹配。如果你的请求与近期某个请求的前 3000 个 Token 相同,这 3000 个 Token 就是一次缓存读取,通常比全新读取便宜约五倍。一旦 Token 流出现分歧,缓存就在请求的剩余部分终止——之后不会重新同步。
那个词——前缀——承担了所有工作,而我之前没有仔细考虑过它。
我的调用代码和所有 SDK 文档里的示例一模一样:
messages = [
{"role": "system", "content": agent_persona}, # differs per agent
{"role": "user", "content": shared_context}, # identical for all 10
]
角色设定很短。只有几百个 Token,描述这个特定 Agent 应该如何推理。共享上下文很大:数千个 Token 的源材料。
把这个消息数组理解为一个扁平的 Token 流——这正是提供商的运作方式。流中的第一个东西是角色设定。每个 Agent 的角色设定都不同。所以前缀在第一个 Token 附近就出现了分歧,而后面那数千个 Token 的相同上下文永远无法匹配任何东西。
十个 Agent。同一份上下文复制了十份。十次全价读取。
我不想靠猜,所以我对每次调用的两个部分分别做了哈希,然后统计了每个工作项的不同值数量。
SELECT
COUNT(DISTINCT prompt_sha256) AS distinct_user,
COUNT(DISTINCT system_sha256) AS distinct_system
FROM agent_calls
WHERE item_id = ?
distinct_user = 1
distinct_system = 10
一个用户 Prompt。十个系统 Prompt。昂贵的那个部分在十次调用中是完全一致的,而排在前面便宜的部分每次都不同。
这与提供商自己的使用报告完全吻合——报告把我的支出分成了缓存和未缓存的输入 Token:
那些低的非零数字是无关调用之间的偶然碰撞,而非我本应获得的结构性复用。如果设计正确,每十次上下文读取中应该有九次是缓存命中。
把共享的、昂贵的、完全相同的部分放在前面。把小的、变化的部分放在最后。
messages = [
{"role": "system", "content": shared_context}, # identical -> caches
{"role": "user", "content": f"{agent_persona}\n\n{question}"}, # varies, small, last
]
现在前数千个 Token 对所有十个 Agent 都是相同的。第一个 Agent 全额付费并预热缓存。其余九个以缓存速率读取。唯一未被缓存的部分是末尾那几百个角色设定 Token,而这才是你真正想为之付费的部分。
一条通用规则,我现在认为它应该作为一条设计约束而非优化技巧:
按照从最共享到最具体的顺序排列你的 Prompt。 缓存奖励稳定的前缀,任何早期变化都会毒化其后所有内容。
这也能和你如何批量处理配合工作。如果你连续用同一模型处理多个项目,你会持续命中一个热的前缀。如果你对每个项目轮流使用不同模型,每次调用都会冷启动缓存。按模型分组(而非按工作项分组)可以保持缓存热度。
把角色设定从 system 移到 user 不是没有代价的。
有些模型对 system 指令的权重高于 user 内容。system prompt 的意义往往就在于此。如果我的某个 Agent 被特别指示要论证一个不受欢迎的立场,而我把这个指令从 system 降级到 user,它可能会更加谨慎。我这是在用行为换支出,而且我不一定会注意到,因为输出仍然会是格式良好且合理的。
所以这不是我会在成本论据的推动下直接推向生产的变化。它需要在样本项目上进行 A/B 测试,比较每种布局产生的实际决策,而不只是检查响应是否能正确解析。
有一条中间路径值得先尝试:在 system 中保留一段对所有 Agent 都相同的简短稳定指令,只把每个 Agent 的差异化部分移到 user 消息中。这样你既有共享前缀,又保留了 system 角色的框架。这是否足够,取决于你有多少 Agent 行为依赖于 system 角色——这是关于你的 Prompt 和模型的经验问题。
Prefix 就是前缀。任何早期变化都会破坏其后所有内容的缓存,无论后面跟随多少相同的材料。
要加埋点。对请求的各个组件做哈希,统计每个工作项的不同值数量。我只花了一个查询就把一个模糊的怀疑变成了确定的结构性 bug。
查看你的使用报告中缓存与未缓存的拆分比例。如果一个明显有共享上下文的工作负载的缓存率接近零,这不是定价异常。这是设计 bug,它在告诉你前缀是坏的。
默认的 SDK 消息结构不是缓存友好的。persona 在 system、内容在 user——这是每个教程里的结构。对于多个角色共享一个上下文的扇出工作负载来说,这恰好是错的。
我在检查是否在十次为同一批 Token 付费之前,花了 real effort 去选择更便宜的模型。模型切换是值得做的。但它只是第二大杠杆,而且我先发现了它,因为它正是我在找的东西。