LangChain4j 聊天记忆简述

聊天记忆(Chat Memory)是 AI 对话系统的核心组件,用于在多轮交互中存储、管理和利用对话上下文信息,让 AI 能够 “记住” 之前的对话内容,实现连贯、有逻辑的多轮对话,而非每次交互都视为独立的“单轮问答”。

简单来说,没有聊天记忆的 AI 就像 “金鱼记忆”—— 你每问一个问题,它都无法关联上一句的内容;而具备聊天记忆的 AI 可以完成复杂的多轮任务,比如 “先帮我写一个活动方案大纲,再把大纲里的宣传部分扩展成 500 字文案”。

那么,AI 对话中如何实现聊天记忆呢?首先你要明确,AI 聊天大模型是没有记忆的,那怎样让它有记忆呢?答案是用户通过 messages 传递过去,如下是一个拥有记忆的 AI 对话请求参数:

[main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP request:
- method: POST
- url: https://api.xty.app/v1/chat/completions
- headers: [Authorization: Beare...00], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "gpt-3.5-turbo",
  "messages" : [ {
    "role" : "system",
    "content" : "你是一个Java编程助手"
  }, {
    "role" : "user",
    "content" : "什么是ChatModel?"
  }, {
    "role" : "assistant",
    "content" : "ChatModel是聊天模型的抽象接口"
  }, {
    "role" : "user",
    "content" : "它的核心方法有哪些?"
  } ],
  "temperature" : 0.7,
  "stream" : false
}
....

上述请求中,messages 是一个数组,包含了多个消息,这些消息中对底部的“它的核心方法有哪些?”是用户最新提出的问题。其他的消息是上下文,也是实现记忆功能的关键。其中:

  • “你是一个Java编程助手”是系统消息,定义 AI 聊天大模型的角色、输出约定等等

  • “什么是ChatModel?”是用户的第一次提问

  • “ChatModel是聊天模型的抽象接口”是 AI 聊天大模型对用户第一提问的回复

  • “它的核心方法有哪些?”是用户最新的问题

如果要实现有记忆聊天,则需要用户自己手动维护这些消息,手动维护和管理 ChatMessage 十分繁琐。因此,LangChain4j 提供了 ChatMemory 抽象概念以及多种现成的实现方式。

ChatMemory 既可以作为独立的低级组件使用,也可以作为 AI Services 这样的高级组件的一部分。

ChatMemory 充当着 ChatMessage 的容器(由一个 List 提供支持),并具备以下附加功能:

  • 驱逐策略

  • 持久性

  • 对 SystemMessage 的特殊处理

  • 对工具(tool)消息的特殊处理

  

记忆与历史

请注意,“记忆” 和 “历史” 是相似但又不同的概念。

  • 历史记录会将用户和人工智能之间的所有消息完整地保存下来。历史记录是用户在用户界面中看到的内容,它代表了实际所说的话。

  • 记忆会保存一些信息,这些信息会呈现给大语言模型(LLM),使其表现得仿佛 “记住” 了对话内容。记忆与历史记录有很大不同。根据所使用的记忆算法,它可以通过多种方式修改历史记录:移除某些消息、总结多条消息、分别总结不同的消息、从消息中删除不重要的细节、向消息中注入额外信息(例如,用于检索增强生成(RAG))或指令(例如,用于结构化输出)等等。

LangChain4j 目前仅提供 “记忆” 功能,而非 “历史记录” 功能。如果您需要保留完整的历史记录,请手动进行操作,如持久化到数据库等。

  

驱逐策略

驱逐策略是必要的,原因如下:

  • 为了适配大语言模型(LLM)的上下文窗口。大语言模型一次能够处理的 tokens 数量是有上限的。在某些时候,对话可能会超出这个限制。在这种情况下,需要移除一些消息。通常,最早的消息会被移除,但如果有需要,也可以实施更复杂的算法。

  • 为了控制成本。每个标记(Token)都有成本(大模型厂商根据 Token 消耗量收费),这使得每次调用大语言模型(LLM)的费用会逐渐增加。清除不必要的消息可以降低成本。

  • 为了控制延迟。发送给大模型的标记越多,处理它们所需的时间就越长。

