同一个问题命中了缓存,答案却属于另一个租户,问题往往出在缓存键过于简单。AI 应用的输出不仅由问题文字决定,还可能取决于租户、模型、提示词版本、检索索引和权限上下文。
一、先列出输出依赖
模型、提示词和索引更新后,旧答案可能不再符合要求。把这些依赖做成显式版本,才能决定哪些结果可以复用。Python 的 lru_cache 缓存可哈希参数,但不会理解业务身份与权限。
二、可运行的最小键
下面只使用固定 fixture,不调用模型。排序键与紧凑分隔符固定 JSON 序列化,再计算 SHA-256。哈希不是加密,不会让敏感问题自动安全。
python
import json, hashlib
def key(tenant, model, prompt_version, index_version, question):
payload = dict(tenant=tenant, model=model, prompt_version=prompt_version, index_version=index_version, question=question)
raw = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
return hashlib.sha256(raw).hexdigest()
base = ["tenant-a", "model-v1", "prompt-v1", "index-v1", "库存是多少?"]
k = key(*base)
assert k == key(*base)
for i in range(5):
other = base.copy()
other[i] += "-changed"
assert key(*other) != k
cache = {k: "fixture answer"}
assert key("tenant-b", *base[1:]) not in cache
print("7 assertions passed; tenant/model/prompt/index/question are isolated")
print("key:", k)
print("No model API or production cache used")
本机 Python 3.13.13 已运行原始实验,结果为 7 assertions passed,键为 a32a751198fef4a514816914286b3a85af820ca1aad196cd594d550b4e383afe。这里验证相同输入可重现、五项依赖分别变化时键不同、不同租户不能命中本例条目,没有测试模型效果、成本或并发性能。
三、键相同还不够
读取缓存仍需核对当前权限。用户撤权后,能定位旧条目不等于仍被授权。应查询权威权限版本,或对敏感答案禁用跨请求缓存。TTL 只限制保存时间,不能保证内容始终最新。保存来源、创建时间与依赖版本;索引更新时隔离命名空间。不要把错误、超时和半截流式输出缓存成正常答案。
四、语义缓存与并发
相似问题只能进入候选,仍须核对租户、语言、时间和版本。"明天库存"和"今天库存"不能因相似度高就共用答案。未经业务验证,不要随意删除标点、转换大小写或规范化空白。
缓存复用已完成结果;single-flight 合并进行中的计算。并发首次请求可能重复计算,锁的释放、取消和异常传播需要另测。对有写入或发消息副作用的工具,缓存不能代替幂等设计。
五、验收清单
覆盖同问题不同租户、新模型、新提示词、新索引、撤权和部分输出。除命中率和延迟外,还记录错误命中和失效原因。本文只验证依赖隔离,没有真实模型、向量库、分布式锁或撤权实验。
参考:https://docs.python.org/3/library/functools.html#functools.lru_cache