Spring AI (第二章)大模型对话上下文记忆

Spring AI (第二章)大模型对话上下文记忆


文章目录

  • [Spring AI (第二章)大模型对话上下文记忆](#Spring AI (第二章)大模型对话上下文记忆)
  • 一、章节目的
    • [1.1 需求分析](#1.1 需求分析)
    • [1.2 方案一:内存存储(In-memory)](#1.2 方案一:内存存储(In-memory))
    • [1.3 方案二:文件存储(Kryo 序列化)](#1.3 方案二:文件存储(Kryo 序列化))
    • [1.4 方案三:MongoDB(或类似文档数据库)](#1.4 方案三:MongoDB(或类似文档数据库))
    • [1.5 选型建议](#1.5 选型建议)
  • 二、代码
    • [2.1 上下文聊天记忆的大模型对象](#2.1 上下文聊天记忆的大模型对象)
    • [2.2 基于本地文件存储的方式实现代码(可选)](#2.2 基于本地文件存储的方式实现代码(可选))
    • [2.3 测试代码](#2.3 测试代码)

一、章节目的

1.1 需求分析

  • 基于第一章实现的内容;
  • 当前对话仅按单轮请求-响应执行,模型不会利用历史消息,导致上下文丢失、回答前后不一致、重复确认较多。
  • 目标:为每个会话维护"对话历史",在下一轮请求时一并提交,从而实现连贯、多轮的上下文记忆。

1.2 方案一:内存存储(In-memory)

  • 官方提供了基于计算机内存存储上下文记忆的依赖、会把用户和大模型的上下文存储在内存当中、可以解决上下文记忆的问题;
  • 优点:实现最简单、延迟最低、无外部依赖,开发/调试成本低;
  • 缺点:进程退出或重启即丢失、无法跨实例共享;数据量大时占用内;
  • 适用场景:PoC、单机小流量、对持久化不敏感的场景

1.3 方案二:文件存储(Kryo 序列化)

  • 通过Kryo序列化对象来实现数据的文件写入、文件读取、根据用户聊天会话ID去写入和读取文件、可以解决上下文记忆的问题;
  • 优点:重启不丢失,部署简单,不依赖外部服务。
  • 缺点:小文件众多(碎片化)、目录遍历和清理成本高、并发写入需加锁;跨机无法共享;备份/检索能力有限。
  • 适用场景:单机生产/测试环境,需要一定持久化但不想引入数据库。

1.4 方案三:MongoDB(或类似文档数据库)

  • 将会话与消息存为文档,按会话ID/时间索引查询与追加。
  • 优点:持久化可靠、天然支持多实例共享与水平扩展、易于做查询、统计与归档;备份/副本集可靠。
  • 缺点:需运维数据库(推荐使用 Docker),引入网络延迟与成本。
  • 适用场景:生产环境、多副本部署、需要可观测性与扩展性。

1.5 选型建议

  • 三种方案选择一种即可;
  • 快速落地/教学演示:选内存存储;配上限与TTL,最快见效。
  • 轻量持久化(单机):选文件存储;注意分桶与锁。
  • 正式生产/多实例/高并发:选 MongoDB(或 Redis Streams/Postgres JSONB),推荐开启 TTL、归档与监控。

二、代码

2.1 上下文聊天记忆的大模型对象

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.exi.exiaiagent.chatmemory.FileBasedChatMemory;
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.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;

import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY;

/**
 * 初始化可以存储上下文聊天记忆的大模型对象
 */
@Component
@Slf4j
public class ChatMemoryApp {

    private final ChatClient chatClient;

    /**
     * 构造函数 初始化ChatClient
     * @param dashscopeChatModel
     */
    public ChatMemoryApp(ChatModel dashscopeChatModel) {

        /**
         * 1 = 内存存储
         * 2 = 计算机资源文件存储
         * 3 = MongoDB数据库存储
         */
        int route = 1;

        ChatMemory chatMemory;
        if (route  == 1) {
            chatMemory = UseMemoryChatMemory();
        } else if (route == 2) {
            chatMemory = UseFileChatMemory();
        } else {
            chatMemory = UseMemoryChatMemory();  // 默认为基于内存存储
        }

        chatClient = ChatClient.builder(dashscopeChatModel)
                .defaultAdvisors(
                    new MessageChatMemoryAdvisor(chatMemory)  // 选择消息格式分层存储的拦截器
                )
                .build();
    }

    /**
     * 基于本地文件存储的方式 实现读取、写入聊天上下文
     * @return
     */
    public ChatMemory UseFileChatMemory(){
        // 项目根目录、定义对话存储文件路径
        String fileDir = System.getProperty("user.dir") + "/tmp/chat-memory";
        // 创建自定义文件存储的对象
        ChatMemory chatMemory = new FileBasedChatMemory(fileDir);
        return chatMemory;
    }

    /**
     * 基于计算机内存存储、实现读取、写入聊天上下文
     * @return
     */
    public ChatMemory UseMemoryChatMemory(){
        /**
         * 默认使用:MessageWindowChatMemory
         * 存储位置: JVM 内存 (InMemoryChatMemoryRepository)
         * 数据持久化: 不持久化(应用重启后丢失)
         * 消息窗口: maxMessages = 20(默认,可配置)
         */
        InMemoryChatMemory chatMemory = new InMemoryChatMemory();
        return chatMemory;
    }

    /**
     * 基于MongoDB数据库存储、实现读取、写入聊天上下文
     */
    public void UseMongoDBChatMemory(){

    }

    /**
     * 根据用户消息和用户的聊天ID进行回复
     * @param message
     * @param chatId
     * @return
     */
    public String doChat(String message, String chatId) {
        ChatResponse chatResponse = chatClient.prompt()
                .user(message)  // 用户消息
                .advisors(advisorSpec -> advisorSpec
                        .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)  // 指定聊天记忆存储ID
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)     // 获取最近10条聊天记忆
                )
                .call()
                .chatResponse();
        String content = chatResponse.getResult().getOutput().getText();  // 获取chatResponse的结果数据
        log.info("【对话请求】用户消息:{}", message);
        log.info("【模型响应】内容:{}", content);
        return content;
    }
}

2.2 基于本地文件存储的方式实现代码(可选)

Kryo序列化依赖

xml 复制代码
 <!-- Kryo高性能序列化框架依赖、作用:提供快速、高效的Java对象序列化和反序列化功能  -->
 <!-- 在自定义对话记忆存储对象序列化中使用到了 -->
 <dependency>
     <groupId>com.esotericsoftware</groupId>
     <artifactId>kryo</artifactId>
     <version>5.6.2</version>
 </dependency>

实现代码

java 复制代码
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.objenesis.strategy.StdInstantiatorStrategy;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 参考内存数据存储InMemoryChatMemory自定义存储实现ChatMemory
 */
public class FileBasedChatMemory implements ChatMemory {

    private String BASE_DIR;  // 会话文件存储路经

    /**
     * 创建一个Kryo序列化对象
     *
     * 问题:Message对象有多种子类型(UserMessage/SystemMessage/AssistantMessage),
     *      字段结构不同,JSON序列化难以统一处理
     *
     * 解决:使用Kryo二进制序列化,自动处理不同Message子类,自动处理复杂对象结构的序列化
     *      保持完整类型信息,提供高性能序列化
     *
     * 总结:可以通过Kryo解决不同子类字段不一致的存储和读取序列化问题
     *
     * 通俗解释:
     * - 就像有3种不同的盒子(UserMessage、SystemMessage、AssistantMessage)
     * - 每个盒子里装的东西都不一样(字段不同)
     * - JSON就像用文字描述盒子内容,很难统一描述
     * - Kryo就像直接拍照保存盒子,不管里面装什么都能完整保存
     * - 读取时也能完整还原出原来的盒子
     */
    private static final Kryo kryo = new Kryo();

    static {
        // 不要求注册类,允许序列化任意类
        kryo.setRegistrationRequired(false);
        // 设置实例化策略
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
    }

    // 构造对象时、初始化存储目录、没有则创建
    public FileBasedChatMemory(String dir) {
        this.BASE_DIR = dir;
        File baseDir = new File(dir);
        if (!baseDir.exists()) {
            baseDir.mkdirs();
        }
    }

    /**
     * 根据会话ID 添加会话列表
     * @param conversationId
     * @param messages
     */
    @Override
    public void add(String conversationId, List<Message> messages) {
        // 根据会话ID、获取之前的消息列表
        List<Message> conversationMessages = getOrCreateConversation(conversationId);
        // 添加新的消息列表
        conversationMessages.addAll(messages);
        // 根据会话ID、写入最新的消息列表
        saveConversation(conversationId, conversationMessages);
    }

    /**
     * 根据会话ID获取会话列表数据
     * @param conversationId
     * @param lastN
     * @return
     */
    @Override
    public List<Message> get(String conversationId, int lastN) {
        List<Message> messageList = getOrCreateConversation(conversationId);
        // 根据lastN获取最近存储的消息列表
        return messageList.stream()
                .skip(Math.max(0, messageList.size() - lastN))
                .toList();
    }

    /**
     * 根据会话ID、删除会话
     * @param conversationId
     */
    @Override
    public void clear(String conversationId) {
        File file = getConversationFile(conversationId);
        // 如果文件对象存储则删除
        if (file.exists()) {
            file.delete();
        }
    }

    /**
     * 根据会话ID创建或获取会话列表
     * @param conversationId
     * @return
     */
    private List<Message> getOrCreateConversation(String conversationId) {
        // 获取本地存储的kryo序列化文件对象
        File file = getConversationFile(conversationId);
        List<Message> messages = new ArrayList<>();
        if (file.exists()) {
            try (Input input = new Input(new FileInputStream(file))) {
                // 通过kryo读取文件输入流并转为ArrayList.class格式
                messages = kryo.readObject(input, ArrayList.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return messages;
    }

    /**
     * 把会话消息列表通过kryo序列化存储至文件当中
     * @param conversationId
     * @param messages
     */
    private void saveConversation(String conversationId, List<Message> messages) {
        File file = getConversationFile(conversationId);
        try (Output output = new Output(new FileOutputStream(file))) {
            kryo.writeObject(output, messages);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据会话ID获取本地存储的文件对象
     * @param conversationId
     * @return
     */
    private File getConversationFile(String conversationId) {
        return new File(BASE_DIR, conversationId + ".kryo");
    }
}

2.3 测试代码

测试代码

java 复制代码
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class ChatMemoryAppTest {

    /**
     * @Resource 注解的作用是先根据字段名后根据类型 来自动匹配注入对应的 Bean
     */
    @Resource
    private ChatMemoryApp chatMemoryApp;

    @Test
    void doChat() {
        String chatId = "a5daa1bb-e5e2-427e-9a74-68650459511f";
        String message = "我有两个朋友:张三和李四,张三喜欢性感火辣的、李四喜欢斯文儒雅的";
        String answer = chatMemoryApp.doChat(message, chatId);

        message = "我有两个朋友,你知道他们分别是谁不、各自喜欢的女生类型";

        answer = chatMemoryApp.doChat(message, chatId);
    }
}

测试结果

相关推荐
赋创小助手44 分钟前
多GPU服务器交付验收:GPU健康、P2P、NCCL与稳定性测试思路
运维·服务器·人工智能·ai·部署·gpu·p2p
王解1 小时前
LP-12_循环工程的未来:AI 编程的终极形态?
人工智能
airank1 小时前
2026年9月AI搜索时代品牌如何被推荐?GEO服务商选型要点与横向对比
人工智能·aigc·geo·生成式引擎优化·ai可见性
杭州华望MBSE1 小时前
应用案例|兵器重工:LLM驱动的SysML v2建模实践
人工智能·mbse·国产工业软件·llm驱动·sysml建模
扬大平仔1 小时前
# 小深:用 AgentScope Java 2.0 Harness 做私人助手(上)mysql
java·开发语言·mysql
今天AI了吗1 小时前
去中心化 AI 反馈系统:数据不上链,凭证与激励分开管
人工智能·windows·python·数据分析·去中心化·区块链·embedding
蓝速科技1 小时前
口岸政务窗口双屏翻译机落地应用指南
运维·数据结构·数据库·人工智能·科技·政务
m0_734571761 小时前
深入理解人工智能 chatGPT的客户端与接入层 (Client & Access Layer)
人工智能·chatgpt
鲜于言悠9051 小时前
Transformer架构优化
人工智能