跳到主要内容

冷缓存、热缓存:为什么你的 LLM 延迟数据在测试环境中具有欺骗性

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的暂存环境显示 p50 延迟为 400ms。你的生产环境仪表盘却显示 1.8 秒。你检查了代码 —— 同样的模型,同样的提示词(Prompt),同样的供应商。部署和发布之间没有任何改动。数据不应该有这么大的分歧,但事实就是如此。

罪魁祸首几乎总是缓存状态。提示词缓存(Prompt caching)—— 大多数团队依赖的最重要的延迟优化手段 —— 在暂存流量模式下的表现与生产流量模式下有着本质的不同。如果你不考虑这种差异,那么你在发布前收集的每一个延迟数据都是虚假的。

你不知道自己正在依赖的缓存

现代 LLM 供应商会进行激进的缓存。Anthropic 对提示词前缀进行缓存,其生存时间(TTL)为 5 分钟,并在每次命中时刷新。OpenAI 会对任何超过 1,024 个 token 的提示词自动进行缓存,其内存留存时间为 5–10 分钟的不活动期,扩展缓存则最长可达 24 小时。Google 则要求至少达到 4,096 个 token 才会激活缓存。

延迟方面的影响是巨大的。对于短提示词(约 1,024 个 token),缓存请求的运行速度快 7%;对于长提示词(150K+ 个 token),速度提升可达 67%。在极端情况下,一个 100K token 的提示词在未缓存时需要 11.5 秒,而在热缓存状态下仅需 2.4 秒 —— 减少了 80%。

问题在于:这些数据描述的是一个完美的“热缓存”状态。在暂存环境中,你运行测试套件,每个请求都会为下一个请求“预热”缓存,前几次之后的每一次测量反映的都是最佳性能。在生产环境中,缓存状态是一个分布 —— 有些请求命中热条目,有些命中冷条目,其比例取决于流量模式,而你无法通过一个仅发送 50 个请求就宣告完成的测试工具来模拟这种模式。

为什么暂存环境会美化数据

暂存环境通过三种方式人为创造了有利的缓存条件。

低流量多样性。 你的测试套件重复发送相同的一小组提示词。每个提示词在第一次调用时预热缓存,随后的每次调用都会受益。在生产环境中,你有成百上千个不同的提示词前缀 —— 其中许多出现的频率太低,以至于在 TTL 过期前无法维持热缓存条目。

可预测的请求时机。 暂存测试是顺序执行或以受控的小规模突发方式执行的。5 分钟的缓存 TTL 永远不会过期,因为你的测试套件在那个窗口期内就完成了。生产环境的流量则是波动且不均匀的。一个在工作时间很热门的提示词前缀在夜间会变冷,而早上的第一个请求将承受完整的未缓存延迟。

单节点路由。 OpenAI 的缓存是针对单机运行的,并在每个前缀每分钟约 15 个请求时开始溢出到其他机器。在暂存环境中,你较低的请求量使所有内容都保留在带有热缓存的一台机器上。在生产环境中,负载均衡将请求分发到多台机器,每台机器都从空缓存开始。那些溢出的请求就是缓存未命中,它们不会出现在你的暂存指标中。

伤及痛处的百分位数

平均值掩盖了损害。你的 p50 看起来可能很合理,因为大多数请求都命中了热缓存。但 p95 和 p99 —— 这些导致用户投诉和 SLO 违规的百分位数 —— 主要是由冷缓存请求主导的。

考虑一个系统,其中 70% 的请求命中热缓存(400ms),30% 命中冷缓存(1.8s)。平均值看起来是 820ms,这可能通过了你的延迟预算。但 p95 是 1.8 秒,如果冷缓存请求还与更长的提示词或更高的供应商负载相关联,p99 可能会更糟。尾部延迟不是噪声 —— 它是一个完全不同的性能状态,暂存环境从未对其进行过演习。

这种效应在 Agent 系统中会成倍放大。一个单 Agent 循环可能会进行 5–15 次 LLM 调用。如果每次调用有 30% 的概率命中冷缓存,那么在 10 步链路中至少有一次调用命中冷缓存的概率超过 97%。Agent 任务的端到端延迟受链路中最差的一次调用支配,而不是平均值。

你需要了解的三个缓存层

生产环境的 LLM 延迟涉及多个缓存层,每一层都有不同的预热特性。

供应商端的提示词缓存。 这是上面讨论的层 —— 由 API 供应商管理的基于前缀的缓存。TTL 从 5 分钟到 24 小时不等,具体取决于供应商和层级。你对缓存驱逐的控制力有限,且缓存状态对你的应用是不可见的。唯一的信号是 API 响应中的 cached_tokens 字段,而大多数团队并不监控它。

GPU 上的 KV 缓存。 对于自托管模型,键值(KV)缓存存储了来自先前 token 的注意力计算结果。冷 KV 缓存意味着模型需要为整个上下文窗口从头开始重新计算注意力。在最大上下文长度下,这就是 1.5 秒与 11 秒以上的区别。虽然像 vLLM 的 PagedAttention 这样的系统将浪费减少到了 4% 以下,但简单实现中的 GPU 显存碎片会浪费 60–80% 的分配 KV 缓存内存。

