infsup

by Zinan Huang 🌸

LLM Prompt Cache 命中率:从 37% 到 97% 的实战手记

2026-07-08


大模型 API 处理一个请求时,最贵的部分是 input tokens——你发给模型的 system prompt、对话历史、工具定义加起来可能有几万 token。每次请求都从头处理一遍,计算量和费用都很可观。

Prompt Cache 是 API 提供商做的优化:如果这次请求的前缀(从第一个 byte 开始的连续相同部分)和上一次请求一致,就直接复用上次的中间计算结果。缓存命中的 token 通常只收正常价格的 10–25%,而且首 token 延迟大幅降低。

这篇文章记录一次实际优化的过程——把一个 agent harness 的 DeepSeek 缓存命中率从 37% 拉到 97%。核心教训只有一个:**字节前缀越长越稳定,命中率越高。**任何破坏前缀稳定性的操作——移动消息位置、重新渲染不变的内容、在中间插入或删除——都会让缓存作废。


一、缓存机制速览

三家主要提供商的做法各有不同:

对于 DeepSeek 和 OpenAI 的自动缓存,优化的核心就是:让请求的字节前缀尽可能长、尽可能不变。

二、一个典型 agent 请求长什么样

一个多轮工具调用的对话请求大致是这样的结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "system": "You are an assistant...",
  "messages": [
    {"role": "user", "content": "写一个 hello.py"},
    {"role": "assistant", "tool_use": [...]},
    {"role": "user", "content": [tool_results...]},
    {"role": "assistant", "content": "完成了"},
    {"role": "user", "content": "再写一个 goodbye.py"}
  ],
  "tools": [...]
}

理论上,system + tools + 已有的对话历史都没变,只在尾部追加了一条新的 user message。前缀应该完全一致,缓存命中率应该很高。

但实际测得的加权命中率只有 37.2%(811 轮实战数据)。

三、37% 的根因

根因一:动态上下文在「漂移」

agent 每轮需要注入一些动态上下文——技能建议、工作记忆、任务检索结果等等。原来的做法是:每次发请求时临时生成一条合成消息,插在最新用户消息前面,请求结束后丢弃。

这意味着什么?

Turn 1 的请求体:

[system] [tools] [volatile_1] [user_1]

Turn 2 的请求体:

[system] [tools] [user_1] [assistant_1] [tool_results] [volatile_2] [user_2]

注意:volatile_1 消失了。user_1 从原来的第 4 个位置移到了第 3 个位置。从这个 byte 开始,所有内容都和上一次请求的对应位置不一样了。

上一轮积累的全部内容——assistant 的回复、tool 的执行结果——全部缓存未命中。每轮只有 system + tools 这个头部(约 3800 tokens)能命中,后面的 messages 数组全部作废。随着对话变长,缓存命中部分的占比越来越低。

根因二:System prompt 的重复渲染

system prompt 里包含一些动态字段(当前工作目录、活跃技能列表、工作记忆等)。原来每轮都重新拼接整个 system prompt。即使内容没变,重新渲染可能导致微小的字节差异(字段顺序、空白字符、时间戳之类),直接破坏掉 system 前缀的缓存。

四、修复方案

修复一:持久化动态上下文

核心思路:动态上下文一旦注入对话,就永久留在那里,不再移动。

把它作为一种特殊的消息类型持久化到数据库。下一轮请求时,这条消息还在原来的位置,字节完全一致。新的动态上下文追加在尾部,而不是替换掉旧的:

Turn 1:

[system] [tools] [snapshot_0] [user_0]

Turn 2(同一轮内的 tool loop,多次请求同一个模型):

[system] [tools] [snapshot_0] [user_0] [assistant_0] [tool_results_0]

snapshot_0 还在原位。整个前缀字节完全一致——缓存全部命中。

Turn 3(新的用户消息):

[system] [tools] [snapshot_0] [user_0] [...] [snapshot_1] [user_1]

新的 snapshot 追加在尾部。旧的一切都没动。对话历史真正做到了 append-only:旧消息永远不动,新消息只在尾部追加。

修复二:System prompt 冻结

system prompt 整个 session 只计算一次,缓存在内存中。每轮请求直接复用同一份 bytes,除非发生了会改变 system 内容的事件(比如加载了新技能)。

这确保 system prompt 在所有请求之间 byte-for-byte 相同,不会因为重复渲染产生意外的字节差异。

修复三:只发增量

如果下一轮的动态上下文和上一轮完全一样(技能建议没变、工作记忆没更新),就不生成新的 snapshot,直接复用上一轮的。只有内容真正发生变化时才追加新 snapshot。

五、修复前后对比

修复后 6 轮实战数据(DeepSeek V4 Flash):

Turn Total Input Cached 命中率
1 3,963 3,840 96.9%
2 4,136 4,096 99.0%
3 4,240 4,096 96.6%
4 4,348 4,224 97.1%
5 4,456 4,352 97.7%
6 4,710 4,608 97.8%

加权命中率从 37.2% 跳到了 97.5%

连 Turn 1 都有 97%——因为 system + tools 的前缀和之前的 session 相同,DeepSeek 的缓存跨 session 也能命中。

六、DeepSeek 与 OpenAI 的差异

同样的 append-only 原则对 OpenAI 也有效,但实测效果不同:

提供商 缓存机制 粒度 实测命中率
DeepSeek 自动前缀匹配 byte 精确 97.5%
OpenAI 自动前缀匹配 128 token 块 78.0%

OpenAI 的 128 token 对齐粒度意味着即使前缀完全 byte 一致,也可能因为对齐边界的原因只命中部分块。此外 OpenAI 的缓存淘汰策略更激进——连续请求之间也会出现命中率波动(66%、79%、98% 三档交替出现)。

一个重要的推论:如果你通过 CLI 中转(比如 codex exec),那 CLI 自己加的 system prompt 你控制不了。 那部分能缓存多少完全取决于 CLI 的实现。只有自己拼 API 请求,才能最大化前缀长度。

七、经济账

以 DeepSeek V4 Flash 的定价为例:

一个 100 轮的 agent session,每轮平均 5000 input tokens:

场景 总 input 缓存命中 未命中 费用
37% 命中率 500K 185K 315K ¥0.334
97% 命中率 500K 485K 15K ¥0.064

费用降低 5.2 倍。对于每天跑几百个 session 的高频 agent 场景,这个差距直接影响运营成本。而且缓存命中的请求首 token 延迟通常减半——省钱的同时还更快。

八、总结:四条工程规则

  1. 对话历史 append-only——旧消息一旦发出就永远不动,新内容只在尾部追加。
  2. System prompt 冻结——计算一次,整个 session 复用,动态内容不放在 system 里。
  3. 动态上下文持久化——不要用「临时注入再丢弃」的模式,注入了就留着,下次 append 新的增量。
  4. 检测回归——hash 每次请求的前 N 个 message 的 bytes,命中率下降时自动报告是哪个组件破坏了前缀。

归根结底就是一句话:字节前缀越长越稳定,命中率越高。


「凡事豫则立,不豫则废。言前定则不跲,事前定则不困。」——《礼记·中庸》

“In all things success depends upon preparation; without preparation there is always failure. If what is to be spoken be previously determined, there will be no stumbling.” — Book of Rites, Doctrine of the Mean