目录
3.2.4 MessageChatMemoryAdvisor
一、前言
在使用AI大模型产品进行多轮对话的时候发现,你可以在第一次输入问题并得到大模型的回复之后,只要是在一定的会话时间窗口期内,再次提问与第一次相关的问题,或者基于第一次的提问的衍生内容,大模型均可以再次回复与此相关的回答,这就是大模型的记忆功能。
默认情况下,我们向大模型每次发起的提问都是新的,大模型就无法把我们的每次对话形成记忆,也无法根据对话上下文给出人性化的答案,因为大模型已经失去了上一次的提问记忆。所以让智能体(如AI助手、机器人、虚拟角色等)拥有记忆功能不仅能提升交互体验,还能增强其功能性、适应性和长期价值。本篇以Spring AI为例,详细说明下基于Spring AI框架下的记忆功能的实现。
二、Spring AI 会话记忆介绍
2.1 Spring AI 会话记忆概述
Spring AI 的会话记忆功能是指让智能体(如AI助手、机器人、虚拟角色等)在多次交互中保持上下文或状态,从而提升交互体验和功能性。这种记忆功能使得智能体能够“记住”用户提供的信息,并在后续对话中参考和使用这些信息,从而提供更加个性化和精准的回复。官方文档:Chat Memory :: Spring AI Reference
2.2 常用的会话记忆实现方式
Spring AI通过 ChatMemory 接口来实现会话记忆功能。具体实现方式有下面几种:
内存存储:
使用 InMemoryChatMemory 来存储对话历史,这种方式适用于临时会话,但不适合长期存储;
数据库存储:
集成 MySQL 、 Mongo、Redis 或其他数据库来存储对话历史,以实现长期的会话记忆
2.2.1 集成数据库持久存储会话实现步骤
在实际应用中,一般是将会话数据进行持久化存储,通常的实现步骤如下:
集成数据库:
选择合适的数据存储组件(如MySQL、Mongo、Redis等),并导入相关依赖。
例如,使用MySQL存储对话历史需要导入 spring-ai-starter-model-chat-memory-repository-jdbc 依赖和 mysql-connector-j 依赖。
配置会话记忆:
在Spring配置中定义会话存储方式和会话记忆Advisor。
例如,使用 InMemoryChatMemory 并通过 MessageChatMemoryAdvisor 配置会话记忆。
添加会话ID:
每次会话时通过前端区分会话ID,确保同一个会话ID对应多次对话的消息列表,从而实现会话隔离。
2.3 适用场景
会话记忆功能适用于需要保持上下文一致性的场景,如多轮对话、用户偏好跟踪等,具体来说,使用会话记忆具有如下优势:
用户信息的持久化:
记住用户的背景信息或个人偏好,提供更加个性化的服务。
上下文跟踪:
在多轮对话中保持上下文一致性,避免用户重复说明。
更好地回答追问:
在复杂的多轮对话中,助手能更好地理解用户的意图,提供更详细的回复。
三、Spring AI基于内存会话记忆存储
3.1 本地开发环境准备
为了后续案例代码能够正常运行,这里列举本次相关的基础环境版本,提供参考:
Jdk 17;
Springboot 3.3.3 ;
Spring AI 1.0.0-M6;
3.2 工程搭建与集成
3.2.1 添加核心依赖
在pom文件中添加下面的依赖
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-ai.version>1.0.0-M6</spring-ai.version>
<spring-ai-alibaba.version>1.0.0-M6.1</spring-ai-alibaba.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
</dependencies>
<repositories>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
3.3.2 添加配置文件
在配置文件中增加下面的配置信息
这里的apikey 用的是阿里云百炼平台上面的key
spring:
ai:
dashscope:
api-key: 你的apikey
chat:
options:
model: qwen-max
3.3.3 添加测试接口
添加一个测试接口用于效果测试
package com.congge.web;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
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;
@RestController
@RequestMapping("/memory")
public class ChatMemoryController {
private ChatClient chatClient;
public ChatMemoryController(ChatClient.Builder builder) {
this.chatClient = builder
.build();
}
//localhost:8082/memory/chatV2?message=你是谁
@GetMapping("/chatV2")
public String chatV2(String message){
String content = chatClient.prompt()
.user(message)
.call()
.content();
return content;
}
}
调用一下接口,看到下面的效果说明集成是没问题的
3.2 ChatMemory 介绍
3.2.1 ChatMemory 概述
ChatMemory 是 Spring AI 中定义聊天记忆行为的核心接口,通过源码点进去查看,可以看到这是一个顶级接口
public interface ChatMemory {
default void add(String conversationId, Message message) {
this.add(conversationId, List.of(message));
}
void add(String conversationId, List<Message> messages);
List<Message> get(String conversationId, int lastN);
void clear(String conversationId);
}
该接口定义了三个核心操作方法:
add()
添加消息到记忆
get()
获取对话历史
clear()
清除对话记忆
3.2.2 InMemoryChatMemory
如果没有引入第三方的会话存储组件,ChatMemory 的默认实现类为InMemoryChatMemory,从源码中可以看到该类对ChatMemory 的几个接口方法进行了实现,开发者可以直接使用。
3.2.3 MessageWindowChatMemory
下面是Spring AI官方给出的MessageWindowChatMemory 的解释:
MessageWindowChatMemory
将消息的窗口保持在指定的最大大小。当消息数超过最大值时,在保存系统消息时会删除较旧的消息。默认窗口大小为20个消息。
MessageWindowChatMemory 是 ChatMemory 的主要实现类,具有以下特点:
维护一个固定大小的消息窗口
自动移除旧消息,保留最新消息
默认保留20条消息(可配置)
特殊处理系统消息(不会被自动移除)
下面是MessageWindowChatMemory 在项目集成中的创建方式:
@Bean
public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(20) // 可选,默认20
.build();
}
参考如下完整示例配置代码:
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemoryRepository chatMemoryRepository() {
return new InMemoryChatMemoryRepository();
}
@Bean
public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(30) // 自定义消息窗口大小
.build();
}
}
3.2.4 MessageChatMemoryAdvisor
MessageChatMemoryAdvisor 是 Spring AI 中用于处理会话历史记录的核心类。我们可以创建一个自定义的 MemoryAdvisor,并使用它来存储对话数据。MessageWindowChatMemory 是 ChatMemory 的主要实现类,具有以下特点:
维护一个固定大小的消息窗口
自动移除旧消息,保留最新消息
默认保留20条消息(可配置)
特殊处理系统消息(不会被自动移除)
官方文档地址:Chat Memory :: Spring AI Reference
在实际开发中,可以通过下面配置bean的方式创建
@Bean
public ChatClient chatClient(ChatMemory chatMemory) {
return ChatClient.builder(chatModel)
//.defaultSystem("你是一个数据库专家,接下来请以这个身份进行回答")
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build();
}
3.3 案例效果演示
基于上述的理论分享,下面通过两个接口进行操作演示说明。
3.3.1 自定义配置类
增加一个自定义配置类,配置全局的chatMemory
package com.congge.config;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatMemoryConfig {
@Autowired
private DashScopeChatModel chatModel;
@Bean
public InMemoryChatMemory chatMemory(){
return new InMemoryChatMemory();
}
@Bean
public ChatClient chatClient(ChatMemory chatMemory) {
return ChatClient.builder(chatModel)
//.defaultSystem("你是一个数据库专家,接下来请以这个身份进行回答")
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build();
}
}
3.3.2 添加测试接口
如下增加一个测试接口
@RestController
@RequestMapping("/memory")
public class ChatMemoryController {
@Resource
private ChatClient chatClient;
//localhost:8082/memory/chat?message=我叫小王
@GetMapping("/chat")
public String chat(String message){
String content = chatClient
.prompt()
.user(message)
.call()
.content();
System.out.println(content);
String content2 = chatClient
.prompt()
.user("请问我是谁")
.call()
.content();
return content2;
}
}
假如上面的会话基于配置不生效,先注释掉,如下:
此时项目启动后,调用下该接口进行验证,不难发现,由于大模型没有上下文记忆,这里是无法记住上一个问题的相关内容的
如果将注释放开再次测试,可以看到,此时会话记忆就生效了
如果不想配置全局的bean ,也可以直接在接口类中参照下面的方式编写
@RestController
@RequestMapping("/memory")
public class ChatMemoryController {
private ChatClient chatClient;
private ChatMemory chatMemory = new InMemoryChatMemory();
public ChatMemoryController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory, UUID.randomUUID().toString(), 10))
.build();
}
//localhost:8082/memory/chatV2?message=我叫小王
@GetMapping("/chatV2")
public String chatV2(String message){
String content = chatClient.prompt()
.user(message)
.call()
.content();
return content;
}
}
四、Spring AI基于Redis记忆存储
Redis是一种高效的数据存取组件,使用Redis可以用于存储大模型的会话记忆,接下来演示在Spring AI 如何基于Redis实现会话存储。
4.1 前置准备
4.1.1 导入redis依赖
pom中导入redis的核心依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
4.1.2 增加配置信息
在工程的配置文件中添加如下配置
spring:
ai:
dashscope:
api-key: 你的apikey
chat:
options:
model: qwen-max
data:
redis:
host: localhost
port: 6379
4.1.4 增加redis配置类
自定义一个redis的配置类,配置序列化等信息
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
4.1.5 启动redis服务
本地启动redis服务
4.2 代码整合过程
4.2.1 增加会话实体对象
自定义一个消息实体类,方便后续解析和传递消息
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ChatMessageInfo {
String chatId;
String type;
String text;
}
4.2.2 自定义ChatMemory
在上文提到,ChatMemory是一个顶级接口,可以自定义一个类实现该接口,重写里面的方法
package com.congge.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.*;
import org.springframework.stereotype.Component;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class ChatStorageMemory implements ChatMemory {
private static final String KEY_PREFIX = "chat:history:";
private final RedisTemplate<String, Object> redisTemplate;
private static final Integer TIME_OUT = 60;
public ChatStorageMemory(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void add(String conversationId, List<Message> messages) {
String key = KEY_PREFIX + conversationId;
List<ChatMessageInfo> listIn = new ArrayList<>();
for (Message msg : messages) {
String[] strs = msg.getText().split("</think>");
String text = strs.length == 2 ? strs[1] : strs[0];
ChatMessageInfo ent = new ChatMessageInfo();
ent.setChatId(conversationId);
ent.setType(msg.getMessageType().getValue());
ent.setText(text);
listIn.add(ent);
}
redisTemplate.opsForList().rightPushAll(key, listIn.toArray());
redisTemplate.expire(key, TIME_OUT, TimeUnit.MINUTES);
}
@Override
public List<Message> get(String conversationId, int lastN) {
String key = KEY_PREFIX + conversationId;
Long size = redisTemplate.opsForList().size(key);
if (size == null || size == 0) {
return Collections.emptyList();
}
int start = Math.max(0, (int) (size - lastN));
List<Object> listTmp = redisTemplate.opsForList().range(key, start, -1);
List<Message> listOut = new ArrayList<>();
ObjectMapper objectMapper = new ObjectMapper();
for (Object obj : listTmp) {
ChatMessageInfo chat = objectMapper.convertValue(obj, ChatMessageInfo.class);
if (MessageType.USER.getValue().equals(chat.getType())) {
listOut.add(new UserMessage(chat.getText()));
} else if (MessageType.ASSISTANT.getValue().equals(chat.getType())) {
listOut.add(new AssistantMessage(chat.getText()));
} else if (MessageType.SYSTEM.getValue().equals(chat.getType())) {
listOut.add(new SystemMessage(chat.getText()));
}
}
return listOut;
}
@Override
public void clear(String conversationId) {
redisTemplate.delete(KEY_PREFIX + conversationId);
}
}
4.2.3 自定义模型配置类
增加一个自定义类,配置 ChatClient,ChatMemory,主要是在chatMemory这个配置bean中,注入redisTemplate
package com.congge.config;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
public class SpringAiChatConfig {
@Autowired
private DashScopeChatModel chatModel;
@Bean
public ChatClient chatClient() {
return ChatClient.builder(chatModel)
.build();
}
@Bean
public ChatMemory chatMemory(RedisTemplate<String, Object> redisTemplate) {
return new ChatStorageMemory(redisTemplate);
}
}
4.2.4 添加测试接口
增加一个测试接口,该接口接收两个参数,一个是会话的唯一ID,另一个是用户输入的问题
package com.congge.web;
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.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/memory/store")
@RestController
@Slf4j
public class ChatStoreController {
@Autowired
private ChatClient chatClient;
@Autowired
private ChatMemory chatMemory;
private final Integer CHAT_HISTORY_SIZE = 10;
//localhost:8082/memory/store/chat?userId=1&inputMsg=我叫小王
//localhost:8082/memory/store/chat?userId=1&inputMsg=你知道我是谁吗
@GetMapping(value = "/chat")
public String chat(@RequestParam String userId, @RequestParam String inputMsg) {
String response = chatClient.prompt()
.user(inputMsg)
.advisors(new MessageChatMemoryAdvisor(chatMemory, userId, CHAT_HISTORY_SIZE))
.call()
.content();
return response;
}
}
4.2.5 效果测试
工程运行起来之后,分别调用两次接口。
1)第一次调用
2)第二次调用
通过2次接口调用,可以看到大模型将会话信息存储到redis了,借助redis完成了会话的持久化存储,在redis客户端工具中,也能清楚看到对话的信息,由于做了持久化存储,即便短期服务挂掉,再次启动后仍然有效,同时由于UserId不同,会话也根据不同的用户ID进行了隔离。
五、写在文末
本文详细介绍了Spring AI 的会话持久化存储技术,并通过案例操作演示详细说明了如何基于Redis实现会话的持久化存储,希望对看到的同学有用,本篇到此结束,感谢观看。