SpringAI会话记忆实现——基于MYSQL进行存储
本文介绍了SpringAI中实现上下文记忆功能的两种主要方式:内存存储和JDBC持久化存储。内存存储基于InMemoryChatMemoryRepository实现,配置简单、性能高,适合开发和测试环境,但存在数据易失和无法共享的缺点。JDBC存储通过JdbcChatMemoryRepository将对话记录持久化到关系型数据库,支持多实例共享和服务重启后的数据恢复,更适合生产环境。文章详细分析了
目录
在 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.Driver
3.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)决定数据的存储位置。
感兴趣的宝子可以关注一波,后续会更新更多有用的知识!!!

更多推荐



所有评论(0)