聊天记忆(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 会被添加到消息列表的末尾。你可以在创建 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 相关的知识后续将通过专题进行介绍……