LangChain4j流式调用、消息注解与会话记忆

我们先用AiService工具类把调用ai大语言模型的代码写出来。因为AiService工具类中整合有记忆、Rag知识库、tools工具等,我们直接配置调用即可。

我用的是qwen-plus模型。

引入依赖:

        <dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai-spring-boot-starter</artifactId><version>1.0.1-beta6</version></dependency>
<!--        langchain4j aiService工具类--><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-spring-boot-starter</artifactId><version>1.0.1-beta6</version></dependency>

yaml文件:

langchain4j:open-ai:chat-model:  # 会自动往容器中注入OpenAI Chat Model(阻塞式的)base-url: https://dashscope.aliyuncs.com/compatible-mode/v1api-key: 你的keymodel-name: qwen-pluslog-requests: truelog-responses: true

然后我们声明个接口,然后用@AiService注解去配置各种信息。

aiService工具类会自动调用,帮助我们创建该接口的代理对象,并注入到IoC容器中

//声明式接口
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配代理对象chatModel = "openAiChatModel", //阻塞式调用的模型
)
public interface ConsultantService {String chat1(String message);
}

阻塞式是把答案全部准备好后在一次性返回给你;  非阻塞式(流式)是每次只返回2~3句话,返回很多次。

然后我们去写个controller去测试一下:

@RestController
@RequestMapping("/ai")
public class ChatController {@Autowiredprivate ConsultantService consultantService;@RequestMapping("/chat1")public String chat1(@RequestParam("message") String message){return consultantService.chat1(message);}
}

运行后,访问http://localhost:8080/ai/chat1?message=帮我写一篇500字文章

在转了1、2分钟的圈后,你会发现文章是一次性出现的。

下面我们看看流式调用:

引入依赖:

<!--        stream流式调用--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-reactor</artifactId><version>1.0.1-beta6</version></dependency>

首先我们需要现在yaml文件中添加流式调用的配置:

langchain4j:open-ai:chat-model:  # 会自动往容器中注入OpenAI Chat Model(阻塞式的)base-url: https://dashscope.aliyuncs.com/compatible-mode/v1api-key: 你的keymodel-name: qwen-pluslog-requests: truelog-responses: truestreaming-chat-model:  # 会自动往容器中注入OpenAI Streaming Chat Model(流式的)api-key: 你的keybase-url: https://dashscope.aliyuncs.com/compatible-mode/v1model-name: qwen-pluslog-requests: truelog-responses: truelogging:level:dev.langchain4j: debug

 streaming-chat-model的配置与chat-model的配置完全一样。

返回到我们的ConsultantService接口中,配置流式调用的模型,写一个新的返回值为Flux的新接口:

@AiService( //aiService工具类会自动调用,帮助我们创建该接口的代理对象,并注入到IoC容器中wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配代理对象chatModel = "openAiChatModel", //阻塞式调用的模型streamingChatModel = "openAiStreamingChatModel", //流式调用的模型
)
public interface ConsultantService {String chat1(String message);Flux<String> chat2(String message);

然后写一个新的controller方法:

@RestController
@RequestMapping("/ai")
public class ChatController {@Autowiredprivate ConsultantService consultantService;@RequestMapping("/chat1")public String chat1(@RequestParam("message") String message){return consultantService.chat1(message);}@RequestMapping(value = "/chat2",produces = "text/html;charset=utf-8") //produces属性指定编码类型public Flux<String> chat2(@RequestParam("message") String message){return consultantService.chat2(message);}
}

 注解中要写produces = "text/html;charset=utf-8",不然返回的文本会乱码

我们再次访问   http://localhost:8080/ai/chat2?message=帮我写一篇500字文章

你会发现它是每次只返回2~3句话。

下面我们来看消息注解(写在接口方法的上面)

消息注解有俩种:

1、@SystemMessage   为系统注解,你可以理解为为ai大模型加一个系统前提,用于修饰ai的,可以用消息注解指明ai的身份,也可以让其只回答某方面的问题。

