看到「Redis 正式接入 AI」这个标题,估计很多人第一反应是:搞缓存的老兵怎么跟大模型扯上关系了?其实这个组合不是标题党,它踩中的是当下所有大语言模型应用都绕不开的短板——AI 的「记忆问题」。大模型本身是相当「健忘」的,而我们天天说 Redis 快、Redis 稳,却很少意识到:记忆这件事,恰恰需要快读快写、需要自动遗忘、需要语义查找,这几样恰好都是 Redis 的看家本领。
这段时间我给几个大模型应用项目做架构改造,核心工作之一就是给它们接上 Redis 作为记忆层。说实话,把 Redis 用在 AI 场景里,并不仅仅是加个向量索引那么肤浅,它需要你重新理解「记忆」到底分几层、什么该存什么该忘、查询怎么做才不会崩。这篇文章就围绕「为什么 Redis 能解决 AI 记忆问题」以及「怎么落地一个能用的记忆层」来写,适合正在做大模型应用、客服机器人、个人知识助手的工程师参考。
1. 先搞清楚问题:大模型的「记忆」为什么是硬伤
1.1 大模型的健忘,是设计使然
大语言模型的本质是一个「无状态函数」:给它一段输入,它预测下一段输出,仅此而已。它不记得你昨天问过什么,不记得上个用户叫什么名字,更不会主动记住「你说过你孩子在读初中」。如果你想让大模型表现得像有记忆,唯一直接的办法,就是把历史内容全部塞进每一次请求的上下文里。
但这个做法有三个硬伤。
第一,上下文窗口是有限的。虽然现在模型动辄几十万 token 的窗口,但你把整段历史对话、用户档案、业务数据全塞进去,很快就把窗口挤爆。越长的上下文,模型对中间信息的关注度越容易下降,反而影响回答质量。
第二,成本会快速失控。大模型 API 基本按 token 计费,每次请求把几万 token 的历史反复回传,等于每聊一句都在为历史内容买单。我见过一个客服机器人项目,改造前一个月仅上下文重复计费就占了总费用的四成。
第三,跨会话能力基本为零。用户今天问「发票怎么开」,明天回来问「我昨天那个发票问题处理到哪了」。如果系统不额外存储,模型只能一脸茫然。所以,AI 的「记忆问题」本质上是个工程问题,不是模型问题。你需要在模型之外,给它配一个比上下文窗口更灵活的「外置记忆」。
1.2 为什么偏偏是 Redis 来收拾这个局面
解决记忆问题,理论上什么数据库都能做,但 AI 应用对记忆层的读写要求非常具体。
记忆层要扛得住高频读写。用户和机器人聊一句,可能要读三次记忆、写两次记忆,QPS 一下就上来了。传统磁盘数据库在这种放大效应下很容易把延迟拉高。而 Redis 是内存数据库,读写延迟是微秒到毫秒级,这个底子决定了它能站在请求路径上而不拖后腿。
记忆层要支持「自动遗忘」。人脑会忘事,AI 的记忆层也必须能忘。Redis 的 TTL 过期机制,天然就是「遗忘」的调度器。给短期记忆设置一个过期时间,到点自动消失,不用你写定时任务去清数据。
记忆层要能「按语义查找」。用户不会每次都说出完全相同的关键词。今天说「发票」,明天说「上次开发票那个事」,系统得能找到同一件事。Redis 生态里的向量检索能力解决的就是这个——把文本变成向量,用余弦相似度找「意思相近」的记忆。
所以,Redis 能接住 AI 记忆这个场景,不是因为某一个功能强,而是因为它把「快读快写」「自动过期」「持久化」「向量检索」这四块拼图凑齐了。打个不严谨的比方:给 AI 配记忆,Redis 既当随手能翻的便利贴,又当长期存档的档案柜,还自带一套按「大概意思」检索的索引卡。便利贴会过期,档案柜不丢数据,索引卡能模糊查找,这正好覆盖了记忆系统的核心需求。
2. AI 记忆体系的三大分层与选型逻辑
2.1 第一层:工作记忆,管住当前会话
工作记忆对应的是「这次对话里发生了什么」。它最明显的特征是:短、快、不需要永久保留。
我之前做智能客服项目时,给工作记忆定的规则很简单:每个会话用 Redis 的 List 结构存最近二十轮对话,并设置 30 到 60 分钟的 TTL。用户在这个窗口内继续提问,机器人能顺着上下文往下聊;超过窗口没说话,就从零开始,相当于「把上一个话题忘掉」。
用 List 存对话轮次非常顺手,伪代码大致是这样:
import redis r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True) def push_chat(user_id: str, role: str, content: str, ttl: int = 3600): key = f"session:{user_id}" r.rpush(key, f"{role}:{content}") # 只保留最近20条,防止单个会话无限增长 r.ltrim(key, -20, -1) # 重置过期时间,用户持续活跃就不会丢 r.expire(key, ttl)这段代码里有两个容易被忽略的细节。ltrim(key, -20, -1)是保尾去头,只留最新二十轮,避免长期会话把内存吃光。expire每次写入都重置一次,只要用户还在持续对话,这个会话就不会半路消失;用户一旦离开,TTL 自然让它过期,这就是「自动遗忘」。
顺带一提,如果对话里带有图片、文件引用,不要硬塞进 List。把内容指针或消息 ID 存进去,实际内容单独放对象存储,Redis 里只保留轻量引用,否则 List 会越来越臃肿。
2.2 第二层:情景记忆,跨会话的事实档案
如果说工作记忆是临时草稿纸,情景记忆就是正式的档案柜。它用来回答这一类问题:「这个用户上个月买过什么」「他最喜欢的付款方式是什么」「他之前投诉过什么」。
情景记忆必须持久化,不能随便 TTL。Redis 的持久化机制在这里派上用场:RDB 做定期快照,AOF 做追加日志,两者配合可以在进程重启后把记忆恢复回来。数据模型上,我推荐优先按「业务事实」存储,而不是按「原始聊天记录」存储。
比较常用的做法是给每个用户维护一组结构化事实,用 Hash 或者 RedisJSON 保存:
def save_user_fact(user_id: str, fact_type: str, fact_value: str): key = f"mem:fact:{user_id}" r.hset(key, mapping={ fact_type: fact_value, }) def get_user_fact(user_id: str, fact_type: str): return r.hget(f"mem:fact:{user_id}", fact_type)这里的关键点是:不要相信模型能自己记住事实,而是把「从对话里抽事实」变成一个独立流程。用户说「我上次投诉过物流太慢」,系统在后台把「物流投诉: 处理中」这个结构写进 Hash;下次用户再来问,直接把这个事实取出来拼进提示词,模型自然回答得准确。
情景记忆的写入时机也值得讲。不要在用户请求的同步路径上做复杂的事实抽取,那样会增加首字延迟。更稳妥的做法是:把原始对话先推入 Redis Stream,由后台异步任务消费,调用大模型做摘要和事实抽取,再回写情景记忆。用户的聊天响应走快路径,记忆加工走慢路径,两边互不拖累。
2.3 第三层:语义记忆,用向量记住「意思」
前两层解决的是「字面记忆」,但很多场景需要「意思记忆」。例如用户第一次说「发票开错了」,第二次说「上次的票要重开」,这两句话没有共同关键词,系统要能发现它们其实是同一件事。办法就是把文本映射成向量,然后做相似度检索。
Redis 生态里承担这部分的是向量索引能力。你可以把一条条事实先转成 embedding,再存进 Redis 的 Hash,并建立向量索引。查询时把用户当前问题转成向量,在索引里做 KNN 搜索,找到语义最接近的若干条记忆。
语义记忆的定位和使用频率,我用一张表来梳理:
| 记忆层级 | 典型内容 | 存放结构 | 生命周期 | 检索方式 |
|---|---|---|---|---|
| 工作记忆 | 当前会话最近对话 | List / Stream | 短,TTL 控制 | 按时间顺序读取 |
| 情景记忆 | 用户偏好、订单、投诉事实 | Hash / RedisJSON | 长,持久化 | 按字段精确读取 |
| 语义记忆 | 历史事件的向量表示 | Hash + 向量索引 | 长,持久化 | 向量相似度检索 |
三层记忆不是各自孤立的。实际运行时,工作记忆提供即时上下文,情景记忆提供精准事实,语义记忆负责「想起来还有这么回事」。三层配合,基本能覆盖一个助手的全部记忆需求。
3. 手把手落地:把 Redis 改造成 AI 记忆层
3.1 先规划 key 的命名空间
很多人在 Redis 里写业务数据比较随意,结果数据一多,连自己都分不清哪块是哪块。做记忆层,key 命名一定要在一开始定好规范。我的习惯是:mem:<记忆类型>:<业务域>:<用户ID>。
例如:
session:10001 当前会话的工作记忆 mem:fact:10001 用户事实档案 mem:summary:10001 历史摘要 mem:event:10001:1690000000 某条情景事件用户 ID 统一放在中间位置,方便按前缀批量清理,也方便后面做多租户隔离。如果你用的是 Redis Cluster,还能靠 key 里的用户 ID 做哈希标签,保证同一个用户的记忆落在同一个分片,避免跨节点查询。
3.2 建立向量索引
接着搭建语义记忆的底座。假设我们统一用 1536 维的 embedding 模型,把用户 ID 设为 TAG 字段,文本内容设为 TEXT 字段,embedding 设为 VECTOR 字段。命令类似:
FT.CREATE idx_mem_fact ON HASH PREFIX 1 mem:fact \ SCHEMA \ user_id TAG \ text TEXT \ embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE这段命令的意思:对前缀为mem:fact:的 Hash 数据自动建立索引,其中user_id用于精确过滤,text用于普通关键词搜索,embedding用 HNSW 算法建向量索引,距离度量选余弦相似度。
HNSW 是当前最常用的向量索引算法,召回率和查询性能平衡得比较好。它有两个参数需要关注:M控制每个节点的最大连接数,越大越精准,但内存和构建时间也越高;EF_CONSTRUCTION控制建索引时的搜索广度,越大索引质量越高。起步可以设M=16、EF_CONSTRUCTION=200,数据量上来后再调。
3.3 写入记忆与语义召回
索引建好后,写入一条事实记忆变得很直接。这里有个坑我一开始踩过:向量是二进制数据,如果用decode_responses=True的连接去写,很容易出现类型问题。建议单独准备一个二进制连接专门处理向量字段。
import redis import uuid import numpy as np # 普通文本操作走 decode 连接 r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True) # 向量二进制操作建议单独用非 decode 连接 vconn = redis.Redis(host="127.0.0.1", port=6379, decode_responses=False) # 这里是你实际用的 embedding 服务的封装 def get_embedding(text: str) -> bytes: vec = MY_EMBEDDING_SERVICE.embed(text) return np.array(vec, dtype=np.float32).tobytes() def save_fact(user_id: str, fact_text: str): fact_id = uuid.uuid4().hex[:8] key = f"mem:fact:{user_id}:{fact_id}" vconn.hset(key, mapping={ "user_id": user_id, "text": fact_text, "embedding": get_embedding(fact_text), })查询的时候,把用户当前问题转成向量,然后做 KNN 搜索。记得一定要带上user_id的 TAG 过滤,否则会跨用户召回,我在初版系统里就吃过这个亏。
def recall_fact(user_id: str, query: str, k: int = 5): query_vec = get_embedding(query) res = r.ft("idx_mem_fact").search( f"@user_id:{{{user_id}}}=>[KNN {k} @embedding $qvec AS score]", query_params={"qvec": query_vec}, dialect=4, ) facts = [] for doc in res.docs: # RediSearch 返回的 score 是距离,COSINE 下 score 越小越相似 similarity = 1 - float(doc.score) facts.append({"text": doc.text, "similarity": similarity}) return facts有个细节容易看懵:RediSearch 里 VECTOR 查询返回的score是距离,不是相似度。余弦距离越小越相似,当两条文本完全一样时距离为 0,相似度为 1。所以代码里要换算成1 - score,不然你会看到一个「相似度越低越好」的诡异结果。
3.4 把检索到的记忆拼进提示词
记忆层的数据结构再好,最终也要喂给大模型才有效。我的做法是:每次用户提问时,并行从三层记忆里取数据,然后组装成结构化的「记忆上下文」,再加到系统提示词里。
def build_memory_context(user_id: str, question: str): recent = get_recent_sessions(user_id, 6) facts = recall_fact(user_id, question, k=3) summary = get_most_recent_summary(user_id) context = [] if summary: context.append(f"[历史摘要] {summary}") if facts: for f in facts: context.append(f"[相关记忆] {f['text']}") if recent: context.append(f"[最近对话] {recent}") return "\n".join(context)注意召回结果的拼接顺序。摘要放最前面,因为它概括性最强;相关记忆紧跟其后;最近对话放最后,因为它和当前问题的时间距离最近,模型处理长文本时对后部信息的权重更高。这个顺序调整,我在测试中明显感觉到回答相关性有提升。
调用大模型时,把记忆上下文拼进 system prompt:
你是一个智能客服助手。以下是关于当前用户的记忆信息,请结合这些信息回答问题: [历史摘要] ... [相关记忆] ... [最近对话] ... 用户当前问题:...这套流程跑通以后,模型在回答「上次那个发票问题」这类跨会话问题时,就不再是瞎猜了,而是真正「想起了」之前的经历。
4. 记忆治理:别让「记住一切」变成灾难
4.1 记忆不是原始日志
最容易犯的错误,是把所有原始对话一股脑写进记忆层。这样做有三个后果:内存快速增长、检索噪音变大、token 成本失控。用户说过一万句话,真正值得长期记住的可能就二十句。记忆层的价值在于提炼,不在于存档。
我在实际项目里把记忆加工链路设计成三步:原始保留、摘要提炼、事实抽取。
原始保留只做短期,就是前面说的工作记忆,TTL 一小时。摘要提炼是后台任务每累计十轮对话就调一次大模型,生成一段 200 字以内的对话摘要,覆盖写入mem:summary:<user_id>。事实抽取是从每轮对话里识别「用户偏好」「待办事项」「历史结论」这类值得长期保留的信息,写入情景记忆。
4.2 设计一套「遗忘策略」
AI 记忆也要讲取舍,不是越多越好。情景记忆虽然没有 TTL,但应该设置容量上限和重要度分级。我给每个用户的事实档案设置上限为 50 条,超过上限时,按「重要度 + 最近访问时间」做综合评分,分低的淘汰。这套逻辑本质上就是把 LRU 和人工优先级结合起来。
常用遗忘策略可以这么设计:
- 工作记忆:TTL 自动过期,默认 30 到 60 分钟
- 会话摘要:保留最近 10 到 20 条摘要,更早的摘要转成年度级总摘要
- 事实档案:单用户不超过 50 条,按重要度和最近访问淘汰
- 语义记忆:每条向量记录保留来源信息,来源摘要被删除时同步清理
这背后有一个思路:AI 助手不是你的私人档案管理员,它更像是你的贴身助理。助理会记住重要的事,也会合理地忘记无关紧要的事。
4.3 成本与性能的权衡
接入向量检索后,内存压力是绕不开的话题。1536 维的 float32 向量,单条大约占 6 KB;一万条就是 60 MB;百万条就是 6 GB。这是纯向量的开销,还没算原始文本和索引结构。如果公司已经部署了 Redis,且记忆量在千万条以内,通常可以接受;如果目标是上亿条向量,还是建议考虑独立向量数据库。
批量写入也能省不少时间。embedding 的到来是有网络开销的,逐条调用接口很慢。我在项目里把历史记忆的向量化做成批量任务,每批 100 条,一次请求拿到所有向量,然后批量写入 Redis。实测下来,十万条历史的向量化时间从数小时压缩到二十分钟以内。
缓存层面的优化更重要。用户的高频问题往往是同一批,比如「怎么退款」「物流到哪了」。如果每次都用向量去 Kafka 式地检索再触发 embedding,延迟和费用都不划算。更好的做法是加一层语义缓存:把「问题和答案」也向量化存进 Redis,新问题进来先算一次语义相似度,命中高度相似的历史问题就直接返回缓存答案,不再调大模型。这既是记忆,也是省钱利器。
5. 实测与踩坑:一个客服机器人的记忆改造复盘
5.1 改造前后的直观对比
我用一个模拟客服机器人项目做过对比实验。改造前,用户跨会话提问的重复率非常高,很多用户会因为「你刚才不是说了吗」而流失。改造后,机器人接入 Redis 记忆层,三层记忆全部启用。运行两周后,几个关键数据还挺能说明问题:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 用户重复提问率 | 约 23% | 约 7% |
| 首字回复延迟 | 约 1.1 秒 | 约 300 毫秒 |
| 语义缓存命中率 | 0% | 约 35% |
| 人工转接率 | 约 18% | 约 10% |
首字延迟下降主要来自语义缓存:高频问题直接命中缓存,不需要等待模型推理。重复提问率下降则要感谢语义记忆,它能跨会话把用户之前的问题捞出来。
5.2 我在这套系统里真实踩过的坑
再顺利的方案,落地也免不了踩坑。我把自己在这套记忆层上遇到的问题整理成了速查表:
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
FT.SEARCH报维度错误 | embedding 维度与索引 DIM 不一致 | 检查模型输出维度,和FT.CREATE对齐 |
| 向量检索结果为空 | 索引字段类型与写入类型不匹配 | 确认 Hash 里字段名和 SCHEMA 完全一致 |
| 跨用户召回混乱 | 查询时没带 TAG 过滤 | KNN 查询用@user_id:{...}前置过滤 |
| 检索结果相似度异常 | 把距离当相似度用 | 用1 - score换算余弦相似度 |
| 内存持续上涨 | TTL 没生效或长期记忆无上限 | 给短期记忆设 TTL,给档案设容量上限 |
| 重启后记忆丢失 | 持久化策略配置不当 | 开启 AOF everysec,并保留 RDB 快照 |
维度错误是最常见的。我当时换过一次 embedding 模型,从 768 维换成 1536 维,但旧索引的 DIM 没改,查询时直接报错。解决方法不是删除重建那么简单——要确认旧数据已经不再依赖旧索引,再删旧索引、建新索引、批量重算向量。
另一个值得提醒的是maxmemory-policy。如果 Redis 实例设置了noeviction,记忆层数据写满后会直接写入失败,对线上系统是致命的。我在测试环境就遇到过:向量索引把内存占满,业务写入全部报错。后来把记忆所在的实例单独拆分,设置allkeys-lru,同时给短期记忆和长期记忆使用不同的 key 前缀,方便单独管理内存配额。
5.3 两个很有价值的改进
第一个是混合检索。纯向量检索在「语义相近但专有名词必须精确」的场景下不够稳。比如用户问「YF-200 订单退款」,向量可能把它匹配到「其他订单退款」,但订单号应该是精确匹配。我的方案是:专有名词先做精确查询,命中直接返回;精确查询没命中,再走向量语义召回。两者互补,准确率明显提升。
第二个是记忆回写闭环。之前的流程是「读记忆、回答问题」,但回答完之后,这段新对话并没有回流到记忆层。我加了一个后台任务:每轮对话结束后,把这轮对话推入 Stream,异步抽取事实、更新摘要、写向量。这样记忆会随着对话持续生长,而不是永远停留在第一次建档的状态。
6. 一些个人体会
跑了几个项目的记忆层改造后,我最大的感受是:Redis 解决 AI 记忆问题,真正值钱的地方不在「向量排名有多精准」,而在它所处的位置——请求链路的正中间,能用极低的延迟把记忆喂给大模型。这一点,很多独立的向量数据库反而不容易做到,因为它们往往需要额外的网络跳转和数据同步。
如果你也想给大模型应用加记忆,我建议从语义缓存这个最小切口开始。它不需要复杂的记忆分层,只需要把「问题向量 + 答案」存进 Redis,再做一次相似度判断。投资小、见效快,还能直接降低大模型调用的 API 费用。等跑通语义缓存,再循序渐进地引入情景记忆和摘要体系,会比一上来就搞全套三层记忆稳妥得多。
另外,始终记住一件事:记忆不是越多越好。好的记忆层要学会「在恰当的时候想起,在恰当的时候忘记」。Redis 的 TTL 给了我们「忘记」的手段,但要不要忘、什么时候忘,终究是设计上的取舍。我见过不少团队把记忆系统做成垃圾收集站,最后模型被大量低质量记忆干扰,反而不如不记。
最后分享一个小技巧:生产环境一定要把记忆系统做成可观测的。给每次记忆读取、缓存命中、向量召回打上日志,出事时你能快速判断是记忆层的哪一环出了问题。我现在的习惯是,任何「模型回答变差」的问题,先看记忆召回日志,再模型调优。多数情况下,问题出在记忆没取对,而不是模型不会答。