你的 LLM Wrapper 正在漏钱:语义缓存的架构

发布: (2025年12月7日 GMT+8 17:25)
6 min read
原文: Dev.to

Source: Dev.to

问题:钱包燃烧的 LLM 调用

在抢速上线 GenAI 功能的过程中,大多数工程团队会遇到三个常见障碍:504 Gateway Timeout、幻觉循环,以及——最痛苦的——钱包燃烧器

生产日志显示,初创公司每月在 OpenAI 账单上花费数千美元,因为他们把 LLM API 当作普通的 REST 端点来使用,并且错误地实现了缓存。

在使用大语言模型时,简单的 key‑value 缓存是无效的。你需要 语义缓存 来避免对同一个查询付费两次,即使用户的表述不同。

坏的模式:精确匹配缓存

大多数后端工程师会先把 API 调用包装在一个简单的 Redis 检查中,哈希用户的提示词,如果字符串完全匹配则返回缓存的响应。

# The Naive Approach
def get_ai_response(user_query, mock_llm, cache):
    # PROBLEM: Only checks exact matches.
    if user_query in cache:
        return cache[user_query]['response']

    response = mock_llm.generate(user_query)
    cache[user_query] = response
    return response

为什么在生产环境会失效

人类语言不一致:

  • 用户 A:“What is your pricing?”
  • 用户 B:“How much does it cost?”
  • 用户 C:“Price list please”

键值存储会把它们视为三个不同的键,导致三次独立的 LLM 调用。在高流量应用中,这种冗余可能占 40‑60 % 的总 token 使用量。

成本示例拆解

Requests / dayWithout semantic cachingWith 50 % cache hit rate
1,000$150 / month*$75 / month

*假设每次请求 1,500 个输入 token,费用为每 1K token $0.005。

Embedding 成本额外约为:≈ $2 / month(text-embedding-3-small,每 1K token $0.00002)。

净节省: $73 / month → $876 / year。

好的模式:语义缓存

要从 词法 相等转向 语义 相似,我们使用 向量嵌入

架构

  1. 嵌入 – 使用廉价模型(例如 text-embedding-3-small)将用户查询转换为向量。
  2. 搜索 – 将该向量与之前查询的已存向量进行比较。
  3. 阈值 – 计算 余弦相似度;如果得分超过设定阈值(例如 0.9),返回缓存的响应。

实现

import math
from openai import OpenAI

# 1. Cosine similarity
def cosine_similarity(v1, v2):
    dot_product = sum(a * b for a, b in zip(v1, v2))
    norm_a = math.sqrt(sum(a * a for a in v1))
    norm_b = math.sqrt(sum(b * b for b in v2))
    return dot_product / (norm_a * norm_b)

def get_ai_response_semantic(user_query, llm, cache, client):
    # 2. Embed the current query
    embed_resp = client.embeddings.create(
        model="text-embedding-3-small",
        input=user_query
    )
    query_embedding = embed_resp.data[0].embedding

    # 3. Threshold (tune as needed)
    threshold = 0.9

    best_sim = -1
    best_response = None

    # 4. Linear search (for learning; replace with ANN DB in prod)
    for cached_query, data in cache.items():
        sim = cosine_similarity(query_embedding, data["embedding"])
        if sim > best_sim:
            best_sim = sim
            best_response = data["response"]

    # 5. Decision
    if best_sim > threshold:
        print(f"Cache Hit! Similarity: {best_sim:.4f}")
        return best_response

    # 6. Cache miss – pay the token tax
    response = llm.generate(user_query)

    # Store both response and embedding for future matches
    cache[user_query] = {
        "response": response,
        "embedding": query_embedding
    }
    return response

注意: 上面的线性搜索仅用于演示。当缓存的查询超过约 100 条时,请切换到具备近似最近邻(ANN)索引的向量数据库(例如 pgvector、Pinecone、Weaviate、Qdrant)。

危险区:误报

如果相似度阈值设得太低(例如 0.7),会导致 误报缓存命中

  • 查询: “Can I delete my account?”
  • 缓存: “Can I delete my post?”
  • 相似度: 0.85

在用户想要删除账户时返回删除帖子的指令,会造成严重的用户体验问题。

生产技巧: 对于敏感操作,加入 重新排序 步骤。找到潜在缓存命中后,使用快速的 cross‑encoder 模型验证两个查询是否真的产生相同的输出。

总结

  • 精确匹配缓存: 实现简单,但在大规模时成本高。
  • 语义缓存: 需要更多工程投入,但可将 API 费用削减约 40 %。

构建盈利的 AI 应用关键在于扎实的系统工程,而不仅仅是模型选择。

练习场所

概念上理解语义缓存是一回事;在生产约束下调试它——平衡阈值调优、误报率和嵌入成本——才是理论与精通的分水岭。

TENTROPY 提供了一个动手挑战,模拟“钱包燃烧器”场景。你将使用真实代码库,看到燃烧的 API 账单,并实现向量逻辑来止血。

Try the Semantic Caching Challenge (The Wallet Burner)

Back to Blog

相关文章

阅读更多 »