基于AiService实现智能文章小助手

顾名思义,这个应用就是希望能利用大模型的能力来帮助我写文章,那这样一个应用该如何利用LangChain4j来实现呢?接下来我们来利用AiService进行实现。

AiService代理

首先,我们定义一个接口Writer,表示作家:

interface Writer {
	String write();
}

然后利用定义AiService来创建一个Writer代理对象:

package com.timi;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;

public class _02_AiService {

    interface Writer {
        String write(String title);
    }

    public static void main(String[] args) {

        ChatLanguageModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        Writer writer = AiServices.create(Writer.class, model);
    }
}

创建代理对象时,我们传入一个提前定义了的ChatLanguageModel,拿到Writer对象后,就可以调用write()方法来写文章了:

String content = writer.write("我最爱的人");
System.out.println(content);

执行代码结果为:

是我的家人,他们是我生命中最重要的人,无论发生什么事情,他们都会一直支持和爱护着我。他们给予我无限的爱和关怀,让我感到无比幸福和幸运。我愿意为他们奉献一切,尽我所能地去照顾和呵护他们。他们是我生命中最珍贵的存在,我永远都会珍惜和爱护他们。

以上例子可以看出AiServices的第一个作用:能够生成特定接口的代理对象,从而使得在调用代理对象方法时,能间接的调用大模型。这种代理模式的实现在Java各种框架中是非常常见的,这样就使得Writer接口或Writer对象具有了大模型的智能能力。

只不过上面的答案并不是一篇作文,大模型似乎是在回答“它最爱的人是谁?”这个问题,而不是在写文章,也就是大模型并不知道它需要扮演为一名作家,因此,我们需要告诉大模型让它先扮演一名作家,然后再回答我的问题,这就需要用到上一节提到的SystemMessage,只不过我们这里用的是@SystemMessage注解。

@SystemMessage

我们只需要在write()方法上定义@SystemMessage,并描述系统提示词,如下:

interface Writer {
	@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
	String write(String title);
}

那么当我们调用write()方法时,LangChain4j就会自动组合SystemMessage和用户输入的标题,然后发送给大模型,这样大模型就知道自己是一名作家了。

比如运行以上代码的结果就变为了:

在我生命中,最爱的人是我母亲。她是我生命中最重要的人,也是我永远的依靠和支持。母亲那温暖的微笑,总是能给我无限的力量和勇气。

我记得小时候,母亲总是在我生病时守在我身边,用温柔的手轻轻拍着我的背,给我端来热腾腾的粥汤。她总是用她的爱和关心包裹着我,让我感受到无比的安全和幸福。

母亲是一个坚强而又温柔的女人,她用她的辛勤劳动支撑起这个家庭,用她的慈爱呵护着我们。我常常想,如果没有母亲,我将会是一个怎样的人呢?母亲是我生命中的灯塔,指引着我前行的方向。

无论我遇到什么困难和挑战,母亲总是在我身边默默支持着我。她的爱如同一股暖流,贯穿着我的整个生命。我爱你,母亲,永远都爱你。

写得比我好多了,并且现在Writer接口就是一名作家了,当然我们可以把创建ChatLanguageModel、代理对象的操作都封装到Writer接口中,比如:

package com.timi;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;

public class _02_AiService {

    interface Writer {

        @SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
        String write(String title);

        static Writer create(){
            ChatLanguageModel model = OpenAiChatModel.builder()
                    .baseUrl("http://langchain4j.dev/demo/openai/v1")
                    .apiKey("demo")
                    .build();

            return AiServices.create(Writer.class, model);
        }
        
    }

    public static void main(String[] args) {
        Writer writer = Writer.create();
        String content = writer.write("我最爱的人");
        System.out.println(content);
    }
}

这样,我们只需要调用Writer.create()就能得到一名作家了,如果你用SpringBoot,那么就可以把创建出来的Writer代理对象注册为一个Bean,在其他Controller、Service中任意使用了,你甚至可以基于同样的思路定义更多的角色扮演,比如算命大师、取名大师、冷笑话大师等等。

源码分析

这一节中主要逻辑为:

  1. 代理对象的创建流程
  2. 代理对象的方法执行流程

代理对象的创建流程

