LLM - 05_LangChain4j: 深入解析 ChatMemory & 案例实战

在这里插入图片描述


概述

在构建多轮对话应用时,管理和维护聊天消息变得尤为重要。然而,手动管理每一条 ChatMessage 消息既繁琐又容易出错。为了解决这个问题,LangChain4j 提供了一个 ChatMemory 抽象,并且提供了多种现成的实现方案来简化这一过程。

接下来我们将深入探讨 LangChain4j 中的 ChatMemory 组件,以及如何通过ChatMemory 更好地管理对话状态。

什么是 ChatMemory?

ChatMemory 作为一个内存管理容器,存储和管理多轮对话中的 ChatMessage。它不仅允许开发者保存消息,还提供了以下几种功能:

  • 消息驱逐策略(Eviction Policy)
  • 持久化存储(Persistence)
  • 特殊消息处理(SystemMessage 和 ToolExecutionMessage)

此外,ChatMemory 还与high-level组件(如 AI 服务)集成,便于开发者更方便地管理对话历史。


Memory vs History

ChatMemory 中,有两个概念是值得注意的:MemoryHistory。这两个概念看似相似,但实际上有重要的区别:

  • History(历史):保留了用户和 AI 之间的所有对话,通常展示给用户看。这代表了实际的对话内容。
  • Memory(记忆):仅保留一些关键的信息,用于让 LLM 模型"记住"对话。不同于历史,记忆可以对历史消息进行处理:删除不重要的消息、合并多条消息、总结信息、注入额外的信息(如用于 RAG 的信息)等。

LangChain4j 提供的是 Memory,而不是 History。如果需要完整的对话历史,开发者需要手动管理。


驱逐策略(Eviction Policy)

为了控制内存和成本,ChatMemory 提供了驱逐策略。以下是驱逐策略的几个主要用途:

  • 适应 LLM 的上下文窗口限制:LLM 在一次生成过程中能够处理的 Token 数量是有限的。如果对话内容超出了这个限制,就需要驱逐掉一些消息。通常会驱逐最老的消息,但也可以实现更复杂的驱逐算法。
  • 控制成本:每个 Token 都有一定的费用,过多的消息会增加调用 LLM 的成本。通过驱逐无关消息,可以有效减少费用。
  • 控制延迟:更多的 Token 需要更多的处理时间,影响响应速度。适当驱逐消息可以减少延迟。

LangChain4j 提供了两种常见的驱逐实现:

  1. MessageWindowChatMemory:这种方式类似滑动窗口,只保留 N 条最新的消息,驱逐掉不再适合的旧消息。适合快速原型开发,但不考虑每条消息的 Token 数量。

  2. TokenWindowChatMemory:这是一种更精确的实现,保留 N 个最新的 Token,而不是简单地保留消息。这要求使用 Tokenizer 来计算每条消息的 Token 数量,确保在驱逐消息时不会超过最大 Token 数量。


持久化存储(Persistence)

默认情况下,ChatMemory 仅将消息存储在内存中,但如果需要长期存储聊天记录,可以实现自定义的持久化存储。例如,可以将 ChatMessages 存储在数据库或文件系统中:

 class PersistentChatMemoryStore implements ChatMemoryStore {

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        // TODO: 实现根据 memoryId 从持久化存储中获取所有消息。
        // 可以使用 ChatMessageDeserializer.messageFromJson(String) 和
        // ChatMessageDeserializer.messagesFromJson(String) 辅助方法将 JSON 反序列化为聊天消息。
        return null; // 暂时返回 null,待实现
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        // TODO: 实现根据 memoryId 更新持久化存储中的所有消息。
        // 可以使用 ChatMessageSerializer.messageToJson(ChatMessage) 和
        // ChatMessageSerializer.messagesToJson(List<ChatMessage>) 辅助方法将聊天消息序列化为 JSON。
    }

    @Override
    public void deleteMessages(Object memoryId) {
        // TODO: 实现根据 memoryId 删除持久化存储中的所有消息。
    }
}

