基于ChatMemory打造AI取名大师

embedded/2024/10/22 18:47:32/
aidu_pl">

当我们真正开发一款应用时,存储用户与大模型的历史对话是非常重要的,因为大模型需要利用到这些历史对话来理解用户最近一句话到底是什么意思。

比如你跟大模型说“换一个”,如果大模型不基于历史对话来分析,那么大模型根本就不知道你到底想换什么,而ChatMemory真是LangChain4j提供的用来存储历史对话的组件,并且还支持窗口限制、淘汰机制、持久化机制等等扩展功能。

ChatMemory取名大师

我们先回顾一下第一节实现历史对话功能的Demo:

java">public class _01_HelloWorld {public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();UserMessage userMessage1 = UserMessage.userMessage("你好,我是Timi");Response<AiMessage> response1 = model.generate(userMessage1);AiMessage aiMessage1 = response1.content(); // 大模型的第一次响应System.out.println(aiMessage1.text());System.out.println("----");// 下面一行代码是重点Response<AiMessage> response2 = model.generate(userMessage1, aiMessage1, UserMessage.userMessage("我叫什么"));AiMessage aiMessage2 = response2.content(); // 大模型的第二次响应System.out.println(aiMessage2.text());}
}

这种实现方式太过麻烦了,我们用ChatMemory来优化,注意ChatMemory需要基于AiService来使用:

java">package com.timi;import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;public class _03_ChatMemory {interface NamingMaster {String talk(String desc);}public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);NamingMaster namingMaster = AiServices.builder(NamingMaster.class).chatLanguageModel(model).chatMemory(chatMemory).build();System.out.println(namingMaster.talk("帮我取一个很有中国文化内涵的男孩名字,给我一个你觉得最好的就行了"));System.out.println("---");System.out.println(namingMaster.talk("换一个"));}
}

代码执行结果:

java">岳霖 (Yuè Lín)
---
岳华 (Yuè Huá)

首先定义一个NamingMaster表示取名大师,通过talk()方法来和大师进行交流,最终得到一个满意的名字。

在构造NamingMaster代理对象时,我们除开设置了ChatLanguageModel,还设置了一个ChatMemory对象,而这个ChatMemory对象就是用来存储历史对话记录的,比如我说的“换一个”时候,大模型是知道到底要换的是什么,从而给了我另外一个名字。

MessageWindowChatMemory

ChatMemory是一个接口,默认提供了两个实现类:

  1. MessageWindowChatMemory
  2. TokenWindowChatMemory

而这两个实现类内部都有一个ChatMemoryStore属性,ChatMemoryStore也是一个接口,默认有一个InMemoryChatMemoryStore实现类,该类的实现比较简单:

java">public class InMemoryChatMemoryStore implements ChatMemoryStore {private final Map<Object, List<ChatMessage>> messagesByMemoryId = new ConcurrentHashMap<>();public InMemoryChatMemoryStore() {}@Overridepublic List<ChatMessage> getMessages(Object memoryId) {return messagesByMemoryId.computeIfAbsent(memoryId, ignored -> new ArrayList<>());}@Overridepublic void updateMessages(Object memoryId, List<ChatMessage> messages) {messagesByMemoryId.put(memoryId, messages);}@Overridepublic void deleteMessages(Object memoryId) {messagesByMemoryId.remove(memoryId);}
}

本质上就是一个ConcurrentHashMap,所以原理上我们可以自定义ChatMemoryStore的实现类来实现将ChatMessage持久化到磁盘,比如:

java">static class PersistentChatMemoryStore implements ChatMemoryStore {private final DB db = DBMaker.fileDB("chat-memory.db").transactionEnable().make();private final Map<String, String> map = db.hashMap("messages", STRING, STRING).createOrOpen();@Overridepublic List<ChatMessage> getMessages(Object memoryId) {String json = map.get((String) memoryId);return messagesFromJson(json);}@Overridepublic void updateMessages(Object memoryId, List<ChatMessage> messages) {String json = messagesToJson(messages);map.put((String) memoryId, json);db.commit();}@Overridepublic void deleteMessages(Object memoryId) {map.remove((String) memoryId);db.commit();}
}

