ChatMemory持久化存储
ChatMemory持久化存储
使用Mysql
第一步:引入依赖
注意依赖名和之前说的不一样,正确的是:
<!-- Spring AI JDBC 记忆模块 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>第二步:配置 application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always # 自动建表,MySQL 非嵌入式数据库需要设置 always默认情况下 initialize-schema 的值是 embedded,即只对嵌入式数据库(H2、HSQL 等)自动建表,所以 MySQL 必须显式设为 always。
第三步:业务代码
Spring AI 自动装配好后,直接注入 ChatMemory 使用,无需任何额外代码:
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
public String chat(String sessionId, String userMessage) {
return chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId)) // 正确的常量位置
.call()
.content();
}
}如果配合 Flyway / Liquibase 管理建表
spring:
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: never # 关闭自动建表然后参考官方提供的 SQL 脚本来配置建表语句,脚本路径为 classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-@@platform@@.sql,其中 @@platform@@ 会被替换为对应数据库名(如 mysql)。你可以在依赖 jar 包里找到这个文件,复制出来交给 Flyway 管理。
自定义持久化存储之redis
原理就是实现 ChatMemoryRepository 接口。
Spring AI 的存储层是面向接口编程的,JdbcChatMemoryRepository、CassandraChatMemoryRepository 都只是 ChatMemoryRepository 的实现类,需要注意如果有其他bean会冲突。
接口定义
按住 Ctrl 点进 ChatMemoryRepository,你会看到三个方法:
public interface ChatMemoryRepository {
// 查询所有会话的ID
List<String> findConversationIds();
// 查询某个会话的所有消息
List<Message> findByConversationId(String conversationId);
// 保存某个会话的消息(全量覆盖)
void saveAll(String conversationId, List<Message> messages);
// 删除某个会话的所有消息
void deleteByConversationId(String conversationId);
}你只需要实现这四个方法,告诉 Spring AI 去 Redis 读写就行了。
核心难点:Message 序列化
Redis 只能存字符串,但 Message 是一个接口,有多种子类:
Message (接口)
├── UserMessage
├── AssistantMessage
├── SystemMessage
└── ToolResponseMessage默认配置下的 Jackson 在反序列化时,由于 Spring AI 的消息体实体类没有提供公开的无参构造函数,会抛出异常。详细的踩坑记录和解决方案请参考:Spring AI + Redis 聊天记忆反序列化异常解决方案
完整实现
第一步:Redis配置及自定义反序列化器
为了解决上述的反序列化问题,我们需要自定义 ObjectMapper 并在 Redis 的配置类中激活多态类型识别以及注册自定义反序列化器。详细原理与完整代码见 踩坑指南。
第二步:实现 ChatMemoryRepository
配合自定义的反序列化器后,我们可以直接通过 RedisTemplate<String, Object> 将消息对象列表存入 Redis,而无需再中间转成 DTO。
根据我们的后端代码对应实现如下:
import jakarta.annotation.Resource;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisRepository implements ChatMemoryRepository {
private static final String CHAT_MEMORY_PREFIX = "chat:memory:";
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取所有对话对应的 ID 列表
*
* @return 包含所有已有对话 ID 的列表
*/
@Override
public List<String> findConversationIds() {
Set<String> keys = redisTemplate.keys(CHAT_MEMORY_PREFIX + "*");
List<String> conversationIds = new ArrayList<>();
if (keys != null) {
for (String key : keys) {
conversationIds.add(key.replace(CHAT_MEMORY_PREFIX, ""));
}
}
return conversationIds;
}
/**
* 根据对话 ID 获取该对话的完整历史消息记录
*
* @param conversationId 对话的唯一标识 ID
* @return 包含该对话所有消息的列表
*/
@Override
@SuppressWarnings("unchecked")
public List<Message> findByConversationId(String conversationId) {
String key = CHAT_MEMORY_PREFIX + conversationId;
Object obj = redisTemplate.opsForValue().get(key);
if (obj instanceof List) {
return (List<Message>) obj;
}
return new ArrayList<>();
}
/**
* 保存指定对话的所有历史消息记录
*
* @param conversationId 对话的唯一标识 ID
* @param messages 需要保存的消息列表
*/
@Override
public void saveAll(String conversationId, List<Message> messages) {
String key = CHAT_MEMORY_PREFIX + conversationId;
// 保存历史消息,并设置30天的过期时间避免Redis内存无限制增长
redisTemplate.opsForValue().set(key, messages, 30, TimeUnit.DAYS);
}
/**
* 根据对话 ID 删除该对话的缓存记录
*
* @param conversationId 需要删除缓存的对话 ID
*/
@Override
public void deleteByConversationId(String conversationId) {
String key = CHAT_MEMORY_PREFIX + conversationId;
redisTemplate.delete(key);
}
}第三步:配置 ChatMemory Bean
通过配置类将刚才编写的 RedisRepository 注入,并结合 MessageWindowChatMemory 来限制上下文窗口大小:
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import top.justq.blogadmin.config.repository.RedisRepository;
@Configuration
public class ChatMemoryConfig {
@Autowired
private RedisRepository redisRepository;
@Bean
@Primary
public ChatMemory chatMemory(){
return MessageWindowChatMemory.builder()
.chatMemoryRepository(redisRepository)
.maxMessages(10) // 限制最大消息数为10条,也就是5轮对话
.build();
}
}有了配置以后,业务代码同样通过 Autowired 或者构造器直接注入 ChatMemory 使用即可,其他地方不需要额外改动:
// 这个 Bean 自动使用你的 RedisRepository 和设定的窗口策略
@Autowired
ChatMemory chatMemory;整体原理图
ChatClient
↓
MessageChatMemoryAdvisor
↓ 调用
ChatMemory(MessageWindowChatMemory) ← 负责窗口截断(如保留最新10条消息)
↓ 调用
ChatMemoryRepository(你的 RedisRepository) ← 负责读写 Redis
↓
Redis每一层只认识紧挨着的下一层接口,完全解耦,这就是为什么换存储实现对上层代码毫无影响。
