 Chat Memory
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