ChatMemory chatMemory = MessageWindowChatMemory.builder()
        .id("12345") // 设置聊天内存的唯一标识符
        .maxMessages(10) // 设置最大消息数为 10
        .chatMemoryStore(new PersistentChatMemoryStore()) // 使用自定义的持久化存储实现
        .build(); // 构建 ChatMemory 实例

每次添加新消息时,updateMessages() 方法会被调用。这个方法会在每次交互中更新消息:一次是添加 UserMessage,另一次是添加 AiMessage

需要注意的是,驱逐的消息也会从持久化存储中移除。


特殊消息处理

SystemMessageToolExecutionMessage 是两种特殊的消息类型,它们在 ChatMemory 中的处理方式有所不同。

  1. SystemMessage

    • SystemMessage 会始终保留在记忆中,不能被驱逐。
    • 一次只能有一个 SystemMessage。如果新的 SystemMessage 内容与旧的相同,则会被忽略;如果不同,则会替换旧的消息。
  2. ToolExecutionMessage

    • 当包含 ToolExecutionRequestsAiMessage 被驱逐时,相关的 ToolExecutionResultMessage 也会被自动驱逐。这是为了防止某些 LLM 提供商(如 OpenAI)禁止发送“孤立”的工具执行结果消息。

Code

以下是几种常见的 ChatMemory 使用场景:

  • 单用户聊天记忆
    使用 MessageWindowChatMemory 来为每个用户保持单独的记忆。

  • 持久化聊天记忆
    将用户的聊天记忆存储到数据库或其他持久化存储中,以便在不同的会话间共享记忆。

  • 与 AI 服务结合使用
    ChatMemory 与 LangChain4j 的高层组件(如 AiServices)结合使用,实现更复杂的记忆管理。

  • 与传统的链式模型结合使用
    使用 ConversationalChainConversationalRetrievalChain 结合 ChatMemory 来实现更智能的对话模型。

Chat memory

package com.artisan.day01;

import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;

import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;

/**
 * AiServices:
 * - Chat memory
 */
public class ServiceWithMemoryExample {

    interface Assistant {

        String chat(String message);
    }

    /**
     * 演示如何使用AI助手进行聊天
     */
    public static void main(String[] args) {
        // 创建一个聊天记忆对象,最多保存10条消息
        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);

        // 构建一个聊天语言模型对象,使用OpenAI的GPT-4模型
        ChatLanguageModel model = OpenAiChatModel.builder()
                .apiKey("demo") // 设置API密钥
                .modelName(GPT_4_O_MINI) // 设置模型名称
                .build();

        // 使用构建器模式创建AI助手对象,传入聊天语言模型和聊天记忆
        Assistant assistant = AiServices.builder(Assistant.class)
                .chatLanguageModel(model)
                .chatMemory(chatMemory)
                .build();

        // 发送消息给AI助手,并打印回复
        String answer = assistant.chat("你好.我的名字是小工匠");
        System.out.println(answer); // 你好,小工匠!很高兴认识你。请问有什么我可以帮助你的吗?

        // 再次发送消息给AI助手,并打印回复
        String answerWithName = assistant.chat("请问我叫什么?");
        System.out.println(answerWithName); // 你叫小工匠。请问还有其他想聊的吗?
    }
}

  1. 创建聊天记忆对象

    ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
    

    创建一个最多保存10条消息的聊天记忆对象。

  2. 构建聊天语言模型

    ChatLanguageModel model = OpenAiChatModel.builder()
            .apiKey("demo")
            .modelName(GPT_4_O_MINI)
            .build();
    

    使用OpenAI的GPT-4模型构建一个聊天语言模型,并设置API密钥和模型名称。

  3. 创建AI助手对象

    Assistant assistant = AiServices.builder(Assistant.class)
            .chatLanguageModel(model)
            .chatMemory(chatMemory)
            .build();
    

    使用构建器模式创建一个AI助手对象,传入聊天语言模型和聊天记忆。

  4. 发送消息并获取回复

    String answer = assistant.chat("你好.我的名字是小工匠");
    System.out.println(answer);
    String answerWithName = assistant.chat("请问我叫什么?");
    System.out.println(answerWithName);
    

    向AI助手发送两条消息,并打印AI助手的回复。

