site logo

Marico's space

LLM 精确缓存与语义缓存对比:各擅胜场,实测数据

AI技术与应用 2026-06-12 20:56:38 3

最近被 LLM API 账单追杀,缓存成了救命稻草。但接下来问题来了——选精确缓存还是语义缓存?说实话,大多数情况下两个都要,只是用途不同。精确匹配几乎零成本运行,而且绝对不会返回错误答案;但在实际生产环境中命中率可能只有十分之一。语义缓存能捕获几倍的流量,但会引入一个必须正视的正确性风险。这篇把各擅胜场的地方说清楚,背后的数学逻辑也捋一遍,最后给个决策框架。

缓存是 AI API 缓存体系的一部分——精确缓存和语义缓算是两层;第三层是提供商原生的透传缓存,这个单独说。

定义,先说清楚

精确匹配缓存的逻辑很直接:对请求做个确定性指纹(通常是对规范化后的消息数组、模型名、温度等参数跑一遍 SHA-256),然后在 Redis 这类键值存储里查这个指纹。命中就返回缓存响应。查询是 O(1),p95 延迟 10ms 以内。存储空间受缓存大小预算限制,条目按 LRU 或 TTL 淘汰。

语义缓存走的是另一条路:用嵌入模型(通常是小型快速的,比如 BGE-small、MiniLM 或者 text-embedding-3-small)把用户 prompt 向量化,然后去向量数据库查最接近的存储向量。如果余弦相似度超过阈值(通常 0.93–0.97),就返回那个存储向量关联的缓存响应。查询是 O(log n),p95 延迟 20–40ms,含嵌入推理。

两层都缓存完整响应。提供商原生透传不一样——它缓存的是前缀处理,在提供商那边完成。这篇只讨论响应缓存的两层。

命中率差距是结构性的,不是你的问题

精确匹配缓存在真实 LLM 流量中命中率惨淡,原因不在实现差。生产环境的 prompt 几乎必然携带每请求的上下文——用户名、session ID、时间戳、最近 RAG 召回的段落、可变的工具列表。即使用户意图完全相同,prompt 字节层面也是不同的,SHA-256 指纹自然分道扬镳。结果就是精确缓存只能命中那 5–15% 真正完全一致的流量——定时跑的内部查询、系统测试调用、用户重复提交。

VERIFY (founder):把上面的 5–15% 替换成过去 30 天 Prism 生产流量的实际精确缓存命中率,按 task_type 拆分。数据源:usage_logs 聚合,条件 cache_status='hit-exact'

语义缓存能抓住精确缓存漏掉的变体。两个用户问"退款政策是什么"和"怎么退款",prompt 字节不同,嵌入后向量几乎平行,余弦相似度能到 0.96–0.98。阈值 0.95 的语义缓存直接返回同一个答案。生产环境语义缓存命中率通常在 25–50%,是在精确缓存基础上叠加的——支持聊天机器人和 FAQ 类场景接近高限;工具调用代理带着可变检索上下文,在低限徘徊。

VERIFY (founder):把上面的 25–50% 替换成 Prism 默认 0.95 阈值下实测语义命中率,按 task_type(simple / code / reasoning / complex)拆分。数据源:usage_logs,条件 cache_status='hit-semantic'

差距的结构性根源在于:用户意图的维度比用户输入低得多。问"退款政策是什么"有千万种表达方式,但退款政策只有一条。嵌入把输入维度压缩到意图维度,这才是语义缓存能跑起来的本质原因。

精确缓存胜场的场景

精确匹配是正确选择——往往也是唯一正确选择——当以下任一条件成立:

  • 流量是确定性的。 Cron 任务、ETL 流水线、评估跑批、回归测试。同一个 prompt 每次完全一样触发,精确匹配命中率能破 90%,还不吃嵌入开销。
  • 正确性是不容商量的。 法律、医疗、金融类场景,返回一个似是而非的错误答案是真实风险。精确缓存在数学上可证明正确:请求字节完全一样才返回同样响应,一五一十。
  • prompt 短、缓存小。 缓存 50K 条目、每条 1KB,加起来 50MB Redis 就装得下,查询也轻巧。语义缓存每个 BGE-small 条目要 1.5KB 加向量索引开销,这种规模下就喧宾夺主了。
  • 嵌入延迟尾巴忍不了。 精确查询 p95 在 10ms 以内;语义缓存要额外扛 20–40ms 嵌入推理。聊天 UX 里用户感知 200ms 以上的,每一毫秒都在刀刃上。

