基于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() {}

    @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);
    }
}

本质上就是一个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();

	@Override
	public List<ChatMessage> getMessages(Object memoryId) {
		String json = map.get((String) memoryId);
		return messagesFromJson(json);
	}

	@Override
	public void updateMessages(Object memoryId, List<ChatMessage> messages) {
		String json = messagesToJson(messages);
		map.put((String) memoryId, json);
		db.commit();
	}

	@Override
	public 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获取当前所存储的ChatMessage
	List<ChatMessage> messages = messages();

	// 如果待添加的是SystemMessage
	if (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();

        @Override
        public List<ChatMessage> getMessages(Object memoryId) {
            String json = map.get((String) memoryId);
            return messagesFromJson(json);
        }

        @Override
        public void updateMessages(Object memoryId, List<ChatMessage> messages) {
            String json = messagesToJson(messages);
            map.put((String) memoryId, json);
            db.commit();
        }

        @Override
        public 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获取或创建ChatMemory
	ChatMemory 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.mfbz.cn/a/751420.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[行业原型] Web端原型案例:康欣医疗后台管理系统

​医疗管理系统是一个业务复杂&#xff0c;功能庞大的系统&#xff0c;以下为HIS医院管理系统的常见模块&#xff0c;供大家参考。 本周为大家带来Web端原型案例&#xff1a;康欣医疗后台管理系统&#xff0c;先上原型&#xff1a; 完整文档加班主任微信号 添加班主任回复 “1…

ansible常用模块详解

一、Ansible 1.1 简介 Ansible是自动化运维工具&#xff0c;能实现跨主机对应用编排管理部署。 Ansible能批量配置、部署、管理上千台主机&#xff0c;是应用级别的跨主机编排工具。 比如以前需要切换到每个主机上执行的一或多个操作&#xff0c;使用Ansible只需在固定的一…

练习实践:ubuntu18.04安装、配置Nginx+PHP环境,两种配置方式,多站点

参考来源&#xff1a; https://help.aliyun.com/document_detail/464753.html https://www.cnblogs.com/laosan007/p/12803287.html https://blog.csdn.net/qq_55364077/article/details/132207083 【安装同版本7.2的php】 需要知道对应php和nginx的安装版本 需要安装php-fpm…

stl之string

构造函数 void test1() {string s1;//不传参cout << s1 << endl;string s2("123456");cout << s2 << endl;string s3(s2);cout << s3 << endl;string s4(s2, 1, 5);cout << s4 << endl;string s5("123456&quo…

PHP 网络通信底层原理分析

大家好&#xff0c;我是码农先森。 引言 我们日常的程序开发大多数都是以业务为主&#xff0c;很少会接触到底层逻辑。对于我们程序员来说&#xff0c;了解程序的底层运行逻辑&#xff0c;更有助于提升我们对程序的理解。我相信大多数的人&#xff0c;每天基本上都是完成业务…

丝杆支撑座:滚珠丝杆稳定运行的守护者!

丝杆支撑座是丝杆和电机之间连接的重要组成部分&#xff0c;发挥着非常重要的功能。提到丝杆支撑座和滚珠丝杆&#xff0c;很多人都会想到支撑关系&#xff0c;但丝杆支撑座作为滚珠丝杆系统中至关重要的角色&#xff0c;其作用远不止于简单的支撑。 丝杆支撑座安装过程非常简单…

第30课 绘制原理图——放置网络标签

什么是网络标签&#xff1f; 我们在很多电路图中都能看到&#xff0c;为了让图纸更加简洁&#xff0c;并不是每一根导线都要确确实实地画出来。可以在导线悬空的一端添加一个名称标签&#xff0c;接着在另一根导线的悬空一端添加上一个同名的名称标签&#xff0c;那么就可以让…

【自监督-MIM】系列方法学习二

Masked image modeling 是一种训练深度学习模型的技术,尤其是在视觉领域,类似于自然语言处理中的掩码语言建模(Masked Language Modeling)。它通过在输入图像中随机遮挡(或称为掩码)部分区域,然后训练模型来预测这些被遮挡部分的内容,从而提高模型的视觉理解能力。 Ma…

IDEA无法输入中文,怎么破

1.导航栏处&#xff0c;点击help菜单&#xff0c;选择Edit Custom VM Options.. 2.编辑文件&#xff0c;在文件末尾添加&#xff1a; -Drecreate.x11.input.methodtrue 3.保存文件即可&#xff0c;如果还是不行&#xff0c;就关闭所有Idea程序&#xff0c;重新启动Idea

机器学习之集成学习

一&#xff1a;概念 顾名思义集成学习就是用多个其他的算法结合起来使用 对于“其他算法”有同类和同质的区别&#xff0c;同质指的是所用的算法都是同一类型的&#xff0c;比如决策树和神经网络&#xff0c;这种也叫基学习器。反之亦然&#xff0c;但一般使用的是同质的。 …

网络治理新模式:Web3时代的社会价值重构

随着Web3技术的崛起&#xff0c;传统的网络治理模式正在经历革新&#xff0c;这不仅仅是技术的进步&#xff0c;更是对社会价值观念的挑战和重构。本文将深入探讨Web3时代的网络治理新模式&#xff0c;其背后的技术基础、社会影响以及未来的发展方向。 1. 引言 Web3时代&#…

文件进行周期性备份后权限更改的解决方案--使用脚本和定时任务

这里写目录标题 背景现象解决方案原因分析面临的问题解决思路操作步骤每个文件夹权限分配表测试chmod和chown两个命令是否可行写脚本实现定时同步同时修改权限 异地同步改权限在NAS上生成SSH密钥对将NAS的公钥复制到Linux服务器在NAS上编写同步脚本在NAS上执行脚本&#xff0c;…

咖啡机器人如何实现定量出水?

咖啡机器人实现定量出水的关键在于流量控制系统的设计&#xff0c;其中霍尔式流量计和光电式流量计是常用的测量设备。这两种流量计均具有精确高、一致性强、多种高低流量控制等特点&#xff0c;能够满足咖啡机器人定量出水的需求。 对于霍尔式流量计&#xff0c;其利用霍尔效…

防近视台灯有效果吗?专业护眼台灯推荐!告诉你台灯怎么选

随着学业负担的加重和电子设备的广泛普及&#xff0c;近视问题在青少年群体中愈发凸显&#xff0c;近视率持续走高。导致近视的因素错综复杂&#xff0c;除了过度使用手机外&#xff0c;遗传因素、不良的用眼习惯、环境因素、营养不均衡以及学习压力等均为重要因素&#xff0c;…

【深海王国】小学生都能玩的语音模块?ASRPRO打造你的第一个智能语音助手(1)

Hi~ (o^^o)♪, 各位深海王国的同志们&#xff0c;早上下午晚上凌晨好呀~ 辛勤工作的你今天也辛苦啦(/≧ω) 今天大都督将为大家带来全新系列——小学生都能玩的语音模块&#xff0c;帮你一周内快速学会语音模块的使用方式&#xff0c;打造一个可用于智能家居、物联网领域的语音…

【SpringBoot3.x】自定义开发通用SDK

1. 前言 相信大家学习SpringBoot到现在&#xff0c;使用Maven构建项目时&#xff0c;会在pom.xml文件中引入各种各样的依赖&#xff0c;那么我们如何将自己常用的一些工具类库进行封装成starter或者SDK供其他项目使用呢&#xff0c;本博客就会带着大家一步一步创建自定义的SDK…

使用 MyFlash 实现 MySQL 数据闪回

文章目录 简介GithubMyFlash 限制MySQL 准备开启 binlogmysqlbinlog 安装 MyFlashflashback 选项生成回滚文件执行回滚操作操作示例 简介 MySQL中的Binlog&#xff08;Binary Log&#xff09;数据闪回&#xff0c;也称为Point-in-Time Recovery (PITR)&#xff0c;是一种强大的…

Ansible-综合练习-生产案例

斌的招儿 网上教程大多都是官网模板化的教程和文档&#xff0c;这里小斌用自己实际生产环境使用的例子给大家做一个详解。涉及到一整套ansible的使用&#xff0c;对于roles的使用&#xff0c;也仅涉及到tasks和files目录&#xff0c;方便大家快速上手并规范化管理。 0.环境配置…

私接路由器导致部分终端(电脑、手机等)无法上网问题分析

目录 【1】私接路由器场景 【2】进行网络基本配置&#xff0c;模拟终端可以正常上网 【2.1】Http-Server配置 【2.2】ISP配置 【2.3】R-hefa配置 【2.4】Client1配置 【2.5】PC配置 【2.6】测试验证上网是否正常 【3】私接路由器后再测试验证公司内网各终端访问外网是…

大模型AI技术实现语言规范练习

人工智能技术可以为语言规范练习提供多种有效的解决方案&#xff0c;帮助学习者更有效地掌握语言规范。以下是一些常见的应用场景。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1. 智能纠错 利用自然语言处理技术&#xff0c;可以…
最新文章