找回密码
 立即注册
首页 业界区 业界 Spring AI 对话记忆大揭秘:服务器重启,聊天记录不再丢 ...

Spring AI 对话记忆大揭秘:服务器重启,聊天记录不再丢失!

雨角 昨天 22:16
还在为 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 自己的仓库里能找到它的踪迹,但这用户量……基本上等于让我们去“开荒”,风险太高了。
1.png

既然官方的路不好走,那我们就自己动手,丰衣足食!
二、另辟蹊径:自定义你的 ChatMemory

我更推荐的方案是:自定义实现 ChatMemory 接口
Spring AI 的设计非常巧妙,它将“存储介质”和“记忆算法”解耦了。这意味着我们可以只替换存储部分,而不用改动整个对话流程。
虽然官方没给示例,但没关系,我们可以“偷师”啊!直接去看默认实现类 InMemoryChatMemory 的源码,模仿它的实现。
ChatMemory 接口的核心方法很简单,就是对消息的增、删、查:
2.png

InMemoryChatMemory 的源码显示,它内部其实就是用一个 ConcurrentHashMap 来存消息,Key 是对话 ID,Value 是这个对话的所有消息列表。
3.png

思路有了,接下来就是实战!
三、实战演练:打造文件版 ChatMemory

为了避免引入数据库等额外依赖的复杂性,我们先来实现一个最简单的:基于文件的持久化 ChatMemory
这里的核心挑战在于 消息对象的序列化与反序列化。我们需要将内存中的 Message 对象转换成文本存入文件,也要能从文件中读出文本并还原成 Message 对象。
你可能会首先想到用 JSON,但很快就会发现困难重重:

  • Message 是个接口,有 UserMessage、SystemMessage 等多种实现。
  • 不同子类的字段各不相同,结构不统一。
  • 这些子类大多没有无参构造函数,也没有实现 Serializable 接口。
4.png

直接用 JSON 序列化,大概率会踩坑。因此,我们请出一位“外援”——高性能序列化库 Kryo。
第一步:引入 Kryo 依赖
在 pom.xml 中添加:
  1. <dependency>
  2.     <groupId>com.esotericsoftware</groupId>
  3.     kryo</artifactId>
  4.     <version>5.6.2</version>
  5. </dependency>
复制代码
第二步:编写 FileBasedChatMemory
新建 chatmemory 包,创建 FileBasedChatMemory.java。别被下面的代码吓到,核心逻辑就是文件的读写和对象的序列化/反序列化,完全可以让 AI 帮你生成。
[code]// ... 省略 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 messages) {        // 获取或创建对话的消息列表        List conversationMessages = getOrCreateConversation(conversationId);        // 将新的消息添加到对话消息列表中        conversationMessages.addAll(messages);        // 保存更新后的对话消息列表到文件        saveConversation(conversationId, conversationMessages);    }    @Override    public List get(String conversationId, int lastN) {        // 获取或创建对话的消息列表        List allMessages = getOrCreateConversation(conversationId);        // 如果消息总数小于等于 lastN,直接返回所有消息        if (allMessages.size()
您需要登录后才可以回帖 登录 | 立即注册