LangChain4J 聊天记忆
手动维护和管理聊天消息非常繁琐。因此,LangChain4j 提供了一个 ChatMemory 抽象,以及多种现成的实现。
ChatMemory 可以作为独立的低级组件使用,或者作为 AI Services(AI 服务)等高级组件的一部分。
ChatMemory 作为 ChatMessages 的容器(由 List 支持),并具有以下附加功能:
- 驱逐策略
- 持久化
- 特殊处理 SystemMessage
- 特殊处理工具消息
记忆与历史
注意:“记忆”和“历史”是相似但不同的概念。
历史( History )保存了用户和 AI 之间的所有消息。历史( History )就是用户在 UI 中看到的内容,代表了实际的对话内容。
记忆( Memory)保存了一些信息,这些信息呈现给 LLM,使其表现得像是“记得”了对话。记忆( Memory)与历史( History )有很大不同。根据使用的记忆算法,它可以以各种方式修改历史:驱逐某些消息、总结多条消息、总结独立消息、从消息中删除不重要的细节、向消息中注入额外的信息(例如,RAG)或指令(例如,结构化输出)等。
LangChain4j 当前只提供“记忆”,而不提供“历史”。如果您需要保存完整的历史记录,请手动完成。
驱逐策略
驱逐策略是必要的,原因有以下几点:
- 适应 LLM 的上下文窗口:LLM 能够同时处理的 token 数量是有限的。在某些情况下,对话可能会超过此限制。在这种情况下,应该驱逐一些消息。通常,最旧的消息会被驱逐,但如果需要,可以实现更复杂的算法。
- 控制成本:每个 token 都有成本,这使得每次调用 LLM 时,成本逐渐增加。驱逐不必要的消息可以降低成本。
- 控制延迟:发送给 LLM 的 token 越多,处理这些 token 所需的时间就越长。
目前,LangChain4j 提供了两种现成的实现:
- 更简单的实现,MessageWindowChatMemory,它作为滑动窗口,保留最近的 N 条消息,驱逐那些不再适合的旧消息。然而,由于每条消息可能包含不同数量的 token,MessageWindowChatMemory 主要适用于快速原型开发。
- 更复杂的选项,TokenWindowChatMemory,也作为滑动窗口工作,但它专注于保留最近的 N 个 token,必要时驱逐旧消息。消息是不可分割的。如果一条消息无法适配,它会被完全驱逐。TokenWindowChatMemory 需要一个 Tokenizer 来计算每条 ChatMessage 中的 token 数量。
持久化
默认情况下,ChatMemory 实现将 ChatMessages 存储在内存中。
如果需要持久化,可以实现一个自定义的 ChatMemoryStore,将 ChatMessages 存储在任何您选择的持久化存储中:
class PersistentChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object memoryId) {
// TODO: 实现通过 memoryId 从持久化存储中获取所有消息。
// 可以使用 ChatMessageDeserializer.messageFromJson(String) 和
// ChatMessageDeserializer.messagesFromJson(String) 辅助方法来轻松地从 JSON 反序列化消息。
}
@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)
.chatMemoryStore(new PersistentChatMemoryStore())
.build();
updateMessages() 方法会在每次向 ChatMemory 添加新的 ChatMessage 时调用。通常在与 LLM 的每次交互中会调用两次:一次是添加新的 UserMessage,另一次是添加新的 AiMessage。updateMessages() 方法的作用是更新与给定 memoryId 关联的所有消息。ChatMessages 可以单独存储(例如每条消息一个记录/行/对象),也可以一起存储(例如将整个 ChatMemory 存储为一个记录/行/对象)。
注意:从 ChatMemory 中驱逐的消息也会从 ChatMemoryStore 中被驱逐。当一条消息被驱逐时,updateMessages() 方法会被调用,传入的消息列表中不包括已被驱逐的消息。
getMessages() 方法会在每次 ChatMemory 的用户请求获取所有消息时调用。通常在与 LLM 的每次交互中调用一次。Object memoryId 参数的值对应于创建 ChatMemory 时指定的 id。它可以用来区分多个用户和/或对话。getMessages() 方法的作用是返回与给定 memoryId 关联的所有消息。
deleteMessages() 方法会在调用 ChatMemory.clear() 时被调用。如果您不使用此功能,可以将该方法留空。
SystemMessage 的特殊处理
SystemMessage 是一种特殊类型的消息,因此与其他消息类型的处理方式不同:
- 一旦添加,SystemMessage 会始终保留。
- 同时只能保留一个 SystemMessage。
- 如果添加一个内容相同的新 SystemMessage,则会被忽略。
- 如果添加一个内容不同的新 SystemMessage,则会替换掉之前的 SystemMessage。
工具消息的特殊处理
如果包含 ToolExecutionRequests 的 AiMessage 被驱逐,以下孤立的 ToolExecutionResultMessage(s) 也会被自动驱逐,以避免与某些 LLM 提供商(如 OpenAI)相关的问题,这些提供商禁止在请求中发送孤立的 ToolExecutionResultMessage(s)。
示例
与 AiServices 一起使用:
与传统链一起使用: