代理中的提示缓存
摘要
本文解释了提示缓存在大语言模型代理中的工作原理,涵盖 KV 缓存机制、预填充和解码阶段,以及其对延迟、成本和代理设计的影响。
<p><a href="https://lobste.rs/s/kq9oh7/prompt_caching_agents">评论</a></p>
查看缓存全文
缓存时间: 2026/07/24 04:58
# 智能体中的提示缓存 | EARENDIL
来源:https://earendil.com/posts/prompt-caching/
大型语言模型常被当作函数来看待:输入一些文本,得到一些文本。这是一个有用的抽象,但它忽略了运行编程智能体时最重要的一个方面:大部分输入和上一次是一样的。换句话说,我们主要是在追加内容。
一个编程智能体会向模型发送系统提示、工具定义、项目指令、对话历史、工具调用和工具结果。在下一轮,它几乎会再次发送所有这些内容,外加少量新内容。一旦一个会话增长到数万或数十万 token,每一轮都重新计算整个提示就会变得既慢又贵。
提示缓存让这件事在经济上变得可行,但它也相当脆弱。一个更改的工具定义、切换模型、或提供商路由决策,都可能把一个本应是廉价的增量请求变成对整个上下文的完全重放。
对于编程智能体来说,缓存行为因此不仅仅是一个实现细节或优化问题。它影响延迟、成本、工具设计、会话设计,甚至影响哪些产品功能应该被提供。
## KV 缓存包含什么
一个 Transformer 处理提示分为两个主要阶段。在**预填充**阶段,它读取输入 token 并计算它们的注意力状态。在**解码**阶段,它逐个生成新 token。
在每个注意力层,每个被处理的 token 都会产生一个键和一个值。它们并不完全像哈希表中的键值查找:两者都是数字数组,通常是浮点数或更低精度的量化数值。当处理一个新 token 时,模型会将这个 token 的**查询**与之前的**键**进行比较,以确定每个之前 token 的相关程度。然后它使用这些相关性分数来形成对应**值**的加权混合。从这个意义上说,键是模型用来匹配的对象,而值则是它检索到的信息(但这种查找是模糊的,而不是像字典查找那样“返回一个完全匹配的结果”)。
这些键和值会被保留,以便下一个生成的 token 可以关注到之前的所有内容,而无需重新计算之前的 token。这个保留的状态就是**KV 缓存**。
概念上,一个请求看起来像这样:
``
请求 1:
[系统][工具][用户][助手][工具结果][用户]
<--------------------- 预填充 -------------------->
|
每个 token 和层的 K 和 V 张量
请求 2:
[系统][工具][用户][助手][工具结果][用户][新内容]
<---------------- 可复用的前缀 ----------------><--->
|
新工作
``
实际的表示更复杂,因模型而异,并且“相当”庞大。重要的特性是它们对应于特定的 token 前缀。两个含义相同但 token 化方式不同的提示不会共享 KV 缓存。如果中间的 token 发生变化,那么该 token 之后的所有内容都是不同的延续。
提示缓存将这个状态的生存期延长到一次生成之外。当下一次来自编程智能体的 API 请求以相同的 token 开始时,推理系统可以重用存储的匹配前缀工作,并只预填充新的后缀。到目前为止,这是理论。
## 缓存的存储位置
为了使缓存工作,它需要存储在某处,并且需要是可寻址的。推理系统使 KV 缓存可供后续请求使用,主要有两种方式。
更简单的方法是**会话亲和性**。它的工作原理是将 KV 缓存保留在计算它的 GPU 上或附近,并将下一个请求路由回同一个工作节点。会话 ID 或提示缓存键成为一个简单的路由提示,因此你甚至可以在 HTTP 负载均衡器层面处理这个问题,而无需查看负载内容。
``
请求(session-42) --> 路由器 --> 工作节点 7 --> GPU 7 KV 缓存
下一个(session-42) --> 路由器 --> 工作节点 7 --> GPU 7 KV 缓存
``
这避免了通过网络移动非常大的缓存。它在有效时速度很快,但会约束调度。选中的工作节点可能过载、重启或驱逐条目。路由器也可能认为平衡整个集群比保留一个会话的缓存更重要。然而,这是一个非常有吸引力的解决方案,因为它只需很少的额外部署基础设施和硬件就能工作。
另一种方法是**分布式缓存**。KV 块可以存储在另一个内存层级中,或跨工作节点可用,这样请求就不会那么紧密地绑定到单个 GPU。
``
+--------------------+
请求 --> 调度器 -->| 工作节点 3 / GPU 3 |
| +--------------------+
|
+----------> 分布式 KV 块
|
+----------> 工作节点 9 / GPU 9
``
这提高了调度灵活性和恢复能力,但移动、索引和保留 KV 块本身就是一个系统问题。实现方式混合使用了 GPU 内存、主机内存、本地存储、远程存储、前缀感知路由和驱逐策略。
从更实际的角度来看 KV 缓存:它们可能很大,但在某些方面比人们想象的要小。通过各种技巧,即使是长时间的对话,KV 缓存的大小也可以减少到几个 GB。
## 缓存与前缀
Pi 会话是树形的,而不是列表。`/tree` 可以将当前对话移回到更早的点,并沿着另一条分支继续。回退可以丢弃当前的后缀,而无需从会话文件中删除。新分支可以共享大部分旧上下文、共享一小部分、或者实际上完全不共享。这种设计并非 Pi 独有,不少编程智能体至少有概念上类似的东西。即使你不将会话表示为树形,智能体拥有某种形式的回退也很常见。
``
+-- E -- F 另一条分支
|
会话 S: 根 -- A -- B -- C -- D 当前分支
|
+-- Z 靠近起点的分支
``
所有三个分支都可以有相同的 Pi 会话 ID。从路由器的角度来看,它们是一个会话。从提示缓存的角度来看,它们是三个 token 序列,只有部分前缀重叠。
如果缓存保留了可重用的前缀块,从 `D` 跳转到 `F` 可能仍然会重用 `根 -> C`。如果它只保留了最热门的延续,如果共享块被驱逐,或者请求被路由到其他地方,命中率可能低得多。跳转到 `Z` 可能只保留了系统提示和初始工具定义,尽管它从 `A` 开始。这里的具体缓存管理行为在很大程度上取决于提供商。
反过来也可能发生。`/fork` 或新会话可以产生新的会话 ID,同时携带大量相同的上下文。一个将会话键隔离缓存的系统可能无法注意到有用的重叠。
可重用的前缀决定了哪些工作可以被缓存。会话身份仅仅帮助基础设施找到可能的内容。在某些系统上,路由键对管理缓存至关重要,而在其他系统上,它仅仅是一个优化。
## 显式与自动前缀缓存
提供商 API 主要以两种风格暴露缓存。
Anthropic 的传统接口使用显式的 `cache_control` 点。客户端在请求的稳定部分(如系统提示、工具定义或最新的可缓存对话内容)之后标记边界。然后服务器可以在这些点写入或查找前缀。边界是显式的,但重用仍然要求边界之前的内容匹配。不仅缓存点是显式的,定价也是显式的。你为缓存写入付费,并且可以选择保留多长时间,这有不同的价格点。
其他 API 使用自动前缀缓存。客户端正常发送请求,提供商找到可重用的前缀,无需客户端放置断点。提示缓存键或会话头部可以改善路由或分组,但不会使不同的前缀变得相同。
## 为什么工具配置会破坏缓存
工具定义通常出现在对话之前,并且在内部被“折叠”到系统提示中。它们的名称、描述和 JSON 模式和其他文本一样是模型输入。添加一个工具、移除一个、更改其模式、甚至以不同顺序序列化工具,都可能使第一个不匹配点靠近提示的开头。
``
第 1 轮:[系统][read][write][bash][对话...........]
第 2 轮:[系统][read][write][bash][deploy][对话...]
|
旧对话现在位于
不匹配点之后
``
这在插件系统和 MCP 风格的工具目录中是一个常见的意外。只在工具相关时才加载它听起来很高效,因为初始发送的模式较少。然而,在大多数模型上,新扩展的负载会使其后缓存的对话失效。节省几个工具模式的 token 可能导致数万个对话 token 被重新处理。
一些较新的模型 API 支持**增量工具加载**。一个工具可以在对话记录中的某个具体工具结果处变得可用,而不是被插入到原始工具列表中。旧前缀保持不变:
``
[系统][初始工具][对话][新工具][下一轮]
<--------- 缓存的前缀 ----------->
``
Pi 现在支持具有原生延迟工具机制的模型。当扩展使用 `setActiveTools()` 进行纯增量更改时,Pi 会在工具结果上记录添加的名称。对于支持的 Anthropic 模型,它使用延迟定义和 `tool_reference`;对于支持的 OpenAI 模型,它会输出相应的工具搜索项。其他模型获得一个安全的回退:Pi 在下一个请求中发送完整的活动工具列表,这在功能上可行,但可能会清除提示缓存。
**增量**这个词很关键,因为移除工具、将一个负载替换为另一个、或更改提示片段仍然会改变早期的输入。一个扩展如果每轮都重建系统提示、打乱工具顺序、注入时间戳或更改活动工具,可能会无意中破坏整个会话的缓存。
可扩展性意味着 Pi 无法代表每个扩展保证缓存稳定性。我们可以提供缓存友好的机制;扩展仍然需要使用它们,而从我们所见,对于许多扩展来说,缓存效率是事后才考虑的事情。这在一定程度上是因为,当你按固定订阅付费时,与缓存未命中相关的成本并不那么明显。
## 中断与 TTL
一些重要的提示缓存的默认寿命很短。Anthropic 默认的五分钟缓存尤其重要,因为它比许多正常的编码活动要短。如果你在使用 Fable 时啜饮一杯咖啡,10 分钟后回来,一条简单的“说嗨”消息将花费比你预期更多的钱。
这是因为,虽然用户可能认为一个编码会话是连续活跃的,但推理提供商看到的是一个独立的请求序列:
``
模型请求 --> 运行测试 7 分钟 --> 模型请求
这里没有缓存流量
``
一次长时间的构建、测试套件、午餐、会议、或者只是停下来审查 diff,都可能使缓存过期。下一个请求包含相同的提示,但存储的 KV 状态已经消失,前缀再次作为输入被计费。
由于 Pi 目前不在 Anthropic 订阅允许的框架内,我们遵循 Anthropic 为 API 用户推荐的 5 分钟默认值。然而,从查看 Claude Code 的代码库我们知道,对于他们自己的订阅用户,他们正在将缓存超时增加到一个小时。但是,当你需要按 API token 价格付费时,这项增加的成本往往并不值得。
但你可以选择加入。一些提供商(如 Anthropic)暴露了更长的保留控制。对于支持的直接 API,Pi 用户可以设置 `PI_CACHE_RETENTION=long` 来请求它们。但这仍然只是一个请求:Pi 不能强制网关保留一个条目、在内存压力下防止驱逐、或者在没有任何模型请求进行时保持缓存活跃。
## 未命中的代价
提供商通常对未缓存的输入、缓存写入和缓存读取采用不同的定价。缓存读取通常有折扣,因为昂贵的预填充工作已经完成。缓存写入可能会产生溢价,因为提供商承诺保留状态以供后续使用。
想象一个包含 100,000 token 历史的编码会话,后面跟着一个像上面 Fable 例子那样的简短新请求。当缓存工作时,几乎所有这些历史都以较低的缓存读取价格计费。只有少量新内容需要以常规输入价格处理,并可能写入缓存。
当缓存未命中时,提供商必须以常规输入价格再次处理整个 100,000 token 的历史。它可能还会收费将该历史写回缓存。这就是为什么一个像 `continue` 这样的简短请求在缓存过期后可能异常昂贵。在一个长时间的编码会话中,重新读取旧的输入可能比生成下一个答案花费更多。
缓存也有可能产生非显而易见的激励。
用户应该希望高命中率,因为这样可以降低延迟和价格。拥有 GPU 的推理运营商也应该希望高命中率:更少的预填充工作意味着用相同的硬件服务更多的请求。一个设计良好的缓存 token 折扣可以使双方利益一致,同时让运营商获得更好的利润率。
网关或转售商可能有不同的激励。如果其收入来自未缓存率计费的输入 token,那么缓存未命中可能产生比命中更大的客户账单。这是否带来更多利润取决于其上游成本、合同以及谁在操作缓存。在一个对齐不良的堆栈中,负责路由的一方可能不承担未命中的全部成本,而向用户开票的一方在未命中发生时获得更多收入。
这并不意味着提供商会破坏缓存,但它意味着缓存性能应该是可观察的。用户不应该只能从一张出奇大的账单来推断。了解缓存是否有异常是一个重要的洞察。
严格的缓存遵守也意味着网关在轮次之间将你路由到最佳选项的灵活性降低。你可能愿意接受一次缓存未命中以继续使用另一个模型,这个模型从那时起可能更经济,或者你可能更愿意将负载均衡到另一个提供商。
## 为什么 Pi 不激进地修剪
既然你已经读到这里,你可能大致明白为什么 Pi 不修剪工具调用。通过不断删除旧的工具结果或重写历史来控制成本是诱人的,有时也是必要的,尤其是在接近上下文窗口限制时。但正如我们所了解的,修剪本身也有缓存成本。
从中间删除内容会改变删除点的前缀。之后所有幸存的对话可能需要重新处理。重写一个长缓存上下文的即时成本可能超过通过删除少量廉价缓存 token 所带来的未来节省。
一个粗略的盈亏平衡对比是:
``
一次性重写成本
~= 编辑后幸存的 token * (未缓存价格 - 缓存读取价格)
每轮未来节省
~= 修剪的 token * 缓存读取价格
``
这不仅仅是一个会计问题,因为旧的工具结果通常包含模型用来做出后续决策的证据。即使摘要保留了要点,移除它们也可能降低行为质量。
因此,Pi 倾向于使用稳定的上下文
相似文章
提示缓存真的能为AI代理节省可观成本吗?
一个实践性讨论,质疑提示缓存是否能为生产环境中的AI代理带来有意义的成本节约,审视了现实因素如缓存命中率、路由策略和规模。
API 中的提示词缓存
OpenAI 推出提示词缓存功能,这是一项自动特性,通过在 GPT-4o、GPT-4o mini、o1-preview 和 o1-mini 模型上重用最近缓存的输入令牌,可将 API 成本降低 50% 并改善延迟。该功能会自动应用于超过 1,024 个令牌的提示词,无需开发者进行集成更改。
解释提示缓存如何在大型语言模型(LLM)中工作,以Claude为案例,详细说明Transformer的KV缓存机制以及在代理工作流中缓存静态前缀的成本效益。
解释提示缓存如何在大型语言模型(LLM)中工作,以Claude为案例,详细说明Transformer的KV缓存机制以及在代理工作流中缓存静态前缀的成本效益。
我构建了面向智能体的提示缓存感知无损压缩
一个专为AI智能体设计的提示缓存无损压缩工具。
我如何在长时间智能体运行中轻松减少约90%的输入token消耗
作者分享了一个实用技巧,通过提示缓存(prompt caching)在长时间智能体运行中将输入token成本降低约90%:将不变文本(系统提示、工具定义、上下文)放在每个提示的开头,以利用LLM提供商的缓存前缀。