基于ChatMemory打造AI取名大师

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

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

ChatMemory取名大师

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

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来使用:

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("换一个"));}
}

代码执行结果:

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

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

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

MessageWindowChatMemory

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

  1. MessageWindowChatMemory
  2. TokenWindowChatMemory

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

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持久化到磁盘,比如:

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从而实现持久化了:

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

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

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

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

@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

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

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的方式:

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

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

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

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

完整代码:

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不同,所以代码执行结果为:

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

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

AiServices整合ChatMemory源码分析

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

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

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

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

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,而传入给大模型的得是所有的对话历史,所以后续会执行:

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.mzph.cn/news/861981.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

恭喜!Z医生喜提世界名校—斯坦福大学访问学者邀请函

➡️【院校简介】 斯坦福大学&#xff08;Stanford University&#xff09;&#xff0c;全称为小利兰斯坦福大学&#xff0c;简称“斯坦福”&#xff0c;位于美国加州旧金山湾区南部帕罗奥多市境内&#xff0c;临近高科技园区硅谷&#xff0c;是私立研究型大学&#xff0c;全球…

HarmonyOS Next开发学习手册——内存管理(GC)

GC&#xff08;全称 Garbage Collection&#xff09;&#xff0c;即垃圾回收。在计算机领域&#xff0c;GC就是找到内存中的垃圾&#xff0c;释放和回收内存空间。当前主流编程语言实现的GC算法主要分为两大类&#xff1a;引用计数和对象追踪&#xff08;即Tracing GC&#xff…

threejs的学习(二)

上次我们说了如何搭建环境&#xff0c;并成功显示了一个静止的方块&#xff0c; 1.通过轨道控制器查看方块&#xff0c;同时添加坐标轴辅助器 main.js import * as THREE from "../assets/js/three.module"; import { OrbitControls } from "../../../three.j…

零拷贝技术(zero copy),DMA,mmap,sendfile

在一些高性能的IO场景下我们经常能听到零拷贝技术&#xff0c;这是个不错的话题。 零拷贝指的是内核态与用户态之间的数据拷贝&#xff0c;而这两个区域的数据拷贝只能依靠CPU&#xff0c;但是CPU最重要的作用应该是运算。 一、DMA的由来 在没有DMA之前&#xff0c;磁盘的IO…

全国211大学名单及排名

序号 名称 省份 985 211 双一流 1 北京大学 北京 是 是 是 2 清华大学 北京 是 是 是 3 复旦大学 上海 是 是 是 4 上海交通大学 上海 是 是 是 5 浙江大学 浙江 是 是 是 6 国防科技大学 湖南 是 是 是 7 中国人民大学 北京 是 …

Vue.js中的虚拟DOM

一.节点和状态 在我们平常对DOM操作的时候,之前在vue没有诞生之前,以命令式的方式对DOM进行操作,页面上的每一个元素都可以看做成一个节点状态。 二.剔除和渲染 框架都有自己渲染的方式,假设一个页面的状态,随着Ajax请求的放松,状态发生改变,有以下的两种方式供你选择&#…

C语言 do while循环练习 上

do while循环 do循环语句; while&#xff08;表达式&#xff09;; 例&#xff1a; do while里的break do while里的continue 练习 1.计算n的阶乘 1*2*3*424 2.计算1&#xff01;2&#xff01;3&#xff01;.......10! 3.在一个有序数组中查找具体的某个数字h&#x…

吴恩达AI系列:教你如何用Langchain封装一本书

教你快速上手AI应用——吴恩达AI系列教程 人工智能风靡全球,它的应用已经渗透到我们生活的方方面面,从自动驾驶到智能家居,再到医疗辅助和量化交易等等。他们逐渐改变了我们的生活方式,然而,对于许多人来说,AI仍然是一个神秘且无法理解的领域。 为了帮助更多的人理解并掌握AI…

一篇文章带你彻底搞懂十大经典排序之——快速排序

一、递归实现快速排序 1.基本思想 通过一趟排序将待排序记录分割成独立的两部分&#xff0c;其中一部分记录的关键字均比两一部分的关键字小&#xff0c;则课分别对这两部分记录继续进行排序&#xff0c;已达到整个序列有序。 2.算法描述 快速排序使用分治法来吧一个“串”…

QT中利用qss来创建一个圆角矩形窗口,并利用Qt::WA_TranslucentBackground属性解决留白问题

