SpringAI聊天记忆ChatMemory
目录
3.4.1 bean 配置mysql 和chat memory
3.5 支持的数据库JdbcChatMemoryRepositoryDialect
1、概述
SpringAI的版本查看 https://blog.ZEEKLOG.net/weixin_45948519/article/details/156327249?spm=1011.2415.3001.5331
Spring AI 的 chat-memory 是支撑多轮连贯对话的核心组件,核心解决大语言模型本身的无状态痛点。它的核心作用是存储多轮对话的交互记录,并在后续请求中把对话历史与新请求合并后发送给模型,实现连贯响应。
- 官方文档 http:// https://docs.spring.io/spring-ai/reference/api/chat-memory.html
- 官方示例的增强器介绍,除了增强器,还需要存储相关配置。下面会示例几个常用的存储示例
2、基于内存存储聊天记忆
2.1 导入依赖
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId> </dependency>2.2 创建基于内存的 chatMemory
import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ChatMemoryConfig { /** * 聊天内存仓库 */ @Bean("inMemoryChatMemoryRepository") ChatMemoryRepository chatMemoryRepository() { return new InMemoryChatMemoryRepository(); } /** * 聊内存储 */ @Bean("inMemoryChatMemory") ChatMemory chatMemory(@Qualifier("inMemoryChatMemoryRepository") ChatMemoryRepository chatMemoryRepository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) // 设置最大消息 默认 20条 也就是10轮对话 .maxMessages(20) .build(); } }2.3 简单的代码示例
import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; @Slf4j @RestController @RequiredArgsConstructor @RequestMapping("chat/memory") public class ChatMemoryController { private final ChatClient ollamaChatClient; // 基于内存的 ChatMemory private final ChatMemory inMemoryChatMemory; @GetMapping("memory") public String memory() { // 会话编号 每个会话一个会话编号 用于会话记忆隔离 String conversationId = UUID.randomUUID().toString(); String one = ollamaChatClient.prompt() .user("请记住黄霸天是女生") .advisors( PromptChatMemoryAdvisor.builder(inMemoryChatMemory) .conversationId(conversationId) .build(), // MessageChatMemoryAdvisor.builder(inMemoryChatMemory) // .conversationId(conversationId) // .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第一次回复消息 {}", one); String two = ollamaChatClient.prompt() .user("黄霸天是男生还是女生") .advisors( PromptChatMemoryAdvisor.builder(inMemoryChatMemory) .conversationId(conversationId) .build(), // MessageChatMemoryAdvisor.builder(inMemoryChatMemory) // .conversationId(conversationId) // .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第二次回复消息 {}", two); return "ok"; } }2.4 结果示例
- 通过日志可看出已经将数据传递给大模型了 此模式为PromptChatMemoryAdvisor 兼容性最好。检索对话历史后,将其整合为一段文本,嵌入到 Prompt 的 “系统文本”(System Prompt)中;不保留对话的原始消息结构,而是把历史转成描述性文本


- 以下为MessageChatMemoryAdvisor的抓包示例 可以看出 MessageChatMemoryAdvisor是将历史对话信息保留原始结构发送,检索对话历史后,以「消息组」的形式直接添加到 Prompt 中。但是注意并非所有 AI 模型都支持这种 “多消息结构” 的 Prompt(部分模型仅接受单段文本 Prompt)

3、基于Mysql jdbc方式存储聊天记忆
这里拦截器使用的是PromptChatMemoryAdvisor。与MessageChatMemoryAdvisor区别可以看内存存储聊天示例
3.1 环境配置
- 安装mysql 这里使用docker 的方式快速布置 如果未安装的可以百度一下如何安装docker。建议使用win11进行开发的话都安装一下docker 很方便
# 这个命令为 创建一个mysql8 密码是123456 端口是 16010 的mysql容器 docker run -d --name mysql8.0.20 -p 16010:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_ROOT_HOST=% mysql:8.0.20 # 使用命令 创建数据库 test_db docker exec -it mysql8.0.20 mysql -u root -p123456 -e "CREATE DATABASE IF NOT EXISTS test_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;"3.2 导入mysql 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> <scope>runtime</scope> </dependency>3.3 配置文件配置ChatMemory初始化配置
# 配置数据库初始化为 always spring.ai.chat.memory.repository.jdbc.initialize-schema=always # 配置数据库初始化表的sql文件 可以使用默认值 spring.ai.chat.memory.repository.jdbc.schema=classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-mysql.sql # 配置数据库名称 spring.ai.chat.memory.repository.jdbc.platform=mysql
- IDEA 按两下 shift 搜索 schema-mysql.sql 可找到默认的数据库文件

- schema-mysql.sql
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( `conversation_id` VARCHAR(36) NOT NULL, `content` TEXT NOT NULL, `type` ENUM('USER', 'ASSISTANT', 'SYSTEM', 'TOOL') NOT NULL, `timestamp` TIMESTAMP NOT NULL, INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`, `timestamp`) );3.4 配置ChatMemory
3.4.1 bean 配置mysql 和chat memory
import com.zaxxer.hikari.HikariDataSource; 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.ai.chat.memory.repository.jdbc.MysqlChatMemoryRepositoryDialect; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; @Configuration public class MysqlChatMemoryConfig { // 创建数据源 @Bean(name = "mysqlChatDataSource") DataSource getDateSource() { HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder.create().build(); String url = "jdbc:mysql://%s:%s/%s" + "?useUnicode=true&characterEncoding=utf-8&useSSL=false" + "&allowPublicKeyRetrieval=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai"; // 数据库相关配置 dataSource.setJdbcUrl(String.format(url, "127.0.0.1", 16010, "test_db")); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } // 创建jdbc模版 @Bean(name = "mysqlChatJdbcTemplate") JdbcTemplate mysqlChatJdbcTemplate(@Qualifier("mysqlChatDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } // 创建事务管理器 @Bean(name = "mysqlChatTransactionManager") PlatformTransactionManager mysqlChatTransactionManager(@Qualifier("mysqlChatDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } // 创建 chat 数据库存储 @Bean(name = "mysqlChatMemoryRepository") JdbcChatMemoryRepository mysqlChatMemoryRepository( @Qualifier("mysqlChatJdbcTemplate") JdbcTemplate jdbcTemplate, @Qualifier("mysqlChatDataSource") DataSource dataSource, @Qualifier("mysqlChatTransactionManager") PlatformTransactionManager transactionManager) { MysqlChatMemoryRepositoryDialect dialect = new MysqlChatMemoryRepositoryDialect(); return JdbcChatMemoryRepository.builder() .jdbcTemplate(jdbcTemplate) .transactionManager(transactionManager) .dialect(dialect) .dataSource(dataSource) .build(); } // 创建 chatMemory @Bean("mysqlChatMemory") ChatMemory mysqlChatMemory(@Qualifier("mysqlChatMemoryRepository") JdbcChatMemoryRepository repository) { return MessageWindowChatMemory.builder() // 使用数据库存储 .chatMemoryRepository(repository) // 保留最多10条记录 .maxMessages(10) .build(); } }3.4.2 starter-jdbc 进行配置
- 导入依赖
<!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>- 配置文件配置
# 可使用spring 默认数据库配置信息配置 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:16010/test_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456- bean配置
@Bean("mysqlChatMemory") ChatMemory mysqlChatMemory(JdbcChatMemoryRepository chatMemoryRepository) { return MessageWindowChatMemory .builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages(10) .build(); }3.5 支持的数据库JdbcChatMemoryRepositoryDialect

3.6 使用
import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; @Slf4j @RestController @RequiredArgsConstructor @RequestMapping("chat/memory/mysql") public class MysqlChatMemoryController { private final ChatClient dashscopeChatClient; private final ChatMemory mysqlChatMemory; @GetMapping("base") public String base() { // 会话编号 每个会话一个会话编号 用于会话记忆隔离 String conversationId = UUID.randomUUID().toString().replace("-", ""); log.info("conversationId: {}", conversationId); String one = dashscopeChatClient.prompt() .user("记住北京今天的天气是晴天,26摄氏度") .advisors( PromptChatMemoryAdvisor.builder(mysqlChatMemory) .conversationId(conversationId) .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第一次回复消息 {}", one); String two = dashscopeChatClient.prompt() .user("北京今天的天气是什么样的") .advisors( PromptChatMemoryAdvisor.builder(mysqlChatMemory) .conversationId(conversationId) .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第二次回复消息 {}", two); return "ok"; } }3.7 结果
- 执行后可查看数据库已经存储到了历史聊天记录了

- 目前最大聊天记录超过了设定的值就会删除,我们想的是最近的数据传递给大模型,而历史的聊天记录保存下来的话可以自己写一个增强器对历史的数据进行保留。并且如图,目前JdbcChatMemoryRepository里面的做法是直接删除所有的然后添加进去。这种方式里面的时间戳也不是用户真实添加的时间戳。而应该算是最后使用时间而不是创建时间。用户所有的历史聊天记录还是推荐使用自定义增强器对历史记录保存。然后短期的历史记录则通过redis或者内存进行存储发送给大模型


- 提供一个简单保存增强器示例
import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.ai.chat.client.ChatClientMessageAggregator; import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; import org.springframework.ai.chat.client.advisor.api.CallAdvisor; import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain; import org.springframework.ai.chat.client.advisor.api.StreamAdvisor; import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain; import org.springframework.ai.chat.messages.Message; import reactor.core.publisher.Flux; import java.util.Date; @Slf4j public class SaveAdvisor implements CallAdvisor, StreamAdvisor { private int order = 0; private String conversationId = "def"; public SaveAdvisor(int order, String conversationId) { this.order = order; this.conversationId = conversationId; } @NotNull @Override public ChatClientResponse adviseCall(@NotNull ChatClientRequest chatClientRequest, @NotNull CallAdvisorChain callAdvisorChain) { this.saveRequest(chatClientRequest); ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest); this.saveResponse(chatClientResponse); return chatClientResponse; } @NotNull @Override public Flux<ChatClientResponse> adviseStream(@NotNull ChatClientRequest chatClientRequest, @NotNull StreamAdvisorChain streamAdvisorChain) { this.saveRequest(chatClientRequest); Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest); return (new ChatClientMessageAggregator()).aggregateChatClientResponse(chatClientResponses, this::saveResponse); } public void saveRequest(ChatClientRequest request) { saveMessage(request.prompt().getUserMessage()); } public void saveResponse(ChatClientResponse chatClientResponse) { saveMessage(chatClientResponse.chatResponse().getResult().getOutput()); } public void saveMessage(Message message) { // 这里可以使用MQ异步保存 log.info("保存消息 {} {} {} {}", this.conversationId, message.getMessageType(), message.getText(), new Date()); } @Override public int getOrder() { return this.order; } @NotNull @Override public String getName() { return "SaveAdvisor"; } public static SaveAdvisor.Builder builder() { return new SaveAdvisor.Builder(); } public static final class Builder { private int order = 0; private String conversationId = "def"; public Builder() { } public SaveAdvisor.Builder conversationId(String conversationId) { this.conversationId = conversationId; return this; } public SaveAdvisor.Builder order(int order) { this.order = order; return this; } public SaveAdvisor build() { return new SaveAdvisor(this.order, this.conversationId); } } }4、基于redis 存储聊天记忆
这里使用的是spring Alibaba 提供的ChatMemoryRepository。
文档所在地址 http://github地址 https://github.com/alibaba/spring-ai-alibaba/tree/1.0.0.3-retriever/community/memories/spring-ai-alibaba-starter-memory-redis
使用版本是1.1.0.0 但是可以查看分支1.0.0.3的文档README

4.1 环境准备
- 安装redis
docker run -d --name redis-simple -p 16379:6379 redis:7.2.4 redis-server --bind 0.0.0.0 --requirepass "123456" --appendonly yes4.2 导入依赖
<properties> <java.version>17</java.version> <spring-ai.version>1.1.2</spring-ai.version> <spring-ai-alibaba.version>1.1.0.0</spring-ai-alibaba.version> </properties> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-memory-redis</artifactId> <version>${spring-ai-alibaba.version}</version> </dependency>4.3 bean 配置
import com.alibaba.cloud.ai.memory.redis.LettuceRedisChatMemoryRepository; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedisChatMemoryConfig { @Bean(name = "lettuceRedisChatMemoryRepository") public LettuceRedisChatMemoryRepository lettuceRedisChatMemoryRepository() { // 创建连接池配置对象 GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>(); // 最大活跃连接数 poolConfig.setMaxTotal(8); // 最大空闲连接数 poolConfig.setMaxIdle(8); // 最小空闲连接数 poolConfig.setMinIdle(2); return LettuceRedisChatMemoryRepository.builder() // redis 地址 .host("127.0.0.1") // 端口 .port(16379) // 密码 .password("123456") // 连接超时时间 .timeout(10000) // 连接池配置对象 .poolConfig(poolConfig) .build(); } @Bean("redisChatMemory") ChatMemory redisChatMemory(@Qualifier("lettuceRedisChatMemoryRepository") LettuceRedisChatMemoryRepository repository) { return MessageWindowChatMemory.builder() // 使用数据库存储 .chatMemoryRepository(repository) // 保留最多10条记录 .maxMessages(10) .build(); } }4.4 基础使用
import lombok.RequiredArgsConstructor; 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.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; @Slf4j @RestController @RequiredArgsConstructor @RequestMapping("chat/memory/redis") public class RedisChatMemoryController { private final ChatClient dashscopeChatClient; private final ChatMemory redisChatMemory; @GetMapping("base") public String base() { // 会话编号 每个会话一个会话编号 用于会话记忆隔离 String conversationId = UUID.randomUUID().toString().replace("-", ""); log.info("conversationId: {}", conversationId); String one = dashscopeChatClient.prompt() .user("记住北京今天的天气是晴天,26摄氏度") .advisors( MessageChatMemoryAdvisor.builder(redisChatMemory) .conversationId(conversationId) .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第一次回复消息 {}", one); String two = dashscopeChatClient.prompt() .user("北京今天的天气是什么样的") .advisors( MessageChatMemoryAdvisor.builder(redisChatMemory) .conversationId(conversationId) .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第二次回复消息 {}", two); return "ok"; } }4.5 redis存储结果
