前言

在第一章chatModel介绍了如何与Ai进行多次聊天,但手动维护ChatMessage过于麻烦,LangChain4j提供ChatMemory接口类解决这个问题

1. 删除旧信息策略

我们知道调用Ai进行聊天如果传输的聊天信息越多,则Ai处理的时间则越长,消耗的tokens也随之更多,代价则变得更高。所以不能无限制的传输所有的历史聊天信息,这时需要提供一些策略去删除旧的聊天信息。langChain4j提供两种策略,分别是MessageWindowChatMemoryTokenWindowChatMemory

1.1 MessageWindowChatMemory

消息窗口聊天缓存保存的最新maxMessages(指定数量)条信息,不论每条信息的tokens长度。常用于测试和快速验证功能。

/**
 * @Author: liuxia
 * @CreateTime: 2025/9/13 下午10:06
 * @Description: 消息窗口聊天缓存测试
 */
public class MessageWindowChatMemoryTest {

    public static void main(String[] args) {
        ChatModel model = ChatModelInit.initOllama();

        MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
                .id("1234")                 // id,通常用于区分多个用户或多个对话
                .maxMessages(2)             // 最大存储消息数量
                .chatMemoryStore(new InMemoryChatMemoryStore())   //内存缓存存储类
                .build();

        UserMessage oneMsg = UserMessage.from("你好,你现在是我的朋友koi");
        memory.add(oneMsg);

        AiMessage oneAiMsg = model.chat(memory.messages()).aiMessage();
        System.out.println("ai回答1: " + oneAiMsg.text());

        UserMessage twoMsg = UserMessage.from("你是谁?");
        memory.add(twoMsg);

        AiMessage twoAiMsg = model.chat(memory.messages()).aiMessage();
        System.out.println("ai回答2: " + twoAiMsg.text());

        UserMessage threeMsg = UserMessage.from("你是叫koi吗");
        memory.add(threeMsg);

        for (int i = 0; i < memory.messages().size(); i++) {        // 只会打印最新的两条信息,删除了第一条
            System.out.println("第" + i + "条信息: "+ memory.messages().get(i).toString());
        }

        AiMessage threeAiMsg = model.chat(memory.messages()).aiMessage();
        System.out.println("ai回答3: " + threeAiMsg.text());
    }
}

1.2 TokenWindowChatMemory

token窗口聊天缓存保存的是最新的maxTokens(指定数量)的数量,但Ollama不支持该类型缓存,需要使用OpenAi。

/**
 * @Author: liuxia
 * @CreateTime: 2025/9/13 下午10:43
 * @Description: tokens窗口聊天缓存test
 */
public class TokenWindowChatMemoryTest {

    public static void main(String[] args) {
        ChatModel model = ChatModelInit.initOpenAi();
        OpenAiTokenCountEstimator estimator = new OpenAiTokenCountEstimator(OpenAiChatModelName.GPT_4);
        TokenWindowChatMemory memory = TokenWindowChatMemory.builder()
                .maxTokens(1000, estimator)
                .chatMemoryStore(new InMemoryChatMemoryStore())   //内存缓存存储类
                .build();
        UserMessage oneMsg = UserMessage.from("你好,你现在是我的朋友koi");
        memory.add(oneMsg);

        AiMessage oneAiMsg = model.chat(memory.messages()).aiMessage();
        System.out.println("ai回答1: " + oneAiMsg.text());

        UserMessage twoMsg = UserMessage.from("你是谁?");
        memory.add(twoMsg);

        AiMessage twoAiMsg = model.chat(memory.messages()).aiMessage();
        System.out.println("ai回答2: " + twoAiMsg.text());

        UserMessage threeMsg = UserMessage.from("你是叫koi吗");
        memory.add(threeMsg);

        AiMessage threeAiMsg = model.chat(memory.messages()).aiMessage();
        System.out.println("ai回答3: " + threeAiMsg.text());
    }
}

2.持久化

通过用户与Ai聊天的信息需要持久化,而不是一直放在内存(langChain4j默认实现InMemoryChatMemoryStore)中,所以我们需要实现ChatMemoryStore接口类,用来持久化用户聊天信息。否则生产环境承受不住全部聊天信息,而且重启项目就丢失所有聊天信息。

2.1 InMemoryChatMemoryStore默认实现类见下:
public class InMemoryChatMemoryStore implements ChatMemoryStore {
    private final Map<Object, List<ChatMessage>> messagesByMemoryId = new ConcurrentHashMap();	// 保存memoryId和聊天集合的map缓存

    public InMemoryChatMemoryStore() {
    }

