上一个系列讲了Spring AI得到反馈效果不错,有人私信我说这个和Langchain4j有什么区别。如果站在使用方面,都是基于Java的大模型应用研发的工具,本质上没太大区别。但是从细节层面来说还是有很多不同之处,所以索性借此机会,给大家分享一下Langchain4j框架。在本系列中会按照Spring AI系列的顺序来写Langchain4j,这样的好处是可以对比两者不同的细节。
注意 :由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是langchain4j-1.9.1,JDK版本使用的是19。另外本系列尽量使用Java原生态,尽量不依赖于Spring和Spring Boot。虽然langchain4j也支持Spring Boot集成,但是如果是使用Spring Boot框架,那为何不索性使用Spring AI。
本系列的所有代码地址: https://github.com/forever1986/langchain4j-study
目录
- [1 工具调用(AI Services)](#1 工具调用(AI Services))
-
- [1.1 基于ToolSpecification的同步示例](#1.1 基于ToolSpecification的同步示例)
- [1.2 基于@Tool的同步示例](#1.2 基于@Tool的同步示例)
- [1.3 Streaming流式模式调用工具](#1.3 Streaming流式模式调用工具)
- [2 源码解析](#2 源码解析)
-
- [2.1 工具定义](#2.1 工具定义)
- [2.2 同步模式下使用工具](#2.2 同步模式下使用工具)
- [2.3 流式模式下使用工具](#2.3 流式模式下使用工具)
- [3 与Spring AI的比较](#3 与Spring AI的比较)
上一章演示了ChatModel方式下调用工具,需要编写代码比较多,用起来非常麻烦。这一章将演示使用AI Services方式,感受一下Langchain4j 为用户所做的事情。
1 工具调用(AI Services)
话不多说,先使用几个示例展现如何使用AI Services调用工具
1.1 基于ToolSpecification的同步示例
代码参考lesson06子模块
1)在lesson06子模块下,创建Assistant接口
java
package com.langchain.lesson06.sync.service;
public interface Assistant {
String chat(String userMessage);
}
2)在lesson06子模块下,创建FunctionCallAiServicesTest类
java
package com.langchain.lesson06.sync.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.langchain.lesson06.tools.ToolUtils;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolExecutor;
import java.util.Map;
public class FunctionCallAiServicesTest {
public static void main(String[] args) {
// 1. 获取API KEY
String apiKey = System.getenv("ZHIPU_API_KEY");
// 2.定义工具
ToolSpecification toolSpecification = ToolSpecification.builder()
.name("getLocalTime")
.description("根据传入国家或地区的名字,获取所在国家或地区时区的当前日期和时间")
.parameters(JsonObjectSchema.builder()
.addStringProperty("zone", "国家或地区的名字")
.required("zone") // 必填的属性应明确加以说明
.build())
.build();
// 3.定义工具执行器
ToolExecutor toolExecutor = (request, memoryId) -> {
String zoneId ="";
try {
JsonNode jsonNode = new ObjectMapper().readTree(request.arguments());
zoneId = jsonNode.get("zone").textValue();
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return ToolUtils.getCurrentDateTime(zoneId);
};
// 4. 构建模型
ChatModel model = OpenAiChatModel.builder()
.apiKey(apiKey)
.baseUrl("https://open.bigmodel.cn/api/paas/v4")
.modelName("glm-4-flash-250414")
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.tools(Map.of(toolSpecification,toolExecutor)) // 传入工具描述以及工具执行器
.build();
// 5. 访问大模型
String response = assistant.chat("请问洛杉矶现在几点?");
System.out.println(response);
}
}
3)运行FunctionCallAiServicesTest测试,结果如下:

说明:首先从代码层面看,比之前ChatModel的方式简便了很多。定义一个ToolSpecification和ToolExecutor,然后使用tools(Map.of(toolSpecification,toolExecutor)) 方法传入给大模型,后续AiServices会自行调用工具并返回最终结果。但是这样还是比较麻烦,下面看看通过@Tool注解方式
1.2 基于@Tool的同步示例
代码参考lesson06子模块
1)在lesson06子模块下,新建FunctionCallAiServices2Test类
java
package com.langchain.lesson06.sync.service;
import com.langchain.lesson06.tools.TimeTool;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
public class FunctionCallAiServices2Test {
public static void main(String[] args) {
// 1. 获取API KEY
String apiKey = System.getenv("ZHIPU_API_KEY");
// 2. 构建模型
ChatModel model = OpenAiChatModel.builder()
.apiKey(apiKey)
.baseUrl("https://open.bigmodel.cn/api/paas/v4")
.modelName("glm-4-flash-250414")
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new TimeTool()) // 这里将工具传入
.build();
// 3. 访问大模型
String response = assistant.chat("请问洛杉矶现在几点?");
System.out.println(response);
}
}
2)运行FunctionCallAiServices2Test测试,结果如下:

说明:从上面的示例可以看到,现在的代码量指数级下降,与没有调用工具的模式只是多了一句代码:tools(new TimeTool()) 。可见Langchain4j 已经封装得多简洁,在这里可以看出AiServices的好处了吧。下面再通过流式模式演示一下AiServices如何调用工具
1.3 Streaming流式模式调用工具
代码参考lesson06子模块
1)在lesson06子模块下,新建StreamAssistant接口
java
package com.langchain.lesson06.streaming.service;
import dev.langchain4j.service.TokenStream;
public interface StreamAssistant {
TokenStream chat(String userMessage);
}
2)在lesson06子模块下,新建FunctionCallStreamingAiServicesTest类
java
package com.langchain.lesson06.streaming.service;
import com.langchain.lesson06.tools.TimeTool;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.TokenStream;
import java.util.concurrent.CompletableFuture;
public class FunctionCallStreamingAiServicesTest {
public static void main(String[] args) throws InterruptedException {
// 1. 获取API KEY
String apiKey = System.getenv("ZHIPU_API_KEY");
// 2. 构建模型
OpenAiStreamingChatModel model = OpenAiStreamingChatModel.builder()
.apiKey(apiKey)
.baseUrl("https://open.bigmodel.cn/api/paas/v4")
.modelName("glm-4-flash-250414")
.build();
StreamAssistant streamAssistant = AiServices.builder(StreamAssistant.class)
.streamingChatModel(model)
.tools(new TimeTool())
.build();
// 3. 访问大模型
TokenStream tokenStream = streamAssistant.chat("请问洛杉矶现在几点?");
// 4.异步输出处理
CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();
tokenStream
// .onPartialResponse(System.out::println) //用于每一段的返回
// .onPartialThinking(System.out::println) //用于大模型COT的思考过程
// .onRetrieved(System.out::println) //用于RAG的检索
// .onIntermediateResponse(System.out::println)
// .beforeToolExecution(System.out::println) //用于工具调用之前
// .onToolExecuted(System.out::println)//用于工具调用
.onCompleteResponse((ChatResponse response) -> {
System.out.println(response.aiMessage().text());
futureResponse.complete(response);
})
.onError(futureResponse::completeExceptionally)
.start();
futureResponse.join();
}
}
3)运行FunctionCallStreamingAiServicesTest测试,结果如下:

说明:从上面示例中可以看到,其代码也只是没有使用工具的代码中增加了tools(new TimeTool())这一句代码。
2 源码解析
从上面的示例中,可以看到Langchain4j 的AiServices为用户封装了很多底层复杂逻辑,让用户能非常方便使用。现在就来通过源码解析一下底层是如何做的,其实在之前使用ChatModel调用工具时,已经可以看到部分类似的逻辑。
2.1 工具定义
1)首先看看tools()方法,在 AiServices 中有几个重载的tools()方法,其最底层都是调用 ToolService 来解析工具

