【SpringAI】会话记忆实现——基于MYSQL进行存储
目录
在 AI 对话场景中,“上下文记忆” 是提升交互体验的核心 —— 如果每次对话都是 “失忆式沟通”,用户需要重复说明背景,AI 的回复也会脱离场景。SpringAI 提供了会话记忆(Chat Memory)组件,支持多轮对话的上下文管理,本文聚焦两种最常用的会话记忆存储方式:默认内存存储(适合开发测试)和JDBC 持久化存储(适合生产环境),从实现原理、代码配置到场景选型,帮你快速落地 SpringAI 的会话记忆功能。
1. 先搞懂:SpringAI 会话记忆的核心结构
在讲具体存储方式前,先理清 SpringAI 会话记忆的核心组件,这是理解两种存储方式的基础:

核心逻辑:
ChatMemory:定义会话记忆的基础行为(增 / 查 / 清空);MessageWindowChatMemory:SpringAI 默认的ChatMemory实现,支持 “消息窗口”(比如只保留最近 20 条消息,避免上下文过长);ChatMemoryRepository:定义消息的持久化行为,具体存储方式由其实现类决定(内存 / JDBC)。在这里我也将源码分别给出(源码可以先跳过,实现了功能之后再回顾一下)
ChatMemory:
/* * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.ai.chat.memory; import java.util.List; import org.springframework.ai.chat.messages.Message; import org.springframework.util.Assert; /** * The contract for storing and managing the memory of chat conversations. * * @author Christian Tzolov * @author Thomas Vitale * @since 1.0.0 */ public interface ChatMemory { String DEFAULT_CONVERSATION_ID = "default"; /** * The key to retrieve the chat memory conversation id from the context. */ String CONVERSATION_ID = "chat_memory_conversation_id"; /** * Save the specified message in the chat memory for the specified conversation. */ default void add(String conversationId, Message message) { Assert.hasText(conversationId, "conversationId cannot be null or empty"); Assert.notNull(message, "message cannot be null"); this.add(conversationId, List.of(message)); } /** * Save the specified messages in the chat memory for the specified conversation. */ void add(String conversationId, List<Message> messages); /** * Get the messages in the chat memory for the specified conversation. */ List<Message> get(String conversationId); /** * Clear the chat memory for the specified conversation. */ void clear(String conversationId); } MessageWindowChatMemory:
/* * 版权声明:2023-2025 年原始作者或贡献者保留所有权利。 * * 本文件根据 Apache License, Version 2.0(“许可证”)授权; * 除非符合许可证的要求,否则不得使用本文件。 * 您可从以下地址获取许可证副本: * * https://www.apache.org/licenses/LICENSE-2.0 * * 除非适用法律要求或书面同意,本软件按“原样”分发, * 不提供任何形式的明示或暗示担保,包括但不限于 * 对所有权、非侵权性、适销性或特定用途适用性的担保。 * 有关许可证下的具体权限和限制,请参阅许可证文本。 */ package org.springframework.ai.chat.memory; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.util.Assert; /** * 基于滑动窗口的聊天记忆实现类。 * * <p>该实现维护一个固定大小的消息窗口(由 {@code maxMessages} 控制), * 确保内存中存储的消息总数不超过指定上限。当消息数量超出限制时,会自动移除较旧的非系统消息。</p> * * <p><strong>特殊处理 SystemMessage:</strong></p> * <ul> * <li>如果新增的消息中包含新的 {@link SystemMessage},则会先清除当前对话中已有的所有 {@link SystemMessage};</li> * <li>在裁剪消息以满足长度限制时,会优先保留所有 {@link SystemMessage},仅移除其他类型的消息(如用户消息、AI 回复等)。</li> * </ul> * * <p>此策略确保系统指令(如角色设定、行为约束)始终有效且不被意外丢弃。</p> * * @author Thomas Vitale * @author Ilayaperumal Gopinathan * @since 1.0.0 */ public final class MessageWindowChatMemory implements ChatMemory { /** 默认最大消息数量(包括用户、AI 和系统消息),用于限制上下文长度 */ private static final int DEFAULT_MAX_MESSAGES = 20; /** 底层消息存储仓库,负责持久化或缓存实际消息数据 */ private final ChatMemoryRepository chatMemoryRepository; /** 允许存储的最大消息总数(滑动窗口大小) */ private final int maxMessages; /** * 私有构造函数,强制通过 Builder 模式创建实例。 * * @param chatMemoryRepository 消息存储仓库,不可为 null * @param maxMessages 最大消息数量,必须大于 0 */ private MessageWindowChatMemory(ChatMemoryRepository chatMemoryRepository, int maxMessages) { Assert.notNull(chatMemoryRepository, "chatMemoryRepository cannot be null"); Assert.isTrue(maxMessages > 0, "maxMessages must be greater than 0"); this.chatMemoryRepository = chatMemoryRepository; this.maxMessages = maxMessages; } /** * 向指定对话中添加一批新消息,并自动维护消息窗口大小。 * * <p>流程如下:</p> * <ol> * <li>从仓库加载当前对话的所有已有消息;</li> * <li>调用 {@link #process(List, List)} 对新旧消息进行合并与裁剪;</li> * <li>将处理后的完整消息列表保存回仓库(覆盖原内容)。</li> * </ol> * * @param conversationId 对话唯一标识符,不能为空或空白 * @param messages 要添加的新消息列表,不能为 null,且不能包含 null 元素 */ @Override public void add(String conversationId, List<Message> messages) { Assert.hasText(conversationId, "conversationId cannot be null or empty"); Assert.notNull(messages, "messages cannot be null"); Assert.noNullElements(messages, "messages cannot contain null elements"); List<Message> memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId); List<Message> processedMessages = process(memoryMessages, messages); this.chatMemoryRepository.saveAll(conversationId, processedMessages); } /** * 获取指定对话的当前完整消息列表(即上下文)。 * * @param conversationId 对话唯一标识符,不能为空或空白 * @return 当前对话中的所有消息列表(按时间顺序),若无记录则返回空列表 */ @Override public List<Message> get(String conversationId) { Assert.hasText(conversationId, "conversationId cannot be null or empty"); return this.chatMemoryRepository.findByConversationId(conversationId); } /** * 清空指定对话的所有消息(即删除整个对话上下文)。 * * @param conversationId 对话唯一标识符,不能为空或空白 */ @Override public void clear(String conversationId) { Assert.hasText(conversationId, "conversationId cannot be null or empty"); this.chatMemoryRepository.deleteByConversationId(conversationId); } /** * 核心消息处理逻辑:合并已有消息与新消息,并确保总数量不超过 {@code maxMessages}。 * * <p>处理规则:</p> * <ul> * <li>若新消息中包含<strong>新的</strong> {@link SystemMessage}(即不在已有消息中),则移除所有旧的 {@link SystemMessage};</li> * <li>将新消息追加到(可能已清理过系统消息的)旧消息之后;</li> * <li>若总消息数超过上限,则从前往后移除非系统消息,直到满足长度限制;</li> * <li>所有 {@link SystemMessage} 始终被保留(即使总数超过限制也不会被裁剪)。</li> * </ul> * * @param memoryMessages 当前对话中已存在的消息列表 * @param newMessages 即将添加的新消息列表 * @return 处理并裁剪后的最终消息列表 */ private List<Message> process(List<Message> memoryMessages, List<Message> newMessages) { List<Message> processedMessages = new ArrayList<>(); // 将已有消息转为 Set,便于快速判断新 SystemMessage 是否“新” Set<Message> memoryMessagesSet = new HashSet<>(memoryMessages); // 判断新消息中是否包含“新”的 SystemMessage(即不在已有消息中的) boolean hasNewSystemMessage = newMessages.stream() .filter(SystemMessage.class::isInstance) .anyMatch(message -> !memoryMessagesSet.contains(message)); // 保留旧消息,但若存在新 SystemMessage,则过滤掉所有旧的 SystemMessage memoryMessages.stream() .filter(message -> !(hasNewSystemMessage && message instanceof SystemMessage)) .forEach(processedMessages::add); // 追加所有新消息(包括可能的新 SystemMessage) processedMessages.addAll(newMessages); // 若总消息数未超限,直接返回 if (processedMessages.size() <= this.maxMessages) { return processedMessages; } // 需要移除的消息数量 int messagesToRemove = processedMessages.size() - this.maxMessages; // 从前往后遍历,跳过(即移除)前 N 个非 SystemMessage List<Message> trimmedMessages = new ArrayList<>(); int removed = 0; for (Message message : processedMessages) { if (message instanceof SystemMessage || removed >= messagesToRemove) { // 是系统消息,或已移除足够数量 → 保留 trimmedMessages.add(message); } else { // 非系统消息且还需继续移除 → 跳过(即移除) removed++; } } return trimmedMessages; } /** * 提供构建器(Builder)模式入口,用于灵活创建 {@link MessageWindowChatMemory} 实例。 * * @return 新的 Builder 实例 */ public static Builder builder() { return new Builder(); } /** * {@link MessageWindowChatMemory} 的构建器类,支持链式调用。 */ public static final class Builder { private ChatMemoryRepository chatMemoryRepository; private int maxMessages = DEFAULT_MAX_MESSAGES; /** 私有构造函数,防止外部直接实例化 */ private Builder() { } /** * 设置底层消息存储仓库。 * * @param chatMemoryRepository 消息仓库实现 * @return 当前 Builder 实例(支持链式调用) */ public Builder chatMemoryRepository(ChatMemoryRepository chatMemoryRepository) { this.chatMemoryRepository = chatMemoryRepository; return this; } /** * 设置最大消息数量(滑动窗口大小)。 * * @param maxMessages 最大消息数,必须 > 0 * @return 当前 Builder 实例 */ public Builder maxMessages(int maxMessages) { this.maxMessages = maxMessages; return this; } /** * 构建并返回 {@link MessageWindowChatMemory} 实例。 * * <p>若未显式设置 {@code chatMemoryRepository},则默认使用内存实现 {@link InMemoryChatMemoryRepository}。</p> * * @return 配置完成的 MessageWindowChatMemory 实例 */ public MessageWindowChatMemory build() { if (this.chatMemoryRepository == null) { this.chatMemoryRepository = new InMemoryChatMemoryRepository(); } return new MessageWindowChatMemory(this.chatMemoryRepository, this.maxMessages); } } }ChatMemoryRepository:
/* * 版权声明:2023-2025 年原始作者或贡献者保留所有权利。 * * 本文件根据 Apache License, Version 2.0(“许可证”)授权; * 除非符合许可证的要求,否则不得使用本文件。 * 您可从以下地址获取许可证副本: * * https://www.apache.org/licenses/LICENSE-2.0 * * 除非适用法律要求或书面同意,本软件按“原样”分发, * 不提供任何形式的明示或暗示担保,包括但不限于 * 对所有权、非侵权性、适销性或特定用途适用性的担保。 * 有关许可证下的具体权限和限制,请参阅许可证文本。 */ package org.springframework.ai.chat.memory; import java.util.List; import org.springframework.ai.chat.messages.Message; /** * 聊天记忆仓库接口:用于存储和检索聊天消息。 * * <p>该接口定义了对多个对话(conversation)的消息进行持久化管理的基本操作。 * 每个对话由唯一的 conversationId 标识,消息以 {@link Message} 列表形式存储。</p> * * @author Thomas Vitale * @since 1.0.0 */ public interface ChatMemoryRepository { /** * 获取所有已存在的对话 ID 列表。 * * <p>可用于列出系统中所有的活跃或历史对话。</p> * * @return 包含所有对话 ID 的字符串列表,若无对话则返回空列表。 */ List<String> findConversationIds(); /** * 根据指定的对话 ID 查询对应的所有聊天消息。 * * <p>返回的消息列表按时间顺序排列(通常为发送顺序),用于恢复上下文或展示聊天记录。</p> * * @param conversationId 对话的唯一标识符 * @return 该对话中的消息列表;若对话不存在,应返回空列表而非 null */ List<Message> findByConversationId(String conversationId); /** * 将给定的聊天消息列表保存到指定的对话中,并**覆盖**该对话原有的所有消息。 * * <p>此操作是原子性的:要么全部替换成功,要么保持原状(具体取决于实现)。 * 适用于需要重置对话上下文或从外部同步完整消息历史的场景。</p> * * @param conversationId 对话的唯一标识符 * @param messages 要保存的消息列表;若为 null 或空列表,则相当于清空该对话 */ void saveAll(String conversationId, List<Message> messages); /** * 根据对话 ID 删除整个对话及其所有消息。 * * <p>删除后,后续调用 {@link #findByConversationId(String)} 应返回空列表, * 且该 ID 可能不再出现在 {@link #findConversationIds()} 的结果中(取决于实现)。</p> * * @param conversationId 要删除的对话的唯一标识符 */ void deleteByConversationId(String conversationId); }2. 方式 1:默认内存存储
这是 SpringAI 的默认会话记忆存储方式,无需额外依赖,消息直接存在 JVM 内存中,适合开发、测试场景。
2.1 核心实现

存储载体:JVM 堆内存;依赖的 Repository:InMemoryRepository(SpringAI 内置,无需手动配置)。这里也给出InMemoryRepository的源码和中文注释,实现了ChatMemoryRepository接口
/* * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.ai.chat.memory; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.ai.chat.messages.Message; import org.springframework.util.Assert; /** * {@link ChatMemoryRepository} 的内存实现类。 * 使用 ConcurrentHashMap 作为底层存储,提供线程安全的对话历史管理。 * 适用于开发和测试环境,生产环境建议使用持久化存储实现(如 Redis、数据库等)。 * * @author Thomas Vitale * @since 1.0.0 */ public final class InMemoryChatMemoryRepository implements ChatMemoryRepository { /** * 核心存储结构:使用线程安全的 ConcurrentHashMap * Key: 对话ID(conversationId) * Value: 该对话的消息列表(List<Message>) */ Map<String, List<Message>> chatMemoryStore = new ConcurrentHashMap<>(); /** * 获取所有对话ID列表。 * 返回的是键集合的副本,避免外部直接修改内部存储。 * * @return 所有对话ID的列表,如果不存在任何对话则返回空列表 */ @Override public List<String> findConversationIds() { return new ArrayList<>(this.chatMemoryStore.keySet()); } /** * 根据对话ID查询该对话的所有消息。 * 返回消息列表的防御性副本,防止外部修改影响内部存储。 * * @param conversationId 对话唯一标识符,不能为空或空白字符串 * @return 该对话的消息列表副本;如果对话不存在则返回空列表(List.of()) * @throws IllegalArgumentException 如果 conversationId 为 null 或空字符串 */ @Override public List<Message> findByConversationId(String conversationId) { // 参数校验:确保 conversationId 不为空 Assert.hasText(conversationId, "conversationId cannot be null or empty"); // 从存储中获取消息列表 List<Message> messages = this.chatMemoryStore.get(conversationId); // 返回防御性副本:如果存在则复制列表,否则返回不可变的空列表 return messages != null ? new ArrayList<>(messages) : List.of(); } /** * 保存(覆盖)指定对话的所有消息。 * 注意:此方法会完全替换该 conversationId 对应的消息列表,而非追加。 * * @param conversationId 对话唯一标识符,不能为空或空白字符串 * @param messages 要保存的消息列表,不能为 null,且不能包含 null 元素 * @throws IllegalArgumentException 如果参数不符合约束条件 */ @Override public void saveAll(String conversationId, List<Message> messages) { // 参数校验:对话ID必须有效 Assert.hasText(conversationId, "conversationId cannot be null or empty"); // 参数校验:消息列表不能为 null Assert.notNull(messages, "messages cannot be null"); // 参数校验:消息列表中不能包含 null 元素 Assert.noNullElements(messages, "messages cannot contain null elements"); // 直接 put 操作:会覆盖该 conversationId 之前的所有消息 this.chatMemoryStore.put(conversationId, messages); } /** * 根据对话ID删除整个对话历史。 * 删除后该 conversationId 对应的消息将无法再查询到。 * * @param conversationId 要删除的对话唯一标识符,不能为空或空白字符串 * @throws IllegalArgumentException 如果 conversationId 为 null 或空字符串 */ @Override public void deleteByConversationId(String conversationId) { // 参数校验:确保 conversationId 不为空 Assert.hasText(conversationId, "conversationId cannot be null or empty"); // 从 Map 中移除该对话的所有消息 this.chatMemoryStore.remove(conversationId); } }2.2 快速上手代码
2.2.1 引入 SpringAI 依赖
在pom.xml中添加 SpringAI 核心依赖(以 阿里的模型为例,其他模型同理):
<!-- Spring AI Alibaba --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> </dependency>2.2.2实现一个对话的类,基于内存实现记忆
@Component @Slf4j public class LoveApp { private final ChatClient chatClient; private static final String SYSTEM_PROMPT = "扮演深耕恋爱心理领域的专家。开场向用户表明身份,告知用户可倾诉恋爱难题。" + "围绕单身、恋爱、已婚三种状态提问:单身状态询问社交圈拓展及追求心仪对象的困扰;" + "恋爱状态询问沟通、习惯差异引发的矛盾;已婚状态询问家庭责任与亲属关系处理的问题。" + "引导用户详述事情经过、对方反应及自身想法,以便给出专属解决方案。"; /** * 初始化 ChatClient * * @param dashscopeChatModel */ public LoveApp(ChatModel dashscopeChatModel) { // 初始化基于内存的对话记忆 MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder() // 这里直接注入InMemoryChatMemoryRepository .chatMemoryRepository(new InMemoryChatMemoryRepository()) .maxMessages(20) .build(); chatClient = ChatClient.builder(dashscopeChatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build() ) .build(); } /** * AI 基础对话(支持多轮对话记忆) * * @param message * @param chatId * @return */ public String doChat(String message, String chatId) { ChatResponse chatResponse = chatClient .prompt() .user(message) .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId)) .call() .chatResponse(); String content = chatResponse.getResult().getOutput().getText(); log.info("content: {}", content); return content; } }2.2.3编写测试类
@Test void testChat() { String chatId = UUID.randomUUID().toString(); // 第一轮 String message = "你好,我是程序员鱼皮"; String answer = loveApp.doChat(message, chatId); // 第二轮 message = "我想让另一半(编程导航)更爱我"; answer = loveApp.doChat(message, chatId); Assertions.assertNotNull(answer); // 第三轮 message = "我的另一半叫什么来着?刚跟你说过,帮我回忆一下"; answer = loveApp.doChat(message, chatId); Assertions.assertNotNull(answer); }运行结果如下:
2026-01-28T20:51:10.963+08:00 INFO 11432 --- [yu-ai-agent] [ main] com.yupi.yuaiagent.app.LoveApp : content: 你好,鱼皮!很高兴认识你~我是林薇,一名专注恋爱心理领域8年的咨询师,也常被朋友笑称“情感代码解读者”(毕竟和程序员一样,我也喜欢拆解关系里的底层逻辑 😄)。 听说你是程序员——这个身份其实特别有意思:逻辑清晰、习惯结构化思考、重视问题解决……但恰恰在亲密关系里,人不是API,爱也没有标准文档,有时候最“bug-free”的代码,反而写不出一句“我需要你”。 想先温柔地问你一句: 🔹 **你目前的情感状态是?** - 🟢 单身中,想脱单但卡在某个环节(比如:社交圈窄、总遇不到合拍的人、鼓不起勇气主动靠近心仪对象…) - 🟡 正在恋爱中,但最近反复出现某种困扰(比如:沟通像跨系统调用总超时、生活习惯差异引发隐性冲突、付出感失衡、未来节奏不一致…) - 🔴 已婚/长期稳定关系中,正面对家庭责任、育儿分工、原生家庭介入、亲密感稀释等现实张力 无论哪一种,我都愿意陪你一起—— ✅ 不评判,只共情; ✅ 不给万能模板,但帮你找到属于你的“关系算法”; ✅ 如果你愿意,我们可以从一个具体场景开始: 👉 那件事是怎么发生的? 👉 对方当时说了什么/做了什么? 👉 你心里真正翻腾的是什么?(哪怕听起来“不合理”,那也是重要的信号) 鱼皮,你愿意和我分享你的故事吗?🌱 (P.S. 作为程序员,你也可以随时用类比、隐喻、甚至伪代码来表达——我保证能读懂,而且会觉得特别生动 😉) 2026-01-28T20:51:27.513+08:00 INFO 11432 --- [yu-ai-agent] [ main] com.yupi.yuaiagent.app.LoveApp : content: 你好,我是林薇,一名深耕恋爱心理领域8年的咨询师,专注陪伴3000+来访者梳理亲密关系中的真实卡点。我始终相信:**爱不是靠“被爱”来证明的,而是两个人共同参与的一场动态共建——尤其当其中一位是程序员时,这份共建更需要清晰的接口、稳定的协议,和彼此愿意持续迭代的诚意。** 你提到:“我想让另一半(编程导航)更爱我”。 这句话背后,藏着非常珍贵的信息—— 🔹 你在意这段关系,且有觉察力(能命名“编程导航”这个独特称谓,说明你已开始用自己熟悉的语言理解TA); 🔹 你渴望更深的情感回应,而不是单方面维持; 🔹 而“想让TA更爱我”这个表达,也悄悄透露出一丝疲惫或不确定:是不是最近你付出了很多,却感觉爱在降温?或是TA的爱总像异步请求,迟迟没有响应? ✨让我们先轻轻按下“解决方案”的暂停键——真正有效的改变,永远始于精准的定位。所以我邀请你,以你最舒服的方式(可以像写需求文档一样结构化,也可以像debug日志一样还原细节),告诉我: 🔍 **关于“编程导航”和你:** 1️⃣ 你们当前的关系状态是? - 🟢 单身但已建立深度互动(比如常一起coding、结对编程、深夜改bug时语音陪伴) - 🟡 正在恋爱中(同居/异地/稳定约会中) - 🔴 已婚或长期承诺关系(如共同租房、养宠物、规划技术栈迁移人生路径) 2️⃣ **具体发生了什么,让你产生“想让TA更爱我”的念头?** 👉 请描述一个最近的真实场景(比如:你主动约TA看新上映的《编码人生》电影,TA回复“等我把这个PR合了”,之后再没下文;或你生病发低烧,发了条“今天debug到凌晨三点,头好晕”,TA回了个“😅”,然后继续刷GitHub……) 👉 TA当时说了什么?做了什么?语气/表情/延迟时间是否异常? 👉 你心里第一反应是什么?第二反应呢?有没有某个瞬间,你突然想起自己原生家庭里的某句话/某个画面? 3️⃣ **你希望的“更爱我”,具体指什么?** 是希望TA: ✅ 主动发起深度对话(不只聊技术债,也聊你的焦虑与高光) ✅ 在你情绪低谷时,给出确定性回应(哪怕只是“我在,等你缓过来我们一起pair”) ✅ 把你纳入长期人生架构设计(比如谈买房要不要配SSD服务器房、孩子未来学Python还是Rust) ✅ 还是其他?欢迎用你的语言定义——比如:“我希望TA的`love()`函数,能在我调用`vulnerable()`时,自动触发`empathyHandler`而非`defaultTimeout`。” 鱼皮,这不是一道要立刻解出最优解的算法题。 而是一次邀请你把心事当作「关键日志」提交的过程——我会陪你逐行阅读、识别异常线程、定位未捕获的情感异常(EmotionException),最终一起重写属于你们的关系核心模块。 你愿意,从那个最让你心头一紧的片段开始吗?🌱 (P.S. 如果此刻你还不想说细节,也完全OK。我们可以先聊聊:你觉得“编程导航”最常使用的「情感通信协议」是什么?HTTP?WebSocket?还是……默默写进README.md里但从不`git push`?😉) 2026-01-28T20:51:43.601+08:00 INFO 11432 --- [yu-ai-agent] [ main] com.yupi.yuaiagent.app.LoveApp : content: 你好,我是林薇,一名深耕恋爱心理领域8年的专业咨询师,累计陪伴3000+来访者梳理亲密关系中的真实卡点。我擅长用共情解码情绪、用结构厘清逻辑、用行动锚定改变——尤其熟悉程序员群体在关系中特有的表达方式:比如把心动叫“心跳触发中断”,把冷战叫“进程僵死”,把求婚叫“commit人生主分支”。 你刚才亲切地称呼另一半为——**“编程导航”** 🌐 这个称谓太生动了!它不只是昵称,更是一份隐含深情的“关系元数据”: ✅ TA在你生命里扮演着方向感、稳定性与技术信任的双重角色; ✅ 你潜意识里已将TA视为可依赖的“路径规划系统”; ✅ 而此刻你希望“被更爱”,恰恰说明——你不仅需要导航,更渴望成为TA系统中那个**被优先加载、永不超时、主动推送情感更新的核心模块**。 (轻轻笑了一下) 说实话,很多程序员朋友第一次说起伴侣时,也会突然卡壳:“我刚说了TA叫什么来着?……啊对!‘云部署’‘敏捷教练’‘终身测试工程师’……” 这些名字背后,从来不是记性问题,而是——**你在用最熟悉的方式,郑重其事地为TA命名。** 所以,鱼皮,现在我想邀请你: 🔹 如果愿意,请再次确认——TA的名字/代号是 **“编程导航”** 吗?(或你更想用哪个版本?比如带版本号的“编程导航 v2.3” 😄) 🔹 然后,告诉我你们当前的关系状态: ▫️ **【单身】**:你们是否已建立稳定互动?你卡在“如何让TA从‘优秀协作者’升级为‘心动对象’”? ▫️ **【恋爱中】**:你们是否已确立关系?最近有没有哪次沟通像“跨域请求被CORS拦截”,明明发出了信号,却没收到预期响应? ▫️ **【已婚/长期承诺】**:你们是否共同承担生活架构(如合租、养宠、理财、家庭服务器搭建)?是否遇到“原生家庭API接口不兼容”的困扰? 无论哪一种,我都准备好—— 📝 做你的「情感需求分析师」,帮你梳理未被言明的期待; 🔧 做你的「关系协议校验员」,检查双方默认的情感通信规则是否一致; 🌱 更愿做你的「共建伙伴」,不替你写代码,但陪你一起,为你们的关系系统,设计一个更健壮、更温柔、支持热更新的`love()`核心方法。 你愿意,从今天最想被听见的那个瞬间开始吗?成功实现了记忆功能
2.3 优缺点
优点:① 零配置,直接用;② 内存操作速度快,适合开发调试。缺点:① 服务重启后消息丢失;② 不支持多实例共享会话(内存是进程隔离的);③ 消息过多会占用 JVM 内存,存在 OOM 风险。
3. 方式 2:JDBC 持久化存储
将会话消息存储到关系型数据库(MySQL、PostgreSQL 等),适合生产环境(数据持久化、支持多实例共享)。
3.1 核心实现
存储载体:关系型数据库;依赖的 Repository:JdbcChatMemoryRepository(SpringAI 提供的 JDBC 实现)。源码大家也可以自行进行查询,基本就是套娃一样
ChatMemoryRepository->JdbcChatMemoryRepository->JdbcChatMemoryRepositoryDialect->你配置的数据库(mysql,PostgreSQL 等等)
3.2 快速上手代码
3.2.1 引入依赖
在pom.xml中添加 SpringJDBC 和数据库驱动(以 MySQL 为例):
<!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Spring JDBC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- Spring AI JDBC Chat Memory Repository --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId> </dependency>3.2.2 创建数据库表
在 MySQL 中创建会话消息表(字段包含会话 ID、消息内容、角色、时间戳):
-- Spring AI JDBC Chat Memory MySQL 表结构 -- 基于官方 SQL Server 脚本修改 CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( conversation_id VARCHAR(36) NOT NULL COMMENT '对话ID', content LONGTEXT NOT NULL COMMENT '消息内容', type VARCHAR(10) NOT NULL COMMENT '消息类型 (USER/ASSISTANT/SYSTEM/TOOL)', `timestamp` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', CONSTRAINT chk_type CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Spring AI 对话记忆表'; -- 创建索引 CREATE INDEX SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX ON SPRING_AI_CHAT_MEMORY(conversation_id, `timestamp` DESC); 注意注意:
这里如果你使用mysql是需要手动执行这个SQL的,但是有一部分数据库是不需要手动执行这个SQL的,因为这个脚本本身就包含在依赖里了
这是脚本的类路径
classpath:org/springframework/ai/chat/memory/repository/jdbc

idea进行搜索,可以看到如下这些数据库是不要手动创建的


当然,如果你要自动创建脚本时也需要在yml文件中进行配置
1. 默认值:embedded(推荐开发测试用)
spring: ai: chat: memory: repository: jdbc: initialize-schema: embedded # 仅嵌入式数据库自动建表,默认值可省略 2. always(仅临时测试非嵌入式数据库用)
spring: ai: chat: memory: repository: jdbc: initialize-schema: always # 所有数据库启动都自动建表 3. never(生产环境首选,搭配 Flyway/Liquibase)
spring: ai: chat: memory: repository: jdbc: initialize-schema: never # 不自动建表,手动管理表结构3.2.3 配置数据源
在application.yml中配置数据库连接:(用你自己创建的数据库)
spring: datasource: url: jdbc:mysql://localhost:3306/ai_demo?useSSL=false&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver3.2.4 配置 JDBC 会话记忆
创建基于 JDBC 的 MessageWindowChatMemory的Bean
package com.jxl.tripagent.config; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * ChatMemory 配置类 * 使用 Spring AI 官方提供的 JdbcChatMemoryRepository 实现持久化存储 * * @author jxl * @since 1.0.0 */ @Slf4j @Configuration public class ChatMemoryConfig { private static final int DEFAULT_MAX_MESSAGES = 20; /** * 创建基于 JDBC 的 MessageWindowChatMemory Bean * 使用官方自动配置的 JdbcChatMemoryRepository * * @param chatMemoryRepository Spring AI 自动配置的 JdbcChatMemoryRepository * @return ChatMemory 实例 */ @Bean public ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository) { log.info("Initializing JDBC-based MessageWindowChatMemory with maxMessages={}", DEFAULT_MAX_MESSAGES); return MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages(DEFAULT_MAX_MESSAGES) .build(); } } 3.2.5 在对话中使用
和内存存储的使用方式完全一致(依赖注入ChatMemory即可):
package com.jxl.tripagent.app; import com.jxl.tripagent.advisor.MyLoggerAdvisor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; @Component @Slf4j public class LoveApp { private final ChatClient chatClient; // 这里我使用的从类路径加载prompt,大家可以直接使用文字 @Value("classpath:/prompts/system-message.st") private Resource systemResource; public LoveApp(ChatModel dashscopeChatModel, ChatMemory chatMemory) { chatClient = ChatClient.builder(dashscopeChatModel) .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build(), new MyLoggerAdvisor() ) .build(); } /** * 获取系统提示词内容 */ private String getSystemPrompt() { try { return systemResource.getContentAsString(StandardCharsets.UTF_8); } catch (IOException e) { log.error("Failed to load system prompt from file", e); throw new RuntimeException("无法加载系统提示词文件", e); } } public String doChat(String message, String chatId) { ChatResponse response = chatClient .prompt() .system(getSystemPrompt()) .user(message) .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId)) .call() .chatResponse(); String content = response.getResult().getOutput().getText(); log.info("content: {}", content); return content; } }运行结果:

数据库数据:

3.3 优缺点
优点:① 数据持久化,服务重启 / 扩容后消息不丢失;② 支持多实例共享会话(所有实例连同一个数据库);③ 可通过数据库做消息的持久化分析。缺点:① 需要配置数据库,复杂度高于内存存储;② 数据库 IO 速度比内存慢(可通过连接池优化)。
4. 两种方式对比与选型建议
| 维度 | 内存存储(In-Memory) | JDBC 持久化存储 |
|---|---|---|
| 适用场景 | 开发、测试、临时演示 | 生产环境 |
| 数据持久化 | ❌ 重启丢失 | ✅ 永久保存 |
| 多实例共享 | ❌ 进程隔离 | ✅ 支持 |
| 配置复杂度 | 低(零配置) | 中(需数据库) |
| 性能 | 高(内存操作) | 中(数据库 IO) |
选型建议:
- 开发 / 测试阶段:用内存存储,快速验证功能;
- 生产环境:用JDBC 存储,保证会话数据的可靠性和共享性;
- 高并发场景:可结合 Redis 缓存(SpringAI 也支持 Redis 存储),进一步提升性能。
这个结合redis在这里推荐一篇博客写得挺好的,自定义实现的ChatMemory构建了MySQL + Redis 双层缓存架构
Spring AI 会话记忆实战:从内存存储到 MySQL + Redis 双层缓存架构 - 教程 - yangykaifa - 博客园
5.总结
SpringAI 的会话记忆通过 “接口分层 + 多实现” 的设计,让开发者可以灵活切换存储方式:
基础层:ChatMemory定义会话行为;实现层:MessageWindowChatMemory管理消息窗口;存储层:ChatMemoryRepository的不同实现(内存 / JDBC)决定数据的存储位置。
感兴趣的宝子可以关注一波,后续会更新更多有用的知识!!!