创建代理对象是通过AiServices.create(Writer.class, model)进行的,由于AiServices是一个抽象类,源码中有一个默认的子类DefaultAiServices,核心实现源码都在DefaultAiServices中。

DefaultAiServices的build方法就是用来创建指定接口的代理对象:

public T build() {

	// 验证是否配置了ChatLanguageModel
	performBasicValidation();

	// 验证接口中是否有方法上加了@Moderate,但是又没有配置ModerationModel
	for (Method method : context.aiServiceClass.getMethods()) {
		if (method.isAnnotationPresent(Moderate.class) && context.moderationModel == null) {
			throw illegalConfiguration("The @Moderate annotation is present, but the moderationModel is not set up. " +
									   "Please ensure a valid moderationModel is configured before using the @Moderate annotation.");
		}
	}

	// JDK动态代理创建代理对象
	Object proxyInstance = Proxy.newProxyInstance(
		context.aiServiceClass.getClassLoader(),
		new Class<?>[]{context.aiServiceClass},
		new InvocationHandler() {

			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
				// ...
			}

		});

	return (T) proxyInstance;
}

可以发现,其实就是用的JDK动态代理机制创建的代理对象,只不过在创建代理对象之前有两步验证:

  1. 验证是否配置了ChatLanguageModel:这一步不难理解,如果代理对象没有配置ChatLanguageModel,那就利用不上大模型的能力了
  2. 验证接口中是否有方法上加了@Moderate,但是又没有配置ModerationModel

@Moderate和ModerationModel

Moderate是温和的意思,这是一种安全机制,比如:

@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
@Moderate
String write(String title);

我们在write()方法上加了@Moderate注解,那么当调用write()方法时,会调用两次大模型:

  1. 首先是配置的ModerationModel,如果没有配置则创建代理对象都不会成功,ModerationModel会对方法的输入进行审核,看是否涉及敏感、不安全的内容。
  2. 然后才是配置的ChatLanguageModel

配置ModerationModel的方式如下:

interface Writer {

	@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
	@Moderate
	String write(String title);

	static Writer create() {
		ChatLanguageModel model = OpenAiChatModel.builder()
			.baseUrl("http://langchain4j.dev/demo/openai/v1")
			.apiKey("demo")
			.build();

		ModerationModel moderationModel = OpenAiModerationModel.builder()
			.baseUrl("http://langchain4j.dev/demo/openai/v1")
			.apiKey("demo")
			.build();

		return AiServices.builder(Writer.class)
			.chatLanguageModel(model)
			.moderationModel(moderationModel)
			.build();
	}

}

虽然ChatLanguageModel和ModerationModel都是OpenAi,但是你可以理解为OpenAiModerationModel在安全方面更近专业。

代理对象的方法执行流程

代理对象创建出来之后,就可以指定代理对象的方法了,而一旦执行代理对象的方法就是进入到上述源码中InvocationHandler的invoke()方法,而这个invoke()方法是LangChain4j中的最为重要的,里面涉及的组件、功能是非常多的,而本节我们只关心是怎么解析@SystemMessage得到系统提示词,然后组合用户输入的标题,最后发送给大模型得到响应结果的。

在invoke()方法的源码中有这么两行代码:

Optional<SystemMessage> systemMessage = prepareSystemMessage(method, args);
UserMessage userMessage = prepareUserMessage(method, args);

分别调用了prepareSystemMessage()和prepareUserMessage()两个方法,而入参都是代理对象当前正在执行的方法和参数。

在看prepareSystemMessage()方法之前,我们需要再了解一个跟@SystemMessage有关的功能,前面我们是这么定义SystemMessage的:

@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")

其中200是固定的,但是作为一名作家不可能永远只能写200字以内的作文,而这个字数应该都用户来指定,也就是说200应该得是个变量,那么我们可以这么做:

@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇{{num}}字以内的作文")
String write(@UserMessage String title, @V("num") int num);

其中{num}就是变量,该变量的值由用户在调用write方法时指定,注意由于write()有两个参数了,需要在title参数前面定义@UserMessage,表示title是用户消息。

这样我们就可以让Write写一篇任意字数以内的文章了:

String content = writer.write("我最爱的人", 300);

知道了这个场景,我们再来看prepareSystemMessage()方法的实现:

