区分隐式前缀缓存(OpenAI)与显式块缓存(Anthropic/Google)两种设计,指出迁移方向不对称,显式迁隐式主要是删除工作。
Provider 主流实现约莫两种设计,迁移工作量的大小取决于你正在跨越哪一种。
Provider 会对请求开头部分做 hash,若之前已计算过且匹配则复用。没有 API surface:无需设置字段、无需创建或删除任何东西,唯一的出错可能就是 prompt 的形态。OpenAI 文档描述了这种自动缓存机制,以最小长度以上的共同前缀为匹配依据,命中情况在 usage 对象中回传。函数库在自动 prompt 缓存中覆盖了这一机制。
你需要标记要缓存的内容。Anthropic 的文档描述了 per-block 的 cache_control 标记,每个请求有有限数量的断点,每个断点缓存其之前的所有内容。Google 的 Gemini API 文档则记录了另一种明确的形态:一种你创建并设置 TTL 的 cached-content 资源,随后在后续请求中按 handle 引用。
迁移方向是不对称的。显式到隐式基本就是删除——去掉标记、保留 prompt 结构,如果前缀足够长且稳定,收益仍在。隐式到显式是困难的方向,因为旧 provider 为你做的决定(可复用部分在哪里结束)现在必须由你来做,而且只能放在固定的小数几个位置,一旦做错就什么都缓存不到。
两种体系的所有设计都要求缓存区域在每次请求间 byte-identical。不是语义等价——是字面上完全一致。实际结果是关于顺序的一条规则:每个请求中会变化的部分必须放在不变的部分之后,没有任何例外,因为 prompt 前面哪怕只有一个字节不同,其后的所有内容都会失效。
破坏它的事情几乎总是意外而非有意为之:
系统 prompt 顶部的时间戳或日期。这是最常见的原因。"Today is 12 August 2026, 09:41" 放在系统 prompt 顶部,缓存命中率就是零;精确到天也只会得到一个在午夜重置的命中率。
租户或用户 ID 被插入了序言部分。把它移到最后,或移入 user turn。
工具定义以非确定性顺序序列化。工具 schema 通常是前缀的一部分,如果它们从迭代顺序不稳定的 map 构建,或者被不固定 key 顺序的 JSON 编码器序列化,那么字节在不同进程间就会不同,尽管内容是一样的。排序 key 并显式修复顺序。
检索到的文档顺序不一致。RAG context 天生是可变的;错误在于把它放在静态指令之前而非之后。
少样本示例是每次请求采样的。随机示例选择和缓存直接对立。二选一。
一个有用的迁移前练习耗时一小时:捕获几百个真实的出站请求体,计算它们之间的最长公共前缀长度,然后与目标 provider 的最小可缓存长度比较。如果公共前缀比下限还短,再怎么配置都不会产生命中,工作重心应该放在重构 prompt 而非缓存 API 上。
三个数字共同决定了一个结构上正确的设置是否真正生效,而且这三个数字在不同 provider 和不同模型间都不一样。
最小可缓存长度。缓存不会在低于某个 token 下限时启动。撰写本文时,OpenAI 文档记录了大约一千 token 以上的 prompt 自动缓存,Anthropic 文档记录了按模型不同的最小可缓存前缀长度,较小的模型比较大的模型需要更长的前缀。一个 prompt 在一个 provider 那里轻松超过下限,在另一个那里可能就低于下限。
断点预算。显式设计限制了一个请求可以承载的缓存标记数量。当你从隐式 provider 迁过来时,你没有任何标记,必须选择放置位置:通常一个放在工具定义之后,一个放在静态指令块之后,让可变内容不被缓存。
生命周期。短生命周期缓存——以分钟计,通常每次命中时刷新——相对于你设置的 TTL 显式创建的缓存。这是决定你的流量模式是否受益的关键数字。
本节中的每个数字都是 provider 参数,已经变过不止一次,以后还会变。 sizing 之前先读目标 provider 的缓存参考以了解当前的下限、断点限制和 TTL;下面的算术是跨时间依然成立的部分。
TTL 与到达率的交互值得记录。如果共享前缀的请求以 rate lambda 独立到达,且缓存在最后一次使用后存活 T,那么一个给定请求发现 warm cache 的概率是前一个请求在 T 内到达的概率。在泊松到达假设下——这是个假设,对突发批量流量不成立——结果是:
P(hit) = 1 - exp(-lambda * T)
lambda = 1/minute, T = 5 minutes -> 1 - exp(-5) = 0.993
lambda = 1/minute, T = 1 minute -> 1 - exp(-1) = 0.632
lambda = 1/hour, T = 5 minutes -> 1 - exp(-1/12) = 0.080
第三行是让人中招的那个。低流量租户无论 prompt 结构多完美,从短生命周期缓存中几乎得不到任何好处,这意味着缓存经济是按租户而非按应用计算的。如果你的流量在租户间分布不均,要把忙的和闲的分开建模。
隐式设计通常对缓存的输入打折,不额外收取填充缓存的费用;显式设计通常对写入收取溢价,对读取大幅打折。这是不同的决策问题,而且只有第二个才有盈亏平衡点。
设 P 为可缓存前缀的 token 大小,n 为一个缓存生命周期内使用它的请求数,w 为写入乘数,r 为读取乘数,两者都相对于普通输入价格表示。那么:
uncached cost = n * P
cached cost = w * P + (n - 1) * r * P
cached < uncached when w + (n - 1) * r < n
n * (1 - r) > w - r
n > (w - r) / (1 - r)
with w = 1.25 and r = 0.1 (the multipliers Anthropic documents
at the time of writing for a write and a read):
n > 1.15 / 0.9 = 1.28
so the second read inside the lifetime already pays.
这个结果是有用的,而且它是稳健的:对于任何远低于一的读取乘数和任何接近一的写入乘数,盈亏平衡点在两次读取以下。因此显式缓存的风险不在于溢价太高——而是你写入了永远不会被读取的缓存,因为 TTL 先过期了或者前缀没有你想象的那么稳定。每一次浪费的写入都等于 w 倍普通价格,什么都没换回来。
这就是为什么上面的命中率公式和盈亏平衡公式必须一起用。两者相乘:缓存下每个请求的期望成本是 P * (r * p_hit + w * (1 - p_hit)),且当 w 大于 1 时,存在一个命中率,低于它缓存还不如不缓存。求解得 p_hit > (w - 1) / (w - r),对于上面的乘数约为 0.22。大约低于五分之一的请求命中时,显式缓存反而在烧钱。
因为不会报错,唯一的证据在 usage 对象里,而且字段名各不相同。OpenAI 在 usage.prompt_tokens_details.cached_tokens 下报告缓存的输入 token。Anthropic 报告两个独立字段,usage.cache_creation_input_tokens 和 usage.cache_read_input_tokens,信息量更大——你可以分别看到写入和读取,上面的浪费写入问题可以直接量化为没有匹配读取的写入。
把这个做成断言而非仪表盘。加一个冒烟测试,连续快速发送两次相同 prompt,要求第二次响应报告非零的缓存计数;在每次 prompt 改动后对每个 provider 绑定跑这个测试。一次把可变 token 移到断点上方的 prompt 编辑看起来是一个正常的 commit,却会静默地成倍增加你的输入账单,而这个测试是唯一能在当天捕捉它的东西。
在那里还要检查两件事。缓存按 model string 隔离,所以模型版本升级会使所有缓存失效,新版本头几分钟看起来会很贵——这是预期的,不是回归。而且缓存按组织或项目隔离,所以在多租户系统中共享前缀默认是跨租户共享的,这很高效,但应该是你的数据处理审查有意做出的决定而非继承下来的。