theme: vue-prohighlight: a11y-dark
前两篇文章是对LangChain4j比较全面的介绍,从本篇文章开始从某一个点进行分析。我们先从ChatMemory开始!
对于聊天记忆的场景、实现原理,在Spring AI 专栏中的# Spring AI 聊天上下文记忆源码分析以及实战文章有介绍,在这里就不多介绍直入主题。
为什么需要ChatMemory
实现聊天记忆实现起来非常简单,就是把用户所有的提问、大模型回答/产生的内容,放在一个List<ChatMessage>
中,随着用户提问将List一并发送给大模型,让大模型具备了聊天记忆功能。实现虽然简单,大家想一想有没有问题呢?
- 随着提问不断增多,上下文会变的很长,很快超出大模型的上下文token限制。如果多人同时使用大模型,如何隔离不同用户的上下文信息。
手动维护和管理 ChatMessage
很麻烦。因此,LangChain4j 提供了一个 ChatMemory
抽象以及多个开箱即用的实现。ChatMemory
可以用作独立的低级组件,也可以用作高级组件(如 AI Services)的一部分。
LangChain4j 目前只提供“内存”,不提供“历史记录”
ChatMemory实现什么能力
- 容器管理机制,充当
ChatMessage
容器,对ChatMessage
进行管理。淘汰机制(Eviction policy),为保证ChatMessage
不会过多。持久化机制(Persistence),防止聊天上下文丢失的问题。消息特殊处理机制SystemMessage
特殊处理。函数调用返回消息特殊处理。淘汰机制(Eviction policy)
出于以下几个原因,数据淘汰机制是必要的:
- 以适应 LLM的上下文窗口。一次LLM可以处理的代币数量是有上限的。
控制成本。每个令牌都有成本,这使得每次调用都LLM越来越昂贵。逐出不必要的邮件可降低成本。一般情况将最旧的消息淘汰,如果有特殊需求,可以实现更复杂的算法。
控制延迟。发送到 LLM的令牌越多,处理它们所需的时间就越多。Token=金钱,目前大模型的收费基本上都是根据Token收费。
ChatMemory源码分析
ChatMemory接口
public interface ChatMemory { // ChatMemory的ID Object id(); // 将message添加到ChatMemory中 void add(ChatMessage message); // 从ChatMemory中获取消息,怎么取取决于实现 List<ChatMessage> messages(); // 清空ChatMemory中的消息 void clear();}
ChatMemory实现类
MessageWindowChatMemory
「简单」:滑动窗口,保留 N
最新的消息并驱逐不再适合的旧消息。
TokenWindowChatMemory
「复杂」:滑动窗口运行, N
但专注于保留最新的令牌,根据需要驱逐较旧的消息。需要Tokenizer
配合使用计算ChatMessage
的Token的数量。
持久化机制(Persistence)
默认情况下,ChatMemory
实现是将 ChatMessage
存储在内存中的,如果需要持久性,可以实现自定义 ChatMemoryStore
,将ChatMessage
存储在您选择的任何持久性存储中。
可以自定义持久化策略!!!!
ChatMemoryStore 接口
public interface ChatMemoryStore { // 根据memoryId从指定的ChatMemoryStore中获取消息 List<ChatMessage> getMessages(Object memoryId); // 根据memoryId,更新存储的消息 void updateMessages(Object memoryId, List<ChatMessage> messages); // 根据memoryId删除存储的消息 void deleteMessages(Object memoryId);}
ChatMemoryStore 实现类
public class InMemoryChatMemoryStore implements ChatMemoryStore { private final Map<Object, List<ChatMessage>> messagesByMemoryId = new ConcurrentHashMap<>(); /* Constructs a new {@link InMemoryChatMemoryStore}. */ public InMemoryChatMemoryStore() {} @Override public List<ChatMessage> getMessages(Object memoryId) { return messagesByMemoryId.computeIfAbsent(memoryId, ignored -> new ArrayList<>()); } @Override public void updateMessages(Object memoryId, List<ChatMessage> messages) { messagesByMemoryId.put(memoryId, messages); } @Override public void deleteMessages(Object memoryId) { messagesByMemoryId.remove(memoryId); }}
仅有一个基于内存实现的存储,如果有特殊需求,我们可以实现ChatMemoryStore
接口,自定义逻辑。
SystemMessage
特殊处理
SystemMessage
是一种特殊类型的消息,因此它与其他消息类型的处理方式不同:
- 一旦添加后,将始终保留
SystemMessage
。一次只能持有一个 SystemMessage
。忽略添加了具有相同内容的新 SystemMessage
。如果添加了具有不同内容的新 SystemMessage
内容,则将替换前一个内容。工具消息的特殊处理
如果包含ToolExecutionRequest
的AiMessage
被淘汰,则与其关联的ToolExecutionResultMessage
也需要一同淘汰。
ChatMemory 代码实践
引入依赖包
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> <version>${langchain4j.version}</version></dependency><dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId> <version>${langchain4j.version}</version></dependency>
共享ChatMemory实现
package org.ivy.chatmemory;import dev.langchain4j.memory.ChatMemory;import dev.langchain4j.memory.chat.MessageWindowChatMemory;import dev.langchain4j.model.openai.OpenAiChatModel;import dev.langchain4j.service.AiServices;import org.ivy.chatmemory.service.Assistant;public class ChatMemoryJavaExample { public static void main(String[] args) { ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(OpenAiChatModel.builder() .baseUrl("xxxx") .apiKey("xxxx") .build() ) .chatMemory(chatMemory) .build(); String answer = assistant.chat("Hello! My name is Klaus."); System.out.println(answer); // Hello Klaus! How can I assist you today? String answerWithName = assistant.chat("What is my name?"); System.out.println(answerWithName); // Your name is Klaus. }}
独享ChatMemory实现
package org.ivy.chatmemory;import dev.langchain4j.memory.chat.MessageWindowChatMemory;import dev.langchain4j.model.openai.OpenAiChatModel;import dev.langchain4j.service.AiServices;import org.ivy.chatmemory.service.EachUserAssistant;public class ChatMemoryEachUserExample { public static void main(String[] args) { EachUserAssistant assistant = AiServices.builder(EachUserAssistant.class) .chatLanguageModel( OpenAiChatModel.builder() .baseUrl("xxx") .apiKey("xxx") .build()) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) .build(); System.out.println(assistant.chat(1, "Hello, my name is Klaus")); // Hi Klaus! How can I assist you today? System.out.println(assistant.chat(2, "Hello, my name is Francine")); // Hello Francine! How can I assist you today? System.out.println(assistant.chat(1, "What is my name?")); // Your name is Klaus. System.out.println(assistant.chat(2, "What is my name?")); }}
定义memoryId,根据memoryId来获取是否是同一组上下文信息。
示例代码
在示例代码中,除上java版本的实现外,还有Spring Boot 版本的实现,这个来的更实际,使用更多,大家可以查看Github中的代码。
总结
对LangChain4j聊天记忆进行了分析,并介绍了ChatMemory四个特性。提供了使用示例,包括共享ChatMemory、会话维度隔离的ChatMemory,自定义存储机制等。实战代码可以参考Github。