2)那继续进入 ToolService 类,看看是如何创建工具的。从下图可以看出,无论传入Map或者Object,都会最终被解析放入变量 toolSpecifications 和 toolExecutors ,其中 toolSpecifications 就是工具描述(这是给大模型的提示词用的),而 toolExecutors 则是工具执行器(这是根据大模型返回的结果调用工具)。(这一点在之前ChatModel手搓调用工具的示例中也可以看出一个大概)

2.2 同步模式下使用工具
1)好了,有了 toolSpecifications 和 toolExecutors ,再来看看 AiServices 如何使用工具。打开到 AiServices 的实现类 DefaultAiServices 的第365行代码,可以看到这里会将工具的定义 toolSpecifications 传入到chatRequst,chatRequst是给大语言模型的请求参数。

2)再看看 DefaultAiServices 的第392行到424行代码,这里就是实现调用工具逻辑的地方,如下图:

3)点进去 ToolService 的executeInferenceAndToolsLoop()方法,如下图所示(这里为了方便展示将代码折叠一下)。可以看到是循环调用工具,中间会将工具的结果进行返回或者再扔给大语言模型结合工具的返回结果和问题进行二次回答

4)至此,关于 AiServices 的同步模式下实现工具调用已经基本明朗。其实在上一章写ChatModel方式调用工具示例时,已经把基本流程写了一遍,只不过是没有AiServices写得这么完整。主要有如下3个关键点:
- ① 先将工具定义放到chatRequest中,让大语言模型可以知道有工具可以选择
- ② 大语言模型如果要使用工具,返回的AiMessage的hasToolExecutionRequests会告诉用户需要调用工具
- ③ 如果需要大语言模型结合工具的调用结果和问题再一次回复,则还需要调用一次大语言模型得到最终答复
2.3 流式模式下使用工具
接下来再看看流式模式的源码实现
1)先定位到 DefaultAiServices 的第332行代码,这里是流式模式下的逻辑,这里会将工具的定义和执行器都传入到参数中