private Optional<SystemMessage> prepareSystemMessage(Method method, Object[] args) {

	// 得到当前正在执行的方法参数
	Parameter[] parameters = method.getParameters();

	// 解析方法参数前定义的@V注解,@V的value为Map的key,对应的参数值为Map的value
	Map<String, Object> variables = getPromptTemplateVariables(args, parameters);

	// 解析方法上的@SystemMessage注解
	dev.langchain4j.service.SystemMessage annotation = method.getAnnotation(dev.langchain4j.service.SystemMessage.class);
	if (annotation != null) {

		// 拼接多个SystemMessage注解
		String systemMessageTemplate = String.join(annotation.delimiter(), annotation.value());
		if (systemMessageTemplate.isEmpty()) {
			throw illegalConfiguration("@SystemMessage's template cannot be empty");
		}

		// 填充变量
		Prompt prompt = PromptTemplate.from(systemMessageTemplate).apply(variables);
		
		// 返回最终的SystemMessage对象
		return Optional.of(prompt.toSystemMessage());
	}

	return Optional.empty();
}

从源码看出@SystemMessage注解的value属性是一个String[]:

@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface SystemMessage {
	
    String[] value();

    String delimiter() default "\n";
}

表示如果系统提示词比较长,可以写成多个String,不过最后会使用delimiter的值将这多个String拼接为一个SystemMessage,并且在拼接完以后会根据@V的值填充SystemMessage中的变量,从而得到最终的SystemMessage。

再来看prepareUserMessage()方法,本节我们只关心:

// 如果有多个参数,获取加了@UserMessage注解参数的值作为UserMessage
for (int i = 0; i < parameters.length; i++) {
	if (parameters[i].isAnnotationPresent(dev.langchain4j.service.UserMessage.class)) {
		String text = toString(args[i]);
		if (userName != null) {
			return userMessage(userName, text);
		} else {
			return userMessage(text);
		}
	}
}

// 如果只有一个参数,则直接使用该参数值作为UserMessage
if (args.length == 1) {
	String text = toString(args[0]);
	if (userName != null) {
		return userMessage(userName, text);
	} else {
		return userMessage(text);
	}
}

还是比较简单的:

  1. 如果有多个参数,获取加了@UserMessage注解参数的值作为UserMessage
  2. 如果只有一个参数,则直接使用该参数值作为UserMessage

这样就得到了最终的SystemMessage和UserMessage,那么如何将他们组装在一起呢?

还记得上一节提到的历史对话吗?请看代码:

List<ChatMessage> messages;
if (context.hasChatMemory()) {
	messages = context.chatMemory(memoryId).messages();
} else {
	
	messages = new ArrayList<>();
	
	// 添加SystemMessage
	systemMessage.ifPresent(messages::add);

	// 添加UserMessage
	messages.add(userMessage);
}

我们还没有设置ChatMemory,所以组装的逻辑其实就是按顺序将SystemMessage和UserMessage添加到一个List中,后续只要将这个List传入给ChatLanguageModel的generate()方法就可以了。

那么ChatLanguageModel的generate()方法是如何处理List的呢?会拼接为一个字符串吗?比如:“请扮演一名作家,根据输入的文章题目写一篇300字以内的作文,我最爱的人”,并不会,我们看看OpenAiChatModel的实现:

public static List<Message> toOpenAiMessages(List<ChatMessage> messages) {
	return messages.stream()
		.map(InternalOpenAiHelper::toOpenAiMessage)
		.collect(toList());
}

在正式调用OpenAi的接口之前,OpenAiChatModel会利用toOpenAiMessages来处理List,不过注意它的返回仍然是一个List,只不过变成了List,ChatMessage是LangChain4j定义的,Message是OpenAi定义的,实际上它们并没有太多的区别,我们看下转换之后的List:
image.png
content确实没有区别,多了role属性,意义其实是一样的,SYSTEM表示系统提示词,USER表示用户提示词,那为什么这样可以呢?是因为OpenAi提供的接口本来就支持通过这种方式来设置系统提示词,比如:
image.png
大家可以访问openai-api-reference详细了解,同时大家也要注意到OpenAi的接口还支持Assistant message和Tool message两种类型,这两种类型是跟工具机制有关系的,我们后续会进行分析。