开始
创建聊天记忆对象
构建聊天语言模型
创建AI助手对象
发送第一条消息
打印第一条回复
发送第二条消息
打印第二条回复
结束

在这里插入图片描述


Separate chat memory for each user

package com.artisan.day01;

import com.artisan.common.ApiKeys;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;

import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;

/**
 * With AiServices:
 * - Separate chat memory for each user
 */
public class ServiceWithMemoryForEachUserExample {


    interface Assistant {

        String chat(@MemoryId int memoryId, @UserMessage String userMessage);
    }

    /**
     * 演示了如何使用OpenAI的GPT-4模型创建一个聊天助手,能够记忆对话上下文并进行个性化对话
     *
     * @param args 命令行参数
     */
    public static void main(String[] args) {

        // 构建一个聊天语言模型实例,需要指定API密钥和模型名称
        ChatLanguageModel model = OpenAiChatModel.builder()
                .apiKey(ApiKeys.OPENAI_API_KEY)
                .modelName(GPT_4_O_MINI)
                .build();

        // 构建一个助手实例,使用上述语言模型,并设置聊天记忆策略,这里设置为最多记忆10条消息
        Assistant assistant = AiServices.builder(Assistant.class)
                .chatLanguageModel(model)
                .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
                .build();

        // 以下代码展示了与助手进行多次对话的过程,每次对话都基于之前的上下文
        System.out.println(assistant.chat(1, "我的名字是小工匠"));
        //你好,小工匠!很高兴认识你。你有什么想聊的或者想分享的事情吗?

        System.out.println(assistant.chat(2, "我的名字是Artisan"));
        // 你好,Artisan!很高兴认识你。有任何我可以帮助你的吗?

        // 询问第一位用户的名字,助手应能正确回答
        System.out.println(assistant.chat(1, "我叫什么名字?"));
        // 你叫小工匠。有什么特别的故事或含义吗?

        // 询问第二位用户的名字,以展示助手能根据不同用户记忆不同的信息
        System.out.println(assistant.chat(2, "我叫什么名字?"));
        // 你叫Artisan。有什么我可以帮助你的吗?
    }
}

在这里插入图片描述


Persistent chat memory

package com.artisan.day01;

import com.artisan.common.ApiKeys;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.mapdb.DB;
import org.mapdb.DBMaker;

import java.util.List;
import java.util.Map;

import static dev.langchain4j.data.message.ChatMessageDeserializer.messagesFromJson;
import static dev.langchain4j.data.message.ChatMessageSerializer.messagesToJson;
import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;
import static org.mapdb.Serializer.STRING;

/**
 * With AiServices:
 * - Persistent chat memory
 */
public class ServiceWithPersistentMemoryExample {


    interface Assistant {

        String chat(String message);
    }

    /**
     * 程序的入口点,用于演示如何使用AI助手进行聊天
     */
    public static void main(String[] args) {
        // 创建一个聊天记忆对象,设置最大消息数为10,并使用持久化聊天记忆存储
        ChatMemory chatMemory = MessageWindowChatMemory.builder()
                .maxMessages(100)
                .chatMemoryStore(new PersistentChatMemoryStore())
                .build();

        // 创建一个聊天语言模型对象,设置API密钥和模型名称为GPT-4
        ChatLanguageModel model = OpenAiChatModel.builder()
                .apiKey(ApiKeys.OPENAI_API_KEY)
                .modelName(GPT_4_O_MINI)
                .build();

        // 使用AI服务创建一个助手对象,设置聊天语言模型和聊天记忆
        Assistant assistant = AiServices.builder(Assistant.class)
                .chatLanguageModel(model)
                .chatMemory(chatMemory)
                .build();

        // 发送消息给助手并打印回复
        String answer = assistant.chat("你好,我的名字叫小工匠");
        System.out.println(answer); // 你好,小工匠!很高兴认识你!有什么想聊的或者需要帮助的地方吗?


        // 现在,注释掉上面的两行代码,取消注释下面的两行代码,然后再次运行。
        // 这样做是为了演示助手如何根据之前的聊天记录记住用户的名字。

//         String answerWithName = assistant.chat("我叫什么名字?");
//         System.out.println(answerWithName); // 你叫小工匠!如果你有其他问题或者需要讨论的事情,随时告诉我!
    }

