参考:Prompt Caching In Agents

Prompt Caching 的核心不在于“是不是同一个会话”,而在于:新请求的 token 序列,是否与已缓存内容拥有足够长、足够稳定的共同前缀。

image-20260724184135579

LLM 推理框架缓存机制设计视角:树状会话与前缀缓存的矛盾#

会话的真实结构通常是树,而非一条线:

+-- E -- F (另一分支)
|
会话 S: 根节点 -- A -- B -- C -- D (当前主分支)
|
+-- Z (早期分支)

这就给缓存机制带来了两个层面的复杂性:

同一会话内切换分支#

用户从 D 回到 C,再走向 E -> F

根 -> A -> B -> C -> E -> F

从 Router 角度看,这仍是 session S,理论上应具备会话亲和性;但从 Cache 角度看,新路径只与旧路径 根 -> A -> B -> C -> D 共享到 C

因此,实际收益取决于:

如果系统只保留了完整的“根到 D”热路径,或 根 -> C 的共享块已经失效,那么去往 E -> F 的请求只能复用较少内容,甚至完全重新预填充。

从节点 Fork 出新会话#

用户从 B 分叉出新会话:

根 -> A -> B -> Z

即使它使用全新的 Session ID,其文本前缀仍与原会话高度重合。若缓存系统只按 Session ID 隔离,就会把它错误地当作完全新请求,浪费原本可以复用的 根 -> A -> B 计算结果。

结论:缓存复用应以实际 token 前缀和缓存块为依据,而不是仅凭会话 ID。Session ID、会话亲和性和路由策略只能提高命中概率,不能替代对真实 token 前缀的精细管理。

以上揭示了为什么一些顶级的推理框架(如 SGLang 的 RadixAttention)要专门设计更精细的缓存寻址机制。这些问题是无法由 Agent 消费侧的设计来解决的,必须由服务侧解决。

Agent 提示词设计视角#

缓存命中与未命中的成本模型#

一般可概括为:

未缓存输入价 > 缓存写入价 > 缓存读取价

因此,缓存过期、早期前缀变动时,一条很短的后续指令也可能异常昂贵。具体情形如下:

中断与缓存生存时间(TTL)— 缓存过期#

提供商的缓存通常有生命周期限制。例如 ClaudeCode API 默认缓存 TTL 只有约 5 分钟。

用户认为自己处于连续会话中:去喝杯咖啡、等待测试跑完、回来发送一句“继续”。

服务商看到的却是两次独立请求,中间可能已有 7–10 分钟无流量。为了释放资源,底层缓存可能已被回收。

当一个长上下文,例如 10 万 token,缓存过期后,即使用户只发送“继续”两个字,也需要重新处理整段前缀。于是成本和延迟会突然显著上升。

为什么“工具加载”容易毁掉缓存?— 早期前缀变动#

工具定义通常会被展开并放入系统提示词或请求前部。此处一旦发生变化:

第一个不匹配 token 往往会非常靠前。由于 KV Cache 遵循前缀匹配,早期的一点变化会使后面数万 token 的对话历史都无法复用。

这是一种典型的“局部节省、全局亏损”:

解决方向:可加性工具加载#

工具不应总是一次性注入到稳定前缀中,而应支持按需追加:

这样,tool_reference 之前的系统提示、历史对话和已有缓存前缀都保持不变,工具扩展不会破坏已有缓存。

为什么不应对“已发送给LLM并已缓存的数据”进行事后剪枝?— 前缀变动#

为了控制上下文长度和成本,系统常尝试删除旧工具结果、重写历史或压缩中间消息。但如果在历史中间删除或修改内容,删除点之后的所有 token 位置都会改变,后续缓存前缀随之失效。

这会产生一个反直觉结果:

因此,短期内重写长缓存上下文的代价,可能远远高于剪去少量内容所带来的收益。

Pi 的工程策略#

基于“位置即缓存”的原则,Pi 这类应用的工程策略:

Pi 无法控制:

缓存命中率突然下降时的检查清单#

  1. 空闲超时:两次请求间隔超过提供商的缓存 TTL。
  2. 模型或提供商切换:KV Cache 通常与具体模型和服务端实现绑定。
  3. 分支导航:回退、/tree、切换分支或 Fork 改变了实际 token 路径。
  4. 压缩或手动重写历史:主动建立了新的前缀。
  5. 工具或推理等级变化:工具定义、顺序、Schema,或 reasoning 配置改变了请求前部。
  6. 动态系统提示词:时间戳、随机值、动态项目上下文等使每次请求前缀不同。
  7. 扩展的上下文转换:第三方插件修改了消息或网络载荷。
  8. 提供商路由或缓存淘汰:请求到达未持有缓存副本的机器,或副本已被回收。