AiService
利用AI服务组件来完成包括创建对象、调用其方法并传递参数,通过@SystemMmessage注解和@V注解来指定系统提示词和变量,以及运用这些元素来控制输出内容。如何扩展功能,如自定义输出长度。
public class _02_AiService {
// 定义一个接口Writer
interface Writer {
String write(String title);
}
public static void main(String[] args) {
// 创建代理对象时,传入一个提前定义了的ChatLanguageModel
ChatLanguageModel model = OpenAiChatModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.build();
// 利用定义AiService来创建一个Writer代理对象,调用write()方法来写文章
Writer writer = AiServices.create(Writer.class, model);
}
}
String content = writer.write("我最爱的人");
System.out.println(content);
执行代码结果为:
是我的家人,他们是我生命中最重要的人,无论发生什么事情,他们都会一直支持和爱护着我。他们给予我无限的爱和关怀,让我感到无比幸福和幸运。我愿意为他们奉献一切,尽我所能地去照顾和呵护他们。他们是我生命中最珍贵的存在,我永远都会珍惜和爱护他们。
@SystemMessage预设系统消息
使用@SystemMessage注解告诉大模型让它先扮演一名作家,然后再回答我的问题。
@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
String write(String title);
public class _02_AiService {
interface Writer {
// 定义@SystemMessage,并描述系统提示词
@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);
}
}
那么当我们调用write()方法时,LangChain4j就会自动组合SystemMessage和用户输入的标题,然后发送给大模型,这样大模型就知道自己是一名作家了。
运行以上代码的结果就变为了:
在我生命中,最爱的人是我母亲。她是我生命中最重要的人,也是我永远的依靠和支持。母亲那温暖的微笑,总是能给我无限的力量和勇气。
我记得小时候,母亲总是在我生病时守在我身边,用温柔的手轻轻拍着我的背,给我端来热腾腾的粥汤。她总是用她的爱和关心包裹着我,让我感受到无比的安全和幸福。
母亲是一个坚强而又温柔的女人,她用她的辛勤劳动支撑起这个家庭,用她的慈爱呵护着我们。我常常想,如果没有母亲,我将会是一个怎样的人呢?母亲是我生命中的灯塔,指引着我前行的方向。
无论我遇到什么困难和挑战,母亲总是在我身边默默支持着我。她的爱如同一股暖流,贯穿着我的整个生命。我爱你,母亲,永远都爱你。
@Moderate和ModerationModel安全机制
@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在安全方面更近专业。
ChatMemory
ChatMemory是LangChain4j提供的用来存储历史对话的组件,并且还支持窗口限制、淘汰机制、持久化机制等等扩展功能。
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("你好,我是周瑜");
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来使用:
1.ChatMemory的使用场景
public class _03_ChatMemory {
// 取名大师,通过talk()方法来和大师进行交流
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á)
实现类MessageWindowChatMemory
而这两个实现类内部都有一个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
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<ChatMessage>的大小,而TokenWindowChatMemory会利用指定的Tokenizer对List<ChatMessage>对应的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);
}
完整代码:
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();
// 这才是正确的写法
NamingMaster namingMaster = AiServices.builder(NamingMaster.class)
.chatLanguageModel(model)
.chatMemoryProvider(userId -> TokenWindowChatMemory.builder().id(userId).maxTokens(10000,new OpenAiTokenizer()).build())
.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。
源码分析
AiService
代理对象的创建流程
创建代理对象是通过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
代理对象的方法执行流程
在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方法时指定
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);
}
}
这样就得到了最终的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);
}
按顺序将SystemMessage和UserMessage添加到一个List<ChatMessage>中,后续只要将这个List<ChatMessage>传入给ChatLanguageModel的generate()方法。
AiServices整合ChatMemory源码分析
AiServices中是如何利用ChatMemory来实现对话历史记录的。
DefaultAiServices中的代理对象中的invoke()方法中:
Object memoryId = memoryId(method, args).orElse(DEFAULT);
memoryId()方法其实就是解析方法参数中加了@MemoryId注解的参数值,仅接着就会执行:
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获取出来,然后传入给大模型。