Spring AI 生产级实战:记忆管理
一、为什么需要记忆管理?
我们平时使用 ChatGPT、通义千问、DeepSeek 等大模型时,经常会感觉模型“记得”前面聊过的内容。
例如:
用户:我叫张三。 AI:好的,张三。 用户:我叫什么? AI:你叫张三。从体验上看,模型似乎拥有了记忆。
但从技术原理上看,大语言模型本身通常是无状态的。
也就是说,模型并不会天然记住之前发生过什么。每一次调用模型,本质上都是一次新的请求。模型之所以能回答“你叫张三”,并不是因为它真的记住了,而是因为系统在第二次请求时,把前面的对话内容重新带给了模型。
这就是记忆管理要解决的问题:
如何把前面的关键对话内容保存下来, 并在下一次调用模型时重新注入上下文, 让模型表现得像是“记得”之前的交流。在简单 Demo 中,我们可以手工把历史消息拼接到 Prompt 里。
但在生产系统中,这种方式很快会遇到问题:
会话怎么区分? 历史消息保存在哪里? 每次带多少历史消息? 消息太多超过模型上下文怎么办? 如何持久化? 如何支持多用户? 如何清理过期记忆? 如何避免把敏感信息长期保存?所以,记忆管理不是一个简单的“把聊天记录拼起来”的问题,而是一个完整的工程问题。
Spring AI 的 Chat Memory 就是为了解决这个问题。
二、Chat Memory 和 Chat History 的区别
在学习 Spring AI 记忆管理之前,必须先区分两个概念:
Chat Memory:对当前模型调用有用的上下文记忆。 Chat History:完整的聊天历史记录。这两个概念很容易混淆。
1. Chat Memory 是给模型用的
Chat Memory 的目标是帮助模型理解当前对话上下文。
例如:
用户刚刚说过自己的姓名; 用户之前选择了某个产品; 用户前面已经上传了一份报告; 用户上一轮要求用中文回答; 用户正在讨论某个具体业务问题。这些信息对下一次模型回答有帮助,所以可以作为 Memory 被带入模型。
2. Chat History 是给系统存档用的
Chat History 更像完整会话日志。
它关注的是:
用户说了什么; AI 回答了什么; 调用了哪些工具; 每次请求的时间; 模型原始输出是什么; 是否发生异常; 最终业务结果是什么。这些内容适合用于审计、追溯、统计、复盘和合规存档。
3. 为什么不能混用?
因为模型上下文窗口是有限的。
如果把完整历史记录全部塞给模型,会带来几个问题:
Token 成本增加; 响应速度下降; 无关内容干扰模型; 敏感信息暴露风险增加; 超过模型上下文长度限制。所以,生产系统中建议这样设计:
Chat Memory:只保存和当前回答相关的上下文。 Chat History:完整记录进入数据库或日志系统。简单说:
Memory 是为了让模型回答得更连贯; History 是为了让系统能够追溯和管理。三、Spring AI 的 ChatMemory 抽象
Spring AI 提供了ChatMemory抽象,用来管理对话中的记忆内容。
从职责上看,ChatMemory主要负责:
按 conversationId 保存消息; 按 conversationId 获取消息; 控制哪些消息应该被保留; 控制哪些消息应该被移除; 为下一次模型调用提供上下文。可以把它理解为模型调用前面的“上下文管理器”。
典型使用方式是:
ChatMemorychatMemory=MessageWindowChatMemory.builder().maxMessages(10).build();这里使用的是MessageWindowChatMemory,表示只保留最近一定数量的消息。
这很符合大多数聊天场景:
最近几轮对话最重要; 很久之前的内容影响较小; 上下文不宜无限增长; 需要控制 Token 成本。四、MessageWindowChatMemory:最常用的窗口记忆
MessageWindowChatMemory是 Spring AI 中默认使用的记忆类型。
它的核心思想很简单:
只保留最近 N 条消息。例如:
ChatMemorychatMemory=MessageWindowChatMemory.builder().maxMessages(10).build();这表示每个会话最多保留 10 条消息。
当消息超过限制后,旧消息会被移除。
这种方式非常适合大多数业务场景,例如:
智能客服; 知识库问答; 报告辅助生成; AI 助手; Agent 对话; 医生报告编辑辅助。为什么窗口记忆适合生产系统?
因为它在效果和成本之间做了平衡:
既能让模型理解最近上下文; 又不会把所有历史都塞进模型; 还能控制 Token 使用量; 实现简单,稳定可靠。但是它也有局限。
如果用户在很早之前说过一个重要偏好,例如:
“以后都用中文回答我。” “我所在公司使用 Java 技术栈。” “这个项目叫 wheat-qc。”这种长期偏好可能会被窗口机制逐渐挤出上下文。
所以,在更复杂的系统中,短期记忆通常还需要配合长期记忆、用户画像、数据库配置或向量检索一起使用。
五、ChatMemoryRepository:记忆存储在哪里?
ChatMemory负责决定保留哪些消息。
ChatMemoryRepository负责把消息存到哪里。
这两个职责要分开理解。
ChatMemory:记忆策略。 ChatMemoryRepository:记忆存储。Spring AI 提供了多种内置存储实现。
六、InMemoryChatMemoryRepository:适合 Demo,不适合生产
默认情况下,如果没有配置其他存储,Spring AI 会使用内存存储。
也就是:
ChatMemoryRepositoryrepository=newInMemoryChatMemoryRepository();它的特点是:
实现简单; 无需数据库; 适合快速测试; 应用重启后数据丢失; 不适合分布式部署。所以,In-Memory 适合以下场景:
本地开发; 功能演示; 单元测试; 概念验证。但不建议用于生产环境。
原因很简单:
服务重启后记忆丢失; 多实例之间无法共享记忆; 无法做审计和追踪; 无法支持长期会话。七、JDBC ChatMemoryRepository:多数业务系统的首选
对于大多数 Spring Boot 业务系统来说,JDBC 是最容易落地的持久化方式。
Spring AI 提供了JdbcChatMemoryRepository,可以把记忆存储到关系型数据库中。
依赖示例:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId></dependency>配置后可以这样使用:
@AutowiredJdbcChatMemoryRepositorychatMemoryRepository;@BeanChatMemorychatMemory(){returnMessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).maxMessages(10).build();}JDBC 方式适合:
企业内部系统; 后台管理系统; 客服系统; 医疗报告系统; 需要持久化会话上下文的业务系统。它的优势是:
容易接入; 方便运维; 便于备份; 便于和现有业务库集成; 适合中小规模生产系统。如果你本来就是 Spring Boot + MySQL / PostgreSQL / Oracle 技术栈,那么 JDBC 记忆存储通常是最稳妥的选择。
八、Cassandra、MongoDB、Neo4j、CosmosDB:根据场景选择
除了 JDBC,Spring AI 还支持多种持久化实现。
1. CassandraChatMemoryRepository
适合高可用、高写入、可设置 TTL 的场景。
例如:
大规模聊天系统; 高并发客服系统; 需要长期保存但又要自动过期的消息; 需要利用时间序列特征进行治理和审计的场景。Cassandra 的优势是扩展性强,但系统复杂度也更高。
2. MongoChatMemoryRepository
适合文档型存储场景。
例如:
消息结构比较灵活; 元数据较多; 系统本身已经使用 MongoDB; 需要 TTL 自动过期。对于很多 AI 对话系统来说,MongoDB 的文档模型也比较自然。
3. Neo4jChatMemoryRepository
适合希望把对话、用户、工具调用、上下文关系以图结构组织的场景。
例如:
复杂 Agent 关系分析; 用户意图关系建模; 多轮会话路径分析; 知识图谱结合记忆管理。但如果只是普通聊天记忆,不一定需要上 Neo4j。
4. CosmosDBChatMemoryRepository
适合 Azure 云环境下的全球分布式应用。
如果系统部署在 Azure,并且对全球分布、弹性扩展有要求,可以考虑这种方式。
九、在 ChatClient 中使用记忆
在 Spring AI 的生产开发中,更推荐通过ChatClient使用记忆。
Spring AI 提供了内置 Advisor 来管理记忆,其中推荐使用:
MessageChatMemoryAdvisor示例:
ChatMemorychatMemory=MessageWindowChatMemory.builder().maxMessages(10).build();ChatClientchatClient=ChatClient.builder(chatModel).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()).build();调用时需要传入会话 ID:
StringconversationId="user-1001";Stringcontent=chatClient.prompt().user("我叫张三,请记住").advisors(a->a.param(ChatMemory.CONVERSATION_ID,conversationId)).call().content();下一次调用继续使用同一个conversationId:
Stringcontent=chatClient.prompt().user("我叫什么?").advisors(a->a.param(ChatMemory.CONVERSATION_ID,conversationId)).call().content();这样模型就可以结合前面的消息进行回答。
这里最关键的是:
conversationId 决定了当前请求属于哪个会话。没有 conversationId,就无法区分不同用户、不同会话的上下文。
十、conversationId 应该怎么设计?
在生产环境中,conversationId 的设计非常重要。
不要随便写成固定值:
StringconversationId="007";这只适合官方示例或本地测试。
真实系统中可以根据业务设计:
用户 ID + 会话 ID; 患者 ID + 检查 ID; 客服工单 ID; 报告编辑任务 ID; Agent 任务执行 ID; 浏览器 session ID; 租户 ID + 用户 ID + 会话 ID。例如医学影像报告质控系统中,可以设计为:
conversationId = tenantId + ":" + userId + ":" + reportId或者:
conversationId = studyInstanceUid + ":" + qcTaskId这样可以保证每个报告质控任务拥有独立上下文。
如果是普通聊天系统,可以设计为:
conversationId = userId + ":" + chatSessionId设计原则是:
同一个业务上下文使用同一个 conversationId; 不同用户、不同任务、不同租户必须隔离。十一、MessageChatMemoryAdvisor 与 PromptChatMemoryAdvisor 的区别
Spring AI 早期提供过PromptChatMemoryAdvisor。
但现在更推荐使用MessageChatMemoryAdvisor。
可以这样理解:
PromptChatMemoryAdvisor: 把历史对话拼进 System Prompt。 MessageChatMemoryAdvisor: 把历史对话作为结构化 Message 对象传给模型。后者更符合现代 Chat Model 的消息结构。
因为现在很多模型本身就区分:
System Message User Message Assistant Message Tool Message使用 Message 方式可以更清楚地保留角色信息,而不是把所有内容混成一段文本。
所以生产项目中建议优先使用:
MessageChatMemoryAdvisor.builder(chatMemory).build()不建议继续依赖已经不推荐的旧方式。
十二、VectorStoreChatMemoryAdvisor:长期记忆的另一种思路
除了窗口记忆,Spring AI 还提供了VectorStoreChatMemoryAdvisor。
它的思路和普通窗口记忆不一样。
窗口记忆关注最近几条消息。
向量记忆关注语义相关的历史内容。
例如用户很久以前说过:
“我正在开发一个医学影像报告质控系统。”过了很多轮对话后,用户又问:
“这个功能应该怎么设计?”单纯窗口记忆可能已经忘掉前面的项目背景。
而向量记忆可以通过语义检索,把历史上相关的内容召回,再放入当前上下文。
它适合:
长期助手; 复杂项目协作; 用户偏好记忆; 跨会话知识延续; 长期 Agent 任务。但也要注意安全问题。
因为向量记忆通常会把用户历史输入重新注入 Prompt,如果历史内容中存在恶意提示词,就可能带来 Prompt Injection 风险。
所以,对于带工具调用能力的 Agent 系统,要谨慎使用长期记忆,并对召回内容进行隔离、转义和权限控制。
十三、记忆管理与 RAG 的区别
Chat Memory 和 RAG 都会给模型补充上下文,但它们不是一回事。
| 能力 | 主要内容 | 典型来源 | 作用 |
|---|---|---|---|
| Chat Memory | 当前会话上下文 | 用户和 AI 的对话 | 保持对话连续性 |
| RAG | 外部知识内容 | 文档、知识库、数据库 | 提供事实依据 |
| Chat History | 完整会话记录 | 日志、数据库 | 审计和追溯 |
例如在医学影像报告质控中:
Chat Memory: 用户刚才说“重点检查性别逻辑问题”。 RAG: 从知识库检索“男性患者不能出现子宫、卵巢”等质控规则。 Chat History: 完整记录用户上传了什么报告、AI 返回了什么结果、医生如何处理。三者可以同时存在,但职责不同。
不要把所有内容都塞进 Memory。
更好的架构是:
Memory 管对话连续性; RAG 管业务知识; History 管审计留痕。十四、生产级记忆管理架构
一个更接近生产环境的 AI 对话系统,可以这样设计:
前端会话 ↓ 后端生成 conversationId ↓ ChatClient 接收用户输入 ↓ MessageChatMemoryAdvisor 加载短期记忆 ↓ RAG 检索业务知识 ↓ Tool Calling 获取外部数据 ↓ ChatModel 生成回答 ↓ 结构化输出解析 ↓ 保存 Chat Memory ↓ 保存完整 Chat History ↓ 审计、统计、反馈闭环其中,记忆管理只是整个 AI 工程链路的一部分。
真正生产级的系统,还需要:
身份认证; 租户隔离; 权限控制; 数据脱敏; 日志审计; 异常兜底; 敏感信息清理; Token 成本控制; 模型输出校验。十五、在医学影像质控系统中的应用示例
假设我们正在开发一个医学影像报告质控助手。
医生在一个报告页面中连续与 AI 交互:
第一轮:请检查这份胸部 CT 报告是否有逻辑问题。 第二轮:重点关注肺结节和肋骨骨折。 第三轮:把质控意见整理成给医生看的建议。 第四轮:不要下诊断,只提示复核风险点。如果没有记忆管理,第四轮时模型可能不知道前面正在讨论哪份报告,也不知道用户已经要求“重点关注肺结节和肋骨骨折”。
有了 Chat Memory 后,可以把当前报告质控任务的上下文连续传递给模型。
示例设计:
conversationId = reportId + ":" + qcTaskId调用示例:
StringconversationId=reportId+":"+qcTaskId;Stringresult=chatClient.prompt().user(""" 请继续基于前面的报告质控上下文, 将结果整理成医生可阅读的复核建议。 """).advisors(a->a.param(ChatMemory.CONVERSATION_ID,conversationId)).call().content();这样模型就可以围绕同一个报告任务持续交流。
但要注意,报告原文、患者信息、DICOM 图像元数据等敏感数据,不一定都适合长期进入 Memory。
更稳妥的做法是:
Memory 中保存必要上下文; 完整数据保存在业务数据库; 敏感字段进入模型前先脱敏; 最终结果进入审计表。十六、记忆管理的常见问题
1. 记忆越多越好吗?
不是。
记忆越多,Token 成本越高,模型干扰越大。
生产环境中应该控制记忆规模。
例如:
普通客服:保留最近 10~20 条消息; 任务型 Agent:保留关键步骤和当前状态; 严肃业务:只保留必要上下文,不保留敏感冗余内容。2. Memory 可以代替数据库吗?
不能。
Memory 是为了增强模型上下文,不是业务数据库。
例如:
订单状态; 患者检查信息; 报告审核状态; 用户权限; 业务配置。这些内容应该存放在业务数据库中,而不是依赖模型记忆。
3. Memory 可以代替用户画像吗?
不建议。
用户画像通常是长期、结构化、可管理的数据。
例如:
用户偏好语言; 所属机构; 常用功能; 权限角色; 历史项目。这些更适合放在数据库、配置中心或专门的画像服务中。
Memory 更适合处理当前对话上下文。
4. 分布式部署时还能用内存记忆吗?
不建议。
如果系统有多个实例,用户第一次请求落到 A 实例,第二次请求落到 B 实例,内存中的记忆就无法共享。
所以生产环境建议使用持久化 Repository,例如 JDBC、MongoDB、Cassandra 等。
5. Tool Calling 的中间消息会自动保存吗?
需要注意,工具调用过程中的某些中间消息不一定会被自动存入 Memory。
所以如果业务需要完整审计工具调用链路,应该单独记录:
工具名称; 工具参数; 工具返回; 调用时间; 调用是否成功; 异常信息; 最终模型输出。不要完全依赖 Chat Memory 做审计。
十七、生产级最佳实践
1. 明确 Memory、History、RAG 的边界
不要把所有上下文都塞进 Memory。
建议分工:
Memory:短期对话上下文。 History:完整会话记录。 RAG:业务知识检索。 DB:权威业务数据。2. 为每个业务任务设计 conversationId
conversationId 不只是技术参数,而是业务隔离边界。
建议包含:
租户; 用户; 业务对象; 会话任务。例如:
tenantId:userId:sessionId tenantId:patientId:studyId tenantId:reportId:qcTaskId3. 生产环境使用持久化存储
开发测试可以用 In-Memory。
生产环境建议使用:
JDBC; MongoDB; Cassandra; Neo4j; CosmosDB。具体选哪个,取决于系统已有技术栈和业务规模。
4. 控制记忆窗口大小
不要无限制保存上下文给模型。
建议根据业务场景设置:
MessageWindowChatMemory.builder().maxMessages(10).build();如果需要长期记忆,可以引入向量检索或结构化用户画像,而不是盲目扩大窗口。
5. 敏感信息要脱敏
在医疗、金融、教育等场景中,Memory 可能包含敏感数据。
建议对以下内容做处理:
姓名; 身份证号; 手机号; 患者 ID; 检查号; 机构信息; 地址; 病历敏感字段。尤其是需要调用外部模型服务时,更要重视数据安全。
6. 记忆不等于事实
AI 记住了某个内容,不代表这个内容一定正确。
例如用户说:
“我是管理员。”模型记住了这句话,并不代表系统应该相信用户真的有管理员权限。
权限、身份、状态等权威信息必须从业务系统查询。
记忆只能作为上下文,不应作为事实来源。
7. 保留审计链路
生产系统应该单独保存完整请求链路:
用户输入; conversationId; 记忆内容摘要; RAG 检索结果; 工具调用结果; 模型原始输出; 最终业务结果; 人工审核意见。这样后续才能追溯问题、评估模型效果、优化 Prompt 和 Memory 策略。
十八、总结
Spring AI 的 Chat Memory 解决的是大模型无状态调用下的上下文连续性问题。
它通过ChatMemory管理会话记忆,通过ChatMemoryRepository负责底层存储,通过MessageChatMemoryAdvisor与ChatClient集成,让模型能够在多轮对话中保持上下文。
但在生产系统中,记忆管理不能简单理解为“保存聊天记录”。
更准确地说:
Chat Memory 是模型上下文管理机制; Chat History 是系统审计记录机制; RAG 是业务知识增强机制; 数据库才是权威业务数据来源。只有把这几个边界分清楚,AI 应用才不会在上下文、事实、记忆和审计之间混乱。
对于生产级 Spring AI 应用来说,记忆管理的核心价值是:
让模型在有限上下文内保持连续交流, 让系统在可控成本下提供更自然的交互体验, 让业务在安全、可追溯、可维护的前提下使用 AI 能力。