RAG缓存问答结果,相同用户问题直接返回缓存降低消耗
很多RAG问答服务上线后,都会遇到同一个问题:用户反复问相似甚至完全一样的问题,系统每次都去向量库检索、拼Prompt、再调用LLM生成答案。
这个过程既慢又烧Token。
其实对重复问题完全可以把第一次生成的答案缓存起来,下次直接返回缓存结果,消耗能降一大截。
本文用Redis做缓存,给你一套可直接照做的实现方案。
先判断你的场景适不适合加缓存
RAG缓存不是万能药,它最适用于高频重复提问的场景。
比如企业内部知识库问答、客服FAQ、产品使用手册问答,用户问来问去就那么几十个问题。
如果你的问题是长尾、几乎没有重复的开放性提问,缓存命中率会很低,加缓存的意义不大。
判断标准很简单:在日志里统计相同问题的重复次数,如果重复率超过20%,就值得加缓存。缓存不仅省Token,还能让响应时间从几秒降到几十毫秒,体验提升明显。
开始前需要准备什么
操作前先确认环境里已有这些组件:
- 一个可用的RAG问答服务,不管你自己写的还是用的LangChain之类的框架。
- Redis数据库,如果还没有,可以直接用宝塔面板装一个,或者跑Docker命令:
docker run -d --name redis-rag -p 6379:6379 redis:7
- Python环境,准备安装
redis和hashlib库(后者是标准库)。
核心思路是:用问题内容的哈希值作为缓存键,如果两次问的问题完全相同,哈希值一样,就能直接命中缓存。
用Redis给RAG问答加缓存的具体步骤
下面以Python为例,改造你的RAG问答函数。
改造前,你的代码大概长这样:
# 原来的问答函数
def ask_question(question):
context = search_vector_db(question) # 从向量库检索
answer = call_llm(question, context) # 调用LLM生成答案
return answer
改造后,加上缓存逻辑:
import redis
import hashlib
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def get_cache_key(question):
# 把问题文本转成MD5,保证同样的问题得到同样的键
return f"rag:q:{hashlib.md5(question.encode('utf-8')).hexdigest()}"
def ask_question_with_cache(question, ttl=86400):
cache_key = get_cache_key(question)
# 1. 先查缓存
cached = r.get(cache_key)
if cached:
print("[缓存命中]")
return cached
# 2. 没命中才走原来的RAG流程
context = search_vector_db(question)
answer = call_llm(question, context)
# 3. 把答案存入缓存,设置24小时过期
r.setex(cache_key, ttl, answer)
print("[缓存未命中,已写入]")
return answer
这里的关键点是:问题文本必须严格一致才能命中缓存。
如果你希望连“怎么重置密码”和“如何重置密码”这种相似问题也能命中,那就得升级成语义缓存,用Embedding把问题向量化后存进向量数据库,计算相似度来匹配。
不过语义缓存复杂度高,建议先跑通精确缓存,再看实际重复率决定要不要做。
避坑指南:这几件事一定要注意
缓存键不要用原始问题文本直接当Key。问题文本可能很长,直接当Key浪费Redis内存,而且容易超出Key长度限制。
用MD5或SHA-1哈希后当Key更稳妥。
答案里如果包含时间、用户昵称等动态信息,不适合缓存。比如“今天天气”这种答案每天都在变,TLL要设短一点,甚至不缓存。
要在日志里加命中标记,方便后续统计。就像上面代码里打印的[缓存命中],生产环境建议记录到结构化日志里,方便用Grafana或者Kibana看命中率。
缓存雪崩问题也需要注意。如果你把缓存过期时间都设为固定的24小时,到了第二天会一起失效,瞬间全部请求打到LLM上。
建议给ttl加一个随机值,比如86400 + random.randint(0, 3600),打散过期时间。
怎么验证缓存真的生效了
跑完改造,别急着部署,先用最简单的方式验证:
- 启动服务,连续两次调用
ask_question_with_cache,传完全一样的问题。 - 观察控制台或日志,第一次会打印
[缓存未命中,已写入],第二次打印[缓存命中]。 - 对比两次返回内容是否一致。如果答案里没有动态参数,两次应该完全相同。
更进一步,可以统计50个真实用户问题里的缓存命中率:
# 假设日志文件为 app.log
grep "缓存命中" app.log | wc -l
grep "缓存未命中" app.log | wc -l
命中数量除以总数就是当前命中率。
命中率越高,省下的Token越多。
你能直观感受到的是,响应时间从原来的好几秒变成几十毫秒,LLM调用次数明显减少,账单自然就降下来了。
如果你正在给RAG服务降本,建议先按本文把精确缓存跑通,再根据重复率决定是否上语义缓存。
实际效果会在高频问答场景里非常明显,而且这套改造对原有逻辑影响很小,随时可以回退。
如果你的问题场景有特殊性,比如答案需要实时更新,记得单独调整缓存过期策略。