    /**
     * 持久化聊天内存存储类
     * 实现了ChatMemoryStore接口,用于持久化存储聊天消息
     * 该类使用MapDB库创建一个持久化的数据库,将聊天消息以键值对的形式存储在文件中
     */
    static class PersistentChatMemoryStore implements ChatMemoryStore {

        // 数据库实例,使用MapDB创建,启用事务处理
        private final DB db = DBMaker.fileDB("chat-memory.db").transactionEnable().make();

        // 存储聊天消息的Map,使用HashMap实现,键和值都是字符串类型
        private final Map<String, String> map = db.hashMap("messages", STRING, STRING).createOrOpen();

        /**
         * 根据memoryId获取聊天消息
         *
         * @param memoryId 聊天消息的唯一标识符
         * @return 聊天消息列表,如果找不到则返回null
         */
        @Override
        public List<ChatMessage> getMessages(Object memoryId) {
            String json = map.get((String) memoryId);
            return messagesFromJson(json);
        }

        /**
         * 更新特定memoryId的聊天消息列表
         *
         * @param memoryId 聊天消息的唯一标识符
         * @param messages 要更新的聊天消息列表
         */
        @Override
        public void updateMessages(Object memoryId, List<ChatMessage> messages) {
            String json = messagesToJson(messages);
            map.put((String) memoryId, json);
            db.commit();
        }

        /**
         * 删除特定memoryId的聊天消息
         *
         * @param memoryId 要删除的聊天消息的唯一标识符
         */
        @Override
        public void deleteMessages(Object memoryId) {
            map.remove((String) memoryId);
            db.commit();
        }
    }
}

 
  1. 创建聊天记忆对象:使用MessageWindowChatMemory创建一个聊天记忆对象,设置最大消息数为10,并使用自定义的持久化聊天记忆存储。
  2. 创建聊天语言模型对象:使用OpenAiChatModel创建一个聊天语言模型对象,设置API密钥和模型名称为GPT-4。
  3. 创建助手对象:使用AiServices创建一个助手对象,设置聊天语言模型和聊天记忆。
  4. 发送消息并打印回复:通过助手对象发送消息并打印回复,演示了如何根据之前的聊天记录记住用户的名字。
  5. 持久化聊天内存存储类:实现ChatMemoryStore接口,使用MapDB库创建一个持久化的数据库,将聊天消息以键值对的形式存储在文件中。
持久化聊天内存存储类
创建存储Map
初始化数据库
获取聊天消息
更新聊天消息
删除聊天消息
开始
创建聊天记忆对象
创建聊天语言模型对象
创建助手对象
发送消息并打印回复
结束

在这里插入图片描述


Persistent chat memory for each user

package com.artisan.day01;

import com.artisan.common.ApiKeys;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.mapdb.DB;
import org.mapdb.DBMaker;

import java.util.List;
import java.util.Map;

import static dev.langchain4j.data.message.ChatMessageDeserializer.messagesFromJson;
import static dev.langchain4j.data.message.ChatMessageSerializer.messagesToJson;
import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;
import static org.mapdb.Serializer.INTEGER;
import static org.mapdb.Serializer.STRING;

public class ServiceWithPersistentMemoryForEachUserExample {

    interface Assistant {

        String chat(@MemoryId int memoryId, @UserMessage String userMessage);
    }

