API费用暴涨340%,67%请求是重复的,语义缓存怎么把Token省回来
说实话,语义缓存这玩意儿我一开始是拒绝的。传统的 Redis 缓存多简单啊,key-value 精确匹配,命中率虽然低但好歹不用动脑子。但当你看到用户用 17 种不同问法问同一个问题,而每个问题都在消耗 Token 的时候,你就知道精确匹配有多无力了。
什么是语义缓存?
简单说,语义缓存不再依赖“字符串完全相同”来判断是否命中缓存,而是通过计算文本之间的语义相似度,把“意思差不多”的请求识别出来,直接返回缓存结果。
举个例子,下面这三个查询在传统缓存里是三次独立请求:
"怎么优化 Python 的循环性能?"
"Python 循环太慢了,有什么加速方法?"
"如何提升 for 循环在 Python 中的执行效率?"但它们的答案完全一样。语义缓存能识别出这种相似性,后两次直接走缓存,零 Token 消耗。
技术架构设计
我目前在生产环境跑的方案是这样的:
用户请求 → Embedding 模型 → 向量相似度搜索 → 命中?
↓
是 → 返回缓存结果
↓
否 → 调用 OpenAI → 存储向量+结果核心组件三个:
- **Embedding 模型**:`text-embedding-3-small`,便宜够用
- **向量数据库**:Milvus,跑了 8 个月了
- **相似度阈值**:0.92
等等,这里我要更正一下——阈值这个数字不是一开始就设对的,后面会详细说踩坑经历。
代码实现
import openai
from pymilvus import Collection, connections
import hashlib
import json
class SemanticCache:
def __init__(self, similarity_threshold=0.92):
self.threshold = similarity_threshold
connections.connect(host='localhost', port='19530')
self.collection = Collection("openai_cache")
def get_embedding(self, text):
response = openai.Embedding.create(
model="text-embedding-3-small",
input=text
)
return response['data'][0]['embedding']
def search_similar(self, query_embedding):
search_params = {"metric_type": "COSINE", "params": {"nprobe": 10}}
results = self.collection.search(
data=[query_embedding],
anns_field="embedding",
param=search_params,
limit=1,
output_fields=["response"]
)
if len(results[0]) > 0 and results[0][0].distance >= self.threshold:
return results[0][0].entity.get('response')
return None
def query_with_cache(self, user_query):
# 生成查询向量
query_embedding = self.get_embedding(user_query)
# 搜索相似缓存
cached_response = self.search_similar(query_embedding)
if cached_response:
return cached_response, True # 命中缓存
# 未命中,调用 OpenAI
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": user_query}]
)
answer = response.choices[0].message.content
# 存入缓存
self.collection.insert([
[query_embedding],
[answer],
[user_query]
])
return answer, False三个关键数据点
1. Token 消耗降低 73%
我们一个客服系统接入语义缓存后,30 天内统计结果:
- 总查询量:1,247,893 次
- 缓存命中:912,456 次(73.1%)
- 实际 API 调用:335,437 次
- 节省 Token:约 1.87 亿
- 费用节省:$2,840 → $767
省了不少。
2. 响应延迟从 2.3 秒降到 180 毫秒
这个提升用户感知特别明显。之前每次请求都要等 OpenAI 生成,现在命中缓存直接返回,P99 延迟从 2.3 秒降到了 180 毫秒。我们客服团队反馈说用户投诉“机器人太慢”的情况直接消失了。
嗯...这里有个细节。180 毫秒是纯缓存命中的情况,如果没命中还是要走 OpenAI,那就回到 2 秒左右了。所以实际体验是“大部分时候很快,偶尔慢一下”。
3. 相似度阈值 0.92 的由来
这个数字我调了整整一周。一开始设的 0.85,结果出现各种“答非所问”——用户问“怎么退款”命中了“怎么退货”的缓存,虽然语义上有点接近,但答案完全不同。
调到 0.95 又太严格,命中率从 73% 掉到 41%。最后在 0.92 这个点找到了平衡,既不会答非所问,又能保持较高的命中率。
踩过的坑
坑一:FAISS 内存爆炸
最开始我用 FAISS 做本地向量索引,想着简单省事。结果跑了三天,内存占用飙到 32GB,服务直接 OOM。FAISS 的 IndexFlatIP 是全量暴力搜索,向量数据全加载到内存,生产环境根本扛不住。
后来切到 Milvus,开了 IVF_FLAT 索引,内存稳在 4GB 以内,查询速度还快了 3 倍。
坑二:Embedding 模型选型失误
一开始图便宜用了开源的 all-MiniLM-L6-v2,结果中文场景下效果稀烂。“笔记本电脑”和“笔记本”的相似度只有 0.67,完全没法用。
换成 OpenAI 的 text-embedding-3-small 后,中文语义理解准确太多了。虽然每次生成 Embedding 也要消耗 Token,但相比 GPT-4 的生成成本,这点开销可以忽略不计(大约 1 Token 能处理 3000 个字符)。
坑三:缓存过期策略没做好
上线第一版的时候,我偷懒设了个全局 24 小时过期。结果有天产品更新了退换货政策,用户问新政策还是返回旧缓存,客服团队差点被投诉淹了。
现在的方案是:对不同类型的查询设置不同的 TTL。事实性内容(如公司地址、联系方式)7 天过期,政策类内容 1 小时过期,实时性内容(如库存、价格)不走缓存。
进阶优化:主动缓存预热
光靠被动缓存还不够,我加了个预热机制。通过分析历史查询日志,提取高频问题模板,在业务低峰期(凌晨 3 点)主动生成 Embedding 并缓存。
# 高频查询预热脚本
hot_queries = [
"如何重置密码?",
"订单物流怎么查?",
"退货流程是什么?",
"客服电话多少?",
# ... 从日志分析出的 Top 100 问题
]
for query in hot_queries:
# 生成标准答案并缓存
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": query}]
)
cache.store(query, response)这个优化让缓存命中率又提升了 8 个百分点,而且用户高峰期完全感受不到冷启动问题。
成本收益总结
| 指标 | 优化前 | 优化后 | 变化 |
|------|--------|--------|------|
| 月 API 调用量 | 124 万次 | 33 万次 | -73% |
| 月 Token 消耗 | 2.56 亿 | 0.69 亿 | -73% |
| 月费用 | $2,840 | $767 | -73% |
| 平均响应延迟 | 2.3 秒 | 0.18 秒 | -92% |
| 缓存命中率 | 0% | 73% | - |
基础设施成本(Milvus 服务器 + Embedding 调用)每月约 $120,相比节省的 $2,073,ROI 相当可观。
适用场景和局限
这套方案最适合的场景:
- 客服问答系统
- 文档检索
- 知识库查询
- 标准化程度高的对话场景
不适合的场景:
- 创意写作(每次要求都不同)
- 代码生成(上下文高度个性化)
- 实时数据分析(数据时刻在变)
如果你的业务中重复查询占比超过 40%,强烈建议上语义缓存。如果不到 20%,可能传统 Redis 缓存就够用了。
你们团队在用语义缓存吗?相似度阈值设的多少?有没有遇到过“答非所问”的尴尬情况?评论区聊聊,我最近在调研用 RAG 结合语义缓存做更精准的召回,有经验的朋友给点建议。
Tags: #语义缓存 #OpenAI #Token优化 #向量数据库 #成本优化
读者评论 5