说明:上面红色框有几个跟工具有关系
- toolSpecifications(toolServiceContext.toolSpecifications()) :把工具定义传入到AiServiceTokenStreamParameters参数中
- toolExecutors(toolServiceContext.toolExecutors()):把工具的执行器传入到AiServiceTokenStreamParameters参数中
- toolArgumentsErrorHandler(context.toolService.argumentsErrorHandler()):参数错误处理handler
- toolExecutionErrorHandler(context.toolService.executionErrorHandler()):执行错误的handler
- toolExecutor(context.toolService.executor()):并行执行器,可以多个工具同时执行的情况下使用
2)在《Langchain4j 系列之四 - AI Services(流式模式)》源码解析中,可以知道流式模式是使用 AiServiceTokenStream 类start()方法来实现最终流式处理,直接看 AiServiceTokenStream 类start()方法的第215行代码,可以看到最终就是调用 StreamingChatModel 类,其参数会传入一个handler,这个handler就是一个 AiServiceStreamingResponseHandler (记住这个handler)

3)从上面知道是流式模式最终都是通过 StreamingChatModel 调用大模型,而在之前《Langchain4j 系列之四 - AI Services(流式模式)》源码解析中已经分析了 StreamingChatModle 调用大语言模型会返回3种流数据,而工具调用的实现逻辑在其中onComplete()方法中实现,如下图

4)看看onComplete()方法处理,该方法是大语言模型有返回最终结果时调用,其中第159行代码是通过调用onCompleteResponse方法。进入onCompleteResponse方法。该方法是 InternalStreamingChatResponseHandlerUtils 类的一个静态方法。这个方法中的通过调用handler的,那么这个handler从哪里来的,是从 AiServiceTokenStream 的第188行代码中创建的 AiServiceStreamingResponseHandler (也就是第2)步中分析到的handler)

5)再看看 AiServiceStreamingResponseHandler 中实现的onCompleteResponse()方法,是不是很熟悉。在前面《Langchain4j 系列之十 - 工具调用(ChatModel)》宗使用原生态ChatModel的实现方式也是这个流程(只不过这里实现的更为完整)

至此关于AI Services方式下调用工具的流程就明朗了。
3 与Spring AI的比较
这里也可以跟前面的Spring AI系列《Spring AI 系列之六 - 工具调用》,这里从两方面比较一下:
- 定义:Spring AI 和Langchain4j 都是可以使用编码和注解方式直接定义,因此这方面不分上下
- 使用:使用上都是比较方便,但是Langchain4j 提供了很多高级用法,比如外部参数传入、聊天记忆等等功能(后面章节会讲到),因此相对来说Langchain4j 的实现更为丰富
- 底层设计:Langchain4j 无论是同步模式还是流式模式,最终在ToolService或者AiServiceStreamingResponseHandler类上面都是设计了一套工具调用流程。也就是说在AI Services下把工具调用流程完全与大模型实现剥离开。而Spring AI的底层也是通过DefaultToolCallingManager来实现工具获取和工具调用,但是需要在大模型ChatModel的实现类的call方法或者stream方法实现。感兴趣可以去看看Spring AI 中的OpenAiChatModel和ZhiPuAiChatModel,其call和stream方法中调用工具基本上都是重复。因此我个人觉得Langchain4j 在工具调用这一块抽象得更好,使得大模型厂商集成更为便利。
结语:本章讲述了使用AI Services的方式实现同步和流式的调用工具,并解析了其中的源码。可以看到AI Services方式下使用工具是非常简便,从源码分析中也知道AI Services底层为用户做了很多事情,这就是AI Services的好处。
Langchain4j 系列上一章:《Langchain4j 系列之十 - 工具调用(ChatModel)》
Langchain4j 系列下一章:《Langchain4j 系列之十二 - 工具调用(高级用法之一)》