  它有俩种使用方法:1、你可以直接把  系统前提  写在注解的后面

@SystemMessage("你是群众的好助手小团团,温柔且幽默") //消息注解
Flux<String> chat2(String message);

此时访问 http://localhost:8080/ai/chat2?message=你是谁

它回答就会是小团团,而不是通义百炼。 

                                  2、如果你的系统前提很多,你也可以把文本文件导在resources目录下,然后在接口注解中指明目录

@SystemMessage(fromResource = "system.txt") //如果注解的消息过长,可以指定对应的文本文件
Flux<String> chat2(String message);

2、@UserMessage 用户消息注解,每次提问时都会加上该内容,用于修饰我们提问的内容。

  它也有俩种使用方法:1、直接把  前提  写在注解的后面。其中的{{it}}固定不变,指的是message

@UserMessage("你是群众的好助手小月月,温柔且幽默。{{it}}")
Flux<String> chat2(String message);

注意:如果这时访问该接口问他:你是谁 。它的回答是它是通义百炼,且你叫它小团团是认错了。

                                        2、如果你不想用{{it}}你也可以换别的,但要用@V注解:

@UserMessage("你是群众的好助手小月月,温柔且幽默。{{msg}}")
Flux<String> chat2(@V("msg")String message);

会话记忆功能:

大模型没有记忆能力,我们需要在web后端建立一个存储对象,在下次发问题时,把聊天记录一起在发给大模型。

所以我们需要写一个配置类,来创建会话记忆对象注入到IoC容器中

@Configuration
public class CommonConfiguration {
//构建会话记忆对象@Beanpublic ChatMemory chatMemory(){return MessageWindowChatMemory.builder().maxMessages(20) //设置存储的最大消息数量.build();}
}

然后在我们的声明式接口中配置我们的记忆存储对象chatMemory

@AiService( //aiService工具类会自动调用,帮助我们创建该接口的代理对象,并注入到IoC容器中wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配代理对象chatModel = "openAiChatModel", //阻塞式调用的模型streamingChatModel = "openAiStreamingChatModel", //流式调用的模型chatMemory = "chatMemory"//配置会话对象,为配置类中定义的记忆对象的方法名(公有的)
)
public interface ConsultantService {String chat1(String message);Flux<String> chat2(String message);

此时该大模型就能简单实现会话记忆的功能,但有个问题,当我们创建一个新的会话时,该新会话保留着原来旧会话的记忆,也就是说我们现在创建的记忆存储对象是所有的会话公有的,每一个会话都会从同一个chatMemory中获取记忆。

为了解决会话记忆隔离问题,我们需要引入ChatMemoryProvider对象,然后为每个会话设置个唯一标识。当创建了一个新的会话时,即不能从容器中获取到对应的id标识时,就会调用ChatMemoryProvider对象的get方法创建一个新的chatMemory对象。

创建好ChatMemoryProvider对象后,把它配置到声明式接口中,最后不要忘了该controller层的方法,要增加一个messageId参数。

  1、定义ChatMemoryProvider对象

@Configuration
public class CommonConfiguration {
//大模型没有记忆能力,我们需要在web后端建立一个存储对象,在下次发问题时,把聊天记录一起在发给大模型//构建会话记忆对象@Beanpublic ChatMemory chatMemory(){return MessageWindowChatMemory.builder().maxMessages(20) //设置存储的最大消息数量.build();}//会话隔离//定义会话记忆对象提供者(但后端重启的话,记忆就没了)@Bean  //如果没有从容器获取指定的id对象,就会调用该提供者的get方法创建一个新的chatMemory对象public ChatMemoryProvider chatMemoryProvider(){ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {@Overridepublic ChatMemory get(Object memoryId) {return MessageWindowChatMemory.builder().id(memoryId).maxMessages(20).build();}};return chatMemoryProvider;}
}

2、配置ChatMemoryProvider对象,也要修改接口方法

@AiService( //aiService工具类会自动调用,帮助我们创建该接口的代理对象,并注入到IoC容器中wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配代理对象chatModel = "openAiChatModel", //阻塞式调用的模型streamingChatModel = "openAiStreamingChatModel", //流式调用的模型//chatMemory = "chatMemory"//配置会话对象,为配置类中定义的记忆对象的方法名(公有的)chatMemoryProvider = "chatMemoryProvider" //配置会话隔离,为配置类中定义的记忆对象提供者的方法名 (瑕疵版)
)
public interface ConsultantService {//有多个参数时,需要用注解分别表明其含义Flux<String> chat2(@UserMessage String message, @MemoryId String memoryId);//@MemoryId注解告诉它哪个参数是那个唯一标识}