需要添加依赖:

<dependency><groupId>org.mapdb</groupId><artifactId>mapdb</artifactId><version>3.0.9</version><exclusions><exclusion><groupId>org.jetbrains.kotlin</groupId><artifactId>kotlin-stdlib</artifactId></exclusion></exclusions>
</dependency>

这样我们就可以自己定义ChatMemory从而实现持久化了:

java">ChatMemory chatMemory = MessageWindowChatMemory.builder().chatMemoryStore(new PersistentChatMemoryStore()).maxMessages(10).build();

这里我们仍然利用的是MessageWindowChatMemory,只是修改了chatMemoryStore属性,同样我们也可以修改TokenWindowChatMemory,这里就不再重复演示了。

那么MessageWindowChatMemory除开可以存储ChatMessage之外,还有什么特殊的吗?

我们直接看它的add()方法实现:

java">@Override
public void add(ChatMessage message) {// 从ChatMemoryStore获取当前所存储的ChatMessageList<ChatMessage> messages = messages();// 如果待添加的是SystemMessageif (message instanceof SystemMessage) {Optional<SystemMessage> systemMessage = findSystemMessage(messages);if (systemMessage.isPresent()) {// 如果存在相同的SystemMessage,则什么都不做,直接返回if (systemMessage.get().equals(message)) {return; // do not add the same system message} else {messages.remove(systemMessage.get()); // need to replace existing system message}}}// 添加messages.add(message);// 如果超过了maxMessages限制,则会淘汰List最前面的,也就是最旧的ChatMessage// 注意,SystemMessage不会被淘汰ensureCapacity(messages, maxMessages);// 将改变了的List更新到ChatMemoryStore中store.updateMessages(id, messages);
}

从以上源码可以看出MessageWindowChatMemory有淘汰机制,可以设置maxMessages,超过maxMessages会淘汰最旧的ChatMessage,SystemMessage不会被淘汰。

TokenWindowChatMemory

TokenWindowChatMemory和MessageWindowChatMemory类似,区别在于计算容量的方式不一样,MessageWindowChatMemory直接取的是List的大小,而TokenWindowChatMemory会利用指定的Tokenizer对List对应的Token数进行估算,然后和设置的maxTokens进行比较,超过maxTokens也会进行淘汰,也是淘汰最旧的ChatMessage。

Tokenizer是一个接口,默认提供了OpenAiTokenizer实现类,是用来估算一条ChatMessage对应多少个Token的,很多大模型的API都是按使用的Token数来收费的,所以在对成本比较敏感时,建议使用TokenWindowChatMemory来对一个会话使用的总Token数进行控制。

独立ChatMemory

我们再看一眼之前的代码:

java">public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();ChatMemory chatMemory = MessageWindowChatMemory.builder().chatMemoryStore(new PersistentChatMemoryStore()).maxMessages(10).build();NamingMaster namingMaster = AiServices.builder(NamingMaster.class).chatLanguageModel(model).chatMemory(chatMemory).build();System.out.println(namingMaster.talk("帮我取一个很有中国文化内涵的男孩名字,给我一个你觉得最好的就行了"));System.out.println("---");System.out.println(namingMaster.talk("换一个"));}

以上代码有什么问题吗?如果只有一个用户用是没问题的,那如果有多个用户用呢?

比如NamingMaster代理对象被多个用户同时使用,那么这多个用户使用的是同一个ChatMemory,那就会出现这多个用户的对话记录混杂在了一起,这肯定是有问题的,所以需要有一种机制能够使得每个用户对应一个ChatMemory。