目前,LangChain4j 提供了 2 种开箱即用的实现:

  • 较简单的一种是 MessageWindowChatMemory,它的功能类似于滑动窗口,会保留 N 条最新的消息,并移除那些超出范围的旧消息。不过,由于每条消息可能包含数量不等的标记,MessageWindowChatMemory 大多用于快速原型开发。

  • 一个更复杂的选项是 TokenWindowChatMemory,它也作为滑动窗口运行,但专注于保留最近的 N 个标记,并在需要时移除较旧的消息。消息是不可分割的。如果一条消息无法容纳,它会被完全移除。TokenWindowChatMemory 需要一个 TokenCountEstimator 来计算每条 ChatMessage 中的标记数量。

  

持久性

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

如果需要持久性,可以实现一个自定义的 ChatMemoryStore,将 ChatMessage 存储在任何你选择的持久化存储中:

class PersistentChatMemoryStore implements ChatMemoryStore {
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
      // TODO: 实现根据记忆ID从持久化存储中获取所有消息的逻辑。
      // 可使用 ChatMessageDeserializer.messageFromJson(String) 和
      // ChatMessageDeserializer.messagesFromJson(String) 工具方法,
      // 便捷地将JSON格式的聊天消息反序列化为对象。
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        // TODO: 实现根据记忆ID更新持久化存储中所有消息的逻辑。
        // 可使用 ChatMessageSerializer.messageToJson(ChatMessage) 和
        // ChatMessageSerializer.messagesToJson(List<ChatMessage>) 工具方法,
        // 便捷地将聊天消息序列化为JSON格式。
    }

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

// 构建基于消息窗口的聊天记忆实例,关联持久化存储实现
ChatMemory chatMemory = MessageWindowChatMemory.builder()
        .id("12345") // AI会话ID,一个会话中包含了与AI大模型进行的多轮问答
        .maxMessages(10) // 最多保留10条消息
        .chatMemoryStore(new PersistentChatMemoryStore()) // 使用自定义持久化存储
        .build();

每当有新的 ChatMessage 添加到 ChatMemory 时,都会调用 updateMessages() 方法。在与 LLM 的每次交互过程中,这种情况通常会发生两次:一次是添加新的 UserMessage 时,另一次是添加新的 AiMessage 时。updateMessages() 方法需要更新与给定内存 ID 相关联的所有消息。ChatMessage 可以单独存储(例如,每条消息对应一条记录 / 一行数据 / 一个对象),也可以一起存储(例如,整个 ChatMemory 对应一条记录 / 一行数据 / 一个对象)

注意,从 ChatMemory 中移除的消息也会从 ChatMemoryStore 中移除。当一条消息被移除时,会调用 updateMessages() 方法,传入的消息列表中不包含被移除的消息。

每当 ChatMemory 的用户请求所有消息时,就会调用 getMessages() 方法。这通常在与 LLM 的每次交互中发生一次。Object memoryId 参数的值与创建 ChatMemory 时指定的 id 相对应。它可用于区分多个用户和/或对话。getMessages() 方法应返回与给定记忆 ID 相关联的所有消息。

每当调用 ChatMemory.clear() 时,就会调用 deleteMessages() 方法。如果不使用此功能,你可以让这个方法保持为空。

  

对 SystemMessage 的特殊处理

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

  • 一旦添加,SystemMessage 就会一直保留。

  • 一次只能持有一条 SystemMessage。

  • 如果添加了一条内容相同的新 SystemMessage,它会被忽略。

  • 如果添加了一条内容不同的新 SystemMessage,它会替换之前的那条。默认情况下,新的 SystemMessage 会被添加到消息列表的末尾。你可以在创建 ChatMemory 时,通过设置 alwaysKeepSystemMessageFirst 属性来改变这一行为。

  

