基于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。

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

极客Kimi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值