spirngAI基于Redis实现自定义chatMemory

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中了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值