
本文译自 Earendil Engineering 的 Prompt Caching In Agents,原文标注发布日期为 2026 年 7 月 22 日,由小墨同学翻译整理。下文的“我们”沿用原作者视角。开篇衔接、章节要点、标注为“译者补充”的解释、配图和文末对照表为整理时补充;服务商政策、缓存时长、Pi 功能与费用示例沿用原文描述,不代表本文重新实测。
第一篇讲了 Harness 怎样帮助模型调用工具、完成任务。第二篇接着解释,对话越来越长时,Pi 怎样通过上下文压缩留出空间。
还有一个问题:每次模型请求都要带上大量已有内容,这些内容需要从头再算一遍吗?
提示缓存,让相同输入已经完成的计算有机会被下一次请求复用。 理解它,也就更容易理解 Pi 为什么尽量保持旧记录稳定,以及为什么一条很短的“继续”,有时也会带来不小的输入费用。
人们常常把大语言模型想象成函数:发送一些文本,接收一些文本。这是一个有用的抽象,但它忽略了运行编程 Agent 时最重要的事实之一:绝大部分输入都与上一次相同。换句话说,我们通常只是在末尾追加内容。
编程 Agent 会把系统提示、工具定义、项目说明、对话历史、工具调用和工具结果发送给模型。下一轮,它会再次发送几乎所有这些内容,只多出少量新材料。当会话增长到数万甚至数十万 token 时,每轮都重新计算整段提示会既慢又昂贵。
提示缓存让这种工作方式在经济上勉强可行,但它也十分脆弱。工具定义发生变化、切换模型或提供商改变路由,都可能让原本应该很便宜的增量请求变成对整个上下文的完整重放。
因此,对编程 Agent 来说,缓存行为不只是实现细节或性能优化。它会影响延迟、成本、工具设计、会话设计,甚至影响哪些产品功能应该提供给用户。
KV 缓存里有什么?(What a KV Cache Contains)
- 它保存什么:模型处理已有输入时产生的中间计算状态。
- 怎样发挥作用:后续请求拥有相同前缀时,可以复用对应的计算。
Transformer 处理提示大致分为两个阶段。在 prefill(预填充)阶段,它读取输入 token,并为它们计算注意力状态;在 decode(解码)阶段,它每次生成一个新 token。
在每一层注意力中,每个处理过的 token 都会产生一个 key(键)和一个 value(值)。它们与哈希表里的键值查找并不完全相同:两者都是数字数组,通常由浮点数或低精度量化值组成。处理一个新 token 时,模型会将这个 token 的 query(查询) 与此前的 key 比较,以判断每个早期 token 与当前 token 的相关程度。随后,它根据这些相关性分数,对相应的 value 进行加权混合。从这个意义上说,key 是模型用来匹配的内容,value 是模型取回的信息;不过这种查找是模糊的,并不像字典那样“只返回一个完全匹配的结果”。
这些 key 和 value 会被保留下来,让下一个生成的 token 无需重新计算早期 token,就能关注此前的全部内容。这个保留下来的状态就是 KV 缓存。
译者补充|KV 缓存保存的是计算状态。 模型下一次仍然可以生成新的回答。可以把它理解为:已经读过的相同材料,对应的部分计算可以接着用。这里复用的不是一条现成答案,也不是把这段对话永久写入模型的知识。
从概念上看,一次请求是这样的:
请求 1:
[系统][工具][用户][助手][工具结果][用户]
<-------------------- 预填充 -------------------->
|
每个 token、每一层的 K 和 V 张量
请求 2:
[系统][工具][用户][助手][工具结果][用户][新增]
<------------------ 可复用前缀 ----------------><-->
|
新工作
真实的表示更加复杂,会因模型而异,而且体积“相当”大。最重要的特性是:这些表示对应某一个特定的 token 前缀。两段意思相同但 token 化结果不同的提示,无法共享 KV 缓存。如果中间有一个 token 发生变化,从它之后的所有内容都会成为不同的延续。
提示缓存把这种状态的生命周期延长到一次生成之外。当编程 Agent 的下一次 API 请求以相同 token 开头时,推理系统可以复用与该前缀匹配的既有计算,只对新增后缀执行预填充。理论部分到此为止。

