LangChain4j-AiServices实现原理解析
Hi,我是阿昌,今天记录下LangChain4j里面AiServices的实现原理。
一开始看这个东西的时候,我觉得它的写法还是挺有意思的。因为我们只是写了一个接口,并没有写实现类,但是最后可以直接调用请求发给大模型。
示例代码如下:
public class AiServicesDemo {
public static void main(String[] args) {
ChatLanguageModel model =
OpenAiChatModel.builder()
.baseUrl(Env.BASE_URL)
.apiKey(Env.APP_KEY)
.modelName("gpt-5.4")
.build();
Assistant assistant = AiServices.create(Assistant.class, model);
String chat = assistant.chat("你好,我叫阿昌,很高兴认识你");
System.out.println(chat);
}
interface Assistant {
String chat(String userMessage);
}
}
这里最容易让人疑惑的点是:
Assistant明明只是一个接口,连实现类都没有,为什么可以调用assistant.chat(...)?
所以下面会,简单看下AiServices到底做了什么。
一、先把这件事说简单点
AiServices做的事情,其实可以理解为:
它帮我们在运行时生成了一个接口实现类。
只不过这个实现类不是我们自己手写的,而是通过JDK动态代理生成的。
调用:
assistant.chat("你好");
表面上看是调用接口方法,实际执行的是代理对象里面的统一拦截逻辑。
这个代理逻辑大概做了这么几步:
拿到方法参数
↓
把String转成UserMessage
↓
组装ChatRequest
↓
调用ChatLanguageModel
↓
拿到ChatResponse
↓
把AiMessage转成方法声明的返回类型
在当前例子里面,返回类型是String,所以最后就是返回AiMessage.text()。
二、AiServices.create不是直接new对象
先看这行代码:
Assistant assistant = AiServices.create(Assistant.class, model);
它不是去找AssistantImpl,也不是帮自动编译生成一个.java文件。
源码里面create方法大概是这样的:
public static <T> T create(Class<T> aiService, ChatLanguageModel chatLanguageModel) {
return builder(aiService).chatLanguageModel(chatLanguageModel).build();
}
也就是说,create只是一个便捷入口。
真正关键的点在后面的build():
aiService记录的是接口类型,也就是Assistant.classchatLanguageModel记录的是模型对象,也就是创建的OpenAiChatModelbuild()负责把这两个东西组装起来,最后返回一个代理对象
所以这里一定要注意:
Assistant assistant = AiServices.create(Assistant.class, model);
返回的assistant不是普通实现类对象,而是一个代理对象。
三、代理对象怎么来的?
继续看DefaultAiServices#build,里面能看到熟悉的Proxy.newProxyInstance:
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 {
//....
}
});
这个就是JDK动态代理。
翻译一下就是:
创建一个对象,让它实现
Assistant接口;以后这个接口里面的方法被调用时,都走InvocationHandler#invoke。
所以当我们调用:
assistant.chat("你好,我叫阿昌,很高兴认识你");
实际上会变成类似下面这种调用:
invoke(proxy, chat方法, ["你好,我叫阿昌,很高兴认识你"]);
到这里就清楚了。
Assistant接口确实没有实现类,但是AiServices在运行时给它做了一个代理实现。
四、方法参数是怎么变成用户消息的?
定义的方法是:
interface Assistant {
String chat(String userMessage);
}
这个方法很简单:
- 没有
@UserMessage - 没有
@V - 只有一个普通的
String参数
LangChain4j对这种情况有一个默认约定:如果方法只有一个无注解参数,那么这个参数就当作用户输入。
源码里有个方法就专门处理这个场景:
private static Optional<String> findUserMessageTemplateFromTheOnlyArgument(
Parameter[] parameters, Object[] args) {
if (parameters != null && parameters.length == 1 && parameters[0].getAnnotations().length == 0) {
return Optional.of(toString(args[0]));
}
return Optional.empty();
}
所以这个调用:
assistant.chat("你好,我叫阿昌,很高兴认识你");
会先被当成一段用户消息内容。
后面还会走一层PromptTemplate:
String template = getUserMessageTemplate(method, args);
Map<String, Object> variables = findTemplateVariables(template, method, args);
Prompt prompt = PromptTemplate.from(template).apply(variables);
return prompt.toUserMessage();
为什么这里还要搞一个PromptTemplate?
因为AiServices不只是支持这种简单字符串,它还支持模板变量。
比如:
interface Assistant {
@UserMessage("请把下面内容翻译成英文:{{content}}")
String translate(@V("content") String content);
}
这种写法里面,{{content}}就会被真实参数替换。
不过回到当前例子,它没有模板变量,所以最后就可以理解成:
UserMessage.from("你好,我叫阿昌,很高兴认识你");
五、UserMessage接下来怎么走?
大模型接口并不是直接吃我们这个String参数。
在LangChain4j里面,对话消息有自己的抽象,比如:
UserMessage:用户消息AiMessage:模型回复SystemMessage:系统消息ChatMessage:上面这些消息的统一抽象
所以AiServices会先把用户输入变成UserMessage,再把它放到消息列表里面:
List<ChatMessage> messages = new ArrayList<>();
messages.add(userMessage);
然后组装成ChatRequest:
ChatRequest chatRequest = ChatRequest.builder()
.messages(messages)
.parameters(parameters)
.build();
最后调用底层模型:
ChatResponse chatResponse = context.chatModel.chat(chatRequest);
这里的context.chatModel,就是我们一开始传进去的OpenAiChatModel。
所以从这里开始,就进入真正的大模型调用逻辑了:
AiServices代理对象
↓
ChatRequest
↓
OpenAiChatModel
↓
OpenAI兼容接口
在OpenAiChatModel#doChat里面,会把LangChain4j自己的ChatRequest再转换成OpenAI格式的请求对象,然后通过HTTP请求发出去。
这一层其实就是适配不同模型厂商的地方。
六、返回值为什么能直接拿到String?
模型返回后,LangChain4j拿到的不是直接的String,而是:
ChatResponse
ChatResponse里面包含模型回复:
AiMessage
但是我们接口方法声明的是:
String chat(String userMessage);
所以这里还有一步返回值转换。
这块逻辑在ServiceOutputParser里面:
AiMessage aiMessage = response.content();
if (rawClass == AiMessage.class) {
return aiMessage;
}
String text = aiMessage.text();
if (rawClass == String.class) {
return text;
}
这段就很好理解了。
如果你的方法返回值是:
String
它就返回:
aiMessage.text()
如果你方法返回值写的是:
AiMessage
那它就直接把AiMessage返回给你。
如果你返回的是Boolean、Integer、List<String>、自定义对象之类的类型,它就会继续找对应的OutputParser去解析。
所以当前例子里:
String chat = assistant.chat("你好,我叫阿昌,很高兴认识你");
最终拿到的就是模型回复文本。
七、用普通代码还原一下
为了方便理解,先不看动态代理。
假设我们自己手写这段逻辑,大概会写成这样:
String userInput = "你好,我叫阿昌,很高兴认识你";
UserMessage userMessage = UserMessage.from(userInput);
ChatRequest chatRequest = ChatRequest.builder()
.messages(userMessage)
.build();
ChatResponse chatResponse = model.chat(chatRequest);
String result = chatResponse.aiMessage().text();
而AiServices的价值就是:
它把上面这些通用代码藏到了代理对象里面。
所以只需要写:
String result = assistant.chat("你好,我叫阿昌,很高兴认识你");
看起来像本地方法调用,实际背后是一次大模型请求。
八、这个设计有什么好处?
直接调用模型当然也可以,比如手动构造UserMessage、ChatRequest、再解析ChatResponse。
但是如果业务里面到处都是这种代码,很快就会变乱。
比如商品场景里面,可能会有这些能力:
interface ProductAiService {
@UserMessage("请根据商品标题生成5个卖点:{{title}}")
List<String> generateSellingPoints(@V("title") String title);
@UserMessage("判断下面的用户评价是正向还是负向:{{content}}")
Boolean isPositiveComment(@V("content") String content);
}
业务代码调用的时候就比较舒服:
List<String> sellingPoints = productAiService.generateSellingPoints("夏季纯棉短袖T恤");
这时候接口方法本身就表达了业务含义。
至于底层怎么组装消息、怎么调模型、怎么解析结果,都交给AiServices统一处理。
这也是它比直接写一堆模型调用代码更适合业务封装的地方。
九、这里不要理解成玄学
刚开始看到这个写法,可能会觉得有点“神奇”:
Assistant assistant = AiServices.create(Assistant.class, model);
assistant.chat("你好");
但拆开看以后,其实都是Java里面比较常见的东西:
JDK动态代理:运行时创建接口实现类反射:读取方法、参数、注解、返回值类型PromptTemplate:处理提示词模板ChatRequest:封装模型请求ServiceOutputParser:处理模型返回值
所以它不是接口自己有能力调用AI,而是:
LangChain4j在接口外面套了一层代理,代理里面帮我们完成了大模型调用。
十、总结
回到最开始的问题:
Assistant只是接口,为什么可以直接调用?
因为:
AiServices.create返回的是一个实现了Assistant接口的动态代理对象。
整个调用过程可以简单记成:
assistant.chat(String)
↓
代理对象拦截
↓
String变成UserMessage
↓
UserMessage变成ChatRequest
↓
ChatLanguageModel发起模型调用
↓
ChatResponse里面取AiMessage
↓
AiMessage.text()作为String返回
所以AiServices最核心的作用,就是把一次普通Java方法调用,翻译成一次大模型调用。
理解到这一层,再看后面的@UserMessage、@SystemMessage、@MemoryId、Tool、RAG这些扩展能力,就会顺很多。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)