语义缓存(Semantic Cache)
为什么需要缓存?
在生产环境中,LLM 调用的主要成本来源是:
- Token 费用:每次调用都消耗 input + output tokens
- 延迟:即使 DeepSeek 很快,也需要几百毫秒到几秒
如果用户问的是意思相近的问题,没必要每次都调用 LLM,直接返回缓存的答案就行。
普通缓存 vs 语义缓存
| 类型 | 匹配方式 | 局限 |
|---|---|---|
| 精确缓存(exact match) | 字符串完全相等 | "手机多少钱" ≠ "这款手机价格是多少" |
| 语义缓存(semantic cache) | Embedding 向量相似度 | 语义相近即命中 |
核心原理
用户提问
↓
把问题转成 Embedding 向量
↓
在缓存里搜索相似向量(cosine similarity)
↓
相似度 > 阈值?
├─ 是 → 直接返回缓存答案(0 token 消耗)
└─ 否 → 调用 LLM → 把问题+答案存入缓存
技术组件
| 组件 | 作用 | 我们用什么 |
|---|---|---|
| Embedding 模 | 把文本转成向量型 | BAAI/bge-m3(SiliconFlow) |
| 向量存储 | 存储和检索向量 | FAISS(本地,无需服务器) |
| 相似度阈值 | 控制命中灵敏度 | cosine similarity,推荐 0.95 |
FAISS(Facebook AI Similarity Search)
- 纯本地的向量检索库,不需要启动服务
- 速度极快,适合缓存这种小规模场景
- 相比 Chroma,FAISS 更轻量
与 RAG 的区别
- RAG:用向量搜索找"相关文档"来增强回答
- 语义缓存:用向量搜索找"相同问题的历史回答",直接返回,不调用 LLM
示例代码
py
class SemanticCache:
def __init__(self, embedder, threshold=0.8):
# 存储结构:list of (向量, 问题, 答案)
self.embedder = embedder
self.threshold = threshold
self.cache = []
def get(self, query: str) -> str | None:
# 1. 把 query 转成向量
query_vector = self.embedder.embed_query(query)
# 2. 与缓存里每条向量计算 cosine similarity
best_match = None
best_score = 0
for vector, question, answer in self.cache:
score = calculate_cosine_similarity(query_vector, vector)
if score > best_score:
best_score = score
best_match = (question, answer)
# 3. 如果最高分 >= threshold,返回对应答案
if best_score >= self.threshold:
print(f" → Hit (best={best_score:.4f})")
return best_match[1]
print(f" → Miss (best={best_score:.4f})")
# 4. 否则返回 None
return None
def set(self, query: str, answer: str):
# 把 (向量, 问题, 答案) 存入缓存
self.cache.append((self.embedder.embed_query(query), query, answer))
def ask_with_cache(query: str, cache: SemanticCache, llm) -> tuple[str, bool]:
# 先查缓存,命中返回 (答案, True)
cached = cache.get(query)
# 未命中调 LLM,存入缓存,返回 (答案, False)
if cached is not None:
return cached, True
answer = llm.invoke(
[
SystemMessage(content="简短回答问题,最多不超过150个字"),
HumanMessage(content=query),
]
).content
cache.set(query, answer)
return answer, False
def main():
env = validate_env()
if not env:
return
api_key, base_url, model, embedding_api_key, embedding_base_url, embedding_model = (
env
)
# 初始化 embedder、llm、cache
llm = ChatOpenAI(api_key=api_key, base_url=base_url, model=model)
embeddings = OpenAIEmbeddings(
base_url=embedding_base_url, api_key=embedding_api_key, model=embedding_model
)
cache = SemanticCache(embeddings, threshold=0.70)
# 用以下测试组验证:
test_pairs = [
("这款手机多少钱?", False), # 首次:未命中,存入缓存
("这款手机的价格是多少?", True), # 同义问句:命中
("请问这款手机售价是多少?", True), # 同义问句:命中
("这款手机有什么颜色?", False), # 不同话题:未命中,存入缓存
("这款手机是什么颜色的?", True), # 同义问句:命中(0.74)
]
# 打印每次是否命中缓存,以及返回的答案
for query, expected_hit in test_pairs:
answer, hit = ask_with_cache(query, cache, llm)
print(f"Query: {query}")
print(f"Answer: {answer}")
print(f"Expected hit: {expected_hit}")
print(f"Hit: {hit}")
print("-" * 50)