所以MessageWindowChatMemory和TokenWindowChatMemory其实都还有一个id属性,而具体的id值则有用于使用时动态传入。

我们改造一下AiServices中设置ChatMemory的方式:

java">NamingMaster namingMaster = AiServices.builder(NamingMaster.class).chatLanguageModel(model).chatMemoryProvider(userId -> MessageWindowChatMemory.withMaxMessages(10)).build();

以上代码表示,NamingMaster代理对象对应的ChatMemory并不是固定的,会根据设置的ChatMemoryProvider来提供,而ChatMemoryProvider是一个Lambda表达式,意思是每个不同的userId对应不同的ChatMemory对象。

同时,我们也需要改造talk()方法来支持动态传入userId:

java">interface NamingMaster {String talk(@MemoryId String userId, @UserMessage String desc);
}

完整代码:

java">package com.timi;import dev.langchain4j.agent.tool.P;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.mapdb.DB;
import org.mapdb.DBMaker;import java.util.List;
import java.util.Map;import static dev.langchain4j.data.message.ChatMessageDeserializer.messagesFromJson;
import static dev.langchain4j.data.message.ChatMessageSerializer.messagesToJson;
import static org.mapdb.Serializer.STRING;public class _03_ChatMemory {interface NamingMaster {String talk(@MemoryId String userId, @UserMessage String desc);}public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();NamingMaster namingMaster = AiServices.builder(NamingMaster.class).chatLanguageModel(model).chatMemoryProvider(userId -> MessageWindowChatMemory.withMaxMessages(10)).build();System.out.println(namingMaster.talk("1", "帮我取一个很有中国文化内涵的男孩名字,给我一个你觉得最好的就行了"));System.out.println("---");System.out.println(namingMaster.talk("2", "换一个"));}static class PersistentChatMemoryStore implements ChatMemoryStore {private final DB db = DBMaker.fileDB("chat-memory.db").transactionEnable().make();private final Map<String, String> map = db.hashMap("messages", STRING, STRING).createOrOpen();@Overridepublic List<ChatMessage> getMessages(Object memoryId) {String json = map.get((String) memoryId);return messagesFromJson(json);}@Overridepublic void updateMessages(Object memoryId, List<ChatMessage> messages) {String json = messagesToJson(messages);map.put((String) memoryId, json);db.commit();}@Overridepublic void deleteMessages(Object memoryId) {map.remove((String) memoryId);db.commit();}}
}

由于以上代码传入的userId不同,所以代码执行结果为:

java">玉山 (Yushan)
---
好的,请问您想要换成什么样的内容呢?

这就表示,两个不同的用户使用的是独立的ChatMemory。

AiServices整合ChatMemory源码分析

最后,我们再来看看AiServices中是如何利用ChatMemory来实现对话历史记录的。

视线转移到第二节提到的DefaultAiServices中的代理对象中的invoke()方法中,在第二节我们解析了invoke()方法源码中会根据当前调用的方法信息和参数解析出SystemMessage和UserMessage,然后就会执行以下代码:

java">Object memoryId = memoryId(method, args).orElse(DEFAULT);

memoryId()方法其实就是解析方法参数中加了@MemoryId注解的参数值,我们的案例就是传入的userId,仅接着就会执行:

java">if (context.hasChatMemory()) {// 根据memoryId获取或创建ChatMemoryChatMemory chatMemory = context.chatMemory(memoryId);// 将SystemMessage、UserMessage添加到ChatMemory中systemMessage.ifPresent(chatMemory::add);chatMemory.add(userMessage);
}

这里的context为AiServiceContext,它内部有一个chatMemories属性,类型为Map<Object, ChatMemory> ,就是专门用来存储memoryId和ChatMemory对象之间的映射关系的。

以上代码只是新增一条UserMessage,而传入给大模型的得是所有的对话历史,所以后续会执行:

