第六节 会话记忆

亮子 | 2026-04-07 14:32:01 | 40 | 0 | 0 | 0

1.会话记忆原理

大模型不具备记忆能力,每次会话都是独立的。要想让大模型产生记忆的效果,唯一的方法就是把之前聊天的内容和新的内容一起发送给大模型。
有了langchian4j了之后就不需要这么麻烦了,它能够帮我们记录聊天消息并自动发送!

请求:后端接收到消息后,会自动把消息存放到存储对象中,然后再获取存储对象中记录的所有会话消息,一块发送给大模型
响应:大模型根据接收到的消息,生成答案,比如说是的,再把答案响应给web后端,此时web后端会把得到的响应消息往存储对象中拷贝一份,然后再把响应消息发送给用户。

2.会话记忆基本实现

langchain4j提供了一个接口叫做ChatMemory,该接口中提供了

  • add方法用于添加一条记录
  • messages方法用于获取所有的会话记录
  • clear方法用于清除所有的会话记录

这里还有一个id方法,它是用于唯一的标识一个存储对象,当然这个id暂时我们用不着,等会儿我们讲解会话记忆隔离的时候再给大家详细的讲解。
同时LangChain4j还提供了该接口的两个实现类,一个是TokenWindowChatMemory,另外一个是MessageWindowChatMemory, 咱们暂时先使用MessageWindowChatMemory来存储会话记录。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package dev.langchain4j.memory;

import dev.langchain4j.data.message.ChatMessage;
import java.util.List;

public interface ChatMemory {
    //记忆存储对象的唯一标识
    Object id();

    //添加一条会话记忆
    void add(ChatMessage var1);

    //获取所有会话记忆
    List<ChatMessage> messages();

    //清除所有会话记忆
    void clear();
}

2.1定义会话记忆对象

在CommonConfig类中,构建MessageWindowChatMemory对象,并注入到IOC容器中。构建的时候我们可以指定该对象中最大的会话存储数量,一般设置20就足够了。

package com.shenma.config;

import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 军哥
 * @version 1.0
 * @description: 会话记录配置类
 * @date 2026/4/7 14:56
 */

@Configuration
public class CommonConfig {
    /**
     * @description: 创建一个会话记录对象
     * @params []
     * @return dev.langchain4j.memory.ChatMemory
     * @author 军哥
     * @date 2026/4/7 14:57
     */
    @Bean
    public ChatMemory chatMemory() {
        return MessageWindowChatMemory.builder()
                .maxMessages(20)//最大保存的会话记录数量
                .build();
    }

}

2.2 配置会话记忆

package com.shenma.service;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
import reactor.core.publisher.Flux;

/**
 * @author 军哥
 * @version 1.0
 * @description: 会话记录服务
 * @date 2026/4/7 14:55
 */


@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "openAiChatModel",
        streamingChatModel = "openAiStreamingChatModel",
        chatMemory = "chatMemory"//配置会话记忆对象
)
public interface ConsultantService {
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(String message);
}

2.3 测试会话记忆

package com.shenma.controller;

import com.shenma.service.ConsultantService;
import dev.langchain4j.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

/**
 * @author 军哥
 * @version 1.0
 * @description: 测试会话记忆
 * @date 2026/4/7 14:59
 */

@RestController
public class ChatController {
    @Autowired
    private ConsultantService consultantService;

    //produces:响应的数据编码类型
    @PostMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> chat(@RequestParam("message") String message) {
        Flux<String> result = consultantService.chat(message);
        return result;
    }

}

2.4 运行效果

  • 问题1
    image.png

  • 问题2
    image.png

3.会话记忆隔离

刚才我们借助于MessageWindowChatMemory实现了会话记忆的效果,看起来还不错, 但是还是有一些小问题的。当不同的用户访问我们的程序时,无法区分不同用户的会话记录,因为刚才实现的会话记忆,所有用户存储会话记录都是用的是同一个会话记忆对象,所以会话记忆并没有做到隔离。

在LangChain4j中可以准备一个容器,专门用于存储当前程序中所有的会话记忆对象。假设有一个用户访问我们的程序,此时它除了要把用户问题message携带给后端,还需要携带一个memoryId,我们只需要把对应memoryId的会话记忆对象的内容传给大模型。

3.1定义会话记忆对象提供者

LangChain4j中提供了一个类ChatMemoryProvider,将来LangChain4j如果从容器中没有找到指定id的ChatMemory对象,就会调用ChatMemoryProvider对象的get方法获取一个新的ChatMemory对象使用,因此我们需要提供这个ChatMemoryProvider对象,实现get方法。这里的get方法,会接收一个参数,这个参数就是memoryId,返回一个结果就是ChatMemory对象。

/**
 * @description: 创建一个会话记录提供者对象
 * @params []
 * @return dev.langchain4j.memory.chat.ChatMemoryProvider
 * @author 军哥
 * @date 2026/4/7 14:57
 */
