跳到正文

devlog/token-math/how-cache-hits-change-the-math-for-repeated-system-prompts-1115.md

重复 system prompt 之后:缓存命中怎么改写你的 token 账单

重复 system prompt 的成本被缓存命中率改写

在 Claude Code 或 Codex 里连着改一天代码,你会发现自己反复发同一段东西:角色设定、仓库约定、工具说明、输出格式。这些加起来常常有几千 token,如果每一轮都当新输入计费,哪怕单次不贵,跑完一天也很难看。

缓存命中把这笔钱拆成了三段:第一次写入缓存的 cache write,之后每一轮的 cache read,以及真正新增的用户消息、diff 和工具返回。system prompt 越长且越稳定,cache read 的比重越高,单轮边际成本就越接近只为新说的话付钱。

举个具体的例子:一份 8000 token 的固定 system prompt 连跑 100 轮。如果每次都是全新输入,你要为 80 万 token 的重复内容付钱;一旦命中缓存,同样的内容变成一次 cache write 加 100 次 cache read,账单的形状完全不一样。在 Fluxo 上这一点看得最清楚,因为它按真实 token 计费,cache read 和 cache write 都按实际用量结算,没有月费把差距抹平。

误区一:system prompt 前缀里塞了时间戳和随机 ID

最常见的缓存杀手是把动态内容放在最前面。很多人为了让日志好读,在 system prompt 第一行写当前时间、会话 ID、分支名,甚至随机生成的 trace ID。前缀缓存按内容匹配,开头几个字一变,后面整段全部失效,命中率直接归零。

  • 把时间戳、会话 ID 和随机 trace ID 移出 system prompt,放到请求元数据或消息末尾去。
  • 让 system prompt 的第一个 token 在一整天里保持完全一致,连空白和换行都不许抖动。
  • 把环境信息整理成固定模板,只在末尾的占位段落里填入真正会变化的内容。
  • 把 system prompt 拼接写成纯函数,禁止时钟、随机数和字典迭代顺序参与生成过程。

顺序也一样。哪怕内容完全相同,只要列表顺序因为哈希表迭代而随机漂移,缓存同样不认。把 system prompt 序列化成仓库里的文本资源文件、当成配置维护,比在运行时临时拼装可靠得多。

误区二:消息顺序和工具定义每次请求都在漂移

第二个误区是前缀结构漂移。有人每轮重新生成一遍 system prompt,或者在历史消息前面插入新的系统说明,还有的把工具定义按字母重新排序,结果真正的前缀每次都不同,缓存只能命中最短的那一小段。

  • 把 system prompt 和工具定义固定在请求最前面,之后按时间顺序追加消息,绝不回头插入或重排历史。
  • 新增工具时追加到定义列表末尾,不要为了整洁按名字重排整份工具定义的顺序。
  • 长会话不要每轮重写完整历史,尽量只追加增量,让前缀能一路命中到底。
  • 用固定字段顺序序列化请求体,避免序列化库的随机性把本可命中的前缀打散。

还有一个隐蔽的坑:改完 system prompt 后保留了微小差异,比如改了一个空格、换了一个标点,然后奇怪命中率为什么骤降。前缀匹配不看语义只看字节,所以版本化的 system prompt 应该显式切换,而不是让两个差不多的版本轮流出现。

误区三:只看 cache read 的便宜,忘了 cache write 的账

cache write 本身也要付钱。短会话、一次性脚本、每轮都换 system prompt 的场景,往往写得多读得少,缓存反而变成额外开销,这时候省钱的方向是减少写入,而不是追求命中。

  • 先算盈亏平衡点,只有同一前缀会被重复读取多次时,主动依赖缓存才真正划算。
  • 把稳定的长内容放进 system prompt,把易变的上下文放到消息里追加,减少不必要的写入量。
  • 短平快的一次性任务可以绕开缓存思路,省掉写入开销比省下读取更重要。

工具返回和检索结果经常占掉大量 token。如果把检索结果塞进 system prompt,等于每更新一次文档就重写一次缓存。把这类内容放在消息流里追加,命中表现通常好得多,也更符合 agent 真实的交互形状。

误区四:切换模型时假设缓存行为完全一致

在 Fluxo 上,一个 key 加一个 base URL,也就是 https://api.fluxorelay.com,就能在 Claude、GPT、Gemini、DeepSeek、Kimi、GLM、Qwen、Grok 等模型之间随手切换。切换很顺手,但缓存的生效范围和计费口径并不一定相同。

比如用 Claude Sonnet 5.5 跑 agent 主循环,system prompt 稳定、命中良好;临时把长尾任务切到 DeepSeek V4 Pro 时,前缀结构、缓存生效粒度和读写价格都可能不同,命中率掉下来属于正常现象,而不是你的代码出了 bug。

  • 换模型后先用同一段 system prompt 跑十轮,看清楚命中数据再沿用原来的缓存假设。
  • 不要把某一家的缓存价格直接套到另一家头上,成本估算要按每个模型的实际口径分别做。
  • 在切换点保留一次完整的基线请求,方便对比命中率和单轮成本的真实差异。

好在 Fluxo 按真实 token 计费,cache read 和 cache write 都单独结算,没有月费,所以你能在同一份账单里直接看出哪种模型组合更省钱,而不是靠猜。

误区五:从不回看命中数据,让优化停在猜测

最后一个误区是把缓存当成自动发生的事。优化命中率和优化查询一样,需要数据支撑,否则每次改 system prompt 都像是在盲调。

  • 每次重构 system prompt 之后都记录命中率和单轮成本,把数字写进提交信息或运维笔记里。
  • 为 agent 的不同模式分别维护 system prompt 版本,避免一次改动影响所有场景的命中表现。
  • 命中率突然下跌时先定位前缀第一个不同的 token 在哪里,而不是先怀疑供应商。

把这几条跑通之后,重复 system prompt 就从账单上的固定负担变成了资产:写一次、读很多次,每一轮只为新增的内容付钱。Fluxo 支持 Claude Code、Codex、Gemini CLI 和任何 OpenAI SDK,你邀请的人付费后还能拿到一份推荐分成,这套账可以一直算下去。如果你还没有 Fluxo 账号,现在注册一个,用一把 key 就能复现上面这些测试。遇到具体问题,登录后在联系页面留个言就好。

缓冲区里的下一篇