Chat Memory
# 聊天记忆
手动维护和管理 ChatMessage
非常繁琐。因此,LangChain4j 提供了 ChatMemory
抽象以及多种开箱即用的实现。
ChatMemory
可以作为独立的低级组件使用,也可以作为诸如 AI 服务 等高级组件的一部分。
ChatMemory
作为 ChatMessage
的容器(由 List
支持),并附加了以下功能:
- 驱逐策略
- 持久化
- 对
SystemMessage
的特殊处理 - 对 工具 消息的特殊处理
# Memory 和 History 的区别
请注意,“Memory”(记忆)和 “History”(历史)是相似但不同的概念。
- History 保留用户与 AI 之间的所有消息,完整。历史是用户在界面中看到的内容,代表实际的对话内容。
- Memory 保留部分信息,用于呈现给 LLM,使其表现得像是“记住”了对话内容。
Memory 与 History 相差甚远,根据使用的记忆算法,它可以以各种方式修改历史:
驱逐某些消息、总结多条消息、独立总结消息、去除消息中的不重要细节、
向消息中注入额外信息(如用于 RAG)或指令(如用于结构化输出)等。
LangChain4j 当前仅提供“Memory”,不提供“History”。如果需要保留整个历史记录,请手动完成。
# 驱逐策略
出于多种原因,驱逐策略是必要的:
- 为了适应 LLM 的上下文窗口。LLM 在一次处理的 token 数量上有上限。当对话超过这个限制时,某些消息需要被驱逐。通常情况下,最旧的消息会被驱逐,但如果需要,也可以实现更复杂的算法。
- 控制成本。每个 token 都有成本,因此对 LLM 的每次调用会越来越昂贵。驱逐不必要的消息可以降低成本。
- 控制延迟。发送给 LLM 的 token 越多,处理时间越长。
目前,LangChain4j 提供了两种开箱即用的实现:
- 简单的实现
MessageWindowChatMemory
,它充当滑动窗口,保留最近的N
条消息并驱逐那些不再适合的旧消息。但由于每条消息可以包含不同数量的 token,MessageWindowChatMemory
主要用于快速原型设计。 - 更复杂的实现是
TokenWindowChatMemory
,它同样作为滑动窗口运行,但着重于保留最近的N
个 token,
根据需要驱逐旧消息。消息是不可分割的。如果一条消息无法适配,它会被完全驱逐。
TokenWindowChatMemory
需要一个Tokenizer
来计算每个ChatMessage
的 token 数量。
# 持久化
默认情况下,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")
.maxMessages(10)
.chatMemoryStore(new PersistentChatMemoryStore())
.build();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
每当新的 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
,将替换之前的。
# 对工具消息的特殊处理
如果包含 ToolExecutionRequest
的 AiMessage
被驱逐,后续孤立的 ToolExecutionResultMessage
也会被自动驱逐,以避免某些 LLM 提供商(如 OpenAI)禁止在请求中发送孤立的 ToolExecutionResultMessage
的问题。
# 示例
With
AiServices
With legacy
Chains