还在为 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,但很快就会发现困难重重:

  1. Message 是个接口,有 UserMessageSystemMessage 等多种实现。
  2. 不同子类的字段各不相同,结构不统一。
  3. 这些子类大多没有无参构造函数,也没有实现 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 对话记忆大揭秘:服务器重启,聊天记录不再丢失!的更多相关文章

  1. 性能追击:万字长文30+图揭秘8大主流服务器程序线程模型 | Node.js,Apache,Nginx,Netty,Redis,Tomcat,MySQL,Zuul

    本文为<高性能网络编程游记>的第六篇"性能追击:万字长文30+图揭秘8大主流服务器程序线程模型". 最近拍的照片比较少,不知道配什么图好,于是自己画了一个,凑合着用,让 ...

  2. 对话机器学习大神Yoshua Bengio(下)

    对话机器学习大神Yoshua Bengio(下) Yoshua Bengio教授(个人主页)是机器学习大神之一,尤其是在深度学习这个领域.他连同Geoff Hinton老先生以及 Yann LeCun ...

  3. Spring之WebSocket网页聊天以及服务器推送

    Spring之WebSocket网页聊天以及服务器推送 转自:http://www.xdemo.org/spring-websocket-comet/ /Springframework /Spring ...

  4. RabbitMD大揭秘

    RabbitMD大揭秘 欢迎关注H寻梦人公众号 通过SpringBoot整合RabbitMQ的案例来说明,RabbitMQ相关的各个属性以及使用方式:并通过相关源码深刻理解. Queue(消息队列) ...

  5. spring boot 从开发到部署(二)—重启服务

    上篇中,我们开发并部署上线了一个 spring boot 项目.现在需要编写服务重启脚本,保证服务器重启后能够自动的运行我们的项目. /home/web/sprint-web/restart-happ ...

  6. Spring - MVC - 修改 Java 类后, 触发重启

    1. 概述 学习 Spring MVC 下, 如何可控的触发重启 2. 背景 学习 Spring 场景 有些时候, 改完类, 需要重启 之前有听说, Spring MVC 可以自动重启 于是想, 尝试 ...

  7. 服务器重启后SQL Server Agent由于"The EventLog service has not been started" 启动失败

    案例环境: 操作系统   : Microsoft Windows Server 2003 Standard Edtion SP2 数据库版本 : SQL Server 2005 Standard Ed ...

  8. 解决oracle服务器重启之后连接报错的问题

    DB服务器重启之后再连接报错如下: 原因是重启之后listener.ora被还原成初始文件,sid被清空. 解决步骤: 1.查看监听服务和数据库服务: 由此找到listener.ora文件的路径:D: ...

  9. 【腾讯Bugly干货分享】iOS黑客技术大揭秘

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/5791da152168f2690e72daa4 “8小时内拼工作,8小时外拼成长 ...

  10. Spark Streaming揭秘 Day3-运行基石(JobScheduler)大揭秘

    Spark Streaming揭秘 Day3 运行基石(JobScheduler)大揭秘 引子 作为一个非常强大框架,Spark Streaming兼具了流处理和批处理的特点.还记得第一天的谜团么,众 ...

随机推荐

  1. 【U-Boot】解决U-Boot的“Unknown command 'help' - try 'help'”问题

    [U-Boot]解决U-Boot的"Unknown command 'help' - try 'help'"问题 零.起因 最近在玩U-Boot,自己编译U-Boot之后输入hel ...

  2. Unity性能优化-降低功耗,发热量,耗电量之OnDemandRendering篇

    公司游戏项目,手机运行严重发烫,耗电量飞快.在暂时无法做其他美术性和技术性优化的情况下,我写了这个公司内部文档,并做了个实验,今天干脆公布出来,希望对大家有用. --官方文档: Unity - Scr ...

  3. idea的deployment没有war包

    一.解决方案

  4. 🎀Java-Exception与RuntimeException

    简介 Exception Exception 类是所有非致命性异常的基类.这些异常通常是由于编程逻辑问题或外部因素(如文件不存在.网络连接失败等)导致的,可以通过适当的编程手段来恢复或处理.Excep ...

  5. Mouse Down鼠标操作指令的用法

    如下图 暂无评论的按钮在整页下方,需要拖动页面才会显示出这个按钮,否则不可点击 Mouse Down  提供拖动页面的能提 这个方法因selenium2library和AutoItLibrary 都有 ...

  6. html中的em和rem到底该如何使用,自适应效果中如何确定文字大小/字号?

    如今手机屏幕繁多,自适应效果中如何确定文字大小/字号? em rem vm vw vh你都了解吗? 先说说em和rem em:继承父级的,假设html的font-size默认为16px,body字体大 ...

  7. 蒟蒻 AstralNahida 的码风

    前言 这里是蒟蒻 OIer AstralNahida 在 OI 中的码风的详细介绍. 个人认为码风相当清晰,供给各位参考. 约定 对于一些表示必要性的关键词,从 must 到 mustn't 排序如下 ...

  8. Spring纯注解的事务管理

    Spring纯注解的事务管理 源码 代码测试 pom.xml <?xml version="1.0" encoding="UTF-8"?> < ...

  9. Oracle ACL (Access Control List) 详细介绍

    参考:https://blog.csdn.net/qq243348167/article/details/87876956 --查询acl信息 SELECT * FROM dba_network_ac ...

  10. 开源的java内网穿透 - 维基代理(wiki-proxy)

    1.简介 维基代理(wiki-proxy).开源的java内网穿透项目. 技术栈:cdkjFramework(维基框架).JPA.Netty 遵循MIT许可,因此您可以对它进行复制.修改.传播并用于任 ...