1、效果 2、实现 QWidget#centralwidget {border-radius: 30px solid default;border-image: url(:/images/bk<

探索认知智能的未来:知识图谱的崛起

知识图谱点燃语言模型的潜能 ©作者| 潇潇 来源|神州问学 一、 人工智能的三个层次 在人工智能的发展历程中&#xff0c;我们见证了从简单计算到复杂认知的飞跃。人工智能的发展可以概括为三个主要层次&#xff1a;计算智能、感知智能和认知智能。这三个层次不仅代表了技…

支持向量回归原理详解及Python代码示例

支持向量回归原理详解 支持向量回归&#xff08;Support Vector Regression, SVR&#xff09;是支持向量机&#xff08;SVM&#xff09;的一种扩展&#xff0c;用于回归问题。SVR通过寻找一个最佳的回归超平面&#xff0c;使得尽可能多的数据点落在超平面附近的ε-管内&#xf…

05-Shell编程之免交互

目录 5.1 Here Document免交互 5.1.1 通过passwd命令给用户设置密码&#xff1a; 5.2. Expect免交互 5.2.1使用Expect自动登录FTP服务器 5.2.2 使用Expect实现免交互磁盘创建 5.1 Here Document免交互 Here Document是Shell编程中实现免交互的一种常用方法。它使用I/O重定…

eNSP中VRRP的配置和使用

一、基础配置 1.新建拓扑图 2.配置vlan a.CORE-S1 <Huawei>system-view [Huawei]sysname CORE-S1 [CORE-S1]vlan 10 [CORE-S1-vlan10]vlan 20 [CORE-S1-vlan20]vlan 30 b.CORE-S2 <Huawei>system-view [Huawei]sysname CORE-S2 [CORE-S2]vlan 10 [CORE…

Git中的变基(Rebase)

Git 操作之变基&#xff08;Rebase&#xff09; ⭐⭐⭐ 目录 &#x1f514; 介绍&#x1f514; 格式&#x1f514; 示例&#x1f514; 特性&#x1f514; 变基与合并的区别&#x1f514; 使用场景 &#x1f514; 介绍 在Git中&#xff0c;下载后运行变基通常是指使用 git pull…

240627_图像24位深度(RGB图)转为8位深度(单通道图)

240627_图像24位深度&#xff08;RGB图&#xff09;转为8位深度&#xff08;单通道图&#xff09; 在使用网络上下载下来的一部分图像分割数据集时&#xff0c;有些标签图你看着是一个黑白图&#xff0c;但是他还是有可能是一张RGB三通道图&#xff0c;具体怎么区分呢。右击图…

FPGA - 图像灰度化

一&#xff0c;灰度图像概念 灰度数字图像是每个像素只有一个采样颜色的图像。这类图像通常显示为从最暗黑色到最亮的白色的灰度&#xff0c;尽管理论上这个采样可以任何颜色的不同深浅&#xff0c;甚至可以是不同亮度上的不同颜色。灰度图像与黑白图像不同&#xff0c;在计算机…

如何预防和处理他人盗用IP地址?

IP地址的定义及作用 解释 IP 地址在互联网中的作用。它是唯一标识网络设备的数字地址&#xff0c;类似于物理世界中的邮政地址。 1、IP地址盗窃的定义 解释一下什么是IP地址盗用&#xff0c;即非法使用他人的IP地址或者伪造IP地址的行为&#xff0c;这种行为可能引发法律和安…

hadoop离线与实时的电影推荐系统-计算机毕业设计源码10338

摘 要 随着互联网与移动互联网迅速普及&#xff0c;网络上的电影娱乐信息数量相当庞大&#xff0c;人们对获取感兴趣的电影娱乐信息的需求越来越大,个性化的离线与实时的电影推荐系统 成为一个热门。然而电影信息的表示相当复杂&#xff0c;己有的相似度计算方法与推荐算法都各…

黑盒测试、白盒测试和灰盒测试的概念

黑盒测试、白盒测试和灰盒测试的概念 黑盒测试、白盒测试和灰盒测试是软件测试中的三种基本策略&#xff0c;它们分别关注不同的测试角度&#xff1a; 黑盒测试&#xff08;Black-box testing&#xff09;&#xff1a; 黑盒测试也称为功能测试或行为测试&#xff0c;它完全基…