    3、修改controller层方法

@RestController
@RequestMapping("/ai")
public class ChatController {@Autowiredprivate ConsultantService consultantService;@RequestMapping(value = "/chat2",produces = "text/html;charset=utf-8") //produces属性指定编码类型public Flux<String> chat2(@RequestParam("message") String message,@RequestParam("memoryId") String memoryId){return consultantService.chat2(message,memoryId);}
}

现在我们就实现了会话记忆的功能了

但现在还有一个小瑕疵:当我们后端重启后,所有的会话记忆就消失了。

我们先看看现在它是把会话记忆存储在哪里了?

 配置类中用的chatMemoryProvider对象存储的会话记忆,chatMemoryProvider对象创建新的chatMemory对象时用的是MessageWindowChatMemory类的build方法,我们进入MessageWindowChatMemory类找到它的store方法:

我们可以发现store为空时,调用的是SingleSlotChatMemoryStore对象的构造方法,我们进入SingleSlotChatMemoryStore对象

我们可以发现该对象是用List对象存储的会话记忆,所以是存储在服务器内存上的。

我们需要存储在redis或者mysql中,就能实现记忆持久化,并且我们自己写一个根据redis或mysql存储的ChatMemoryStore对象,并把该ChatMemoryStore对象传给MessageWindowChatMemory对象即可。

我以存储在redis为例:我是在本地运行的redis

   1、首先我们引入redis依赖

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

 2、然后在yaml文件中配置我们的redis的相关信息

spring:data:redis:host: localhostport: 6379password: 你的密码(没有可不写)

  3、接下来我们写一个新的RedisChatMemoryStore对象让其实现 ChatMemoryStore接口,并重写它的三个方法:获取记忆、更新记忆、删除记忆。

@Repository //把该实现类对象交给IoC容器
public class RedisChatMemoryStore implements ChatMemoryStore {//用redis缓存记忆@Autowiredprivate StringRedisTemplate template;private String preName="langchain4j:";@Overridepublic List<ChatMessage> getMessages(Object memoryId) {//获取会话消息String str = template.opsForValue().get(preName+memoryId.toString());//把josn转为ListList<ChatMessage> list = ChatMessageDeserializer.messagesFromJson(str);return list;}@Overridepublic void updateMessages(Object memoryId, List<ChatMessage> list) {//更新会话记忆//1、把list中的记忆转为json格式String str = ChatMessageSerializer.messagesToJson(list);//2、把json格式记忆存入redis,并设置过期时间为1天template.opsForValue().set(preName+memoryId.toString(),str, Duration.ofDays(1));}@Overridepublic void deleteMessages(Object memoryId) {template.delete(preName+memoryId.toString());}
}

