云厂商负载均衡器悄然忽略的会话亲和性
你的仪表盘显示缓存命中率为 71%。你的财务伙伴很满意。你的 p50 延迟也表现正常。然而,一个来自长时间运行的智能体(agent)会话的客户支持工单传了过来:第 14 轮对话花了 11 秒才产生首个 token,第 15 轮花了 8 秒,第 16 轮花了 9 秒。你调出链路数据(trace)。每一轮对话报告的 cache_read_input_tokens 值都是 0。系统提示词有 1.6 万个 token。用户认为智能体坏了,你认为你的供应商坏了。你们两个都不对。总体的命中率是一个幸存者统计数据 —— 它平均了那些容易命中缓存的短对话,并悄悄吸收了那些在会话中期崩溃为“首轮冷启动”的长对话。
这是任何供应商的复盘报告都不会向你描述的故障模式,因为从他们的遥测数据来看,系统正在按设计运行。负载均衡器正在做出它被要求做出的路由决策。缓存正按照它被要求遵循的时间表进行填充和置换。你传递的提示 —— prompt_cache_key、会话 ID、用户 ID,或者你序列化到该字段中的任何字符串 —— 始终都只是建议性的,而“建议性”意味着“在方便时会被忽略”。在负载压力下、发生扩缩容事件时、上游节点(pod)正在排空时,或者亲和性感知层饱和时,你的提示会悄无声息地降级为均匀的路由决策。请求落在一个冷启动的节点上。原本可以以亚毫秒级成本提供服务的前缀 KV 张量就在 16 英尺外的兄弟机架上,却无法访问。你的对话再次支付了全额前缀成本,而你仪表盘上的标题数字纹丝不动,因为另外 2000 个只有一轮的对话都正常命中了缓存。
提示词不是契约
仔细阅读任何主要供应商的提示词缓存文档。你找不到的一个短语是“会话亲和性保证”。你会看到的是诸如“路由提示”、“增加可能性”、“提高机会”和“可能有所帮助”之类的措辞。OpenAI 的 prompt_cache_key 被记录为一个与前缀哈希结合以影响路由的值;发布的数据显示,采用该功能后,编程类客户的命中率从 60% 攀升至 87%,但文档也指出,对于同一个 key,如果每分钟请求超过大约 15 次,可能会溢出到其他机器,从而降低效果。Anthropic 的缓存保证了缓存存在于具有 TTL 的模型层,但并未说明哪个节点将服务来自同一对话的下一个请求。OpenRouter 的文档明确描述了针对缓存供应商的“粘性路由”层,这等于承认了如果没有供应商之上的显性粘性,你就无法获得它。
这种区别就是问题的全部。亲和性是尽力而为的。负载均衡器将你的提示视为在负载低且容量均衡时的破局点(tie-breaker),用于提高缓存局部性;而在负载高且容量不均时,它将其视为可以丢弃的建议 —— 而这恰恰是你的延迟预算最关键的时候。你的应用程序建立在缓存是对话属性的隐含假设之上。供应商的架构则将缓存视为节点的属性。一旦请求跨越了节点,对话就会再次支付冷前缀成本,而你收到的唯一信号是每个请求的 cache_read_input_tokens 值,而在你的团队中,没有人会在对话粒度上观察这个值。
仪表盘隐藏了核心问题
计算缓存命中率有两种方法,它们给出的数字截然不同。第一种是集群级别的:在一段时间窗口内的所有请求中,用 cache_read_input_tokens 的总和除以可缓存输入 token 的总和。这是你在成本仪表盘、供应商状态页面以及平台团队季度回顾幻灯片上看到的数字。它看起来很棒,因为大多数对话都很短,而短对话要么在系统提示词上命中缓存,要么根本不在乎。
第二种是按对话计算:在单个 conversation_id 内部,第一轮之后的对话中,有多大比例具有非零的缓存读取?这是预测客户痛苦程度的数字,而且几乎没有人计算它。当亲和性提示被遵守时,这个数字在对话的整个生命周期内都保持在接近 100%。当亲和性崩溃时 —— 通常发生在第 6 到第 10 轮左右,当后端扩缩容事件或滚动发布与你的会话相遇时 —— 该特定对话的这个数字会急剧下降,用户会感觉到智能体变慢了,或者“忘记”了它刚刚做过的事情。集群级别的数字没有变化。对话级别的分布则出现了一个重尾。
一个有用的练习:获取一周的链路数据,按 conversation_id 分组,并绘制每个对话“首轮后调用中缓存命中的比例”的直方图。一个健康的系统在 1.0 附近 有一个尖峰。一个退化的系统在同样的地方有峰值,但带有一条向下延伸至 0.0 的长尾,而你的支持工单正是来自那条长尾。该分布的平均值是核心指标。平均值看不见长尾。但客户看得见。
亲和性在负载下是如何失效的
其运作机制至关重要,因为它们限制了你应对问题的手段。服务商的边缘负载均衡器(LB)通过类似于一致性哈希(consistent hashing)的机制,将你的 prompt_cache_key(或其内部对应的标识——如前缀哈希、会话令牌等)映射到某个 pod。当该 pod 状态健康且负载较低时,请求会落到那里并触发 KV 缓存命中。而当该 pod 达到并发上限时,负载均衡器有三种选择,但这三种都会破坏亲和性:
第一种是排队(queueing)。请求会在目标亲和 pod 处等待,直到出现空位。这对你的应用程序是透明的,但表现为首字延迟(time-to-first-token)增加,而缓存读取并未下降。有些服务商会这样做;从你的角度看,这是最不坏的选择,但它最容易破坏服务商自身的延迟 SLO,因此比较少见。
第二种是溢出路由(overflow routing)。负载均衡器会选择另一个不同的 pod 并将请求转发过去。由于新 pod 对于你的前缀是冷缓存,它会重新计算 KV 张量。你的首字延迟会飙升,且该轮对话的 cache_read_input_tokens 会降至零。这是容量压力下最常见的失效模式。
第三种是完全重新平衡(full rebalancing)。在扩缩容事件期间——例如 pod 启动、pod 排空(draining)或区域故障转移——亲和性映射表本身处于变动中。即使是发往“健康”pod 的请求,也可能由不同于一分钟前为你构建缓存的工作节点(worker)来处理,因为该端点背后的工作节点已经更换。这种失效模式会导致多轮对话的冷启动期,而非单次的偶发故障。
从你的应用程序视角来看,这些情况除了表现为“缓存读取字段变为零”之外,没有任何区别。你无法分辨是哪种机制导致了亲和性失效。你同样无法通过重试来解决问题——重新发送相同的请求通常会被路由到相同的路径,因为在重试之间的几毫秒内,负载均衡器对 pod 健康状况的视图不会发生改变。
你实际能做的事
第一个务实的步骤是低成本的:监测每条对话的缓存连续性,并针对尾部指标(tail)而非平均值(mean)设置告警。为每个 trace span 添加 conversation_id 标签,将每轮对话的 cache_read_input_tokens > 0 计算为布尔值,并根据对话长度桶(bucket)展示“缓存命中轮数比例”的 p10 指标(不是 p50,也不是平均值)。当 p10 下降时,说明你的尾部正在扩大,即使你的核心指标看起来没变。这是一个信号,表明服务商的路由行为发生了变化——可能是部署、容量事件或区域性事故。
第二个步骤是显式传递提示词(hint)。令人惊讶的是,许多团队在提示词前缀层面启用了缓存,却从未设置 routing-hint 参数,因为文档将其视为可选。从请求成功的角度看,它是可选的;但从缓存一致性的角度看,它绝非可选。为每条对话传递一个稳定的 prompt_cache_key(或服务商对应的参数)。不要使用随机的单次请求值。除非你的用户一次只进行一条对话,否则不要使用用户 ID。如果可能,直接使用对话 ID。
第三个步骤是考虑你的对话速率限制(rate-limit)交互。OpenAI 的文档指出,针对同一前缀和 Key 组合的请求,如果每分钟超过约 15 次,亲和性就会开始溢出。对于交互式聊天用户来说,这没问题,因为他们永远不会达到这个速率。但对于同时处理多个条目且共用相同系统提示词的批处理智能体(batch agent)来说,这就不妙了,因为你在流量高峰期很容易超过这个阈值。如果你这样做,批处理作业的有效缓存命中率会在你最需要性能的时候遭遇断崖式下跌。将批处理流量分散到多个缓存 Key 可以有所帮助;在服务商限制你之前,先自行限制并发也是一种办法。
第四个步骤是选择具备服务商意识的服务等级。一些服务商提供具有更强亲和性保证的专用容量层(预置吞吐量、专用 pod、企业级端点)。虽然价格反映了这一点,但如果你的客户满意度取决于对话级的 p10 缓存命中率,那么这种容量保证正是你所购买的核心价值,而不仅仅是吞吐量。共享层的亲和性在设计上就是“尽力而为”的,因为服务商需要路由灵活性来吸收其他租户的流量高峰——这意味着你的对话在为你看不见的某个 pod 上的他人流量买单。
不要混淆缓存读取与缓存存在
这里有一个微妙的测量陷阱值得注意。当服务商报告缓存未命中时,并不一定意味着你的前缀缓存已被逐出(evicted)。这通常意味着缓存依然存在,但请求被路由到了一个不存在该缓存的 pod 上。缓存状态是在集群中分片存储的,而“未命中”是单个 pod 的属性,而非全局属性。在未命中的那一刻,你的前缀可能正躺在其他三个 pod 的 GPU 显存里;你只是被路由到了第四个。
这一点很重要,因为面对一波未命中,人们的自然反应是延长 TTL 或购买显式缓存写入功能。但如果问题出在路由而非逐出上,这些手段都无济于事。即使你购买了一小时的 TTL,如果负载均衡器将每一轮对话都发送到不同的 pod,你依然会每轮都错过缓存。诊断“路由引起的未命中”与“逐出引起的未命中”需要要么拥有你无法获得的服务商侧遥测数据,要么观察经验模式:逐出引起的未命中通常与 TTL 边界相关(从会话开始起的 5 分钟或 1 小时的倍数);路由引起的未命中则与服务商侧的容量事件(扩缩容、部署、区域负载偏移)相关,你有时可以从状态页面事故或同一时间段内更广泛的延迟异常中推断出来。
文化层面的修复
“缓存作为会话属性(Cache-as-conversation-property)”是你的团队在早期建立且很少重新审视的心智模型。供应商的文档强化了这一点,因为文档讨论的是前缀和 TTL,而不是 pod 和路由。构建替代的心智模型 —— 缓存作为 pod 属性、亲和性作为尽力而为(Affinity-as-best-effort)、命中率作为分布而非平均值 —— 不需要任何金钱成本,但需要刻意努力去建立。到 2026 年,那些能够解决智能体延迟问题的团队,是已经经历过这类 导致产品崩溃的事故,并围绕“单次会话的连续性”而非“集群层面的平均值”重建了仪表板的团队。
核心指标会欺骗你。即使在没有出现故障的情况下,它也会通过平均掉那些最不满意用户的体验来欺骗你。当出现问题时,它的欺骗性会更加明显,因为故障案例只占总流量的一小部分,而集群数据依然保持平稳。关注长尾,传递提示(Hint),按会话进行测量,并假设负载均衡器在容量紧张的那一刻会背叛你。这一假设是正确的。这就是架构本身;这不是他们会修复的 bug。
- https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- https://developers.openai.com/cookbook/examples/prompt_caching_201
- https://developers.openai.com/api/docs/guides/prompt-caching
- https://openrouter.ai/docs/guides/best-practices/prompt-caching
- https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/prompt-caching
- https://www.pointfive.co/blog/prompt-caching-on-azure-openai-the-token-tax-youre-paying-on-every-call
- https://www.digitalocean.com/blog/advanced-prompt-caching
- https://sankalp.bearblog.dev/prompt-cache/
- https://lovable.dev/blog/routing-billions-of-tokens-per-minute
