聊天记忆

月伴飞鱼 2025-04-07 20:05:06
实战相关 > AI实战
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!

实现大模型聊天记忆就是把用户所有的提问、大模型回答/产生的内容,放在一个List<ChatMessage>中。

随着用户提问将List一并发送给大模型,让大模型具备了聊天记忆功能。

需要注意的问题

随着提问不断增多,上下文会变的很长,很快超出大模型的上下文Token限制。

如果多人同时使用大模型,如何隔离不同用户的上下文信息。

手动维护和管理 ChatMessage 很麻烦。

因此,LangChain4j 提供了一个 ChatMemory 抽象以及多个开箱即用的实现。

ChatMemory 可以用作独立的低级组件,也可以用作高级组件(如 AI Services)的一部分。

LangChain4j 目前只提供内存,没有提供持久化的方式,如有特殊需求需要自行实现。

ChatMemory能力

容器管理机制,充当ChatMessage容器,对ChatMessage进行管理。

淘汰机制(Eviction policy),为保证ChatMessage不会过多。

持久化机制(Persistence),防止聊天上下文丢失的问题。

消息特殊处理机制:

  • SystemMessage特殊处理。
  • 函数调用返回消息特殊处理。

淘汰机制(Eviction Policy)

出于以下几个原因,数据淘汰机制是必要的:

适应 LLM的上下文窗口:一次LLM可以处理的Token数量是有上限的。

  • 一般情况将最旧的消息淘汰,如果有特殊需求,可以实现更复杂的算法。

控制成本:每个Token都有成本,这使得每次调用LLM越来越昂贵,逐出不必要的Token可降低成本。

  • Token=金钱,目前大模型的收费基本上都是根据Token收费。

控制延迟:发送到 LLM的Token越多,处理它们所需的时间就越多。

ChatMemory源码分析

ChatMemory接口:

public interface ChatMemory {
    // ChatMemory的ID
    Object id();
   
    // 将message添加到ChatMemory中
    void add(ChatMessage message);

    // 从ChatMemory中获取消息,怎么取取决于实现
    List<ChatMessage> messages();
    
    // 清空ChatMemory中的消息
    void clear();
}

ChatMemory实现类:

MessageWindowChatMemory 「简单」:

  • 滑动窗口,保留 N 最新的消息并淘汰不再适合的旧消息。

TokenWindowChatMemory 「复杂」:

  • 滑动窗口运行, N 但专注于保留最新的令牌,根据需要淘汰较旧的消息。
  • 需要Tokenizer配合使用计算ChatMessage的Token的数量。

持久化机制(Persistence):

默认情况下,ChatMemory 实现是将 ChatMessage 存储在内存中的。

如果需要将会话持久化,可以自定义 ChatMemoryStore ,将ChatMessage 存储到您选择的任何持久化存储中。

ChatMemoryStore接口:

public interface ChatMemoryStore {

    // 根据memoryId从指定的ChatMemoryStore中获取消息
    List<ChatMessage> getMessages(Object memoryId);

    // 根据memoryId,更新存储的消息
    void updateMessages(Object memoryId, List<ChatMessage> messages);
    
    // 根据memoryId删除存储的消息
    void deleteMessages(Object memoryId);
}

InMemoryChatMemoryStore实现类:

public class InMemoryChatMemoryStore implements ChatMemoryStore {
    private final Map<Object, List<ChatMessage>> messagesByMemoryId = new ConcurrentHashMap<>();

    /**
     * Constructs a new {@link InMemoryChatMemoryStore}.
     */
    public InMemoryChatMemoryStore() {}

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

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

    @Override
    public void deleteMessages(Object memoryId) {
        messagesByMemoryId.remove(memoryId);
    }
}

LangChain4j仅提供一个基于内存实现的存储。

如果有特殊需求,可以实现ChatMemoryStore接口,自定义逻辑。

SystemMessage 特殊处理:

SystemMessage 是一种特殊类型的消息,因此它与其他消息类型的处理方式不同:

  • 一旦添加后,将始终保留SystemMessage
  • 一次只能持有一个 SystemMessage
  • 忽略添加了具有相同内容的新 SystemMessage
  • 如果添加了具有不同内容的新 SystemMessage 内容,则将替换前一个内容。

工具消息的特殊处理:

所谓的工具消息就是函数调用请求消息和函数调用执行结果的消息。

如果包含ToolExecutionRequestAiMessage被淘汰。

则与其关联的返回消息ToolExecutionResultMessage需要一同淘汰,否则会影响大模型的生成效果。

ChatMemory代码实践

引入依赖包:

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-spring-boot-starter</artifactId>
    <version>${langchain4j.version}</version>
</dependency>

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
    <version>${langchain4j.version}</version>
</dependency>

共享ChatMemory实现:

import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import org.ivy.chatmemory.service.Assistant;

public class ChatMemoryJavaExample {

    public static void main(String[] args) {
        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
        Assistant assistant = AiServices.builder(Assistant.class)
                .chatLanguageModel(OpenAiChatModel.builder()
                        .baseUrl("xxxx")
                        .apiKey("xxxx")
                        .build()
                )
                .chatMemory(chatMemory)
                .build();

        String answer = assistant.chat("Hello! My name is Klaus.");
        System.out.println(answer); // Hello Klaus! How can I assist you today?

        String answerWithName = assistant.chat("What is my name?");
        System.out.println(answerWithName); // Your name is Klaus.
    }
}

独享ChatMemory实现:

定义memoryId,根据memoryId来获取是否是同一组上下文信息。

import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import org.ivy.chatmemory.service.EachUserAssistant;

public class ChatMemoryEachUserExample {
    public static void main(String[] args) {
        EachUserAssistant assistant = AiServices.builder(EachUserAssistant.class)
                .chatLanguageModel(
                        OpenAiChatModel.builder()
                                .baseUrl("xxx")
                                .apiKey("xxx")
                                .build())
                .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
                .build();

        System.out.println(assistant.chat(1, "Hello, my name is Klaus"));
        // Hi Klaus! How can I assist you today?

        System.out.println(assistant.chat(2, "Hello, my name is Francine"));
        // Hello Francine! How can I assist you today?

        System.out.println(assistant.chat(1, "What is my name?"));
        // Your name is Klaus.

        System.out.println(assistant.chat(2, "What is my name?"));
    }
}

自定义MemoryStore:

自定义的关键几点:

  • 消息的序列化/反序列化,可以使用LangChain4j提供的工具。
  • 选择存储方式,比如示列中使用的mapdb 文件形式。
import dev.langchain4j.data.message.ChatMessage;
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 org.mapdb.Serializer.INTEGER;
import static org.mapdb.Serializer.STRING;

/**
 * 自定义的持久化聊天存储器
 */
public class PersistentChatMemoryStore implements ChatMemoryStore {
    private final DB db = DBMaker.fileDB("./chat-memory.db").transactionEnable().make();
    private final Map<Integer, String> map = db.hashMap("messages", INTEGER, STRING).createOrOpen();

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        String json = map.get((int) memoryId);
        return messagesFromJson(json);
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        String json = messagesToJson(messages); // 序列化消息
        map.put((int) memoryId, json);
        db.commit(); // 提交事务
    }

    @Override
    public void deleteMessages(Object memoryId) {
        map.remove((int) memoryId);
        db.commit();
    }
}

执行流程

用户发起提示词 prompt 提问。

根据chatMemoryId 查询历史回话,并返回。

通过提示词模板将 prompt 和 history messages 组合成一个提示词发送给大模型。

大模型根据提示词进行回答,将prompt + AiMessages同时放入到ChatMemory中。

最后将大模型生成的结果返回给用户。

支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!