通过embedding相似度去重重复prompt,实测多数场景40%-70%请求可命中缓存,直接降低API消耗。附可运行脚本。
你的免费额度并非死于独特 prompt,而是死于同一个问题被四十种不同方式反复询问。
不信?去跑一下你自己日志里的重复检测。把过去一万条 prompt 做 embedding,按余弦相似度聚类,数数有多少条有语义孪生兄弟。在大多数 support、QA 和 code-review 场景里,这个数字落在 40% 到 70% 之间。
这周所有人都在聊 LLM 的记忆——推理账本、agent 状态、向量存储。你能加的最便宜的记忆层,其实是你已经回答过的问题。一个语义缓存,四十行 Python。我来展示完整实现,再来说它在哪几个地方会骗你。
先别急着建缓存。先测量。
导出过去一万条真实 prompt。不是测试 prompt,是真实的。
用任意 embedding 模型做向量化。
计算两两之间的余弦相似度。
统计至少有一个邻居相似度超过 0.93 的 prompt 数量。
import numpy as np
from openai import OpenAI
client = OpenAI()
prompts = [p.strip() for p in open("prompts.txt") if p.strip()]
embs = np.array([
np.array(client.embeddings.create(
model="embedding-model", input=p
).data[0].embedding, dtype="float32")
for p in prompts
])
embs /= np.linalg.norm(embs, axis=1, keepdims=True)
sim = embs @ embs.T
np.fill_diagonal(sim, 0)
has_twin = (sim >= 0.93).any(axis=1)
print(f"{has_twin.sum()} of {len(prompts)} prompts have a semantic twin")
print(f"duplicate ratio: {has_twin.mean():.0%}")
python3 duplicate_check.py
如果你的重复率低于 10%,可以关掉这篇文章了。缓存救不了你。如果高于 30%,继续往下看——缓存在一周内就能回本。
原理很简单。每条 prompt 变成一个向量。新 prompt 到来时,与已回答过的所有内容做比对。如果最相似的那个匹配超过了阈值,就返回旧答案,而不是调用模型。
那什么是正确的阈值?这就是核心问题。
太高:每条 prompt 都 miss,什么都省不了。太低:缓存开始回答不该回答的问题。"Is the payment API down?" 和 "Is the payment API up?" 几乎共用每一个词,它们的 embedding 非常接近。但答案截然相反。0.93 的阈值能区分它们,0.80 不行。
完整代理实现。FastAPI,一个字典,无数据库,无 Redis,无编排。
# cache_proxy.py
import numpy as np
from fastapi import FastAPI, Request
from openai import OpenAI
client = OpenAI() # point base_url at any OpenAI-compatible endpoint
app = FastAPI()
store = {} # embedding bytes -> (embedding, answer)
def embed(text: str):
r = client.embeddings.create(model="embedding-model", input=text)
return np.array(r.data[0].embedding, dtype="float32")
def similarity(a, b):
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
@app.post("/chat")
async def chat(req: Request):
body = await req.json()
prompt = body["prompt"]
threshold = float(body.get("threshold", 0.93))
emb = embed(prompt)
for key, (cached_emb, cached_answer) in store.items():
if similarity(emb, cached_emb) >= threshold:
return {"answer": cached_answer, "hit": True}
resp = client.chat.completions.create(
model=body.get("model", "default"),
messages=[{"role": "user", "content": prompt}],
)
answer = resp.choices[0].message.content
store[emb.tobytes()] = (emb, answer)
return {"answer": answer, "hit": False}
pip install fastapi uvicorn openai numpy
uvicorn cache_proxy:app --port 8000
就这么简单。一个 endpoint,一个字典,一个阈值。
用五种不同方式问同一件事来测试:
# probe.py
import json, time, urllib.request
prompts = [
"How do I paginate in Django?",
"What's the best way to paginate Django querysets?",
"Django pagination, how does it work?",
"Explain Django's Paginator class",
"How do I split a queryset into pages in Django?",
]
for p in prompts:
data = json.dumps({"prompt": p}).encode()
req = urllib.request.Request(
"http://localhost:8000/chat", data=data,
headers={"Content-Type": "application/json"},
)
t0 = time.time()
resp = json.load(urllib.request.urlopen(req))
print(f"hit={resp['hit']} {time.time() - t0:.2f}s {p[:45]}")
hit=False 1.20s How do I paginate in Django?
hit=True 0.05s What's the best way to paginate Django querysets?
hit=True 0.04s Django pagination, how does it work?
hit=True 0.05s Explain Django's Paginator class
hit=True 0.04s How do I split a queryset into pages in Django?
节省了四次模型调用。延迟从 1.2 秒降到 50 毫秒。这就是取舍:embedding 上花一点,生成上省一大截。
缓存是傻的。它不理解 prompt,只知道两个向量方向相近。这会导致四种特定失败:
否定陷阱。"Is the API down?" 和 "Is the API up?" 相似度 0.95,但逻辑上完全相反。你的阈值需要高到足以区分它们。
答案过期。模型更新了,但缓存里还装着昨天的答案。对于易变主题,加一个 TTL,让条目在几小时后过期。
内存增长。store 永远增长。生产环境里,把 dict 换成 LRU 缓存,限制在几千条上限。
Embedding 成本。现在每个请求都要花一次 embedding 调用。如果你的 embedding 模型很贵,而 prompt 又很短,花在 embedding 上的钱可能比省下的生成费用还多。
这些都不是致命的。但这就是为什么阈值应该放在配置文件里,而不是写死在常量里。
三类场景完全不应该用这个模式:
实时数据。股价、部署状态、天气——任何答案每分钟都在变的场景。缓存的答案就是错误的答案。
个性化响应。如果每个答案都依赖用户上下文,语义相似性毫无意义。两个用户问同一个问题,需要的答案不同。
合规要求严格的流水线。如果你必须能证明某个响应是由哪个模型版本生成的,一个没有版本追踪的内存缓存过不了审计。
对这些场景,老老实实付费吧。缓存不是预算的替代品,只是让你不再为重复提问烧预算。
这就是我喜欢这个模式的地方:整个实验零成本。
MonkeyCode 是一个开源项目,提供免费模型访问和免费服务器选项。当前的配额包含一千万免费 token——足够跑重复检测、代理层,加上几千条真实 prompt。
披露:本文是 MonkeyCode 产品推广的一部分。
免费服务器在这里很关键。你有一个可以跑代理的机器,而免费 token 覆盖了背后的模型调用。不需要信用卡,不需要 GPU,没有试用倒计时。只有这个实验。
先在你自己的日志上跑重复检测。如果重复率高于 30%,建好缓存后跑一周看看命中率。然后回来告诉我你的阈值落在哪里——我很好奇这个数字在不同场景下会怎么漂移。