本节总结

本节我们学习了什么是AiService以及基本应用,我们制作了一个用户可以指定字数和标题的作家应用,同时我们还研究了AiService的基本工作原理和源码,其中我们再次提到了ChatMemory,那么下节内容我们就来介绍到底什么是ChatMemory。

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

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

相关文章

高质量AIGC/ChatGPT/大模型资料分享

2023年要说科技圈什么最火爆&#xff0c;一定是ChatGPT、AIGC&#xff08;人工智能生成内容&#xff09;和大型语言模型。这些技术前沿如同科技世界的新潮流&#xff0c;巨浪拍岸&#xff0c;引发各界关注。ChatGPT的互动性和逼真度让人们瞠目&#xff0c;它能与用户展开流畅对…

谷歌如何进行失效链接建设?

失效链接建设是一种高效的外链建设策略&#xff0c;通过发现并利用失效链接来提升自己网站的SEO。以下是详细的步骤&#xff1a; 寻找失效页面&#xff1a;你需要使用SEO工具&#xff0c;如Ahrefs&#xff0c;来查找与你的网站内容相关的失效页面。这些页面可能是竞争对手的失…

Vue项目生产环境的打包优化

Vue项目生产环境的打包优化 前言 在这篇文章我们讨论Vue项目生产环境的打包优化&#xff0c;并按步骤展示实际优化过程中的修改和前后对比。 背景 刚开始的打包体积为48.71M 优化 步骤一&#xff1a;删除viser-vue viser-vue底层依赖antv/g2等库一并被删除&#xff0c;…

【EI会议】2024年机械、计算机工程与材料国际会议 (MCEM 2024)

2024年机械、计算机工程与材料国际会议 (MCEM 2024) 2024 International Conference on Mechanical, Computer Engineering and Materials 【重要信息】 大会地点&#xff1a;广州 官网地址&#xff1a;http://www.ismcem.com 投稿邮箱&#xff1a;ismcemsub-conf.com 【注意…

《XR应用开发者头显运行需求调研报告》重磅发布 ,开发者更加关注集成和可扩展性!

近期&#xff0c;LarkXR发布了一项新的解决方案&#xff0c;实现了3D/XR企业级应用全面接入Apple Vision Pro等头显设备。作为长期陪伴在XR行业开发者身边的技术伙伴&#xff0c;Paraverse平行云发起了此次行业调研&#xff0c;希望通过调研更直观地了解开发者在使用头显运行XR…

IDEA 导出ER图无表关系

一、通过IDEA导出的ER图无表关系&#xff0c;如下&#xff1a; 二、解决无表关系方法 1)这是建表时&#xff0c;user_work表中的t_id不规范&#xff0c;导致idea 找不到虚拟外键&#xff0c;也就不能绘制虚拟外键关系。那我们把user_work表t_id命名规范&#xff0c;t_id是user表…

VBA 批量变换文件名