语义缓存胜场的场景

语义匹配在复杂度上值得投入,当:

  • 用户会用十种方式问同一件事。 客服聊天机器人、产品内帮助、FAQ 界面。精确匹配命中率在这里通常是个位数;语义缓存设 0.95 能拉到 40%+。
  • 跑的是知识增强型 LLM,底层答案变化不频繁。 文档问答、策略查询、"怎么操作 X"教程。缓存有效期按小时甚至天算,因为源内容更新节奏慢。
  • 单位经济账算得过来。 一次语义命中省下一通 $0.015 的调用(常见 Sonnet 类输入输出费用)。BGE-small 嵌入推理成本每通 $0.00002 左右。盈亏平衡命中率不到 0.2%——只要假阳性率能接受,语义缓存几乎是稳赚。

假阳性问题才是大多数语义缓存实现翻车的地方。缓存返回了错误答案比没缓存更糟——用户带着错误信息离开,记在产品账上,你还可能浑然不知。安全兜底靠的是阈值工程,下一节展开。

阈值背后的数学

余弦相似度阈值是语义缓存唯一的调参杠杆。设太低,自信满满给错误答案;设太高,命中太少覆盖不了嵌入开销。可辩护的默认值是0.95,理由如下。

把它理解成"这是真匹配吗"这个问题的精准率/召回率问题。阈值调节的是边界:

  • 阈值 0.99:假阳性几乎为零,但只抓规范化后字节完全一致的请求。跟精确匹配效果一样,还多了复杂度,白搭。
  • 阈值 0.95(默认):大多数真实工作负载下假阳性低个位数。召回率好——"同一件事换说法问"基本都在 0.96 以上相似度。能跑。
  • 阈值 0.90:假阳性跳到 8–15%,在宽泛聊天场景下尤其明显。这里的误命中是语义相关但实际不同的问题——"退款政策是什么"和"运费政策是什么"嵌入后很近,0.90 阈值直接混为一谈。几乎从不是正确选择。
  • 阈值 0.85:假阳性灾难级——缓存变成了一个"知道点相关内容但会瞎答"的随机响应生成器。除非下游有 LLM 判断器逐个复核命中,否则离这个阈值远点。

曲线形状跟工作负载强相关。范围窄的工作负载(比如单一产品文档聊天机器人)可以安心跑 0.92,因为相关问题天然聚堆。范围宽的工作负载(比如通用助手)必须跑 0.96 以上,因为问题空间更分散。

正确姿势是上监控。 跑 0.95,日志里记每个命中的相似度分数,定期抽样 100 个让人工评判缓存答案是否合适。假阳性 <2%,可以试试降低阈值多抓命中;假阳性 >5%,往上调。

一个算账例子

假设你跑一个接在通义千问上的客服聊天机器人。流量画像:

  • 每天 20,000 次对话完成
  • 平均 prompt 长度:800 输入 token(system prompt + 检索上下文 + 用户消息)
  • 平均响应:300 输出 token
  • 通义千问定价(参考):输入 $0.004/千 token,输出 $0.012/千 token

不加缓存的提供商成本:20,000 × (800 × $0.004 + 300 × $0.012) / 1,000 = $136 / 天(约 ¥4,100 / 月)。

叠加上缓存:

  • 精确缓存抓 8% 流量。省下:8% × $136 = $10.9/天。
  • 语义缓存在剩余流量上抓 38%(阈值 0.95)。省下:38% × 92% × $136 = $47.7/天。
  • 合计省下:$58.6/天,约43% 的账单

语义缓存的嵌入成本:20,000 × $0.00002 = $0.40/天。可以忽略。

基础设施成本:阿里云 Redis(≈¥70/月托管版)+ Upstash Vector(≈¥210/月,50 万向量)。合计 ¥280/月,对比月省 ¥5,800。投产比一天出头。

VERIFY (founder):用真实 Prism 客户画像或代表性聚合数据替换算例,用当前定价。上面举例的数字合理,值得锚定在真实客户形态上。

重点不是具体数字——而是两层缓存的运行成本相比 savings 是毛毛雨。唯一真正的问题是假阳性率,阈值工程能解。

Prism 怎么跑两层

Prism 默认在每个付费请求上并发跑三层缓存——精确、语义、提供商原生透传。调度器先查精确缓存(Redis,p95 <8ms),miss 透传语义缓存(Upstash Vector + BGE-small 嵌入,阈值 0.95,p95 含嵌入调用约 30ms),再 miss 才代理给提供商并挂上 cache-control 标记触发提供商原生透传。每个响应带 X-Prism-Cache-Status 头标记命中哪层,还带 X-Prism-Cache-Saved-Cents 显示省了多少。

几个设计决策值得说说:

指纹规范化。 Prism 在指纹之前先规范化消息数组——剥离内部 cache-control 标记、给确定性 key 排序、tokenize 策略一致——这样字面上等价的请求哈希到同一个 key。踩过的坑记录在 Prompt cache fingerprinting pitfalls

阈值按作用域可配置,Pro+ 限定。 默认 0.95,Pro+ 账户可通过 X-Prism-Cache-Threshold 头按项目调。/dashboard/cache 的缓存检查器展示命中率-阈值曲线,调之前能先看预期效果。

流式兼容。 缓存命中无论请求是否带 stream=true 都返回非流式 JSON。中途缓存是地雷(流中断会污染缓存);直接绕开。

可以在 savings calculator 里用自己的流量参数跑 ROI 估算,跟内部用的定价模型一样。

决策清单

给工作量选缓存方案:

  1. 精确匹配必跑。 成本可忽略,命中纯赚,正确性兜底。不跑只有更亏。
  2. 语义缓存看工作量是否有可改写的意图。 客服、产品帮助、FAQ、文档问答——值得跑。工具调用代理带着高基数上下文——可能不值得。
  3. 阈值从 0.95 起步,上监控,调。 默认保守是有原因的。基于采样的假阳性率调参,别靠直觉。
  4. 叠上提供商原生透传。 稳定系统 prompt 占几百 token 的工作量必选。通义千问的缓存折扣和上层缓存独立,叠加无冲突。

LLM API 响应缓存的经济账异常友好——假阳性风险是唯一真实成本,这是工程纪律问题,不是无解的。

FAQ

余弦相似度阈值应该从多少起步?

0.95。在大多数生产工作量上保守到假阳性低个位数,同时能抓到大多数真实改写变体。从这里基于采样的假阳性率调参,别靠直觉。

语义缓存对代码 prompt 不适用?

通常是的,视嵌入模型而定。同一个意图但变量名不同,在大多数通用嵌入空间里会嵌入得很远,代码工作量的语义命中率通常偏低。两条路:换代码专用嵌入模型(比如 BGE-code),或者坦然接受语义缓存在代码 prompt 上不是主战场,靠精确 + 提供商原生。

能不能不依赖嵌入模型跑语义缓存?

不能。语义缓存在定义上就是基于嵌入的相似度匹配。能做的是只跑精确 + 提供商原生透传,不用嵌入依赖也能抓到真实流量的一部分。

底层答案变了缓存会不会被污染?

缓存失效问题是真实的。两招:TTL(条目在可配置时间窗口后过期)和显式失效(源内容变更时按模式清除匹配条目)。Prism 两个都支持——Pro+ 按项目配置 TTL,缓存检查器支持按模式逐个驱逐。

语义缓存必须用向量数据库?

实际上必须。要在成千上万甚至上百万条存储嵌入上做相似度搜索,需要 HNSW 或类似的索引。自托管选 pgvector、Qdrant;托管选 Pinecone、Upstash Vector。Prism 内部用 Upstash Vector。