Spring AI 会话记忆:从内存到 Redis 持久化

发布时间:2026-08-25 21:59  浏览量:1

大模型是无状态的——每一次请求,对它来说都是"第一次见到你"。你上一句说"我住在北京",下一句问"我这里今天天气怎么样",它根本不知道"我这里"是哪儿。想让 Agent 记住上下文,就得给它配记忆。

做这件事的价值不言而喻:Gartner 预测到 2028 年,33% 的企业软件将内嵌 Agentic AI 能力,而这些 Agent 里,几乎没有一个是不需要记忆的。一个"每句话都失忆"的对话 Agent,谈不上可用。

记忆的功能,说穿了就是让 Agent 具备"连贯性"。没有记忆,它是"一问一答的答题机器";有了记忆,它才像"和你聊天的人"。这个差别,直接决定了用户是把它当成工具用一次,还是愿意持续和它对话。

Spring AI 把记忆抽象成了一个干净的接口 ChatMemory,接一个 Advisor 就能用。这篇文章从最简的 3 行内存记忆讲起,一路讲到持久化和多用户会话隔离。

先把原理讲透,后面每个配置都顺理成章。

大模型本身不保存任何对话状态

,它"记得"的每一句话,都是框架在每次请求前,把历史消息拼进 prompt 再发过去的。所以"记忆"这件事,本质是框架替模型做的一件苦力活:

请求前读历史、拼进去;请求后把新对话写回去。

在 Spring AI 里,这件苦力活由一个 Advisor(顾问)完成,叫 MessageChatMemoryAdvisor。它包在 ChatClient 外面,每次调用自动完成"读历史、拼 prompt、写回新消息"这三步,开发者不用手动处理。

理解了这个机制,后面就很清楚:

所谓"配置记忆",其实就是给这个 Advisor 指定一个"存在哪里"的存储。

这里顺便解释一个容易混淆的点:Advisor 是"逻辑",ChatMemory 是"存储",两者是分开的。Advisor 负责"什么时候读、读多少、怎么拼进 prompt",ChatMemory 负责"历史消息实际存在哪"。所以换存储(内存换 Redis)不影响逻辑,改窗口(20 条改 50 条)也不影响存储——这正是这套抽象设计得好的地方。

最省事的配置,是用内存存储,3 行代码搞定:

ChatClient chatClient = builder .defaultAdvisors( new MessageChatMemoryAdvisor(new InMemoryChatMemory) ) .build;

InMemoryChatMemory 把历史消息存在 JVM 内存里,一问一答都会自动追加进去。现在问它"我刚才说的城市是哪个",它能接得上了。

但它的短板也很明显:

内存是进程内的,应用一重启,记忆全清空。

而且所有用户的对话混在一起,没有任何隔离。它适合本地开发、跑 demo、验证逻辑,不适合上生产。

还有一个隐患值得一提:内存记忆是全局共享的。所有请求、所有用户,都往同一个内存对象里写,没有隔离。单用户自己玩没问题,一旦有第二个用户进来,A 的历史就会污染 B 的对话。这也是为什么内存实现只能停在 demo 阶段。

直接用无界的内存记忆,有个隐藏的坑:

对话越长,每次请求发给模型的 token 越多,成本滚雪球。

解决方案是"窗口记忆"——只保留最近 N 条消息,超出就丢弃最旧的。Spring AI 提供了 MessageWindowChatMemory,默认窗口大小是 20 条:

ChatMemory memory = MessageWindowChatMemory.builder .maxMessages(20) .build;ChatClient chatClient = builder .defaultAdvisors(new MessageChatMemoryAdvisor(memory)) .build;

窗口大小的取舍要权衡

:太小,模型丢失上下文,答非所问;太大,token 成本上升、还可能塞进无关历史干扰模型。20 条是官方给的一个比较中庸的默认值,实际可以按业务调。

窗口记忆和内存记忆并不冲突——MessageWindowChatMemory 本身就是"有界的记忆",它解决了"记忆无限增长"的问题,但数据还是在内存里,重启依然会丢。

到这里,已经有三种形态可以放在一起对比了:

无界内存记忆

:最简单,但无限增长、重启丢失、无隔离。

窗口内存记忆

:控制长度,但仍在内存,重启丢失。

持久化窗口记忆

:控制长度 + 持久化 + 会话隔离,生产首选。

按需选择

:跑 demo 用内存,本地开发用窗口,上生产用持久化。

要真正上生产,记忆得落到外部存储。Spring AI 内置了对多种存储的支持,包括 Redis、Cassandra、MongoDB、PostgreSQL 这几类,选一个加上对应依赖即可。

以 Redis 为例,引入对应 starter:

org.springframework.aispring-ai-redis-store-spring-boot-starter

引入之后,把记忆换成 Redis 实现,业务代码几乎不用动:

@Beanpublic ChatMemory chatMemory(RedisChatMemoryRepository repository) { return MessageWindowChatMemory.builder .chatMemoryRepository(repository) .maxMessages(20) .build;}

核心变化只有一处:把"存储仓库"从内存换成了 Redis。

Advisor 的用法、窗口控制、业务代码,全都不变。这也体现了 Spring AI 抽象的好处——记忆的"逻辑"和"存储"是解耦的,换存储不换逻辑。

选择哪种存储,取决于现有技术栈。团队已经用了 Redis,就接 Redis;用了 MongoDB 或 Cassandra,同样有对应实现。原则只有一个:选团队已经在维护的组件,而不是为记忆单独引入一个新中间件,白白增加运维负担。

上了多用户,马上遇到一个新问题:A 用户和 B 用户的对话,绝不能串在一起。否则 A 问"我的订单呢",Agent 可能把 B 的订单历史拼进来。

Spring AI 的记忆是按 conversationId 隔离的——每个会话一个 ID,历史消息按 ID 分开存、分开取:

String conversationId = "user-1001";chatClient.prompt .user(message) .advisors(a -> a.param("chat_memory_conversation_id", conversationId)) .call .content;

同一个 conversationId 的请求,共享同一段历史;不同 ID 的请求,互不干扰。实际项目里,这个 ID 通常来自登录用户的标识或会话标识。

会话隔离是生产级记忆的硬要求。

没有它,单用户 demo 没问题,多用户一上线就出事故——而且是那种很难排查的"串话"事故。

实现上,conversationId 的传递方式不止一种。可以像上面那样通过 Advisor 参数每次显式传入,也可以在业务层封装一个方法,从登录态里自动取出用户 ID 作为 conversationId,让调用方无感知。后者更接近真实项目的做法。

到这里,一套能上生产的会话记忆就配好了:

窗口控制长度、Redis 保证持久、conversationId 隔离用户。

三个能力叠加,Agent 就能在多用户环境下稳定地"记住该记的、忘掉该忘的"。

还有几个坑值得记在心里:

成本

:记忆越长,每轮 token 越多,窗口要结合成本调。

隐私

:历史消息存在 Redis 里,敏感信息要考虑脱敏和保留期限。

记忆的边界

:ChatMemory 管的是"短期记忆"——当前这段对话的上下文。至于"跨会话的长期记忆"(用户的长期偏好、历史事实),那不是 ChatMemory 的活,得交给 RAG 和向量数据库。

分清这两个边界很重要:

短期记忆靠 ChatMemory,长期记忆靠向量检索

。前者让 Agent"记得刚才说了什么",后者让 Agent"一直记得你是谁"。这篇文章解决的是前者,而它也是所有 Agent 记忆的地基。

事实上,从 Spring AI 这一路的配置方式也能看出来:短期记忆这件事,框架已经帮开发者把"脏活"包得差不多了,剩下的就是选存储、定窗口、管隔离这三件需要业务判断的事。

顺着这个思路,也能理解为什么 Spring AI 把记忆设计成一个接口而不是一个类:因为"记忆"在不同项目里的答案完全不同。小型应用一块内存就够了,中大型应用要上 Redis 还要配窗口,涉及合规的场景还得考虑数据保留期限和脱敏。接口留出了这个灵活性,让记忆能跟着业务一起长大。