你的 AI 功能无法使用 CDN 边缘缓存,因为响应因用户而异
产品团队将新 AI 摘要器的 SLO 设置为 200ms TTFB,因为这是产品其他部分在 p50 下的表现。会议上没人问这 200ms 是怎么来的。它源于十年来通过 CDN 边缘缓存提供的静态资源和 JSON 响应,其缓存命中率为 85%,大多数请求从未到达源站,即便到达了,数据量也很小。而这个摘要器是针对每个用户的,每次调用都是重新生成的,且每次请求都要经过“边缘 → 源站 → 模型提供商”的路径。从第一天起,这个 SLO 在结构上就是无法实现的。团队在第六周才发现这一点,而此时仪表盘已经红了整整六周。
这是 AI 功能发布中反复出现的一种模式。组织在某种物理规律基础上建立的延迟标准,被一个遵循完全不同物理规律的功能所继承。于是,继承目标与可实现底线之间的差距,变成了一个长达数月的缓解项目,而不是第 0 天的设计约束。数字并不关心你是否出于诚意与客户协商了该 SLO。
十年静态资源带来的隐藏馈赠
那些默默无闻的产品工程师们习以为常的缓存命中率,并不是应用程序的属性,而是工作负载的属性。静态资源在不同用户之间是字节一致的。目录页面的 API 响应对于未登录用户是字节一致的,并且可以在已登录用户的不同群体之间共享。CDN 距离用户仅几毫秒之遥,处理了绝大多数请求,仅将极少数长尾请求转发至源站。团队将边缘延迟视为典型情况,因为对于这种工作负载而言,事实确实如此。
这种馈赠背后的数据差异巨大。在典型的设置中,缓存内容的 TTFB 约为 37ms,而未缓存内容则为 136ms —— 在你进行任何处理之前,缓存未命中就已经带来了大约 73% 的延迟惩罚。这还仅仅是“缓存未命中但源站响应迅速”的情况。当源站本身需要向数百毫秒之外的模型提供商发起请求时,延迟惩罚就不再是三倍,而是十倍甚至更多。
关键之处在于,这种性能表现从来不是应用程序团队凭实力赢得的,它是 Web 构建方式带来的隐藏补贴。静态资源之所以可缓存,是因为 URL 是稳定的标识符且字节是确定的。JSON 目录响应之所以可缓存,是因为相关输入是粗粒度的 —— 语言区域、地区,或者某个 A/B 测试桶 —— 以这些字段为键的缓存命中率很高。在此基础上构建的团队从未需要将可缓存性视为自己负责维护的属性。它是开箱即用的。
个性化的 AI 功能并不具备这些特性。Prompt 前缀包含了用户的历史记录、偏好以及最近的操作。输出是生成的而非检索的,且模型是一个无状态函数,其输入分布在本质上是每次请求都唯一的。由于结构如此,每次请求都会导致缓存穿透。团队继承了 SLO,却没有继承背后的物理规律。
属于另一套技术栈的延迟预算
这里被忽略的准则是:当可缓存模型发生变化时,应重新制定延迟预算。在设计阶段进行这种沟通可能会令人不快,因为它迫使你与批准面向客户 SLO 的人重新谈判,但这是成本最低的时机。六周后,当仪表盘一片通红且合同义务已进入法律程序时,同样的沟通将耗费一个季度的时间。
这里失效的思维模型是将 SLO 视为一个可移植的目标,而不是产生它的工作负载的属性。缓存目录响应的 200ms TTFB 与每个用户生成的摘要的 200ms TTFB 根本不是一回事。前者衡量的是 CDN 边缘与用户的距离;后者则衡量模型生成一个在请求到达前并不存在的响应的首个 Token 的速度。这两个数字恰好单位相同,但它们属于不同的问题领域。
在任何 AI 功能发布之前,一个值得思考的问题是:考虑到该功能本身的可缓存性(而非其相邻页面的可缓存性),它能达到的延迟底线是多少?底线不是平均值。底线是不可削减的最小值:无法压缩的网络往返、无法超越的模型 TTFT,以及任何同步设置。如果面向客户的 SLO 低于这个底线,那么在你写代码之前,该 SLO 就已经破产了。你可以在发布前争取将 SLO 降至底线,也可以在接下来的两个季度里对着仪表盘自寻烦恼。
功能类别分类法:与可缓存性匹配的延迟预算
解决此问题的第一个具体模式是拒绝统一的全产品延迟 SLO,转而定义一套功能类别分类法,为每个类别分配与其可缓存模型相匹配的预算。以下是几个有用的类别:
- 静态可缓存:跨用户一致的确定性响应(如自动补全词典、常见文档查询)。p50 TTFB 目标在边缘侧衡量,大多数命中无需调用模型。
- 语义可缓存:响应并非字节一致,但在足够多的用户之间语义等效,从而使向量相似度缓存能频繁触发(如 FAQ 式回答、常见的代码审查模式)。p50 预算反映了 Embedding 查找加上向量索引命中的时间;p99 则反映了通过模型的冷路径时间。
- 按用户生成:Prompt 前缀包含用户特定数据,且每次调用各不相同。延迟底线由模型提供商的 TTFT 决定;如果不改变模型或前缀结构,团队无法突破这一底线。
诚实的分类法承认“按用户生成”类永远无法达到“可缓存”类的 SLO,并拒绝继承后者的指标。随后,面向用户的界面要么接受更高的延迟,要么通过功能设计将工作从“按用户生成”类转移到“可缓存”类中 —— 通常是通过将个性化层与生成层分离来实现。
在模型提供商边界你仍可以构建哪些缓存
拒绝继承错误的 SLO 并不意味着完全放弃缓存。它意味着将缓存从 CDN 边缘移动到模型提供商边界,那里的可缓存性单元有所不同。有两层值得刻意构建。
提示词前缀缓存。 两个主要的提供商现在都提供了这一功能。Anthropic 的 cache_control 断点允许你标记一个稳定的前缀,缓存读取时大约只需支付 10% 的输入价格,而写入时则有 25% 的溢价。OpenAI 会自动缓存超过阈值的稳定前缀,并按正常输入的 50% 对缓存的 Token 计费。获益的形式是相同的:如果你能让大型系统提示词、大型工具目录或大型检索块在多次请求中保持不变,你就不必为每次调用付费,并能显著缩短 TTFT。
破坏提示词前缀缓存的模式,恰恰是个性化功能默认交付的模式:将用户名、账号 ID 或最近的历史记录放在提示词的靠前位置,位于任何稳定结构之前。缓存键是前缀;如果前缀因用户而异,缓存就永远无法预热。修复方法很机械但容易忘记——将所有针对每个请求、每个用户的内容移到缓存边界之后,并将系统提示词、工具定义以及任何分群共享的上下文保留在边界之上。几分钟的提示词重构可能就是 90% 的缓存折扣与零折扣之间的区别。
语义缓存。 这是另一层,它与提示词缓存有本质的不同。提示词缓存降低了仍然发生的调用的成本和延迟;语义缓存则消除了调用。你对传入的查询进行嵌入(Embedding),在过去查询的嵌入索引中查找超过相似度阈值的内容,并提供过去的响应。在实际应用中,针对查询型流量报告的命中率在 60-70% 范围内——这已经足够高,使得节省的成本足以抵消嵌入加向量查找的开销。
语义缓存比提示词缓存更棘手。相似度阈值是一个具有真实故障模式的调优问题:太松,你会对 表面相似的问题给出错误的答案;太严,命中率就会崩塌。关键是,个性化功能的语义缓存面临与边缘缓存相同的作用域问题——如果响应取决于用户历史记录,则缓存必须按用户划分作用域,由于每个用户的查询分布很小,这会导致命中率崩塌。对于共享的查询模式,获益是真实的;但它无法迁移到真正的用户生成的个性化内容中。
你必须明确意识到的架构真相
被忽视的潜在真相是:个性化 AI 功能继承了与缓存化 Web 不同的物理规律。悄然支撑以往每个功能的 CDN 经济学并不是技术栈的免费属性——它们是工作负载的属性。静态资源和可共享的 JSON 响应赢得了缓存命中率;而个性化生成的内容则不然。
那些在交付 AI 功能时没有指明这一点的团队,最终会在每次请求中支付源站级别的延迟,而他们的仪表盘上显示的仍是针对边缘级别负载校准的目标。仪表盘没有撒谎;只是目标设定在错误的宇宙里。
尽早明确意识到这一点的团队会做三件事。他们发布功能类别分类法,并拒绝在具有不同底线的类别之间协商统一的全产品延迟 SLO。他们构建模型提供商边界实际支持的缓存——针对系统提示词的提示词前缀缓存,针对共享查询模式的语义缓存——并设计提示词结构以触发这些缓存。他们书面告知面向客户的利益相关者,个性化生成类别的延迟底线与产品的其余部分不同,并且 SLO 必须反映这一点。
没有意识到这一点的团队会一直调试那个在功能交付时就已经飘红的 仪表盘。无论如何,迁移到正确 SLO 的工作量都是一样的。唯一的区别在于,这件事是在合同签署之前还是之后完成。
- https://blog.cloudflare.com/rethinking-cache-ai-humans/
- https://akshayghalme.com/blogs/how-llm-caching-actually-works/
- https://strongmocha.com/ai-infrastructure/llm-caching-strategies/
- https://sureprompts.com/blog/semantic-caching-vs-prompt-caching
- https://www.getmaxim.ai/articles/semantic-caching-for-llms-cut-cost-and-latency-at-scale/
- https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- https://www.prompthub.us/blog/prompt-caching-with-openai-anthropic-and-google-models
- https://web.dev/articles/optimize-ttfb
- https://www.ioriver.io/terms/cdn-latency