    /**
     * 如何使用聊天AI助手进行对话
     * 它首先设置了一个持久化的聊天记忆存储,然后配置了一个聊天记忆提供者
     * 接着,它创建了一个聊天语言模型,并将其与记忆提供者一起用于构建一个AI助手
     * 最后,它展示了如何与AI助手进行聊天,并记住对话上下文
     */
    public static void main(String[] args) {

        // 创建一个持久化的聊天记忆存储实例
        PersistentChatMemoryStore store = new PersistentChatMemoryStore();

        // 定义一个聊天记忆提供者,它根据记忆ID构建消息窗口聊天记忆
        // 这里设定了每个对话最多保留10条消息
        ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(10)
                .chatMemoryStore(store)
                .build();

        // 创建一个聊天语言模型实例,需要指定API密钥和模型名称
        ChatLanguageModel model = OpenAiChatModel.builder()
                .apiKey(ApiKeys.OPENAI_API_KEY)
                .modelName(GPT_4_O_MINI)
                .build();

        // 使用上述模型和聊天记忆提供者构建一个AI助手实例
        Assistant assistant = AiServices.builder(Assistant.class)
                .chatLanguageModel(model)
                .chatMemoryProvider(chatMemoryProvider)
                .build();

        // 与AI助手进行聊天,并打印助手的回复
        // 这里是第一次聊天,分别与ID为1和2的对话对象打招呼
        System.out.println(assistant.chat(1, "你好,我的名字叫小工匠"));
        System.out.println(assistant.chat(2, "你好,我的名字叫Artisan"));

        // 现在,注释掉上面的两行代码,取消下面两行代码的注释,然后再次运行。
        // 这样做是为了演示AI助手如何根据之前的对话记住用户的名字
        // System.out.println(assistant.chat(1, "我是谁?"));
        // System.out.println(assistant.chat(2, "我是谁?"));
    }

    /**
     * 持久化聊天内存存储类
     * 实现了ChatMemoryStore接口,用于持久化存储聊天消息
     * 该类使用MapDB库创建一个持久化的数据库,用于存储聊天消息
     */
    static class PersistentChatMemoryStore implements ChatMemoryStore {

        // 创建一个持久化的数据库实例
        private final DB db = DBMaker.fileDB("multi-user-chat-memory.db").transactionEnable().make();
        // 创建一个整数到字符串的哈希映射,用于存储聊天消息
        private final Map<Integer, String> map = db.hashMap("messages", INTEGER, STRING).createOrOpen();

        /**
         * 根据memoryId获取聊天消息
         *
         * @param memoryId 聊天消息的标识符
         * @return 聊天消息的列表,如果memoryId不存在,则返回null
         */
        @Override
        public List<ChatMessage> getMessages(Object memoryId) {
            String json = map.get((int) memoryId);
            return messagesFromJson(json);
        }

        /**
         * 更新特定memoryId的聊天消息
         *
         * @param memoryId 聊天消息的标识符
         * @param messages 要更新的聊天消息列表
         */
        @Override
        public void updateMessages(Object memoryId, List<ChatMessage> messages) {
            String json = messagesToJson(messages);
            map.put((int) memoryId, json);
            db.commit();
        }

        /**
         * 删除特定memoryId的聊天消息
         *
         * @param memoryId 聊天消息的标识符
         */
        @Override
        public void deleteMessages(Object memoryId) {
            map.remove((int) memoryId);
            db.commit();
        }
    }
}
AI助手
使用模型和聊天记忆提供者构建AI助手
聊天语言模型
指定API密钥和模型名称
聊天记忆提供者
根据memoryId构建消息窗口聊天记忆
持久化聊天记忆存储
创建持久化数据库实例
创建整数到字符串的哈希映射
实现getMessages方法
实现updateMessages方法
实现deleteMessages方法
开始
创建持久化聊天记忆存储实例
定义聊天记忆提供者
创建聊天语言模型实例
构建AI助手实例
与AI助手进行聊天并打印回复
结束

在这里插入图片描述

调整代码如下

在这里插入图片描述
输出:

在这里插入图片描述


总结

LangChain4j 的 ChatMemory 提供了灵活的对话管理功能,帮助开发者管理聊天消息并处理多轮对话中的记忆问题。通过驱逐策略、持久化存储以及特殊消息处理,开发者可以实现高效的聊天管理系统。此外,ChatMemoryAiServices 等高层组件的集成,也让开发者可以快速构建复杂的对话应用。

通过合理使用 ChatMemory,开发者不仅能确保 LLM 在对话中的状态记忆,还能有效控制成本、延迟以及存储需求,是构建 AI 驱动的应用的基础工具之一。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值