译者示意图:方框表示输入片段,宽度不代表实际 token 数量。缓存仍可用、且满足提供商条件时,相同前缀的计算才有机会被复用;生成新回答仍需要计算。
缓存存在哪里?(Where the Cache Lives)
缓存要发挥作用,就必须存储在某个地方,并且能被再次寻址。推理系统大致有两种方式,让后续请求能够使用 KV 缓存。
比较简单的方式是会话亲和性(session affinity)。它把 KV 缓存保留在最初执行计算的 GPU 上或其附近,并将下一次请求重新路由到同一个工作节点。会话 ID 或提示缓存 key 可以成为一个简单的路由提示,所以这个问题甚至可能只在 HTTP 负载均衡层就得到处理,无需检查请求内容。
请求(session-42) --> 路由器 --> 工作节点 7 --> GPU 7 的 KV 缓存
下次(session-42) --> 路由器 --> 工作节点 7 --> GPU 7 的 KV 缓存
这样可以避免通过网络移动体积很大的缓存。它在正常工作时速度很快,但也限制了调度方式。选中的工作节点可能过载、重启或淘汰对应缓存。路由器也可能认为平衡整个集群的负载比保留某个会话的缓存更重要。不过,这种方案依然很有吸引力,因为它几乎不需要额外的部署基础设施和硬件。
另一种方式是分布式缓存。KV 块可以存放在另一层内存中,或在多个工作节点之间共享,让请求不必过度绑定到某一块 GPU。
译者补充|先记住两种存放方式。 “会话亲和性”尽量让下一次请求回到原来的工作节点;“分布式缓存”让多个工作节点有机会访问已经保存的状态。这些通常是推理服务端的安排,不能只靠修改本地 Pi 设置来决定。
+--------------------+
请求 --> 调度器 -------->| 工作节点 3 / GPU 3 |
| +--------------------+
|
+----------> 分布式 KV 块
|
+----------> 工作节点 9 / GPU 9
这样能提高调度的灵活性和故障恢复能力,但移动、索引和保留 KV 块本身就是一个系统工程问题。不同实现会以各自的方式组合 GPU 内存、主机内存、本地存储、远程存储、前缀感知路由和淘汰策略。
客观来看,KV 缓存虽然很大,但在某些方面又没有想象中那么大。借助各种技巧,即使是很长的对话,也可以把 KV 缓存压缩到几 GB 的量级。
缓存与前缀(Caches and Prefixes)
- 前缀是什么:从输入开头起,连续相同的那一段内容。
- 为什么要看它:会话 ID 相同,并不意味着每次请求都能复用同一份缓存。
Pi 的会话是树,而不是列表。/tree 可以把当前对话移回较早的节点,然后沿着另一条分支继续。回退操作可以丢弃当前活动后缀,却不把它从会话文件中删除。新分支可能与旧上下文共享大部分内容、只共享少量内容,也可能实际上完全不共享。这种设计并非 Pi 独有;不少编程 Agent 至少在概念上有类似机制。即使会话不以树的形式表示,Agent 具有某种回退功能也并不少见。
+-- E -- F 另一条分支
|
会话 S:根 -- A -- B -- C -- D 当前分支
|
+-- Z 靠近起点的分支
这三条分支可以拥有相同的 Pi 会话 ID。从路由器的角度看,它们属于同一个会话;从提示缓存的角度看,它们是三段只共享部分前缀的 token 序列。
如果缓存保留了可复用的前缀块,从 D 跳到 F 时,仍可能复用 根 -> C。但如果缓存只保留最热门的延续、共享块已被淘汰,或请求被路由到了别处,缓存命中就可能大幅减少。跳到 Z 时,即使它从 A 处分叉,也可能只保留系统提示和最初的工具定义。具体缓存管理行为在很大程度上取决于提供商。
反过来的情况也可能发生。/fork 或新会话可以生成新的会话 ID,却带上大量完全相同的上下文。如果路由系统按会话 key 隔离缓存,它可能无法发现这种有用的重叠。
真正决定哪些工作可以缓存的是可复用前缀。会话身份只是在帮助基础设施找到可能相关的内容。在某些系统中,路由 key 对缓存管理至关重要;在另一些系统中,它只是一项优化。
译者补充|会话 ID 像记录编号,前缀是实际内容。 在同一段会话中回到旧节点、走向另一条分支,编号可以不变,但发给模型的内容已经不同。前缀匹配决定哪些计算具有复用的可能,服务商的路由与缓存状态决定这次能否真正用上。
显式缓存与自动缓存(Explicit vs Automatic Prefix Caching)
提供商 API 主要以两种方式开放缓存。
Anthropic 的传统接口使用显式的 cache_control 节点。客户端在系统提示、工具定义或最新可缓存对话内容等稳定部分之后标记边界。服务器随后可以写入或查找截止到这些节点的前缀。边界是显式的,但复用仍然要求它之前的内容完全一致。不仅缓存节点是显式的,价格也是显式的:缓存写入需要付费,你还可以选择不同价格的保留时长。
其他 API 使用自动前缀缓存。客户端按平常方式发送请求,提供商会自行寻找可复用前缀,不需要客户端设置断点。提示缓存 key 或会话标头可能改善路由或分组,但它无法让不同的前缀变成相同。
译者补充|两种方式都要满足内容匹配。 显式缓存由客户端指出希望缓存到哪里;自动缓存由服务商自行寻找可复用的部分。具体的最短缓存长度、有效期和计费规则,仍由各提供商决定。
为什么改工具也会影响缓存?(Why Tool Loadouts Trash Caches)
- 工具也占上下文:工具名称、描述和参数格式,都会作为输入交给模型。
- 变化发生在哪里很重要:若改动出现在对话之前,后面的旧对话也可能需要重新计算。
工具定义通常出现在对话之前,并在模型内部被“折叠”进系统提示。它们的名称、描述和 JSON Schema 与其他文本一样,都是模型输入。增加一个工具、移除一个工具、改变其 Schema,甚至只是换一种顺序序列化工具,都可能让第一次不匹配出现在提示开头附近。
轮次 1:[系统][读取][写入][Shell][对话...........]
轮次 2:[系统][读取][写入][Shell][部署][对话...]
|
旧对话现在位于
不匹配位置之后
在插件系统和 MCP 风格的工具目录中,这种意外很常见。只在工具变得相关时再加载它,听起来很高效,因为一开始需要发送的 Schema 更少。然而,对大多数模型来说,后来扩展工具配置会让它之后的已缓存对话全部失效。节省几个工具 Schema token,可能导致数万个对话 token 被重新处理。