1. 页面布局 在“main”Sheet中按照下面的格式编辑。 2. 实现代码 Private wsMain As Worksheet Private intIdx As LongPrivate Sub getExcelBookList(strPath As String)Dim fso As ObjectDim objFile As ObjectDim objFolder As ObjectSet fso = CreateObject("Scrip…

Steam新用户怎么参加夏促 Steam最新注册账号+下载客户端教程

steam夏促来了&#xff0c;这里给新玩家科普一下&#xff0c;steam就是一个游戏平台&#xff0c;里面的海量的各种游戏&#xff0c;而steam经常会有各种打折的活动&#xff0c;夏促就是其中之一&#xff0c;并且是其中规模最大的之一&#xff0c;涵盖游戏数量多&#xff0c;优惠…

ZW3D二次开发_CAM_添加刀具

在ZW3D2025中可以通过库添加刀具&#xff0c;代码如下&#xff1a; int idx_tool;int ret ZwCamToolInsertFromLibrary("", "001 METRIC TOOLS.xlsx", "10 mm Flat Endmill", &idx_tool); 平台功能添加刀具如下&#xff1a;

【Linux】进程间通信_2

文章目录 七、进程间通信1. 进程间通信分类管道 未完待续 七、进程间通信 1. 进程间通信分类 管道 管道的四种情况&#xff1a; ①管道内部没有数据&#xff0c;并且具有写端的进程没有关闭写端&#xff0c;读端就要阻塞等待&#xff0c;知道管道pipe内部有数据。 ②管道内部…

esp8266 GPIO

功能综述 ESP8266 的 16 个通⽤ IO 的管脚位置和名称如下表所示。 管脚功能选择 功能选择寄存器 PERIPHS_IO_MUX_MTDI_U&#xff08;不同的 GPIO&#xff0c;该寄存器不同&#xff09; PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U,FUNC_GPIO12);PERIPHS_IO_MUX_为前缀。后面的…

uniapp开发企业微信内部应用

最近一直忙着开发项目&#xff0c;终于1.0版本开发完成&#xff0c;抽时间自己总结下在项目开发中遇到的技术点。此次项目属于自研产品&#xff0c;公司扩展业务&#xff0c;需要在企业微信中开发内部应用。因为工作中使用的是钉钉&#xff0c;很少使用企业微信&#xff0c;对于…

C# 警告 warning MSB3884: 无法找到规则集文件“MinimumRecommendedRules.ruleset”

警告 warning MSB3884: 无法找到规则集文件“MinimumRecommendedRules.ruleset” C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\amd64\Microsoft.CSharp.CurrentVersion.targets(129,9): warning MSB3884: 无法找到规则集文件“MinimumRe…

Android Lint

文章目录 Android Lint概述工作流程Lint 问题种类Lint 警告严重性 用命令运行 LintAndroidStudio 使用 Lint忽略 Lint 警告gradle 配置 Lint查找无用资源文件 Android Lint 概述 Lint 是 Android 提供的 代码扫描分析工具&#xff0c;它可以帮助我们发现代码结构/质量问题&am…

springboot加载注入bean的方式

在SpringBoot的大环境下&#xff0c;基本上很少使用之前的xml配置Bean&#xff0c;主要是因为这种方式不好维护而且也不够方便。 springboto注入bean主要采用下图几种方式&#xff0c;分为本地服务工程注解声明的bean和外部依赖包中的bean。 一、 springboot装配本地服务工程…

国产Cortex-A55人工智能教学实验箱_基于Python机械臂跳舞实验案例分享

一、实验目的 本实验通过TL3568-PlusTEB教学实验箱修改机械臂不同舵机的角度&#xff0c;增加延迟时间&#xff0c;从而做到机械臂跳舞的效果。 二、实验原理 ROS&#xff08;机器人操作系统&#xff09; ROS&#xff08;机器人操作系统&#xff09;&#xff0c;是专为机器人…

报工计件工资核算h5开源版开发

报工计件工资核算h5开源版开发 小型计件工资管理系统&#xff0c;支持后台制定工价&#xff0c;核算工资。支持员工H5端报工&#xff0c;和查看工资情况。 H5手机端 支持在线报工&#xff0c;支持查看我的工资。 自定义费用项 在基础计件工资基础上增加扣除和增加项&#xff…

分布式服务测试各节点调用第三方服务连通性

背景&#xff1a;分布式部署 一个主节点往各个节点下发任务&#xff08;调用第三方服务&#xff09;&#xff0c;目的是为了测试各节点与第三方的连通性 思路&#xff1a; 主节点实现 创建Spring Boot项目&#xff1a;作为主节点的后端服务。 集成Eureka客户端&#xff1a;在…

Python28 十大机器学习算法之线性回归和逻辑回归

1.三类广义上的机器学习算法 监督学习。工作原理&#xff1a;该算法由一个目标/结果变量&#xff08;或因变量&#xff09;组成&#xff0c;该变量将从一组给定的预测变量&#xff08;自变量&#xff09;进行预测。使用这组变量&#xff0c;我们生成了一个将输入数据映射到所…

阿里云服务器入门使用教程——购买及操作系统选择并进行远程连接

文章目录 一、首先选择一个你自己要买的云服务器类型二、能选的就一个地域和一个操作系统&#xff0c;其他都是固定的三、创建完实例并使用finalshell连接的效果(要在完成后续步骤后才能连接)四、购买之后进入阿里云控制台&#xff0c;开通资源中心五、然后就可以看到已经帮你创…