工具消息的特殊处理

如果包含 ToolExecutionRequest 的 AiMessage 被移除,那么后续的孤立 ToolExecutionResultMessage 也会被自动移除,以避免与某些大型语言模型提供商(如 OpenAI)产生问题,这些提供商禁止在请求中发送孤立的 ToolExecutionResultMessage。

  

简单示例

下面通过 MessageWindowChatMemory 简单演示聊天记忆的功能,代码如下:

package com.hxstrive.langchain4j.chatMemory;

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

public class ServiceWithMemoryExample {
    // 推荐:将OPEN_API_KEY设置成环境变量, 避免硬编码或随着代码泄露
    // 注意,设置完环境变量记得重启IDEA,不然可能环境变量不会生效
    private static final String API_KEY = System.getenv("OPEN_API_KEY");

    interface Assistant {
        String chat(String message);
    }

    public static void main(String[] args) {
        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);

        ChatModel model = OpenAiChatModel.builder()
                .baseUrl("https://api.xty.app/v1")
                .apiKey(API_KEY)
                .modelName("gpt-3.5-turbo")
                .temperature(0.7)
                .logRequests(true)
                .logResponses(true)
                .build();

        // 使用 AiServices 创建服务,后续介绍 AiServices
        // 这里简单理解为创建一个代理类,代理类会调用指定的模型进行对话
        Assistant assistant = AiServices.builder(Assistant.class)
                .chatModel(model)
                .chatMemory(chatMemory)
                .build();

        // 发起对话
        String answer = assistant.chat("你好! 我的名字是张三。");
        System.out.println(answer); // Hello Klaus! How can I assist you today?

        String answerWithName = assistant.chat("我叫什么名字?");
        System.out.println(answerWithName); // Your name is Klaus.
    }
}

运行示例,输出如下:

14:31:40.796 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP request:
- method: POST
- url: https://api.xty.app/v1/chat/completions
- headers: [Authorization: Beare...00], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "gpt-3.5-turbo",
  "messages" : [ {
    "role" : "user",
    "content" : "你好! 我的名字是张三。"
  } ],
  "temperature" : 0.7,
  "stream" : false
}

14:31:45.705 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP response:
- status code: 200
- body: {"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"content":"你好,张三!很高兴认识你 😊  \n有什么我可以帮你的吗?","role":"assistant"}}],"created":1768977104,"id":"chatcmpl-G7DZCbAZxd2FFcmhVtHwDrt1P8EyC","model":"gpt-3.5-turbo","object":"chat.completion","system_fingerprint":"fp_b28b39ffa8","usage":{"completion_tokens":31,"prompt_tokens":19,"total_tokens":50}}

你好,张三!很高兴认识你 😊  
有什么我可以帮你的吗?

14:31:45.767 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP request:
- method: POST
- url: https://api.xty.app/v1/chat/completions
- headers: [Authorization: Beare...00], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "gpt-3.5-turbo",
  "messages" : [ {
    "role" : "user",
    "content" : "你好! 我的名字是张三。"
  }, {
    "role" : "assistant",
    "content" : "你好,张三!很高兴认识你 😊  \n有什么我可以帮你的吗?"
  }, {
    "role" : "user",
    "content" : "我叫什么名字?"
  } ],
  "temperature" : 0.7,
  "stream" : false
}

14:31:48.942 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP response:
- status code: 200
- body: {
    "id": "chatcmpl-fFwaB60bER9xXcOXsvinQBmhI1mza",
    "object": "chat.completion",
    "created": 1768977108,
    "model": "gpt-3.5-turbo-0613",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "你叫 **张三**。"
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 67,
        "completion_tokens": 8,
        "total_tokens": 75
    },
    "system_fingerprint": "fp_b28b39ffa8"
}

你叫 **张三**。

Process finished with exit code 0

仔细观察输出的日志,ChatMemory 实际上就是帮我们维护 messages 数组。

更多关于 ChatMemory 相关的知识后续将通过专题进行介绍……

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号