译者示意图:对比的是“修改最初工具定义”的情况。前缀从工具定义处发生变化;后文介绍的增量工具加载,在模型支持时可以避免这类前缀改写。
译者补充|Schema 可以理解为工具的“参数说明书”。 它告诉模型,这个工具需要哪些参数、分别是什么类型。这份说明书本身也属于输入,所以改工具不只是改变能做什么,也可能改变模型看到的上下文。
一些较新的模型 API 支持增量工具加载。工具可以在转录记录中的某个工具结果位置变为可用,而不是被插入最初的工具列表。这样,原有前缀就不会变化:
[系统][初始工具][对话][新工具][下一轮]
<---------- 已缓存前缀 ---------->
现在,Pi 已经在具备原生延迟工具机制的模型上支持这种方式。当扩展(Extension)通过 setActiveTools() 做出纯增量修改时,Pi 会把新加入的工具名称记录在工具结果中。对于支持此功能的 Anthropic 模型,它使用延迟定义和 tool_reference;对于支持此功能的 OpenAI 模型,它会发出相应的工具搜索项。其他模型会采用安全的回退方案:Pi 在下一次请求中发送完整的活动工具列表。功能上仍然正常,但可能清空提示缓存。
“增量”二字十分重要,因为删除工具、用一套工具替换另一套工具,或修改提示片段,仍然会改变早期输入。如果 Extension 重建系统提示、打乱工具顺序、注入时间戳,或每轮都改变活动工具,就可能意外破坏整个会话的缓存。
译者补充|关键是把新内容放在哪里。 在支持这种机制的模型上,Pi 可以把新增工具接在对话中的某个位置,保留此前的输入前缀。是否可用取决于模型 API;这里不能简单理解为“任何工具变化都不会影响缓存”。
Pi 的可扩展性意味着,它无法替每个 Extension 保证缓存稳定。Pi 可以提供对缓存友好的机制,但 Extension 仍需正确使用它们。根据我们的观察,许多 Extension 对缓存效率的重视都不够。其中一部分原因是,使用固定订阅时,缓存未命中所带来的成本并不明显。
暂停之后,缓存还在吗?(Interruptions and TTLs)
- TTL 是什么:Time to Live,缓存的保留时长。
- 容易忽略的地方:会话还开着,不代表提供商仍然保留着缓存。
一些重要的提示缓存默认生命周期很短。Anthropic 默认的五分钟缓存尤其值得注意,因为它短于许多正常的编程活动。如果你使用 Fable 时去喝杯咖啡,十分钟后回来,只发送一句“say hi”,花费可能比预期高得多。
这是因为用户可能认为一个编程会话始终连续活跃,而推理提供商看到的却是一系列相互分离的请求:
模型请求 --> 运行测试 7 分钟 --> 模型请求
此处没有缓存流量
一次很长的构建、一套测试、午餐、会议,甚至只是停下来检查 diff,都可能超过缓存生命周期。下一次请求虽然包含相同提示,但已保存的 KV 状态已经消失,整个前缀会再次按输入计费。

