主题
聊天记忆
手工维护和管理 ChatMessage 十分繁琐。 因此,LangChain4j 提供了 ChatMemory 抽象,以及多种开箱即用的实现。
ChatMemory 既可以作为独立的底层组件使用, 也可以作为 AI 服务 这样的高层组件的一部分。
ChatMemory 是 ChatMessage 的容器(底层由 List 支撑),并附带以下特性:
- 淘汰策略
- 持久化
- 对
SystemMessage的特殊处理 - 对工具消息的特殊处理
记忆(Memory)与历史(History)
请注意,“记忆”与“历史”概念相近,却并不相同。
- 历史保留用户与 AI 之间所有的消息,完好无损。历史是用户在界面上看到的内容,它代表真实说过的话。
- 记忆只保留某些信息,用于呈现给 LLM,让它表现得像是“记得”这段对话。 记忆与历史截然不同。根据所采用的记忆算法,它可能以各种方式改写历史: 淘汰某些消息、把多条消息合并摘要、对单条消息分别摘要、删去消息中不重要的细节、 向消息中注入额外信息(例如用于 RAG)或指令(例如用于结构化输出),如此种种。
LangChain4j 目前只提供“记忆”,不提供“历史”。如果你需要保留完整历史,请自行处理。
淘汰策略
之所以需要淘汰策略,原因有以下几点:
- 以适配 LLM 的上下文窗口。LLM 一次能处理的词元数是有上限的。 对话在某个时刻可能超出这一限制,此时就需要淘汰部分消息。 通常淘汰最旧的消息,但如有需要,也可以实现更精细的算法。
- 以控制成本。每个词元都有成本,这会让每次调用 LLM 的开销不断攀升。 淘汰不必要的消息可以降低成本。
- 以控制延迟。发送给 LLM 的词元越多,处理所需的时间就越长。
目前,LangChain4j 提供 2 种开箱即用的实现:
- 更简单的一种是
MessageWindowChatMemory,它充当滑动窗口, 保留最近的N条消息,并淘汰装不下的较旧消息。不过,由于每条消息包含的词元数各不相同,MessageWindowChatMemory主要用于快速原型开发。 - 更精细的一种是
TokenWindowChatMemory, 它同样是滑动窗口,但重点是保留最近的N个词元, 并按需淘汰较旧的消息。消息不可拆分;若某条消息装不下,就会被整条淘汰。TokenWindowChatMemory需要一个TokenCountEstimator来统计每条ChatMessage中的词元数。
持久化
默认情况下,ChatMemory 的实现将 ChatMessage 存储在内存中。
如果需要持久化,可以实现自定义的 ChatMemoryStore, 把 ChatMessage 存入你选择的任意持久化存储:
java
class PersistentChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object memoryId) {
// TODO: Implement getting all messages from the persistent store by memory ID.
// ChatMessageDeserializer.messageFromJson(String) and
// ChatMessageDeserializer.messagesFromJson(String) helper methods can be used to
// easily deserialize chat messages from JSON.
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
// TODO: Implement updating all messages in the persistent store by memory ID.
// ChatMessageSerializer.messageToJson(ChatMessage) and
// ChatMessageSerializer.messagesToJson(List<ChatMessage>) helper methods can be used to
// easily serialize chat messages into JSON.
}
@Override
public void deleteMessages(Object memoryId) {
// TODO: Implement deleting all messages in the persistent store by memory ID.
}
}
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.id("12345")
.maxMessages(10)
.chatMemoryStore(new PersistentChatMemoryStore())
.build();每当有新的 ChatMessage 加入 ChatMemory 时,updateMessages() 方法都会被调用。 这通常在每次与 LLM 的交互中发生两次: 一次是加入新的 UserMessage 时,另一次是加入新的 AiMessage 时。 updateMessages() 方法应当更新与给定 memory ID 关联的全部消息。 ChatMessage 既可以分开存储(例如每条消息一条记录/一行数据/一个对象), 也可以合并存储(例如整个 ChatMemory 存为一条记录/一行数据/一个对象)。
NOTE
请注意,从 ChatMemory 中被淘汰的消息,也会同时从 ChatMemoryStore 中淘汰。 当一条消息被淘汰时,updateMessages() 方法会收到一个不包含该消息的列表。
每当 ChatMemory 的使用者请求全部消息时,getMessages() 方法都会被调用。 这通常在每次与 LLM 的交互中发生一次。 参数 Object memoryId 的值对应于 创建 ChatMemory 时指定的 id, 可用于区分多个用户和/或多段对话。 getMessages() 方法预期返回与给定 memory ID 关联的全部消息。
每当调用 ChatMemory.clear() 时,deleteMessages() 方法就会被调用。 如果你不使用该功能,可以留空这个方法。
对 SystemMessage 的特殊处理
SystemMessage 是一种特殊的消息类型,因此它的处理方式与其他消息类型不同:
- 一旦加入,
SystemMessage将始终被保留。 - 同一时间只能保留一条
SystemMessage。 - 如果新增的
SystemMessage内容相同,则被忽略。 - 如果新增的
SystemMessage内容不同,则替换之前那条。
对工具消息的特殊处理
如果一条包含 ToolExecutionRequest 的 AiMessage 被淘汰, 其后紧随的孤立 ToolExecutionResultMessage 也会被自动淘汰, 以避免某些 LLM 提供者(如 OpenAI) 禁止在请求中发送孤立 ToolExecutionResultMessage 而导致的问题。
NOTE
执行 I/O 的 ChatMemory 或 ChatMemoryStore 可以实现对应的异步方法 (addAsync/messagesAsync/setAsync,getMessagesAsync/updateMessagesAsync/deleteMessagesAsync), 以便在 AI 服务以非阻塞模式使用时不会阻塞线程。 参见非阻塞与响应式。
示例
- 配合
AiServices: - 配合旧版
Chain
所有受支持的聊天记忆存储可在这里找到。