    public List<ChatMessage> getMessages(Object memoryId) {
        return (List)this.messagesByMemoryId.computeIfAbsent(memoryId, (ignored) -> {
            return new ArrayList();
        });
    }

    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        this.messagesByMemoryId.put(memoryId, messages);
    }

    public void deleteMessages(Object memoryId) {
        this.messagesByMemoryId.remove(memoryId);
    }
}
2.2 PersistentChatMemoryStore实现可参考如下所示:
/**
 * @Author: liuxia
 * @CreateTime: 2025/9/13 下午11:11
 * @Description: 持久化聊天缓存test
 */
public class PersistentChatMemoryStoreTest {

    @Resource
    PersistentChatMemoryStore store;

    @Test
    public  void test() {
        ChatModel model = ChatModelInit.initOllama();

        MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
                .id("1234")                 // id,通常用于区分多个用户或多个对话
                .maxMessages(4)             // 最大存储消息数量
                .chatMemoryStore(store)     // 持久化缓存存储类
                .build();

        UserMessage oneMsg = UserMessage.from("你好,你现在是我的朋友koi");
        memory.add(oneMsg);
        AiMessage oneAiMsg = model.chat(memory.messages()).aiMessage();
        memory.add(oneAiMsg);
        System.out.println("ai回答1: " + oneAiMsg.text());

        UserMessage twoMsg = UserMessage.from("你是我的朋友koi吗");
        memory.add(twoMsg);
        AiMessage twoAiMsg = model.chat(memory.messages()).aiMessage();
        memory.add(twoAiMsg);
        System.out.println("ai回答2: " + twoAiMsg.text());
        System.out.println("数据库已有对话数:" + memory.messages().size());  // 输出的是4
    }

}

PersistentChatMemoryStore类相关代码如下:

/**
 * @Author: liuxia
 * @CreateTime: 2025/9/14 下午3:16
 * @Description:
 *      持久化聊天缓存存储
 *      实际场景应该结合redis做用户聊天信息处理
 */
@Component
public class PersistentChatMemoryStore implements ChatMemoryStore {

    private ChatMessageService chatMessageService;

    PersistentChatMemoryStore(ChatMessageService chatMessageService) {
        this.chatMessageService = chatMessageService;
    }

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        List<ChatMessage> messages = new ArrayList<>();
        List<CustomChatMessage> msgs = chatMessageService.getMessageListById(Integer.valueOf(memoryId.toString()));
        for (CustomChatMessage msg : msgs) {
            if (msg.getType().equals(1)) {
                UserMessage userMessage = UserMessage.from(msg.getMessage());
                messages.add(userMessage);
            } else if (msg.getType().equals(2)){
                AiMessage aiMessage = AiMessage.from(msg.getMessage());
                messages.add(aiMessage);
            }
        }
        return messages;
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> list) {
        // list 保存的是最新的全部信息,包含用户这次的聊天信息,只需要获取最新一条保存的数据库
        ChatMessage latestMsg = list.get(list.size() - 1);
        ChatMessageType typeEnum = latestMsg.type();
        Integer type = 1;
        String message = "";
        if (typeEnum.equals(ChatMessageType.AI)) {
            type = 2;
            AiMessage aiMessage = (AiMessage) latestMsg;
            message = aiMessage.text();
        } else if (typeEnum.equals(ChatMessageType.USER)) {
            type = 1;
            UserMessage userMessage = (UserMessage) latestMsg;
            message = userMessage.singleText();
        }
        chatMessageService.save(Integer.valueOf(memoryId.toString()), type, message);
    }

    @Override
    public void deleteMessages(Object memoryId) {
        chatMessageService.delByUserId(Integer.valueOf(memoryId.toString()));
    }
}

ChatMessageService类相关代码如下:


/**
 * @Author: liuxia
 * @CreateTime: 2025/9/13 下午11:14
 * @Description:  聊天信息service
 */
public interface ChatMessageService {

    /**
     * 获取用户的历史聊天信息
     *
     * @param userId
     * @return
     */
    List<CustomChatMessage> getMessageListById(Integer userId);

    /**
     * 保存聊天信息
     *
     * @param userId
     * @param type
     * @param message
     * @return
     */
    boolean save(Integer userId, Integer type, String message);

    /**
     * 删除用户聊天信息
     *
     * @param userId
     * @return
     */
    boolean delByUserId(Integer userId);
}

/**
 1. @Author: liuxia
 2. @CreateTime: 2025/9/14 下午3:09
 3. @Description: 聊天信息实现类
 */