译者示意图:时间沿用原文的示例,仅用于说明等待可能超过缓存保留时长。五分钟不是所有模型和服务商的统一设置;实际续期、淘汰和计费以对应服务为准。
按原文描述,Anthropic 当时不允许 Pi 作为其订阅服务的使用工具,因此我们采用 Anthropic 为 API 用户推荐的五分钟默认值。不过,从 Claude Code 的代码库可以看出,Anthropic 为自己的订阅用户把缓存时长延长到了一小时。但如果你按 API token 价格付费,延长缓存的额外成本往往并不值得。
当然,你也可以主动启用更长的保留时长。Anthropic 等提供商提供了更长的保留控制。使用受支持的直连 API 时,Pi 用户可以设置 PI_CACHE_RETENTION=long 来提出这一请求。但它终究只是一项请求:Pi 无法强制网关保留缓存项,无法阻止内存压力下的淘汰,也无法在没有模型请求时让缓存一直保持活跃。
一次缓存未命中的代价(The Price of a Miss)
提供商通常会对未缓存输入、缓存写入和缓存读取采用不同价格。缓存读取往往有折扣,因为昂贵的预填充工作已经完成。缓存写入可能有溢价,因为提供商承诺为后续使用保留状态。
仍以上面的 Fable 场景为例:设想一次编程会话已经积累了 10 万 token 的历史,之后只发出一条很短的新请求。缓存正常时,几乎全部历史都会按更低的缓存读取价计费,只有少量新材料需要按正常输入价处理,并可能写入缓存。
缓存未命中时,提供商必须按正常输入价重新处理完整的 10 万 token 历史,还可能对把它们重新写入缓存另行收费。因此,缓存过期后,一句 continue 这样的短请求也可能出奇昂贵。在长会话里,重新读取旧输入的成本可能远高于生成下一条回复。
译者补充|聊天框里的字数,不等于这次请求的输入量。 你只写了“继续”,Pi 为了让模型接上任务,还会带上需要的历史。这些历史是按缓存读取价还是普通输入价处理,会影响本次费用。原文的 10 万 token 是说明场景,不是固定收费门槛。
缓存也可能造成不那么直观的激励关系。
用户希望命中率高,因为这样能降低延迟和价格。拥有 GPU 的推理运营商也应该希望命中率高:更少的预填充计算意味着相同硬件能服务更多请求。设计合理的缓存 token 折扣,可以让双方利益一致,同时让运营商获得更好的利润率。
网关或转售商的激励可能不同。如果它从按未缓存费率计费的输入 token 中获得收入,那么缓存未命中可能让客户账单更高。是否也能带来更多利润,则取决于它的上游成本、合同以及由谁运营缓存。在利益没有对齐的技术栈中,负责路由的一方可能不必承担缓存未命中的全部成本,而向用户收费的一方却会在未命中时获得更多收入。
这并不意味着提供商会故意破坏缓存,但它说明缓存表现应该是可观察的。用户不应只能从一张出乎意料的高额账单中推测出了问题。了解缓存是否出现异常,可能是一条重要线索。
严格遵循缓存也意味着,网关在两个轮次之间无法灵活地把请求路由到最佳选项。你可能愿意牺牲一次缓存命中,换用另一个从此以后更经济的模型;也可能最好把负载均衡到另一家提供商。
为什么 Pi 不会频繁删改旧记录?(Why Pi Does Not Prune Aggressively)
- 删减能省什么:后续请求需要携带的 token 变少。
- 还要算什么:前缀改变后,保留下来的内容可能要重新处理。
读到这里,你大概已经明白 Pi 为什么不会剪除工具调用。人们很容易想到通过不断删除旧工具结果或重写历史来控制成本;有时这样做确实必要,尤其是在接近上下文窗口上限时。但正如前面所说,剪枝本身也有缓存成本。
译者补充|“剪枝”在这里指删减已有记录。 例如从历史中移除某段旧工具结果。第二篇讲到的“压缩”则会用摘要等新表示替换部分内容。两种操作都可能改变输入前缀,需要一起考虑上下文空间和缓存代价。
从中间删除内容,会在删除位置改变前缀。它之后仍保留的全部对话都可能需要重新处理。重写一段很长的已缓存上下文所带来的一次性成本,可能超过删除少量低价缓存 token 所节省的未来成本。
粗略的盈亏平衡比较如下:
一次性重写成本
≈ 编辑点之后保留的 token ×(未缓存价格 - 缓存读取价格)
每轮未来节省
≈ 被剪除的 token × 缓存读取价格
译者补充|这组公式是在比较“一次代价”和“后续收益”。 删掉内容后,保留下来的后半段可能先按较高价格重算;后续每一轮才逐渐省下被删部分的读取费用。它是粗略估算,价格应换算到同一 token 单位,并不包含所有缓存写入、输出或路由费用。
这还不只是一个计费问题。旧工具结果经常包含模型后来据以做出决定的证据。删除它们可能让模型表现变差,即使摘要保留了大意也一样。
因此,Pi 偏好稳定、以追加为主的转录记录,不会把每个旧 token 都视为浪费。
当上下文压力足以证明有必要进行有损重写时,可以执行压缩。由于压缩是在有意建立新上下文,而不是意外地对未改变的提示重复计费,因此 Pi 在会话统计中把它视为一次缓存重置,而不是缓存失败。
目标不是让提示尽可能短,而是在模型上下文、缓存复用、延迟和价格之间取得最佳平衡。
与此同时,剪枝有时也有价值。如果你使用的提供商不会因为良好缓存表现而给出折扣,或由于某种原因无法获得高命中率,那么剪枝可能更合适。它也确实能让路由器更容易在不同后端之间均衡负载,因为缓存无法转移。
Pi 能做什么、不能做什么?(What Pi Can and Cannot Do)
- Pi 可以做的:尽量保持输入稳定,并展示缓存使用情况。
- 取决于服务商的:缓存保存多久、请求被分配到哪里,以及缓存是否被提前淘汰。
Pi 会努力让稳定的输入保持稳定。它会传递一致的会话 ID 和提供商特定的缓存提示,在 API 需要时设置显式缓存节点,记录缓存读取和写入用量,并在模型允许时支持锚定在消息位置的增量工具加载。它默认的转录行为也会避免无端重写旧上下文。
请求离开本机以后,Pi 无法控制之后的每一层。它不能决定提供商的淘汰策略,不能把缓存延长到 API 允许的期限之外,不能保证某块 GPU 一直存活,也不能确保网关遵守亲和性路由。它同样无法为被 Extension 改变的前缀保留缓存。
Pi 能做的是让缓存健康状况变得可见。
交互界面的底部状态栏会用 R 和 W 显示累计缓存读取量和写入量,并用 CH 显示最近一次请求的缓存命中率。/session 命令会显示更完整的信息:缓存和未缓存输入总量、累计命中率、成本,以及由显著缓存未命中导致的重复计费 token 和金额估算。
原文中的 /session 统计示例(非本文实测):
消息
总计:178
用户:6
助手:58
工具:114 次调用,114 个结果
Token
输入:7,129,883
已缓存:6,776,832(95.0%)
未缓存:353,051
输出:30,013
总计:7,159,896
成本
总计:$6.054
缓存重复计费:$0.728(161,744 token,2 次未命中)
译者补充|这里的输入量是多次请求累计的。 示例中的约 713 万输入 token,不表示模型一次读取了这么多内容。相同历史在后续请求中反复出现,也会累积进使用统计。“缓存重复计费”是 Pi 的估算指标,应结合实际提供商账单理解。
希望在缓存未命中发生时收到提示的用户,可以在 /settings 中启用 Show cache miss notices(显示缓存未命中通知),对应 settings.json 中的 showCacheMissNotices。之后,Pi 会在发生显著未命中时插入一条警告,其中包含重复计费 token 数量和成本估算。当它能够观察到模型切换,或空闲时间超过通常的短 TTL 时,也会说明原因。对于其他未命中,它只报告事实,不会假装知道提供商内部发生了什么。
缓存表现变差的常见原因(Common Reasons for Worse Cache Performance)
当一个会话的缓存命中率看起来不对时,常见原因包括:
- 空闲。 某个命令、代码审查或对话暂停超过了提供商的保留窗口。
- 切换模型或提供商。 KV 状态与模型绑定,通常不能跨提供商移动。
- 分支导航。
/tree、回退、fork 和替代分支可能改变当前 token 序列,即使会话 ID 没变。 - 压缩或手动重写历史。 这些操作会有意替换部分提示,并建立新的前缀。
- 工具与推理级别变化。 增加、移除、重新排序或编辑工具定义,都会改变请求较早的部分,除非模型支持锚定在消息位置的加载,而且变化是纯增量的。改变推理级别通常也有同样影响。
- 动态系统提示。 时间戳、随机值、不断变化的项目上下文和 Extension 提供的提示片段,可能让它们之后的一切失效。
- Extension 上下文转换。 修改旧消息或提供商请求载荷的 Extension,可能让表面稳定的 Pi 转录记录在实际传输时发生变化。
- 提供商路由与淘汰。 即使提示完全一致,如果请求到达的位置已无法取得对应 KV 块,缓存仍会未命中。
放在一起看
| 概念 | 主要作用 | 一个便于理解的说法 |
|---|---|---|
| 预填充(Prefill) | 读取输入并计算对应的注意力状态 | 先处理这次交给模型的材料 |
| KV 缓存(KV Cache) | 保存已有 token 对应的中间计算状态 | 已经算过的部分先保留下来 |
| 提示缓存(Prompt Caching) | 在后续请求中复用相同前缀的计算 | 相同的开头,有机会接着用 |
| 前缀(Prefix) | 确定从开头起连续匹配的输入范围 | 从第一项开始逐项核对 |
| 保留时长(TTL) | 规定缓存可以保留多久 | 过了期限,旧状态可能已不可用 |
| 缓存未命中(Cache Miss) | 本次请求未能复用所需缓存 | 需要重新处理对应输入 |
| 上下文压缩(Compaction) | 用较短表示替换部分历史,留出空间 | 重整继续工作所需的材料,也会改变前缀 |
译者整理:提示缓存可以减少重复计算,却不会扩大上下文窗口;缓存读取也不等于免费。上下文压缩与提示缓存解决的问题不同,需要配合使用。
三篇放在一起,就能看到 Pi 工作方式中相互关联的几个部分:Harness 组织模型与工具完成任务;上下文压缩让长对话可以继续;提示缓存则帮助重复出现的输入复用计算。
理解 Pi 为什么尽量保持旧记录稳定,也就理解了它怎样在上下文、速度和成本之间做取舍。
本文由 Earendil Engineering 撰写,中文由小墨同学翻译整理。
英文原文 · Prompt Caching In Agents · Pi 学习蓝皮书中文译文
Adapted and translated with permission from Earendil.
中文译文及适配部分经 Earendil 授权按 CC BY 4.0 发布;英文原文版权归 Earendil 所有。封面与三张解释示意图为小墨同学使用 AI 辅助整理制作,原文的文字示意、公式与统计示例保留于对应章节。如有歧义,请以英文原文为准。
