Spring AI:ChatMemory 实现聊天记忆功能

目前我们已经实现了和 DeepSeek 大模型进行聊天,但是存在一个问题!没有聊天记忆功能!

相关文章:
Spring AI:对接DeepSeek实战
Spring AI:对接官方 DeepSeek-R1 模型 ------ 实现推理效果
Spring AI:ChatClient实现对话效果
Spring AI:使用 Advisor 组件 - 打印请求大模型出入参日志

大模型具备记忆功能吗?

大模型是 "无状态" 的,每次请求(如 API 调用)都是独立的。模型在生成回复时,只会关注当前输入中包含的上下文信息,不会主动保留之前的对话记录。

如果要让模型 "记住" 之前的对话,需要开发者或用户显式地将历史对话内容附加到当前输入中。例如,在聊天机器人应用中,每次请求需附带完整的对话历史,模型才能基于完整上下文生成连贯回复。

动手实现记忆功能

所以,为了实现记忆功能,需要在调用 DeepSeek 时,主动将之前的历史会话再次发送给它。接下来,我们来看看,在 Spring AI 中,要如何实现记忆功能!

配置 ChatMemory

在项目中的 /config 包下,新增一个 ChatMemoryConfig 配置类:

java 复制代码
@Configuration
public class ChatMemoryConfig {

    /**
     * 记忆存储
     */
    @Resource
    private ChatMemoryRepository chatMemoryRepository;

    /**
     * 初始化一个 ChatMemory 实例,并注入到 Spring 容器中
     * @return
     */
    @Bean
    public ChatMemory chatMemory() {
        return MessageWindowChatMemory.builder()
                .maxMessages(50) // 最大消息窗口为 50,默认值为 20
                .chatMemoryRepository(chatMemoryRepository) // 记忆存储
                .build();
    }
}

解释一下上述代码:

  • 通过 @Resource 注解注入一个 ChatMemoryRepository 的实例。它用于存储聊天会话,当我们没有自定义外部存储时(如 Cassandra、MySQL),默认情况下,Spring AI 使用的是 InMemoryChatMemoryRepository 实现类,会将消息存储在内存中的 ConcurrentHashMap 中。
  • MessageWindowChatMemory:它是具体的 ChatMemory 实现类,使用 "滑动窗口" 策略管理聊天记录。
    滑动窗口策略:保留最近的 N 条消息(这里是50条,默认值20),超出数量限制时自动移除旧消息。因为在 AI 对话中,限制上下文长度,可以避免超出模型的 Token 限制。

与 ChatClient 整合

完成 ChatMemory 的初始化工作后,我们需要和 ChatClient 整合一下,依然是使用上小节的 Advisor 组件,来实现调用大模型时,将之前的聊天内容,设置到入参中,代码如下:

java 复制代码
@Configuration
public class ChatClientConfig {

    @Resource
    private ChatMemory chatMemory;

    /**
     * 初始化 ChatClient 客户端
     * @param chatModel
     * @return
     */
    @Bean
    public ChatClient chatClient(DeepSeekChatModel chatModel) {
        return ChatClient.builder(chatModel)
                .defaultSystem("请你扮演一个智能客服")
                .defaultAdvisors(new SimpleLoggerAdvisor(), // 添加 Spring AI 内置的日志记录功能
//                                 new MyLoggerAdvisor(), // 添加自定义的日志打印 Advisor
                                MessageChatMemoryAdvisor.builder(chatMemory).build()
                        )
                .build();
    }
}

重启项目,在第一个问题后,再问相关问题,会发现已经有了记忆功能。会携带上之前的聊天

会话隔离问题

看似完美了,但是依然存在问题,我们在浏览器中,重新打开一个页面,再次请求相关接口, 按道理这是开启了一个 "新对话",它应该忘记了之前的记忆。但是事实上还是被记住了。

如何解决会话隔离的问题呢? 我们可以为每个对话定义一个 ID 唯一标识, 后端在存储聊天内容到 ConcurrentHashMap 中时,可以使用这个 ID 来作为键,从而实现每个对话的隔离:

回到 ChatClientController 控制器中,为 /v2/ai/generateStream 接口,添加参数 chatId, 表示当前对话 ID, 代码如下:

java 复制代码
@RestController
@RequestMapping("/v2/ai")
public class ChatClientController {

    @Resource
    private ChatClient chatClient;

    // 省略...

    /**
     * 流式对话
     * @param message
     * @return
     */
    @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message,
                                       @RequestParam(value = "chatId") String chatId) {

        // 流式输出
        return chatClient.prompt()
                .user(message) // 提示词
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
                .stream()
                .content();

    }

}

并且在调用 DeepSeek 时,通过 .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId)) 为当前聊天请求绑定一个会话 ID(chatId), 以便实现对话隔离功能。

重启项目进行测试。

相关推荐
Raink老师6 小时前
【AI面试临阵磨枪-48】GraphRAG、多模态 RAG、自适应 RAG 原理
人工智能·ai 面试题
波动几何6 小时前
模式驱动的学术选题方法论——四种AI模式处理能力的系统建构与论证
人工智能
飞哥数智坊7 小时前
我为我的龙虾斩分身:OpenClaw 多智能体实操
人工智能·agent
七牛开发者7 小时前
HTML is the new Markdown:来自 Claude Code 团队的实践
前端·人工智能·语言模型·html
飞哥数智坊7 小时前
在二线城市做AI社群,我的五一节后到底有多疯狂?
人工智能
视***间7 小时前
智启边缘,魔盒藏锋——视程空间Pandora系列魔盒,解锁边缘计算普惠新范式
人工智能·区块链·边缘计算·ai算力·视程空间
蛐蛐蛐7 小时前
昇腾910B4上安装新版本CANN的正确流程
人工智能·python·昇腾
庞轩px7 小时前
第七篇:Spring扩展点——如何优雅地介入Bean的创建流程
java·后端·spring·bean·aware·扩展点
沪漂阿龙7 小时前
AI大模型面试题:线性回归是什么?最小二乘法、平方误差、正规方程、Ridge、Lasso 一文讲透
人工智能·机器学习·线性回归·最小二乘法
Lyon198505287 小时前
《文字定律》让AI体验,汉字逻辑与字母逻辑的差异——ChatGPT
人工智能·ai·chatgpt·ai写作