@Bean
public ChatMemoryProvider chatMemoryProvider() {
    ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
        @Override
        public ChatMemory get(Object memoryId) {
            return MessageWindowChatMemory.builder()
                    .id(memoryId)//id值
                    .maxMessages(20)//最大会话记录数量
                    .build();
        }
    };
    return chatMemoryProvider;
}

3.2配置会话记忆对象提供者

既然提供了ChatMemoryProvider,之前提供的这个公有的ChatMemory就没有必要了,可以把它注释掉。

@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "openAiChatModel",
        streamingChatModel = "openAiStreamingChatModel",
//        chatMemory = "chatMemory"
        chatMemoryProvider = "chatMemoryProvider"//配置会话记忆对象提供者
)

3.3ConsultantService接口的方法中添加参数memoryId

package com.shenma.service;

import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
import reactor.core.publisher.Flux;

/**
 * @author 军哥
 * @version 1.0
 * @description: 会话记录服务
 * @date 2026/4/7 14:55
 */


@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "openAiChatModel",
        streamingChatModel = "openAiStreamingChatModel",
//        chatMemory = "chatMemory"
        chatMemoryProvider = "chatMemoryProvider"//配置会话记忆对象提供者
)
public interface ConsultantService {
    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(String message);

    @SystemMessage(fromResource = "system.txt")
    public Flux<String> chat(@MemoryId String memoryId, @UserMessage String message);
}

3.4ChatController中chat接口接收前端传递的memoryId

package com.shenma.controller;

import com.shenma.service.ConsultantService;
import dev.langchain4j.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

/**
 * @author 军哥
 * @version 1.0
 * @description: 测试会话记忆
 * @date 2026/4/7 14:59
 */

@RestController
public class ChatController {
    @Autowired
    private ConsultantService consultantService;

    //produces:响应的数据编码类型
    @PostMapping(value = "/chat1", produces = "text/html;charset=utf-8")
    public Flux<String> chat(@RequestParam("message") String message) {
        Flux<String> result = consultantService.chat(message);
        return result;
    }

    @PostMapping(value = "/chat2",produces = "text/html;charset=utf-8")
    public Flux<String> chat(@RequestParam("memoryId") String memoryId, @RequestParam("message") String message){
        Flux<String> result = consultantService.chat(memoryId, message);
        return result;
    }

}

3.5前端访问/chat接口是提交memoryId参数

我们使用的是Date.now.toString()来充当memoryId,每次初始化页面会生成一个新的memoryId,当新建对话的时候会重置memoryId

image.png

4.会话记忆持久化

4.1MessageWindowChatMemory不持久化的原理

刚才我们完成了会话记忆隔离,其实我们的会话记忆还是有一些瑕疵的,只要后端重启,会话记忆就没有了,丢失了。先分析一下为什么会存在这种问题,之前我们一直构建的用于存储会话记录的对象是MessageWindowChatMemory,而这个对象内部维护了一个成员变量ChatMemoryStore,其实我们使用MessageWindowChatMemory对象的add方法添加会话记录的时候,真正用于存储的对象是这个ChatMemoryStore,所以要分析为什么重启之后会话记录会丢失,我们得分析ChatMemoryStore是如何存储会话记录的。

public class MessageWindowChatMemory implements ChatMemory {
    private final String id;
    private final ChatMemoryStore store;//这个对象用于存储会话记录

    public void add(ChatMessage message) {
        this.store.updateMessages(this.id, messages);
    }
    public List<ChatMessage> messages() {
        return this.store.getMessages(this.id);
    }
    public void clear() {
        this.store.deleteMessages(this.id);
    }
 }

ChatMemoryStore是一个接口,它里面提供了getMessages、updateMessages、deleteMessages方法分别用于根据memoryId获取会话记录,根据memoryId更新会话记录以及根据memoryId删除会话记录。

public interface ChatMemoryStore {
    List<ChatMessage> getMessages(Object memoryId);
    void updateMessages(Object memoryId,List<ChatMessage> messages);
    void deleteMessages(Object memoryId);
}

LangChain4j为该接口提供了两个实现类,分别是InMemoryChatMemoryStore和SingleSlotMemoryStore。而我们MessageWindowChatMemory中默认使用的Store对象就是这个SingleChatMemoryStore。

image.png

接下来我们重点分析它里面又是如何存储会话记录的。

class SingleSlotChatMemoryStore implements ChatMemoryStore {
    private List<ChatMessage> messages = new ArrayList();//用于存储会话记录
    public List<ChatMessage> getMessages(Object memoryId) {
        return this.messages;
    }
    public void updateMessages(Object memoryId,
                               List<ChatMessage> messages) {
        this.messages = messages;
    }
    public void deleteMessages(Object memoryId) {
        this.messages = new ArrayList();
    }
}

在SingleSlotChatMemoryStore中维护了一个集合对象messages,它就是使用这个集合存储会话消息的,所以很明显这是内存存储,一旦当服务器重启后这些消息必然会丢失!

4.2会话记录持久化——Redis

(1)准备Redis环境
下载 docker和redis

(2)引入redis起步依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

(3) 配置redis连接信息

spring:
  data:
    redis:
      host: localhost
      port: 6379