1. Langchain4j工具调用深度解析
作为一名长期深耕Java生态的开发者,我最近在多个生产项目中实践了Langchain4j的工具调用功能。与Spring AI相比,Langchain4j在工具调用方面提供了更灵活的底层控制能力。本文将基于1.9.1版本,通过完整案例演示两种工具定义方式及其在ChatModel中的实现。
提示:本文所有代码示例均可在GitHub仓库langchain4j-study的lesson06模块中找到,环境要求JDK19+。
2. 工具调用的核心概念
2.1 工具调用的本质
工具调用(Tool Calling)本质上是让大语言模型具备执行外部操作的能力。当模型识别到用户请求需要外部数据或操作时,会生成工具调用请求,开发者处理后返回结果,模型再基于结果生成最终响应。
这种机制突破了纯文本生成的限制,使模型能够:
- 查询实时数据(如天气、股价)
- 执行系统操作(发邮件、写数据库)
- 访问专有知识库
- 进行复杂计算
2.2 工具调用的工作流程
典型工具调用包含三个阶段:
- 意图识别:模型判断是否需要调用工具
- 参数生成:模型生成工具调用参数(JSON格式)
- 结果整合:开发者执行工具后,模型解析结果生成响应
java复制// 伪代码示例
if (response.containsToolCall()) {
ToolRequest request = response.getToolRequest();
Object result = executeTool(request);
return model.generateWithToolResult(result);
}
3. 工具定义的两种方式
3.1 ToolSpecification方式
这是显式的工具定义方法,适合需要精细控制参数的场景。下面是一个天气预报工具的完整定义:
java复制ToolSpecification weatherTool = ToolSpecification.builder()
.name("getWeather")
.description("获取指定城市的天气预报")
.parameters(JsonObjectSchema.builder()
.addStringProperty("city", "城市名称")
.addEnumProperty("unit", Arrays.asList("CELSIUS", "FAHRENHEIT"))
.required("city")
.build())
.build();
关键要素说明:
- name:工具的唯一标识,需与实现代码保持一致
- description:直接影响模型是否选择该工具,应明确说明功能边界
- parameters:使用JsonObjectSchema定义参数结构和类型
- required:标记必填参数,避免调用时缺少关键数据
3.2 @Tool注解方式
基于注解的方式更符合Java开发习惯,适合与业务代码集成。以下是等效的天气工具实现:
java复制public class WeatherService {
@Tool("获取指定城市的天气预报")
public WeatherData getWeather(
@P("城市名称") String city,
@P(defaultValue = "CELSIUS") TemperatureUnit unit) {
// 实现代码
}
}
注解说明:
- @Tool:标记工具方法,描述用于模型理解功能
- @P:定义参数,支持默认值和必要标记
- 返回值:可以是任意类型,非String类型会自动转为JSON
经验:复杂工具建议使用@Tool方式,简单工具或需要动态生成时用ToolSpecification
4. ChatModel工具调用实战
4.1 同步调用实现
4.1.1 基础配置
首先建立工具类处理时区时间查询:
java复制public class TimeUtils {
private static final Map<String, String> TIME_ZONES = Map.of(
"北京", "Asia/Shanghai",
"伦敦", "Europe/London",
"洛杉矶", "America/Los_Angeles"
);
public static String getCurrentTime(String location) {
ZoneId zone = ZoneId.of(TIME_ZONES.getOrDefault(location, "Asia/Shanghai"));
return LocalDateTime.now(zone).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}
4.1.2 ToolSpecification实现
完整同步调用流程:
java复制// 1. 定义工具规范
ToolSpecification timeTool = ToolSpecification.builder()
.name("getLocalTime")
.description("获取指定地点的当前时间")
.parameters(JsonObjectSchema.builder()
.addStringProperty("location", "城市名称")
.required("location")
.build())
.build();
// 2. 构建聊天请求
ChatRequest request = ChatRequest.builder()
.messages(UserMessage.from("洛杉矶现在几点?"))
.toolSpecifications(timeTool)
.build();
// 3. 调用模型
ChatResponse response = chatModel.chat(request);
// 4. 处理工具调用
if (response.hasToolCall()) {
ToolExecutionRequest toolCall = response.getToolCall();
String location = parseLocation(toolCall.arguments());
String time = TimeUtils.getCurrentTime(location);
// 5. 提交工具结果
ToolExecutionResultMessage result = ToolExecutionResultMessage.from(toolCall, time);
ChatRequest followUp = ChatRequest.builder()
.messages(Arrays.asList(
request.messages().get(0),
response.aiMessage(),
result
))
.build();
// 6. 获取最终响应
System.out.println(chatModel.chat(followUp).aiMessage().text());
}
4.1.3 @Tool注解实现
更简洁的实现方式:
java复制public class TimeTool {
@Tool("获取指定地点的当前时间")
public String getLocalTime(@P("城市名称") String location) {
return TimeUtils.getCurrentTime(location);
}
}
// 使用ToolService自动处理
ToolService toolService = new ToolService();
toolService.registerTool(new TimeTool());
ChatRequest request = ChatRequest.builder()
.messages(UserMessage.from("伦敦现在时间?"))
.toolSpecifications(toolService.getSpecifications())
.build();
// ...后续处理与ToolSpecification方式类似
4.2 流式调用实现
流式调用需要处理部分响应和工具调用:
java复制OpenAiStreamingChatModel streamingModel = OpenAiStreamingChatModel.builder()
.apiKey(apiKey)
.modelName("glm-4")
.build();
streamingModel.chat(request, new StreamingChatResponseHandler() {
@Override
public void onCompleteToolCall(CompleteToolCall toolCall) {
String result = toolService.executeTool(toolCall);
ToolExecutionResultMessage resultMessage =
ToolExecutionResultMessage.from(toolCall, result);
// 发起后续请求
streamingModel.chat(newFollowUpRequest(resultMessage), this);
}
@Override
public void onCompleteResponse(ChatResponse response) {
System.out.println("最终结果:" + response.aiMessage().text());
}
});
5. 关键问题与解决方案
5.1 工具选择问题
现象:模型未调用预期工具
解决方案:
- 检查工具描述是否准确反映功能
- 确认参数定义完整且包含必填标记
- 测试提示词是否清晰表达需求
5.2 参数解析问题
现象:参数格式错误或缺失
解决方案:
java复制// 健壮的参数解析方法
private String parseLocation(String jsonArgs) {
try {
JsonNode node = new ObjectMapper().readTree(jsonArgs);
if (!node.has("location")) {
throw new IllegalArgumentException("缺少location参数");
}
return node.get("location").asText();
} catch (Exception e) {
throw new RuntimeException("参数解析失败", e);
}
}
5.3 多工具协调
当注册多个工具时,模型可能同时请求多个工具调用。需要处理工具执行顺序和结果合并:
java复制List<ToolExecutionResultMessage> results = new ArrayList<>();
for (ToolExecutionRequest toolCall : response.toolCalls()) {
String result = toolService.executeTool(toolCall);
results.add(ToolExecutionResultMessage.from(toolCall, result));
}
// 将所有结果一次性提交
ChatRequest followUp = ChatRequest.builder()
.messages(combineMessages(originalMessages, response.aiMessage(), results))
.build();
6. 性能优化建议
- 工具缓存:对数据查询类工具实现结果缓存
- 批量执行:合并多个工具调用减少网络往返
- 超时控制:设置工具执行超时避免长时间阻塞
- 异步处理:对耗时工具采用异步执行
java复制// 异步工具执行示例
CompletableFuture<String> futureResult = CompletableFuture.supplyAsync(() -> {
return expensiveTool.execute(request);
});
futureResult.thenAccept(result -> {
// 处理异步结果
});
在实际项目中,我通过组合ToolSpecification和@Tool方式,既保持了代码的清晰度,又能灵活应对动态工具需求。特别是在金融领域的数据分析场景中,这种混合方式显著提升了开发效率。