顾名思义,这个应用就是希望能利用大模型的能力来帮助我写文章,那这样一个应用该如何利用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中任意使用了,你甚至可以基于同样的思路定义更多的角色扮演,比如算命大师、取名大师、冷笑话大师等等。
源码分析
这一节中主要逻辑为:
- 代理对象的创建流程
- 代理对象的方法执行流程
代理对象的创建流程
创建代理对象是通过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动态代理机制创建的代理对象,只不过在创建代理对象之前有两步验证:
- 验证是否配置了ChatLanguageModel:这一步不难理解,如果代理对象没有配置ChatLanguageModel,那就利用不上大模型的能力了
- 验证接口中是否有方法上加了@Moderate,但是又没有配置ModerationModel
@Moderate和ModerationModel
Moderate是温和的意思,这是一种安全机制,比如:
@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
@Moderate
String write(String title);
我们在write()方法上加了@Moderate注解,那么当调用write()方法时,会调用两次大模型:
- 首先是配置的ModerationModel,如果没有配置则创建代理对象都不会成功,ModerationModel会对方法的输入进行审核,看是否涉及敏感、不安全的内容。
- 然后才是配置的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);
}
}
还是比较简单的:
- 如果有多个参数,获取加了@UserMessage注解参数的值作为UserMessage
- 如果只有一个参数,则直接使用该参数值作为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:
content确实没有区别,多了role属性,意义其实是一样的,SYSTEM表示系统提示词,USER表示用户提示词,那为什么这样可以呢?是因为OpenAi提供的接口本来就支持通过这种方式来设置系统提示词,比如:
大家可以访问openai-api-reference详细了解,同时大家也要注意到OpenAi的接口还支持Assistant message和Tool message两种类型,这两种类型是跟工具机制有关系的,我们后续会进行分析。
本节总结
本节我们学习了什么是AiService以及基本应用,我们制作了一个用户可以指定字数和标题的作家应用,同时我们还研究了AiService的基本工作原理和源码,其中我们再次提到了ChatMemory,那么下节内容我们就来介绍到底什么是ChatMemory。