应用级语义缓存。 如果你构建了一个语义缓存层,为语义相似的查询返回存储的响应,那么它的命中率完全取决于生产查询的分布 —— 这在你进入生产环境之前是无法知晓的。一个在你的评估集上显示 40% 命中率的语义缓存,在面对真实用户流量时可能会下降到 8%。

不会骗人的负载测试

标准的负载测试工具(k6、Locust、JMeter)在处理 LLM 工作负载时会失效,因为它们没有对缓存热力学(cache thermodynamics)进行建模。这里有一种能产生真实数据的方案。

第一阶段:冷启动基准。 在缓存完全冷却的情况下测量延迟。每次只触发一个独特的提示词前缀,并在请求之间留出足够的间隔,确保没有缓存项存留。这能为你提供最坏情况的底线。生产环境中的每个请求都有一定概率触及这个底线,你的系统需要能够处理它。

第二阶段:预热曲线。 以不断增加的时间间隔(1 秒、30 秒、2 分钟、4 分钟、6 分钟)重复发送相同的提示词前缀。绘制 TTFT 与请求间隔的关系图。这能揭示缓存的生存时间(TTL)边界,并让你清楚地看到延迟从“已缓存”跳跃到“未缓存”的具体位置。对于 Anthropic,你会看到在 5 分钟处有一个断崖;对于 OpenAI,这个界限则比较模糊,因为缓存是自动的且淘汰策略不透明。

第三阶段:模拟流量形状的负载测试。 回放类似于生产环境的流量模式,而不是合成的均匀负载。使用来自类似功能或模型的访问日志,根据分析数据来建模请求分布。关键变量包括:独特提示词前缀的数量、这些前缀的频率分布(齐普夫分布 / Zipfian 或其他分布)以及到达间隔时间。像 GuideLLM 这样的工具可以模拟符合这些分布的可配置工作负载。

第四阶段:衡量核心指标。 分别为缓存命中(cache-hit)和缓存未命中(cache-miss)的请求追踪 p50、p95 和 p99。在每个请求中标记其是否接收了缓存的 token(大多数供应商会在响应中返回此信息)。这样你会得到四个数字而不是一个,而且这四个数字都很重要:p50-已缓存、p50-未缓存、p99-已缓存、p99-未缓存。你的 SLO 应该针对预期缓存命中率下的混合分布来设定,而不是仅针对缓存命中的数据。

缩小差距的运维模式

一旦你接受了缓存状态是驱动延迟波动的核心因素,几种运维模式就会变得显而易见。

将缓存命中率作为一级指标进行监控。 从每个 API 响应中提取 cached_tokens 字段,并计算每个提示词模板、每个时间窗口的命中率。当命中率降至基准以下时触发告警 —— 这意味着流量模式发生了偏移,或者供应商更改了某些设置。缓存命中率下降 10% 可能会导致 p95 延迟增加 30% 以上。

在部署后进行预热。 部署通常会重置缓存状态,对于自托管模型尤其如此,但有时 API 供应商也会受到影响(如果你的部署更改了提示词前缀)。在引导生产流量之前,运行一个预热脚本,为最常用的提示词模板发送代表性的请求。这能让前几百名用户从“冷缓存小白鼠”变成享受正常延迟的客户。

为缓存稳定性设计提示词。 供应商缓存需要前缀精确匹配。哪怕一个字符的变化都会使缓存失效。结构化你的提示词,使静态内容(系统提示词、指令、工具 schemas)放在前面,动态内容(用户输入、对话历史)放在最后。避免在可缓存的前缀中注入时间戳、请求 ID 或其他针对每个请求变化的值。研究表明,仅缓存系统提示词能提供最一致的收益 —— 盲目地缓存全部上下文可能会因为动态内容的缓存写入开销,反而出人意料地增加延迟。

为未命中的长尾情况预留预算。 根据冷缓存延迟而非热缓存延迟来设置超时和重试逻辑。如果你的超时设置是根据已缓存的性能调整的,那么冷缓存请求将会超时并重试,这会让你的成本翻倍,而且重试时可能依然会超时。将超时时间设置为 p99-未缓存延迟的 2 倍,比设置为 p50-已缓存延迟的 2 倍是一个更安全的起点。

为交互式和批处理路径设置独立的延迟 SLO。 交互式请求需要严格的 TTFT 预算,因此缓存未命中会带来极大痛感。批处理则可以容忍冷缓存延迟,因为没有用户在等待。如果你的系统混合了这两种请求,单一的 SLO 要么对批处理来说太严(导致误报),要么对交互式来说太松(漏掉真正的性能退化)。

真实的延迟曲线

预发布环境和生产环境之间的延迟差距并非未解之谜,也不是供应商的错。这是在缓存始终处于预热状态且流量始终均匀的环境中衡量性能所产生的必然结果。

在发布之前,获取你的冷缓存数据。测量预热曲线。按缓存状态标记请求并分别追踪百分位。围绕生产流量的真实面貌来构建你的 SLO,而不是基于你的测试套件所呈现的虚假面貌。

最昂贵的延迟惊吓是你在用户投诉中发现的。而成本最低的,则是你在某个周二下午 —— 在第一个真实用户访问端点之前 —— 通过在冷缓存上运行一次诚实的负载测试所发现的。

References:Let's stay in touch and Follow me for more thoughts and updates