0%

langchain4j-聊天记忆(Chat Memory)

为了维护和管理 ChatMessage ,LangChain4j 提供了 ChatMemory 抽象以及多种开箱即用的实现,。ChatMemory 既可以作为独立底层组件使用,也可以作为高级组件(如 AI Services)的一部分。

ChatMemory 充当 ChatMessage 的容器(底层由 List 支持),并附带以下功能:

  • 淘汰策略(Eviction Policy)
  • 持久化(Persistence)
  • 对 SystemMessage 的特殊处理
  • 对工具消息的特殊处理

记忆 vs 历史

“记忆(memory)”和”历史(history)”是相似但不同的概念。

⚠️ LangChain4j 目前仅提供”记忆”功能,不提供”历史”功能。如果您需要保留完整历史,请手动实现。

  • 历史(History):完整保留用户与 AI 之间的所有消息。历史是用户在 UI 中看到的内容,代表实际发生的对话。
  • 记忆(Memory):保留部分信息,将其呈现给 LLM,使 LLM 表现得好像”记住了”对话。记忆与历史截然不同。

根据所使用的记忆算法,它可以以多种方式修改历史:淘汰部分消息、汇总多条消息、汇总单独的消息、从消息中移除不重要的细节、向消息中注入额外信息(如用于 RAG)或指令(如用于结构化输出)等。


淘汰策略(Eviction Policy)

淘汰策略的存在有以下几点原因:

  1. 适应 LLM 的上下文窗口:LLM 一次能处理的 token 数量有上限。当对话超过这个限制时,需要淘汰部分消息。通常淘汰最早的消息,但也可以根据需要实现更复杂的算法。
  2. 控制成本:每个 token 都有成本,发送给 LLM 的 token 越多,每次调用的费用就越高。淘汰不必要的消息可以降低成本。
  3. 控制延迟:发送给 LLM 的 token 越多,处理所需的时间就越长。

两种开箱即用的实现

实现类 淘汰策略 适用场景
MessageWindowChatMemory 按消息条数淘汰 快速原型开发
TokenWindowChatMemory 按 Token 数量淘汰 需要精确控制 token 成本的场景

MessageWindowChatMemory

较为简单的一种,以滑动窗口方式运行,保留最近的 N 条消息,淘汰不再适合的旧消息。然而,由于每条消息可能包含不同数量的 token,MessageWindowChatMemory 主要用于快速原型设计。

TokenWindowChatMemory

更复杂的选项,同样以滑动窗口方式运行,但专注于保留最近的 N 个 token,根据需要淘汰旧消息。消息是不可分割的——如果一条消息放不下,它会被整体淘汰。TokenWindowChatMemory 需要一个 Tokenizer 来计算每条 ChatMessage 中的 token 数。


持久化(Persistence)

默认情况下,ChatMemory 实现将 ChatMessage 存储在内存中。如果需要持久化,可以实现自定义的 ChatMemoryStore,将 ChatMessage 存储在您选择的任何持久化存储中:

1
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
28
29
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();

ChatMemoryStore 方法详解

updateMessages()

每次向 ChatMemory 添加新的 ChatMessage 时,都会调用 updateMessages() 方法。这通常在与 LLM 的每次交互中发生两次:一次是添加新的 UserMessage 时,另一次是添加新的 AiMessage 时。updateMessages() 方法预期会更新与给定 memory ID 关联的所有消息。ChatMessage 可以单独存储(例如每条消息一条记录/行/对象),也可以一起存储(例如整个 ChatMemory 一条记录/行/对象)。

⚠️ 请注意,从 ChatMemory 中淘汰的消息也将从 ChatMemoryStore 中淘汰。当消息被淘汰时,会调用 updateMessages() 方法,传入不包含被淘汰消息的消息列表。

getMessages()

ChatMemory 的用户请求所有消息时,会调用 getMessages() 方法。这通常在与 LLM 的每次交互中发生一次Object memoryId 参数的值对应于创建 ChatMemory 时指定的 id,可用于区分多个用户和/或对话。getMessages() 方法预期会返回与给定 memory ID 关联的所有消息。

deleteMessages()

当调用 ChatMemory.clear() 时,会调用 deleteMessages() 方法。如果您不使用此功能,可以将此方法留空。


SystemMessage 的特殊处理

SystemMessage 是一种特殊类型的消息,因此与其他消息类型处理方式不同:

  • SystemMessage 一旦添加就会一直保留,不会被淘汰策略自动移除。
  • 每个 ChatMemory 中只能保留一个 SystemMessage
  • 添加相同的 SystemMessage 会被忽略,添加不同的 SystemMessage 会保留最新的那个。

工具消息的特殊处理

如果包含 ToolExecutionRequestAiMessage 被淘汰,后续孤立的 ToolExecutionResultMessage 也会自动被淘汰,以避免某些 LLM 提供商(如 OpenAI)出现问题——它们禁止在请求中发送孤立的 ToolExecutionResultMessage