LLM 缓存技术:前缀缓存与语义缓存实战
AI 应用的账单里,最扎眼的一行是「同样的请求反复花钱」。客服问答、文档总结、内容审核、批量生成——这些场景的输入高度重复,而每一次重复都是一次完整的推理计费。缓存,就是把这部分「重复计算」变成「一次计算」最直接的杠杆。做得好的团队,靠缓存能把 API 账单砍掉 50%~70%,同时让命中请求的延迟趋近于零。
LLM 缓存有两条技术路线:前缀缓存(利用厂商的 Prompt Caching,改请求结构,零运维)和语义缓存(自建一层,按语义相似度命中,需要运维)。两者互补,本文全部讲透,并给出缓存键设计、TTL 策略与命中率监控的实战细节。
一、先理解计费结构:缓存省在哪
一次 LLM API 调用的费用 ≈ 输入 token × 输入单价 + 输出 token × 输出单价。绝大多数应用输入远大于输出(长 system prompt、长文档、长对话历史),所以输入侧是省钱的主战场。两类缓存的省钱机制不同:
- 前缀缓存:厂商按「前缀」计费——命中缓存的前缀 token 大幅降价(主流厂商普遍有 50%~90% 的折扣),同时省掉前缀的重复计算时间。
- 语义缓存:完全相同的语义问题直接返回历史答案,一分钱不花,延迟从秒级变毫秒级。
一句话:前缀缓存省「重复读」,语义缓存省「重复算」。前者靠改请求,后者靠加一层。
二、前缀缓存:让厂商帮你省
主流模型厂商(GPT-5 系列、Claude 4、DeepSeek、Gemini 等)都支持 Prompt Caching,机制大同小异:请求前缀与最近请求一致时自动命中,命中部分按折扣价计费。使用上几乎零成本,但有三个工程纪律:
- 稳定内容放前面:把 system prompt、few-shot 示例、固定工具定义放在消息列表最前面,把「每次变化的部分」(用户消息、动态数据)放最后。前缀一旦插入一个变化的 token,后面全部失效。
- 结构保持一致:同一场景用同一套消息模板(相同的 role 序列、相同的 system 内容),别每次拼装出微妙的差异——缓存是字节级前缀匹配,多一个空格都可能 miss。
- 注意缓存生命周期:各厂商缓存保留时间不同(几分钟到几小时不等),高并发场景命中率更高;低流量场景可能还没来得及命中就过期了。
实测效果参考:长 system prompt(2000+ token)的应用,开启前缀缓存后输入成本普遍下降 40%~70%。注意:折扣比例和计费口径各厂商不同,以官方文档为准;聚合网关场景下,缓存折扣通常已体现在返回的 usage 字段里,可以直接对账。
三、语义缓存:自建的第二道防线
前缀缓存管不住「换了个说法问同一个问题」的场景。语义缓存的做法:把用户输入做 embedding,与缓存里的问题向量比对,相似度超过阈值直接返回缓存答案。实现很轻:
import redis, numpy as np
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
def semantic_get(query: str, threshold: float = 0.93):
qv = embed(query) # 向量化用户输入
for key in r.scan_iter(match="qa:*", count=100):
v = np.frombuffer(bytes.fromhex(r.hget(key, "vec")), dtype=np.float32)
if cos_sim(qv, v) >= threshold:
return r.hget(key, "answer") # 命中,零推理成本
return None
def semantic_put(query: str, answer: str, ttl: int = 86400):
key = f"qa:{hash(query)}"
r.hset(key, mapping={"vec": embed(query).tobytes().hex(), "answer": answer})
r.expire(key, ttl)
三个关键参数:阈值(0.90~0.95 之间调,太低误命中、太高命中率低)、TTL(文档类 24h~7d,动态数据要更短)、embedding 模型(用中文效果好的模型,和文档检索共用一套即可)。命中时建议同时返回「缓存命中」标记,方便监控和排查。
四、缓存键设计:粒度决定命中率
语义缓存只解决「问法不同、意思相同」,更常见的是「完全相同的请求」——这类用精确缓存更便宜更快。缓存键设计是命中率的胜负手:
- 按场景分桶:键 = 场景 ID + 规范化后的输入。场景(客服、审核、摘要)混在一个桶里会互相污染。
- 规范化输入:去空白、统一大小写、剥离时间戳/用户 ID 等噪音字段——否则「同一个问题」因为带了不同 session id 就永远 miss。
- 参数参与键:model、temperature、max_tokens 等生成参数不同,结果可能不同,必须进键;而 system prompt 版本要显式带版本号,提示词一改,旧缓存自动失效。
- 缓存要能一键清空:语义缓存 + 精确缓存都挂版本号(键前缀带 cache_v1),模型升级或内容策略调整时整体失效,避免「旧答案毒害新系统」。
五、TTL、一致性:缓存的另一半
缓存省钱的代价是「可能返回旧内容」,管理这个风险靠 TTL 与一致性策略:
| 场景 | TTL | 策略 |
|---|---|---|
| FAQ / 产品文档问答 | 24h~7d | 内容变更时主动失效(写操作时清相关键) |
| 新闻 / 行情 / 时效性内容 | 1~10 分钟 | 过期即失效,不主动缓存「明显过时」的答案 |
| 翻译 / 格式化 / 确定性任务 | 长期(30d+) | 同一输入命中即返回,几乎无风险 |
| 个性化回答(含用户信息) | 不缓存 | 隐私与正确性双重风险 |
一个重要的边界:含有用户隐私的请求不要进共享缓存——缓存键里不能有明文个人信息,答案也不能落盘到共享存储,这是合规底线,也是LLM API 安全的基本要求。
六、哪些场景不该缓存
- 创意生成:文案、故事、广告语——用户要的就是「不一样」,缓存反而奇怪。
- 代码生成:同样的问题在不同上下文里正确解法不同,缓存容易给「看起来对但已过时」的代码。
- 含时间/地点/用户状态的回答:答案会随上下文漂移,缓存即错误。
- 流式 + 语义缓存的组合:命中时没有「流」可推,需要降级为一次性返回,客户端要做好兼容(我们延迟优化里讲过流式的实现)。
判断标准就一句话:这个问题的答案,在 TTL 内对所有人(或同组人)都成立吗?成立就缓存,不成立就别碰。
七、命中率监控与成本测算
缓存要「看得见」才能持续优化。三个指标上墙:前缀缓存命中率(usage 里的 prompt_tokens_details.cached_tokens / total)、语义缓存命中率(命中数 / 请求数)、缓存节省金额(未命中成本 − 命中成本)。用聚合网关的用量统计功能最省事——我们 DrAI 控制台直接给出每 Key 的 token 明细与缓存命中情况,不用自己埋点。
落地顺序建议:先开前缀缓存(零成本改造,立刻见效)→ 再给高频确定性场景加精确缓存 → 最后上语义缓存(收益最大但最需要调阈值)。三层叠加,实测大部分知识型应用能把账单砍掉 60% 以上,这笔账值得细算——《AI API 成本优化》里有完整的成本拆解方法。
八、常见坑
- 前缀顺序没管住:动态内容插在中间,前缀缓存永远 miss——先做「消息结构审计」。
- 缓存键漏了模型参数:同一输入在不同 temperature 下答案不同,串了缓存就是事故。
- 无限膨胀:语义缓存的向量和答案都存 Redis,一定要设 TTL + 容量上限,别让缓存本身吃掉内存。
- 缓存穿透与击穿:热点问题并发首次 miss 会同时打爆上游,加「单飞」锁(singleflight)或本地短 TTL。
- 不验证缓存答案:缓存返回的东西要过同一套输出校验(Schema、安全过滤),别让旧答案绕过新防线。
缓存是 AI 应用降本增效的第一杠杆,也是架构成熟度的试金石。想看清自己的请求里有多少「重复计算」?注册 DrAI,控制台直接看每 Key 用量明细,顺手把前缀缓存和模型路由一起开了。