  4、最后把创建好的RedisChatMemoryStore传给MessageWindowChatMemory对象

@Configuration
public class CommonConfiguration {@Autowiredprivate RedisChatMemoryStore store;//会话隔离//定义会话记忆对象提供者(但后端重启的话,记忆就没了)@Bean  //如果没有从容器获取指定的id对象,就会调用该提供者的get方法创建一个新的chatMemory对象public ChatMemoryProvider chatMemoryProvider(){ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {@Overridepublic ChatMemory get(Object memoryId) {return MessageWindowChatMemory.builder().id(memoryId).maxMessages(20)//配置redis存储.chatMemoryStore(store).build();}};return chatMemoryProvider;}
}

这样就能实现把会话记忆根据memoryId存储在redis中了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/87766.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/87766.shtml

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

相关文章

NtfsWriteLog函数分析之OpenAttributeTableDump

第一部分&#xff1a; NtfsWriteLog( IrpContext, Vcb->MftScb, //注意&#xff1a;Vcb->MftScb NULL, OpenAttributeTableDump, …

DCM4CHEE ARCHIVE LIGHT 源代码解析(2)-STOWRS

系列文章目录 DCM4CHEE ARCHIVE LIGHT 源代码解析(1)-前言DCM4CHEE ARCHIVE LIGHT 源代码解析(2)-STOWRS文章目录 系列文章目录概述一、背景资料1、RESTful服务2、传输存储规范3、服务连接策略4、响应消息状态二、业务分析1、对象关系2、项目结构3、业务流程三、代码解析1、w…

Java中间件简介:构建现代软件的“隐形桥梁”

Java中间件简介&#xff1a;构建现代软件的“隐形桥梁” 在软件开发的世界里&#xff0c;中间件&#xff08;Middleware&#xff09;是一个既熟悉又神秘的存在。它不像数据库那样直接存储数据&#xff0c;也不像前端那样与用户交互&#xff0c;但它却是现代软件架构中不可或缺…

Scale AI 的王晓磊带着对整个 AI 行业动态的深入了解加入 Meta

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

冒烟测试概念速解

最近很多人对冒烟测试这个概念发出疑问。那么我就来简单解释一下什么是冒烟测试&#xff0c;以及冒烟测试的由来。 1.冒烟测试的由来 硬件测试的起源&#xff1a;从 “冒烟” 到基础功能验证 在电子工程领域&#xff0c;早期工程师在调试新硬件&#xff08;如电路板、芯片&am…

嵌入式学习笔记——day36-多路IO复用

一、基本概念 &#xff08;服务器多客户端模型&#xff09; 定义&#xff1a; 单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力 作用&#xff1a; 应用程序通常需要处理来自多条事件流中的事件&#xff0c;比如我现在用的电脑&#xff0c;需要同时处理键盘鼠…

微服务数据一致性技术解析:从单体到微服务的数据困局

关键词: 微服务数据一致性, 企业应用, 技术架构, 最佳实践 本文基于多位资深架构师在大型互联网公司的实战经验总结&#xff0c;希望能为正在进行微服务改造的团队提供有价值的参考。如果您在实践中遇到问题&#xff0c;欢迎交流讨论&#xff01; 目录 一、引言&#xff1a;从…

华为云Flexus+DeepSeek征文 | 基于华为云ModelArts Studio搭建Chatbox AI聊天助手

华为云FlexusDeepSeek征文 | 基于华为云ModelArts Studio搭建Chatbox AI聊天助手 引言一、ModelArts Studio平台介绍华为云ModelArts Studio简介ModelArts Studio主要特点 二、Chatbox介绍Chatbox简介主要特点 三、安装Chatbox应用下载Chatbox软件安装Chatbox工具 四、开通Deep…

基于cpolar的GPT-SoVITS远程访问实践过程

文章目录 前言1.GPT-SoVITS V2下载2.本地运行GPT-SoVITS V23.简单使用演示4.安装内网穿透工具4.1 创建远程连接公网地址 5. 固定远程访问公网地址 前言 在人工智能技术持续革新之际&#xff0c;语音合成领域涌现出突破性进展。由开发者团队"花儿不哭"研发的GPT-SoVI…

Redis数据结构之HyperLogLog

本文作者没有设置VIP可见&#xff0c;并首发在我的博客&#xff1a;https://blog.liuzijian.com/post/redis-data-structure-hyperloglog.html 目录 1.概述2.常用命令2.1 添加元素2.2 返回基数估算值2.3 合并hyperloglog 3.总结 1.概述 基数统计是一种去重复统计功能的基数估计…

django调用 paramiko powershell 获取cpu 核数

在 Django 应用中使用 paramiko 库通过 SSH 连接到远程服务器并执行命令&#xff08;例如获取 CPU 核数&#xff09;是一个常见的需求。下面是一个如何实现这一过程的步骤指南&#xff1a; 步骤 1: 安装必要的库 首先&#xff0c;确保你的 Django 项目中安装了 paramiko 库。如…

08-Python文件处理

08-Python文件处理 一、打开关闭文件 可以用 file 对象做大部分的文件操作。 file()在python3中已经被废除&#xff0c;使用open()打开文件 open 函数 先用open()打开一个文件&#xff0c;创建一个file 对象&#xff0c;再用相关方法才可以调用它进行读写。 语法 file ob…

增强现实—Multimodal text style transfer for outdoor vision-and-language navigation

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

黑马程序员新版Linux学习笔记——第二部分 基础命令

一、Linux目录结构 二、命令基础 三、ls 列目录内容 3.1 命令 3.2 参数 3.3 总结 四、cd 切换工作目录 4.1命令 五、pwd 查看当前工作目录 5.1命令 六、相对路径、绝对路径、特殊路径符 七、mkdir 创建目录命令 7.1命令 八、touch、cat、more 文件操作命令 8.1 touch 8.2c…

日常运维问题汇总-25

76.销售订单交货单状态更新 实务中偶有发生交货已完成&#xff0c;无需开票或开票已经完成&#xff0c;交货单状态为&#xff1a;处理中&#xff0c;且仍然出现在VF04中&#xff0c;如下图所示&#xff1a; 解决方法&#xff1a; T-CODE:VL_COMPLETE,可对错误的DN状态进行更新…

【2025 年】软件体系结构考试试卷-期末考试

2025 年软件体系结构考试试卷 考试学期&#xff1a;2025 考试形式&#xff1a;闭卷 考试时间&#xff1a;120 分钟 年级&#xff1a;______ 专业&#xff1a;软件工程 班级&#xff1a;______ 一、单选题&#xff08;每小题 1.5 分&#xff0c;共 24 分&#xff09; 关于策略…

4.查看、删除数据库

1.显示所有数据库 SHOW DATABASE 2.显示数据库创建语句 SHOW CREAT DATABASE db_name 例如想查看某个数据库是怎样创建的&#xff0c;用的什么字符集啥的。 3.数据库删除语句【慎用】 DROP DATABASE [IF EXISTS] db_name 删除某个数据库之前一定要确定是否进行了备份。

设计模式 - 原型模式

原型模式&#xff08;Prototype&#xff09;&#xff0c;在制造业种通常是指大批量生产开始之前研发出的概念模型&#xff0c;并基于各种参数指标对其进行检验&#xff0c;效果达到了质量要求&#xff0c;即可参照这个原型进行批量生产。即&#xff0c;原型模式可以用对象创建对…

MySQL数据库基础:从零开始的第一步【Linux】

前言 各位小伙伴们&#xff0c;好久不见&#xff01;近期&#xff0c;我的文章更新频率确实有些缓慢&#xff0c;在此诚挚地向大家道歉。这个月是我的期末考试月&#xff0c;正处于紧张的复习&#xff08;也可以说是重新学习&#xff09;阶段。尽管学业繁忙&#xff0c;但我依然…

502 Bad Gateway:服务器作为网关或代理时收到无效响应处理方式

502 Bad Gateway 错误是 Web 开发和服务器管理中常见的问题&#xff0c;通常表示网关或代理服务器收到无效响应。这种错误可能由多种原因引起&#xff0c;包括后端服务故障、网络问题或配置错误等。了解502错误的原因及其处理方式&#xff0c;对于维护网站的可用性和用户体验至…