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