文章目录
概述
在构建多轮对话应用时,管理和维护聊天消息变得尤为重要。然而,手动管理每一条 ChatMessage
消息既繁琐又容易出错。为了解决这个问题,LangChain4j 提供了一个 ChatMemory 抽象,并且提供了多种现成的实现方案来简化这一过程。
接下来我们将深入探讨 LangChain4j 中的 ChatMemory
组件,以及如何通过ChatMemory 更好地管理对话状态。
什么是 ChatMemory?
ChatMemory
作为一个内存管理容器,存储和管理多轮对话中的 ChatMessage
。它不仅允许开发者保存消息,还提供了以下几种功能:
- 消息驱逐策略(Eviction Policy)
- 持久化存储(Persistence)
- 特殊消息处理(SystemMessage 和 ToolExecutionMessage)
此外,ChatMemory
还与high-level组件(如 AI 服务)集成,便于开发者更方便地管理对话历史。
Memory vs History
在 ChatMemory
中,有两个概念是值得注意的:Memory 和 History。这两个概念看似相似,但实际上有重要的区别:
- History(历史):保留了用户和 AI 之间的所有对话,通常展示给用户看。这代表了实际的对话内容。
- Memory(记忆):仅保留一些关键的信息,用于让 LLM 模型"记住"对话。不同于历史,记忆可以对历史消息进行处理:删除不重要的消息、合并多条消息、总结信息、注入额外的信息(如用于 RAG 的信息)等。
LangChain4j 提供的是 Memory,而不是 History。如果需要完整的对话历史,开发者需要手动管理。
驱逐策略(Eviction Policy)
为了控制内存和成本,ChatMemory
提供了驱逐策略。以下是驱逐策略的几个主要用途:
- 适应 LLM 的上下文窗口限制:LLM 在一次生成过程中能够处理的 Token 数量是有限的。如果对话内容超出了这个限制,就需要驱逐掉一些消息。通常会驱逐最老的消息,但也可以实现更复杂的驱逐算法。
- 控制成本:每个 Token 都有一定的费用,过多的消息会增加调用 LLM 的成本。通过驱逐无关消息,可以有效减少费用。
- 控制延迟:更多的 Token 需要更多的处理时间,影响响应速度。适当驱逐消息可以减少延迟。
LangChain4j 提供了两种常见的驱逐实现:
-
MessageWindowChatMemory:这种方式类似滑动窗口,只保留 N 条最新的消息,驱逐掉不再适合的旧消息。适合快速原型开发,但不考虑每条消息的 Token 数量。
-
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
。
需要注意的是,驱逐的消息也会从持久化存储中移除。
特殊消息处理
SystemMessage
和 ToolExecutionMessage
是两种特殊的消息类型,它们在 ChatMemory
中的处理方式有所不同。
-
SystemMessage:
SystemMessage
会始终保留在记忆中,不能被驱逐。- 一次只能有一个
SystemMessage
。如果新的SystemMessage
内容与旧的相同,则会被忽略;如果不同,则会替换旧的消息。
-
ToolExecutionMessage:
- 当包含
ToolExecutionRequests
的AiMessage
被驱逐时,相关的ToolExecutionResultMessage
也会被自动驱逐。这是为了防止某些 LLM 提供商(如 OpenAI)禁止发送“孤立”的工具执行结果消息。
- 当包含
Code
以下是几种常见的 ChatMemory 使用场景:
-
单用户聊天记忆:
使用MessageWindowChatMemory
来为每个用户保持单独的记忆。 -
持久化聊天记忆:
将用户的聊天记忆存储到数据库或其他持久化存储中,以便在不同的会话间共享记忆。 -
与 AI 服务结合使用:
将ChatMemory
与 LangChain4j 的高层组件(如AiServices
)结合使用,实现更复杂的记忆管理。 -
与传统的链式模型结合使用:
使用ConversationalChain
或ConversationalRetrievalChain
结合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); // 你叫小工匠。请问还有其他想聊的吗?
}
}
-
创建聊天记忆对象:
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
创建一个最多保存10条消息的聊天记忆对象。
-
构建聊天语言模型:
ChatLanguageModel model = OpenAiChatModel.builder() .apiKey("demo") .modelName(GPT_4_O_MINI) .build();
使用OpenAI的GPT-4模型构建一个聊天语言模型,并设置API密钥和模型名称。
-
创建AI助手对象:
Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .build();
使用构建器模式创建一个AI助手对象,传入聊天语言模型和聊天记忆。
-
发送消息并获取回复:
String answer = assistant.chat("你好.我的名字是小工匠"); System.out.println(answer); String answerWithName = assistant.chat("请问我叫什么?"); System.out.println(answerWithName);
向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();
}
}
}
- 创建聊天记忆对象:使用
MessageWindowChatMemory
创建一个聊天记忆对象,设置最大消息数为10,并使用自定义的持久化聊天记忆存储。 - 创建聊天语言模型对象:使用
OpenAiChatModel
创建一个聊天语言模型对象,设置API密钥和模型名称为GPT-4。 - 创建助手对象:使用AiServices创建一个助手对象,设置聊天语言模型和聊天记忆。
- 发送消息并打印回复:通过助手对象发送消息并打印回复,演示了如何根据之前的聊天记录记住用户的名字。
- 持久化聊天内存存储类:实现
ChatMemoryStore
接口,使用MapDB库创建一个持久化的数据库,将聊天消息以键值对的形式存储在文件中。
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();
}
}
}
调整代码如下
输出:
总结
LangChain4j 的 ChatMemory
提供了灵活的对话管理功能,帮助开发者管理聊天消息并处理多轮对话中的记忆问题。通过驱逐策略、持久化存储以及特殊消息处理,开发者可以实现高效的聊天管理系统。此外,ChatMemory
与 AiServices
等高层组件的集成,也让开发者可以快速构建复杂的对话应用。
通过合理使用 ChatMemory
,开发者不仅能确保 LLM 在对话中的状态记忆,还能有效控制成本、延迟以及存储需求,是构建 AI 驱动的应用的基础工具之一。