spirngAI基于Redis实现自定义chatMemory
1.聊天记忆
大型语言模型 (LLM) 是无状态的,这意味着它们不会保留有关以前交互的信息。当您希望在多个交互中维护上下文或状态时,这可能是一个限制。为了解决这个问题,Spring AI 提供了聊天内存功能,允许您在与 LLM 的多次交互中存储和检索信息。
这ChatMemory抽象允许您实现各种类型的内存以支持不同的使用案例。消息的底层存储由ChatMemoryRepository,其唯一职责是存储和检索消息。这取决于ChatMemoryimplementation 来决定要保留哪些消息以及何时删除它们。策略示例可能包括保留最后 N 条消息、将消息保留一段时间或将消息保持在某个Tokens限制内。
在选择内存类型之前,必须了解聊天内存和聊天记录之间的区别。
-
聊天记录。大型语言模型保留并用于在整个对话中保持上下文感知的信息。
-
聊天记录。整个会话历史记录,包括用户与模型之间交换的所有消息。
这ChatMemoryabstraction
旨在管理聊天内存。它允许您存储和检索与当前对话上下文相关的消息。但是,它并不是存储聊天记录的最佳选择。如果您需要维护所有交换消息的完整记录,您应该考虑使用不同的方法,例如依靠 Spring Data 来有效存储和检索完整的聊天历史记录。
2.默认的聊天记忆方法
下面是springAI官方的接口代码,这里定义了聊天记忆
的方法
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.ai.chat.memory;
import java.util.List;
import org.springframework.ai.chat.messages.Message;
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);
}
spring官方有一个默认的实现类InMemoryChatMemory
,实现了这个接口,其核心就用到了 Map<String, List<Message>>来存储记忆
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.ai.chat.memory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.ai.chat.messages.Message;
public class InMemoryChatMemory implements ChatMemory {
Map<String, List<Message>> conversationHistory = new ConcurrentHashMap();
public InMemoryChatMemory() {
}
public void add(String conversationId, List<Message> messages) {
this.conversationHistory.putIfAbsent(conversationId, new ArrayList());
((List)this.conversationHistory.get(conversationId)).addAll(messages);
}
public List<Message> get(String conversationId, int lastN) {
List<Message> all = (List)this.conversationHistory.get(conversationId);
return all != null ? all.stream().skip((long)Math.max(0, all.size() - lastN)).toList() : List.of();
}
public void clear(String conversationId) {
this.conversationHistory.remove(conversationId);
}
}
而想要实现聊天记忆,还需要用到advisor
这个环绕增强器,其底层核心就是利用了AOP
的原理。
下面是配置方式
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.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MySpringAIConfiguration {
@Bean
public ChatMemory chatMemory(){
return new InMemoryChatMemory();
}
@Bean
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient
.builder(model)
.defaultSystem("你是一个热心,可爱的人工智能助手,你的名字叫做贾维斯,你是钢铁侠3里面的贾维斯,请你以贾维斯的身份和语气来和我说话。")
.defaultAdvisors(
new SimpleLoggerAdvisor(),//拦截器,可以进行配置log日志,或者对上
new MessageChatMemoryAdvisor(chatMemory) //拦截器,可以进行配置,将消息保存到内存中
)
.build();
}
}
配置完成后就可以在对话模型中添加了
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chatStream(String prompt, String chatId) {
return chatClient.prompt()
.user(prompt) //用户输入的提示词
.advisors(advisorSpec -> advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)) //将配置好的拦截器注入启用
.stream() //表示流式调用,会一直返回数据,直到数据结束
.content();
}
现在聊天就有了基本的记忆功能了
3.基于Redis来实现自定义ChatMemory
默认的聊天记忆方法实现方式简单,但是聊天记忆没有持久化,一旦服务器重启,就会失去记忆
下面就来讲解用redis
来持久化的存储聊天记忆
3.1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2.自定义类实现ChatMemory
首先定义一个Msg
的类,用来规定存储结构
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.*;
import java.util.List;
import java.util.Map;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Msg {
//信息类型:是AI的,还是用户的,或者是系统的
MessageType messageType;
//对话的文本信息
String text;
//其他信息
Map<String, Object> metadata;
//将SpringAI的Message转为我们的Msg
public Msg(Message message) {
this.messageType = message.getMessageType();
this.text = message.getText();
this.metadata = message.getMetadata();
}
//实现将我们的Msg转为SpringAI的Message
public Message toMessage() {
return switch (messageType) {
case SYSTEM -> new SystemMessage(text);
case USER -> new UserMessage(text, List.of(), metadata);
case ASSISTANT -> new AssistantMessage(text, metadata, List.of(), List.of());
default -> throw new IllegalArgumentException("Unsupported message type: " + messageType);
};
}
}
然后新建一个RedisChatMemory
类:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.List;
/**
* 基于Redis实现的聊天记忆存储
* 实现了ChatMemory接口,用于存储和检索聊天消息历史
*/
@RequiredArgsConstructor
public class RedisChatMemory implements ChatMemory {
// Redis操作模板
private final StringRedisTemplate redisTemplate;
// JSON序列化工具
private final ObjectMapper objectMapper;
// Redis键前缀,用于区分不同类型的键
private final static String PREFIX = "reidsChatMemory:";
/**
* 添加消息到聊天记忆
* @param conversationId 会话ID,用于区分不同对话
* @param messages 要添加的消息列表
*/
@Override
public void add(String conversationId, List<Message> messages) {
// 如果消息列表为空,直接返回
if (messages == null || messages.isEmpty()) {
return;
}
// 将Message对象转换为Msg对象,然后序列化为JSON字符串
List<String> list = messages.stream().map(Msg::new).map(msg -> {
try {
return objectMapper.writeValueAsString(msg);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}).toList();
// 将消息列表存入Redis,使用左推方式(最新消息在列表头部)
redisTemplate.opsForList().leftPushAll(PREFIX + conversationId, list);
}
/**
* 从聊天记忆中获取指定数量的最近消息
* @param conversationId 会话ID
* @param lastN 要获取的消息数量
* @return 消息列表,如果没有消息则返回空列表
*/
@Override
public List<Message> get(String conversationId, int lastN) {
// 从Redis获取指定范围的聊天记录(从0到lastN-1)
List<String> list = redisTemplate.opsForList().range(PREFIX + conversationId, 0, lastN);
// 如果结果为空,返回空列表
if (list == null || list.isEmpty()) {
return List.of();
}
// 将JSON字符串反序列化为Msg对象,再转换为Message对象
return list.stream().map(s -> {
try {
return objectMapper.readValue(s, Msg.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}).map(Msg::toMessage).toList();
}
/**
* 清除指定会话的聊天记忆
* @param conversationId 要清除的会话ID
*/
@Override
public void clear(String conversationId) {
// 从Redis删除指定会话的键
redisTemplate.delete(PREFIX + conversationId);
}
}
3.3 配置记忆存储方式
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xiaowu.repository.RedisChatMemory;
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.ai.openai.OpenAiChatModel;
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.StringRedisTemplate;
@Configuration
public class MySpringAIConfiguration {
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
ObjectMapper objectMapper;
@Bean
public ChatMemory chatMemory(){
return new RedisChatMemory(redisTemplate,objectMapper);
}
@Bean
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient
.builder(model)
.defaultSystem("你是一个热心,可爱的人工智能助手,你的名字叫做贾维斯,你是钢铁侠3里面的贾维斯,请你以贾维斯的身份和语气来和我说话。")
.defaultAdvisors(
new SimpleLoggerAdvisor(),//拦截器,可以进行配置log日志,或者对上
new MessageChatMemoryAdvisor(chatMemory) //拦截器,可以进行配置,将消息保存到内存中
)
.build();
}
}
4.测试
查看Redis
中存储的内容
发现存储记忆已经生效,并且成功持久化存储到redis中了