AI推理结果不能直接用map或ristretto做全量缓存,因其响应含非确定性字段(如created、id、usage)、体积大(单条超10KB)、易致OOM,且ristretto未设MaxCost会失控。

AI 应用的缓存不能简单套用传统 Web 缓存逻辑——模型推理结果往往带上下文、参数敏感、成本高、体积大,直接 cache.Set(key, result, 60) 很容易命中率低、内存爆掉、甚至返回错误响应。
为什么 AI 推理结果不能直接用 map 或 ristretto 做全量缓存
AI 推理输出(比如 LLM 的 JSON 响应)通常含非确定性字段:created 时间戳、id 请求唯一标识、usage token 统计等。哪怕 prompt 完全一致,原始响应体也不可直接比对或复用。
更关键的是体积:一个中等长度的 ChatCompletion 响应轻松超 10KB;若缓存 1000 个,仅响应体就占 10MB+ 内存,而 ristretto.Cache 的 MaxCost 默认按字节计价,不设限等于放任 OOM。
- map / sync.Map 只适合极小规模、固定参数、无副作用的纯函数缓存(如 tokenizer 预处理结果)
- ristretto 若未配置
MaxCost(推荐设为100 即 100MB),极易因单次大响应拖垮整个服务 - 响应里含时间戳、随机 seed、token usage 等字段,导致相同 prompt 的
md5.Sum([]byte(resp))每次都不同 → 缓存永远不命中
如何构造稳定、可复用的缓存 key
key 必须排除所有非决定性输入,只保留真正影响输出语义的部分。例如 OpenAI-style API 调用:
立即学习“go语言免费学习笔记(深入)”;
func buildAICacheKey(req *openai.ChatCompletionRequest) string {
// 只取决定性字段:model、messages(需标准化)、temperature(四舍五入到 0.1)、top_p
b, _ := json.Marshal(struct {
Model string `json:"model"`
Messages []openai.ChatCompletionMessage `json:"messages"`
Temp float32 `json:"temperature"`
TopP float32 `json:"top_p"`
}{
Model: req.Model,
Messages: normalizeMessages(req.Messages), // 去除 role: "assistant" 的历史回复、trim 空格、统一换行
Temp: math.Round(req.Temperature*10) / 10,
TopP: math.Round(req.TopP*10) / 10,
})
return fmt.Sprintf("ai:%x", md5.Sum(b))
}
注意:normalizeMessages 必须移除所有非 prompt 输入(比如上一轮 assistant 回复),否则用户连续多轮对话会生成无限多 key。
- 绝不能包含
seed、stream、user字段(除非业务强依赖 user 隔离) - temperature/top_p 建议量化(如 round to 0.1),避免 0.7000001 和 0.6999999 被当成两个 key
- 如果使用 function calling,需 deep-equal 函数定义结构体,而非只看 name
该缓存什么:只缓存“确定性输出片段”,不是整条 HTTP 响应
直接缓存 *http.Response 或完整 JSON body 是危险的:HTTP header 含动态时间、server 信息;JSON 含 created、id,下次返回会暴露伪造痕迹。
正确做法是解包响应,只缓存语义确定部分:
type AICacheValue struct {
Content string `json:"content"` // 仅 message.content
Role string `json:"role"` // 仅 role
Finish string `json:"finish_reason"`
CachedAt time.Time `json:"cached_at"`
}
// 缓存时:
val := AICacheValue{
Content: resp.Choices[0].Message.Content,
Role: resp.Choices[0].Message.Role,
Finish: resp.Choices[0].FinishReason,
CachedAt: time.Now(),
}
cache.Set(key, val, 300) // TTL 5 分钟
- 不缓存
Usage字段:它随每次调用变化,且前端通常不依赖它做逻辑判断 - 不缓存
ID和Created:它们是请求生命周期标识,不应复用 - 若需支持流式响应(stream=true),缓存完全不适用——流式本质是不可缓存的
缓存失效必须主动触发,不能只靠 TTL
AI 模型升级、prompt 修改、系统配置变更都会让旧缓存失效。单纯依赖 max-age=300 会导致用户看到过期逻辑(比如旧版提示词下的错误回答)。
推荐组合策略:
- 给每个 prompt 版本加 hash 后缀,如
prompt_v2:abc123→ key 变更自动淘汰旧缓存 - 维护一个全局
model_version计数器,写入 cache key 前拼接,版本号变更后全部失效 - 对敏感 prompt(如客服话术),提供 HTTP 管理端点
POST /cache/invalidate?pattern=ai:prompt_faq_*,调用ristretto.Cache.Clear()或 Redis SCAN + DEL
最易被忽略的一点:LLM 输出可能含幻觉或事实错误,缓存会把错误固化。上线前务必在缓存层外加一层轻量校验(如关键词黑名单、正则匹配失败提示),否则“越缓存越错”。


















