1. ChatMemory是什么?
ChatMemory是LangChain4j提供的用来存储历史对话的组件,并且还支持窗口限制、淘汰机制、持久化机制等等扩展功能。
2. 使用
1. 基本使用
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
| import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.zhipu.ZhipuAiChatModel; import dev.langchain4j.service.AiServices; import dev.langchain4j.service.UserMessage;
public class Main {
interface NamingMaster { String name(@UserMessage String desc); }
public static void main(String[] args) {
ChatLanguageModel model = ZhipuAiChatModel .builder() .apiKey("xxx") .build();
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
NamingMaster namingMaster = AiServices.builder(NamingMaster.class).chatLanguageModel(model) .chatMemory(chatMemory) .build();
String name = namingMaster.name("给我一个姓王男孩名字,不用解释"); System.out.println("===========================第一次返回====================================="); System.out.println(name);
String name2 = namingMaster.name("换一个"); System.out.println("===========================第二次返回====================================="); System.out.println(name2); } }
|
返回结果:
2. 基于MemoryId
上面的代码,一个用户使用没有问题,但如果多个用户使用的是同一个ChatMemory,那就会出现这多个用户的对话记录混杂在一起的问题。
因此,需要改造一下AiServices中设置ChatMemory的方式,让每个不同的userId对应不同的ChatMemory对象。
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
| import dev.langchain4j.memory.chat.TokenWindowChatMemory; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiTokenizer; import dev.langchain4j.model.zhipu.ZhipuAiChatModel; import dev.langchain4j.service.AiServices; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.UserMessage;
public class Main {
interface NamingMaster { String name(@MemoryId String userId, @UserMessage String desc); }
public static void main(String[] args) { ChatLanguageModel model = ZhipuAiChatModel .builder() .apiKey("xxx") .build();
NamingMaster namingMaster = AiServices.builder(NamingMaster.class).chatLanguageModel(model) .chatMemoryProvider(userId -> TokenWindowChatMemory.builder().id(userId).maxTokens(10000,new OpenAiTokenizer()).build()) .build();
String name = namingMaster.name("1", "给我一个姓王的男孩名字,不用解释"); System.out.println("===========================第一次返回====================================="); System.out.println(name);
String name1 = namingMaster.name("2", "给我一个最好吃的水果名, 不用解释"); System.out.println("===========================第二次返回====================================="); System.out.println(name1);
String name2 = namingMaster.name("1", "换一个"); System.out.println("===========================第三次返回====================================="); System.out.println(name2); } }
|
返回结果:
3. 源码实现
ChatMemory是一个接口,默认提供了两个实现类:
- MessageWindowChatMemory
- TokenWindowChatMemory
1. ChatMemoryStore
这两个实现类内部都有一个ChatMemoryStore属性,ChatMemoryStore也是一个接口,默认有一个InMemoryChatMemoryStore实现类。
本质上就是一个ConcurrentHashMap,所以原理上我们可以自定义ChatMemoryStore的实现类来实现将ChatMessage持久化到磁盘,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class InMemoryChatMemoryStore implements ChatMemoryStore { private final Map<Object, List<ChatMessage>> messagesByMemoryId = new ConcurrentHashMap();
public InMemoryChatMemoryStore() { }
public List<ChatMessage> getMessages(Object memoryId) { return (List)this.messagesByMemoryId.computeIfAbsent(memoryId, (ignored) -> { return new ArrayList(); }); }
public void updateMessages(Object memoryId, List<ChatMessage> messages) { this.messagesByMemoryId.put(memoryId, messages); }
public void deleteMessages(Object memoryId) { this.messagesByMemoryId.remove(memoryId); } }
|
2. MessageWindowChatMemory
那么MessageWindowChatMemory除开可以存储ChatMessage之外,还有淘汰机制,可以设置maxMessages,超过maxMessages会淘汰最旧的ChatMessage,SystemMessage不会被淘汰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public void add(ChatMessage message) { List<ChatMessage> messages = this.messages(); if (message instanceof SystemMessage) { Optional<SystemMessage> systemMessage = findSystemMessage(messages); if (systemMessage.isPresent()) { if (((SystemMessage)systemMessage.get()).equals(message)) { return; } messages.remove(systemMessage.get()); } }
messages.add(message); ensureCapacity(messages, this.maxMessages); this.store.updateMessages(this.id, messages); }
|
3. TokenWindowChatMemory
TokenWindowChatMemory和MessageWindowChatMemory类似,区别在于计算容量的方式不一样,MessageWindowChatMemory直接取的是List的大小,而TokenWindowChatMemory会利用指定的Tokenizer对List对应的Token数进行估算,然后和设置的maxTokens进行比较,超过maxTokens也会进行淘汰,也是淘汰最旧的ChatMessage。
Tokenizer是一个接口,默认提供了OpenAiTokenizer实现类,是用来估算一条ChatMessage对应多少个Token的,很多大模型的API都是按使用的Token数来收费的,所以在对成本比较敏感时,建议使用TokenWindowChatMemory来对一个会话使用的总Token数进行控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public void add(ChatMessage message) { List<ChatMessage> messages = this.messages(); if (message instanceof SystemMessage) { Optional<SystemMessage> maybeSystemMessage = findSystemMessage(messages); if (maybeSystemMessage.isPresent()) { if (((SystemMessage)maybeSystemMessage.get()).equals(message)) { return; }
messages.remove(maybeSystemMessage.get()); } }
messages.add(message); ensureCapacity(messages, this.maxTokens, this.tokenizer); this.store.updateMessages(this.id, messages); }
|