Spring AI 对话记忆大揭秘:服务器重启,聊天记录不再丢失!
还在为 Spring AI 应用重启后对话上下文丢失而烦恼吗?本文将带你深入 Spring AI 的对话记忆机制,并手把手教你实现一个基于文件的持久化方案,让你的 AI 应用拥有 “过目不忘” 的超能力!
哈喽,各位程序员朋友们!
在之前的文章里,我们一起探索了如何使用 Spring AI 构建能理解上下文的对话机器人。但一个棘手的问题很快就浮现了:我们的对话记忆都存在内存里,服务器一旦重启,珍贵的聊天记录就灰飞烟灭了。这可不行!
想象一下,用户正和你的 AI 聊得火热,结果服务器一更新,AI 就 “失忆” 了,之前的对话全忘了。这体验感,简直一言难尽。
那么,有没有办法让对话记忆像数据一样被持久化,存到文件、数据库或者 Redis 里呢?
答案是:当然有!Spring AI 早就为我们考虑到了这一点。
一、官方方案:理想与现实的差距
Spring AI 官方文档中提到,它提供了一些现成的持久化方案,可以将对话记忆保存到不同的数据源中。听起来很不错,对吧?
InMemoryChatMemory
:默认的内存存储,我们一直在用。CassandraChatMemory
:用 Cassandra 持久化,还带过期时间。Neo4jChatMemory
:用 Neo4j 持久化,永不过期。JdbcChatMemory
:用 JDBC 持久化到关系型数据库。
看到 JdbcChatMemory
,我们可能两眼放光:这不就是我们想要的吗?然而,现实却给我们泼了一盆冷水。spring-ai-starter-model-chat-memory-jdbc
这个依赖不仅版本稀少,相关文档也几乎没有,甚至在 Maven 中央仓库都搜不到。
虽然在 Spring 自己的仓库里能找到它的踪迹,但这用户量……基本上等于让我们去“开荒”,风险太高了。
既然官方的路不好走,那我们就自己动手,丰衣足食!
二、另辟蹊径:自定义你的 ChatMemory
我更推荐的方案是:自定义实现 ChatMemory
接口。
Spring AI 的设计非常巧妙,它将“存储介质”和“记忆算法”解耦了。这意味着我们可以只替换存储部分,而不用改动整个对话流程。
虽然官方没给示例,但没关系,我们可以“偷师”啊!直接去看默认实现类 InMemoryChatMemory
的源码,模仿它的实现。
ChatMemory
接口的核心方法很简单,就是对消息的增、删、查:
InMemoryChatMemory
的源码显示,它内部其实就是用一个 ConcurrentHashMap
来存消息,Key 是对话 ID,Value 是这个对话的所有消息列表。
思路有了,接下来就是实战!
三、实战演练:打造文件版 ChatMemory
为了避免引入数据库等额外依赖的复杂性,我们先来实现一个最简单的:基于文件的持久化 ChatMemory
。
这里的核心挑战在于 消息对象的序列化与反序列化。我们需要将内存中的 Message
对象转换成文本存入文件,也要能从文件中读出文本并还原成 Message
对象。
你可能会首先想到用 JSON,但很快就会发现困难重重:
Message
是个接口,有UserMessage
、SystemMessage
等多种实现。- 不同子类的字段各不相同,结构不统一。
- 这些子类大多没有无参构造函数,也没有实现
Serializable
接口。
直接用 JSON 序列化,大概率会踩坑。因此,我们请出一位“外援”——高性能序列化库 Kryo。
第一步:引入 Kryo 依赖
在 pom.xml
中添加:
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.2</version>
</dependency>
第二步:编写 FileBasedChatMemory
新建 chatmemory
包,创建 FileBasedChatMemory.java
。别被下面的代码吓到,核心逻辑就是文件的读写和对象的序列化/反序列化,完全可以让 AI 帮你生成。
// ... 省略 package 和 import ...
/**
* @author BNTang
* @version 1.0
* @description 基于文件持久化的对话记忆,实现 ChatMemory 接口
**/
public class FileBasedChatMemory implements ChatMemory {
/**
* 文件存储的基础目录
*/
private final String BASE_DIR;
// Kryo 实例,用于序列化和反序列化消息对象
private static final Kryo KRYO = new Kryo();
static {
// 设置 Kryo 的注册要求为 false,允许未注册的类进行序列化
KRYO.setRegistrationRequired(false);
// 设置 Kryo 的实例化策略为标准实例化策略
KRYO.setInstantiatorStrategy(new StdInstantiatorStrategy());
}
/**
* 构造函数,初始化文件存储目录。
*
* @param dir 文件存储目录路径
*/
public FileBasedChatMemory(String dir) {
// 设置基础目录
this.BASE_DIR = dir;
// 确保目录存在
File baseDir = new File(BASE_DIR);
// 如果目录不存在,则创建目录
if (!baseDir.exists()) {
// 尝试创建目录,如果失败则抛出异常
boolean created = baseDir.mkdirs();
// 如果目录创建失败,抛出运行时异常
if (!created) {
// 目录创建失败,抛出异常
throw new RuntimeException("Failed to create directory: " + BASE_DIR);
}
}
}
@Override
public void add(String conversationId, List<Message> messages) {
// 获取或创建对话的消息列表
List<Message> conversationMessages = getOrCreateConversation(conversationId);
// 将新的消息添加到对话消息列表中
conversationMessages.addAll(messages);
// 保存更新后的对话消息列表到文件
saveConversation(conversationId, conversationMessages);
}
@Override
public List<Message> get(String conversationId, int lastN) {
// 获取或创建对话的消息列表
List<Message> allMessages = getOrCreateConversation(conversationId);
// 如果消息总数小于等于 lastN,直接返回所有消息
if (allMessages.size() <= lastN) {
return allMessages;
}
// 否则,返回最后 N 条消息
return allMessages.subList(allMessages.size() - lastN, allMessages.size());
}
@Override
public void clear(String conversationId) {
// 获取对话文件
File file = getConversationFile(conversationId);
// 如果文件存在,则删除该文件
if (file.exists()) {
// 尝试删除文件,如果删除失败则打印警告信息
file.delete();
}
}
/**
* getOrCreateConversation 方法用于获取或创建一个对话的消息列表。
*
* @param conversationId 对话 ID,用于标识特定的对话
* @return 一个包含对话消息的列表,如果文件不存在则返回一个空列表
*/
private List<Message> getOrCreateConversation(String conversationId) {
// 获取对话文件
File file = getConversationFile(conversationId);
// 如果文件不存在,则创建一个新的空列表
if (!file.exists()) {
return new ArrayList<>();
}
// 如果文件存在,则读取文件中的消息列表
try (Input input = new Input(new FileInputStream(file))) {
// 使用 Kryo 反序列化读取的对象
return KRYO.readObject(input, ArrayList.class);
} catch (Exception e) {
// 如果读取文件失败,打印异常堆栈跟踪,并返回空列表以防程序崩溃
e.printStackTrace();
return new ArrayList<>();
}
}
/**
* saveConversation 方法用于将对话消息列表保存到文件中。
*
* @param conversationId 对话 ID,用于标识特定的对话
* @param messages 对话消息列表,包含要保存的消息对象
*/
private void saveConversation(String conversationId, List<Message> messages) {
// 获取对话文件
File file = getConversationFile(conversationId);
// 确保父目录存在
try (Output output = new Output(new FileOutputStream(file))) {
// 使用 Kryo 序列化消息列表并写入文件
KRYO.writeObject(output, messages);
} catch (IOException e) {
// 如果写入文件失败,打印异常堆栈跟踪
e.printStackTrace();
}
}
/**
* getConversationFile 方法用于获取特定对话 ID 的文件。
*
* @param conversationId 对话 ID,用于标识特定的对话
* @return 一个 File 对象,表示存储该对话消息的文件
*/
private File getConversationFile(String conversationId) {
// 返回一个新的 File 对象,表示存储对话消息的文件
return new File(BASE_DIR, conversationId + ".kryo");
}
}
第三步:配置 ChatClient
修改 App
的构造函数,告诉 ChatClient
使用我们新的文件版对话记忆。
public App(ChatModel ollamaChatModel) {
// 指定一个用于存放记忆文件的目录
String fileDir = System.getProperty("user.dir") + "/temp/chat-memory";
// 实例化我们自定义的 ChatMemory
ChatMemory chatMemory = new FileBasedChatMemory(fileDir);
// 构建 ChatClient,并注入 ChatMemory
chatClient = ChatClient.builder(ollamaChatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory),
new MyLoggerAdvisor()
)
.build();
}
第四步:见证奇迹
运行你的应用,进行几轮对话,然后查看项目根目录下的 temp/chat-memory
文件夹。你会发现,对话记录已经被成功保存为 .kryo
文件了!
现在,即使你重启应用,AI 也能找回之前的对话,继续和用户愉快地交流。
Spring AI 开发中的常见痛点:对话记忆的持久化。通过自定义 ChatMemory
接口,我们成功地将对话历史从易失的内存转移到了稳定的文件中,让我们的 AI 应用拥有了“长期记忆”。
这个方法不仅限于文件存储,你可以举一反三,将其改造为基于 Redis、MongoDB 或任何你喜欢的存储方案。这正是 Spring AI 框架灵活性的体现。
希望这篇文章能对你有所启发!动手试试吧,给你的 AI 装上一个“超级大脑”!
如果你觉得本文对你有帮助,欢迎点赞、在看、分享三连! 你的支持是我持续创作的最大动力!
Spring AI 对话记忆大揭秘:服务器重启,聊天记录不再丢失!的更多相关文章
- 性能追击:万字长文30+图揭秘8大主流服务器程序线程模型 | Node.js,Apache,Nginx,Netty,Redis,Tomcat,MySQL,Zuul
本文为<高性能网络编程游记>的第六篇"性能追击:万字长文30+图揭秘8大主流服务器程序线程模型". 最近拍的照片比较少,不知道配什么图好,于是自己画了一个,凑合着用,让 ...
- 对话机器学习大神Yoshua Bengio(下)
对话机器学习大神Yoshua Bengio(下) Yoshua Bengio教授(个人主页)是机器学习大神之一,尤其是在深度学习这个领域.他连同Geoff Hinton老先生以及 Yann LeCun ...
- Spring之WebSocket网页聊天以及服务器推送
Spring之WebSocket网页聊天以及服务器推送 转自:http://www.xdemo.org/spring-websocket-comet/ /Springframework /Spring ...
- RabbitMD大揭秘
RabbitMD大揭秘 欢迎关注H寻梦人公众号 通过SpringBoot整合RabbitMQ的案例来说明,RabbitMQ相关的各个属性以及使用方式:并通过相关源码深刻理解. Queue(消息队列) ...
- spring boot 从开发到部署(二)—重启服务
上篇中,我们开发并部署上线了一个 spring boot 项目.现在需要编写服务重启脚本,保证服务器重启后能够自动的运行我们的项目. /home/web/sprint-web/restart-happ ...
- Spring - MVC - 修改 Java 类后, 触发重启
1. 概述 学习 Spring MVC 下, 如何可控的触发重启 2. 背景 学习 Spring 场景 有些时候, 改完类, 需要重启 之前有听说, Spring MVC 可以自动重启 于是想, 尝试 ...
- 服务器重启后SQL Server Agent由于"The EventLog service has not been started" 启动失败
案例环境: 操作系统 : Microsoft Windows Server 2003 Standard Edtion SP2 数据库版本 : SQL Server 2005 Standard Ed ...
- 解决oracle服务器重启之后连接报错的问题
DB服务器重启之后再连接报错如下: 原因是重启之后listener.ora被还原成初始文件,sid被清空. 解决步骤: 1.查看监听服务和数据库服务: 由此找到listener.ora文件的路径:D: ...
- 【腾讯Bugly干货分享】iOS黑客技术大揭秘
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/5791da152168f2690e72daa4 “8小时内拼工作,8小时外拼成长 ...
- Spark Streaming揭秘 Day3-运行基石(JobScheduler)大揭秘
Spark Streaming揭秘 Day3 运行基石(JobScheduler)大揭秘 引子 作为一个非常强大框架,Spark Streaming兼具了流处理和批处理的特点.还记得第一天的谜团么,众 ...
随机推荐
- 【U-Boot】解决U-Boot的“Unknown command 'help' - try 'help'”问题
[U-Boot]解决U-Boot的"Unknown command 'help' - try 'help'"问题 零.起因 最近在玩U-Boot,自己编译U-Boot之后输入hel ...
- Unity性能优化-降低功耗,发热量,耗电量之OnDemandRendering篇
公司游戏项目,手机运行严重发烫,耗电量飞快.在暂时无法做其他美术性和技术性优化的情况下,我写了这个公司内部文档,并做了个实验,今天干脆公布出来,希望对大家有用. --官方文档: Unity - Scr ...
- idea的deployment没有war包
一.解决方案
- 🎀Java-Exception与RuntimeException
简介 Exception Exception 类是所有非致命性异常的基类.这些异常通常是由于编程逻辑问题或外部因素(如文件不存在.网络连接失败等)导致的,可以通过适当的编程手段来恢复或处理.Excep ...
- Mouse Down鼠标操作指令的用法
如下图 暂无评论的按钮在整页下方,需要拖动页面才会显示出这个按钮,否则不可点击 Mouse Down 提供拖动页面的能提 这个方法因selenium2library和AutoItLibrary 都有 ...
- html中的em和rem到底该如何使用,自适应效果中如何确定文字大小/字号?
如今手机屏幕繁多,自适应效果中如何确定文字大小/字号? em rem vm vw vh你都了解吗? 先说说em和rem em:继承父级的,假设html的font-size默认为16px,body字体大 ...
- 蒟蒻 AstralNahida 的码风
前言 这里是蒟蒻 OIer AstralNahida 在 OI 中的码风的详细介绍. 个人认为码风相当清晰,供给各位参考. 约定 对于一些表示必要性的关键词,从 must 到 mustn't 排序如下 ...
- Spring纯注解的事务管理
Spring纯注解的事务管理 源码 代码测试 pom.xml <?xml version="1.0" encoding="UTF-8"?> < ...
- Oracle ACL (Access Control List) 详细介绍
参考:https://blog.csdn.net/qq243348167/article/details/87876956 --查询acl信息 SELECT * FROM dba_network_ac ...
- 开源的java内网穿透 - 维基代理(wiki-proxy)
1.简介 维基代理(wiki-proxy).开源的java内网穿透项目. 技术栈:cdkjFramework(维基框架).JPA.Netty 遵循MIT许可,因此您可以对它进行复制.修改.传播并用于任 ...