java">List<ChatMessage> messages;
if (context.hasChatMemory()) {messages = context.chatMemory(memoryId).messages();
} else {messages = new ArrayList<>();systemMessage.ifPresent(messages::add);messages.add(userMessage);
}

根据memoryId把对应的ChatMemory中存储的所有ChatMessage获取出来,然后传入给大模型就可以了。

本节总结

以上就是关于ChatMemory的作用和实现原理,在实际应用开发中,ChatMemory的作用是重要的,下一节将介绍LangChain4j的工具机制时,其中也离不开ChatMemory的应用的,敬请期待。


http://www.ppmy.cn/embedded/53928.html

相关文章

数据库工具之 —— SQLite

数据库工具之 —— SQLite SQLite 是一个非常流行的轻量级数据库&#xff0c;它是一个嵌入式的数据库&#xff0c;意味着数据库文件是存储在磁盘上的一个单一文件。SQLite 不需要一个独立的服务器进程&#xff0c;这使得它非常适合用于小型应用、移动应用、桌面应用&#xff0…

企业工程项目管理系统源码:Java版源码解析

一、项目概述 鸿鹄工程项目管理系统是基于Spring Cloud、Spring Boot、Mybatis、Vue和ElementUI技术栈&#xff0c;采用前后端分离架构构建的工程管理软件。它旨在应对企业快速发展中的管理挑战&#xff0c;提升工程管理效率&#xff0c;减轻工作负担&#xff0c;加速信息处理…

android:exported=“false“

书籍&#xff1a; 《第一行代码 Android》第三版 开发环境&#xff1a; Android Studio Jellyfish | 2023.3.1 问题&#xff1a; A launchable activity must be exported as of Android 12, which also makes it available to other apps 新建的activity中的android:exp…

LearnOpenGL - Android OpenGL ES 3.0 使用 FBO 进行离屏渲染

系列文章目录 LearnOpenGL 笔记 - 入门 01 OpenGLLearnOpenGL 笔记 - 入门 02 创建窗口LearnOpenGL 笔记 - 入门 03 你好&#xff0c;窗口LearnOpenGL 笔记 - 入门 04 你好&#xff0c;三角形OpenGL - 如何理解 VAO 与 VBO 之间的关系LearnOpenGL - Android OpenGL ES 3.0 绘制…

Android高级面试_2_IPC相关

Android 高级面试-3&#xff1a;语言相关 1、Java 相关 1.1 缓存相关 问题&#xff1a;LruCache 的原理&#xff1f; 问题&#xff1a;DiskLruCache 的原理&#xff1f; LruCache 用来实现基于内存的缓存&#xff0c;LRU 就是最近最少使用的意思&#xff0c;LruCache 基于L…

同三维T700转换器 USB转HDMI转换器

让USB摄像头变成HDMI输出&#xff0c;支持4K60输出 一、产品简介&#xff1a; 此转换器可以把USB信号转成HDMI信号&#xff0c;支持4K60 HDMI输出&#xff0c;有效解决了USB摄像头连接电视、显示器、导播台的问题&#xff0c;带USB控制口&#xff0c;可升级/接蓝牙接收器&#…

Linux运维:MySQL数据库(1)

1.信息与数据&#xff1a; 数据是信息的载体&#xff0c;信息是数据的内涵。数据库就是存储数据的仓库&#xff0c;并长期存储在计算机磁盘中&#xff0c;可由多个用户和应用程序共享的数据集合&#xff0c;就是数据库。 2.数据库中的数据的特点&#xff1a; 2.1.数据是按照某…

上古世纪战争台服官网地址+台服预约+预创建角色教程

上古世纪战争台服上线啦&#xff0c;在《上古世纪战争》中&#xff0c;通过主要势力和地区&#xff0c;剧情和角色可以想起原作。《上古世纪战争》的主要背景为&#xff0c;原大陆消失之后&#xff0c;完成移民的种族们定居在诺伊大陆之后遇到的多个势力之间的冲突。同时&#…