@Service
public class ChatMessageServiceImpl implements  ChatMessageService {

    /**
     * 假设这是数据库
     */
    private List<CustomChatMessage> messages = new ArrayList<>();

    @Override
    public List<CustomChatMessage> getMessageListById(Integer userId) {
        return messages.stream().filter(f -> f.getUserId().equals(userId)).toList();
    }

    @Override
    public boolean save(Integer userId, Integer type, String message) {
        CustomChatMessage msg = CustomChatMessage.builder()
                .userId(userId)
                .type(type)
                .message(message)
                .build();
        messages.add(msg);
        return true;
    }

    @Override
    public boolean delByUserId(Integer userId) {
        messages.removeIf(f -> f.getUserId().equals(userId));
        return true;
    }
}

3. SystemMessage的特殊处理

在上一篇文章简单提到了SystemMessage,SystemMessage在ChatMemoryStore有以下约束:
1)一旦添加,始终保留。意味着删除策略不会删除该条。
2)只能有一条该类型消息;重复添加相同内容会忽略;重复添加不同内容会覆盖;
在这里插入图片描述SystemMessage测试代码如下:

public class SystemMessageTest {

    @Resource
    PersistentChatMemoryStore store;

    /**
     * 测试SystemMessage 不会删除
     */
    @Test
    public  void noDelTest() {
        MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
                .id("1234")                 // id,通常用于区分多个用户或多个对话
                .maxMessages(2)             // 最大存储消息数量
                .chatMemoryStore(store)     // 持久化缓存存储类
                .build();

        SystemMessage systemMessage = SystemMessage.from("你是写诗小能手,能够帮助用户提供优美的七言等类型诗词。");
        memory.add(systemMessage);
        memory.add(UserMessage.from("测试数量1"));
        memory.add(UserMessage.from("测试数量2"));
        for (ChatMessage message : memory.messages()) {
            /**
             * 只会展示system信息和测试数量2,如下:
             * SYSTEM : SystemMessage { text = "你是写诗小能手,能够帮助用户提供优美的七言等类型诗词。" }
             * USER : UserMessage { name = null contents = [TextContent { text = "测试数量2" }] }
             */
            System.out.println(message.type() +  " : " + message);
        }
        System.out.println(memory.messages().size());
    }

    /**
     * 测试SystemMessage 重复添加相同内容
     */
    @Test
    public void multipleAddSameTest() {
        MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
                .id("1234")                 // id,通常用于区分多个用户或多个对话
                .maxMessages(4)             // 最大存储消息数量
                .chatMemoryStore(store)     // 持久化缓存存储类
                .build();
        SystemMessage systemMessage = SystemMessage.from("你是写诗小能手,能够帮助用户提供优美的七言等类型诗词。");
        memory.add(systemMessage);
        memory.add(systemMessage);
        System.out.println("总数:" + memory.messages().size());
        System.out.println("数据:" + memory.messages().get(0));
        /**
         * 打印数据如下:
         * 总数:1
         * 数据:SystemMessage { text = "你是写诗小能手,能够帮助用户提供优美的七言等类型诗词。" }
         */
    }

    /**
     * 测试SystemMessage 重复添加不相同内容
     */
    @Test
    public void multipleAddNoSameTest() {
        MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
                .id("1234")                 // id,通常用于区分多个用户或多个对话
                .maxMessages(4)             // 最大存储消息数量
                .chatMemoryStore(store)     // 持久化缓存存储类
                .build();
        SystemMessage systemMessage1 = SystemMessage.from("你是写诗小能手,能够帮助用户提供优美的七言等类型诗词。");
        memory.add(systemMessage1);
        SystemMessage systemMessage2 = SystemMessage.from("你是画画小能手,能够帮助用户提供画画思路。");
        memory.add(systemMessage2);

        System.out.println("总数:" + memory.messages().size());
        System.out.println("数据:" + memory.messages().get(0));
        /**
         * 打印数据如下:
         * 总数:1
         * 数据:SystemMessage { text = "你是画画小能手,能够帮助用户提供画画思路。" }
         */
    }
}

总结

ChatMemory在实际生产环境中是必须要实现,可能有人会觉得这样用还是不太方便,目前讲的毒是LangChain4j的低级API,后续会使用AIService类高级API关联ChatMemory,会隐藏添加,更新的细节,更关注业务实现,但如果不理解低级API,在解决bug时会面临无从下手的困难。

以上完整代码在我的仓库,地址:https://gitee.com/liutao-lx/langchain4j-agent-dev/tree/master/agent-example/src/main/java/com/koicarp/agent/example/chatmemery

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