聊天记忆 手动维护和管理ChatMessage是很麻烦的。 因此,LangChain4j提供了ChatMemory抽象以及多种开箱即用的实现。
ChatMemory可以作为独立的低级组件使用, 或者作为高级组件(如AI服务 )的一部分。
ChatMemory作为ChatMessage的容器(由List支持),具有以下额外功能:
淘汰策略
持久化
对SystemMessage的特殊处理
对工具 消息的特殊处理
记忆与历史 请注意,”记忆”和”历史”是不同的概念。
历史保持用户和AI之间的所有 消息完整无缺 。历史是用户在UI中看到的内容。它代表实际对话内容。
记忆保存一些信息 ,这些信息呈现给LLM,使其表现得好像”记住”了对话。 记忆与历史有很大不同。根据使用的记忆算法,它可以以各种方式修改历史: 淘汰一些消息,总结多条消息,总结单独的消息,从消息中删除不重要的细节, 向消息中注入额外信息(例如,用于RAG)或指令(例如,用于结构化输出)等等。
LangChain4j目前只提供”记忆”,而不是”历史”。如果您需要保存完整的历史记录,请手动进行。
淘汰策略 淘汰策略是必要的,原因如下:
为了适应LLM的上下文窗口。LLM一次可以处理的令牌数量是有上限的。 在某些时候,对话可能会超过这个限制。在这种情况下,应该淘汰一些消息。 通常,最旧的消息会被淘汰,但如果需要,可以实现更复杂的算法。
控制成本。每个令牌都有成本,使每次调用LLM的费用逐渐增加。 淘汰不必要的消息可以降低成本。
控制延迟。发送给LLM的令牌越多,处理它们所需的时间就越长。
目前,LangChain4j提供了2种开箱即用的实现:
较简单的一种,MessageWindowChatMemory,作为滑动窗口运行, 保留最近的N条消息,并淘汰不再适合的旧消息。 然而,由于每条消息可能包含不同数量的令牌, MessageWindowChatMemory主要用于快速原型设计。
更复杂的选项是TokenWindowChatMemory, 它也作为滑动窗口运行,但专注于保留最近的N个令牌 , 根据需要淘汰旧消息。 消息是不可分割的。如果一条消息不适合,它会被完全淘汰。 TokenWindowChatMemory需要一个Tokenizer来计算每个ChatMessage中的令牌数。
持久化 默认情况下,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) { } @Override public void updateMessages (Object memoryId, List<ChatMessage> messages) { } @Override public void deleteMessages (Object memoryId) { } } ChatMemory chatMemory = MessageWindowChatMemory.builder() .id("12345" ) .maxMessages(10 ) .chatMemoryStore(new PersistentChatMemoryStore ()) .build();
每当向ChatMemory添加新的ChatMessage时,都会调用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。
案例:实现多用户会话记忆 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 30 31 32 33 34 35 36 public interface Assistant { String chat (@MemoryId String memoryId, @UserMessage String userMessage) ; } @Bean public Assistant chatLanguageModel () { OpenAiChatModel model = OpenAiChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10 )) .build(); return assistant; } @GetMapping("/chat/memory") public void chat () { String resp1 = assistant.chat("用户1" , "hello my name is golang" ); log.info("LLM resp:[{}]" , resp1); String resp2 = assistant.chat("用户2" , "hello my name is c++" ); log.info("LLM resp:[{}]" , resp2); String resp3 = assistant.chat("用户1" , "what is my name" ); log.info("LLM resp:[{}]" , resp3); String resp4 = assistant.chat("用户2" , "what is my name" ); log.info("LLM resp:[{}]" , resp4); }
案例:实现多用户会话持久化 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public interface Assistant { String chat (@MemoryId String memoryId, @UserMessage String userMessage) ; } @Bean public Assistant ChatMemoryStoreAssistant () { OpenAiChatModel model = OpenAiChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .build(); ChatMemoryStore chatMemoryStore = new ChatMemoryStore () { @Override public List<ChatMessage> getMessages (Object o) { String json = redisUtil.get((String) o); return ChatMessageDeserializer.messagesFromJson(json); } @Override public void updateMessages (Object o, List<ChatMessage> list) { String json = ChatMessageSerializer.messagesToJson(list); redisUtil.set((String) o, json); } @Override public void deleteMessages (Object o) { redisUtil.delete((String) o); } }; Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(10 ) .chatMemoryStore(chatMemoryStore) .build()) .build(); return assistant; } @GetMapping("/chat/history") public String chatHistory (@RequestParam String memoryId, @RequestParam String question) { String resp = assistant.chat(memoryId, question); log.info("LLM resp:[{}]" , resp); return resp; }