LangChain4j Tools 机制详解:如何让大模型调用本地工具
引言
在上一篇《Java程序员的大模型入门:ChatMemory 打造长会话记忆》中,我们介绍了 LangChain4j 的基本概念和核心组件。今天,我们将深入探讨 LangChain4j 中一个非常强大的功能——Tools(工具)机制。
你是否曾问过大模型“今天是几月几号?”,却得到一个错误的答案?这是因为大模型的知识截止于其训练数据,无法感知实时信息。Tools 机制正是为了解决这类问题而生,它允许大模型调用我们编写的本地方法,从而获取实时数据、执行特定操作或访问私有系统。
本文将带你:
- 理解 Tools 机制的核心原理
- 掌握两种创建 ToolSpecification 的方法
- 实战构建一个城市天气预报自动客服系统
- 了解 Tools 机制在实际项目中的应用场景
一、为什么需要 Tools 机制?
1.1 大模型的局限性
大模型解决问题,都是以它学习过的问题为经验。互联网上的各种大模型,只能学习互联网上的各种公开资料,以此为基础解决新的用户问题。但是这也意味着大模型无法理解用户个性化的问题。
例如问大模型“今天是几月几号?”,大模型大概率是给不出正确答案的,因为大模型肯定没有去学习今天产生的资料。代码执行的结果五花八门,甚至多执行几次,还会产生不一样的结果。
既然大模型无法知道当前的时间,那么与今天相关的其他问题,例如“今天天气怎么样”等,大模型自然也就无能为力了。
1.2 Tools 机制的解决方案
Tools 机制可以允许将一个本地方法封装成一个工具集(ToolSpecification)。把这个工具集一起发给大模型后,大模型就可以结合这个工具集处理一些和今天的日期相关的任务。
简单来说,Tools 机制就是给大模型“装上了手和脚”,让它能够:
- 获取实时数据(如当前时间、天气、股票价格)
- 执行本地操作(如读写文件、调用API)
- 访问私有系统(如企业内部数据库)
二、ToolSpecification 定制本地工具
2.1 环境准备
首先,我们需要配置 LangChain4j 环境。注意:LangChain4j 提供的 “demo” API Key 不支持 Tools 机制,需要大家自行获取 OpenAI 的 ApiKey,或者找一些代理间接调用 OpenAI。
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
public class ModelUtil {
public static ChatLanguageModel getOpenAIModel() {
return OpenAiChatModel.builder()
.apiKey("your-api-key-here") // 替换为你的真实 API Key
.modelName("gpt-4o-mini")
.build();
}
}
2.2 基础示例:获取当前日期
让我们从一个简单的例子开始,创建一个获取当前日期的工具:
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.ToolExecutionResultMessage;
import dev.langchain4j.data.message.ToolExecutionRequest;
import dev.langchain4j.model.chat.Response;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.agent.tool.ToolSpecifications;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Collections;
public class ToolsDemo1 {
@Tool("获取当前日期")
public static String getCurrentDate() {
return LocalDateTime.now().toString();
}
public static void main(String[] args) throws Exception {
// 1. 获取模型实例
ChatLanguageModel model = ModelUtil.getOpenAIModel();
// 2. 通过方法构建工具集
Method dateMethod = ToolsDemo1.class.getMethod("getCurrentDate");
ToolSpecification toolSpecification = ToolSpecifications.toolSpecificationFrom(dateMethod);
// 3. 构建用户消息
UserMessage userMessage = UserMessage.from("今天是几月几号?");
// 4. 调用大模型(传入工具集)
Response<AiMessage> response = model.generate(
Collections.singletonList(userMessage),
toolSpecification
);
// 5. 解析响应
AiMessage aiMessage = response.content();
System.out.println("AI 响应: " + aiMessage);
// 6. 检查是否有工具调用请求
if (aiMessage.hasToolExecutionRequests()) {
for (ToolExecutionRequest toolExecutionRequest : aiMessage.toolExecutionRequests()) {
System.out.println("工具调用请求: " + toolExecutionRequest);
System.out.println("方法名: " + toolExecutionRequest.name());
System.out.println("参数: " + toolExecutionRequest.arguments());
// 7. 执行工具方法
String methodName = toolExecutionRequest.name();
Method method = ToolsDemo1.class.getMethod(methodName);
String result = (String) method.invoke(null);
System.out.println("工具执行结果: " + result);
// 8. 创建工具执行结果消息
ToolExecutionResultMessage toolResultMessage =
ToolExecutionResultMessage.from(
toolExecutionRequest.id(),
toolExecutionRequest.name(),
result
);
// 9. 将结果返回给大模型
AiMessage finalMessage = model.generate(
java.util.Arrays.asList(
userMessage,
aiMessage,
toolResultMessage
)
).content();
System.out.println("最终回答: " + finalMessage.text());
}
}
}
}
2.3 代码解析
让我们分解一下这个示例的关键步骤:
- 定义工具方法:使用
@Tool注解标记方法,并提供描述 - 创建 ToolSpecification:通过
ToolSpecifications.toolSpecificationFrom()将方法转换为工具规范 - 发送请求:将用户消息和工具集一起发送给大模型
- 解析工具调用:大模型返回的不是文本,而是
ToolExecutionRequest - 执行本地方法:根据请求调用对应的 Java 方法
- 返回结果:将执行结果封装为
ToolExecutionResultMessage并发送给大模型 - 获取最终回答:大模型结合工具结果生成最终回答
三、Tools 机制完整流程
3.1 消息类型回顾
在深入之前,我们先回顾一下 LangChain4j 中的消息类型:
- UserMessage:用户发送的消息
- AiMessage:AI 生成的消息(可能包含工具调用请求)
- SystemMessage:系统提示消息
- ToolExecutionResultMessage:工具执行结果消息
3.2 完整交互流程
一个完整的 Tools 机制交互包含以下步骤:
3.3 使用 Class 创建工具集
除了通过单个方法创建工具,我们还可以通过整个类来创建工具集:
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.P;
import java.util.List;
public class WeatherUtil {
@Tool("获取某一个具体城市的天气")
public String getWeather(@P("指定的城市") String city) {
// 这里可以调用真实的天气 API
// 为了演示,我们返回模拟数据
return "明天 " + city + " 天气晴朗,温度 20-25°C,空气质量优";
}
@Tool("获取未来三天的天气预报")
public String getThreeDayForecast(@P("城市名称") String city) {
return city + " 未来三天天气预报:\n" +
"第一天:晴,20-25°C\n" +
"第二天:多云,18-23°C\n" +
"第三天:小雨,15-20°C";
}
}
使用类创建工具集:
import java.util.List;
public class ToolsDemo2 {
public static void main(String[] args) {
ChatLanguageModel model = ModelUtil.getOpenAIModel();
// 通过类构建工具集(获取所有 @Tool 注解的方法)
List<ToolSpecification> toolSpecifications =
ToolSpecifications.toolSpecificationsFrom(WeatherUtil.class);
System.out.println("可用工具数量: " + toolSpecifications.size());
toolSpecifications.forEach(spec -> {
System.out.println("工具名: " + spec.name());
System.out.println("描述: " + spec.description());
});
}
}
四、实战:城市天气预报自动客服
现在,让我们构建一个完整的城市天气预报自动客服系统:
import com.google.common.collect.Lists;
import dev.langchain4j.agent.tool.*;
import dev.langchain4j.data.message.*;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.service.tool.DefaultToolExecutor;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class WeatherAssistant {
public static void main(String[] args) {
// 1. 构建模型
ChatLanguageModel model = ModelUtil.getOpenAIModel();
// 2. 指定工具集
List<ToolSpecification> toolSpecifications =
ToolSpecifications.toolSpecificationsFrom(WeatherUtil.class);
// 3. 构建对话消息列表
List<ChatMessage> chatMessages = new ArrayList<>();
// 用户询问北京天气
UserMessage userMessage = UserMessage.from("北京今天的天气怎么样?");
chatMessages.add(userMessage);
System.out.println("用户问题: " + userMessage.text());
// 4. 调用大模型,生成工具调用请求
AiMessage aiMessage = model.generate(chatMessages, toolSpecifications).content();
List<ToolExecutionRequest> toolExecutionRequests = aiMessage.toolExecutionRequests();
// 5. 处理工具调用请求
if (!toolExecutionRequests.isEmpty()) {
chatMessages.add(aiMessage);
WeatherUtil weatherUtil = new WeatherUtil();
for (ToolExecutionRequest toolExecutionRequest : toolExecutionRequests) {
System.out.println("\n=== 工具调用详情 ===");
System.out.println("调用工具方法: " + toolExecutionRequest.name());
System.out.println("调用参数: " + toolExecutionRequest.arguments());
// 6. 执行工具方法
DefaultToolExecutor toolExecutor = new DefaultToolExecutor(
weatherUtil,
toolExecutionRequest
);
String result = toolExecutor.execute(
toolExecutionRequest,
UUID.randomUUID().toString()
);
System.out.println("工具执行结果: " + result);
// 7. 创建工具执行结果消息
ToolExecutionResultMessage toolResultMessage =
ToolExecutionResultMessage.from(toolExecutionRequest, result);
chatMessages.add(toolResultMessage);
}
// 8. 调用大模型,生成最终结果
AiMessage finalResponse = model.generate(chatMessages).content();
System.out.println("\n=== 最终回答 ===");
System.out.println(finalResponse.text());
} else {
System.out.println("AI 直接回答: " + aiMessage.text());
}
}
}
4.1 运行结果示例
用户问题: 北京今天的天气怎么样?
=== 工具调用详情 ===
调用工具方法: getWeather
调用参数: {"city": "北京"}
工具执行结果: 明天 北京 天气晴朗,温度 20-25°C,空气质量优
=== 最终回答 ===
根据天气预报,明天北京天气晴朗,温度在20到25摄氏度之间,空气质量优,适合外出活动。
五、Tools 机制的高级用法
5.1 参数注解 @P
@P 注解可以为工具方法的参数提供更明确的说明,帮助大模型更好地理解参数含义:
public class AdvancedTools {
@Tool("计算两个数字的和")
public int addNumbers(
@P("第一个加数") int a,
@P("第二个加数") int b
) {
return a + b;
}
@Tool("根据城市和日期查询天气")
public String queryWeather(
@P("城市名称,如:北京、上海") String city,
@P("查询日期,格式:YYYY-MM-DD") String date
) {
return String.format("%s 在 %s 的天气是:晴,温度 20-25°C", city, date);
}
}
5.2 多个工具的组合使用
大模型可以智能地组合使用多个工具:
public class MultiToolDemo {
public static void main(String[] args) {
ChatLanguageModel model = ModelUtil.getOpenAIModel();
// 创建包含多个工具的工具集
List<Class<?>> toolClasses = Arrays.asList(
WeatherUtil.class,
Calculator.class,
DateTimeUtil.class
);
List<ToolSpecification> allTools = new ArrayList<>();
for (Class<?> toolClass : toolClasses) {
allTools.addAll(ToolSpecifications.toolSpecificationsFrom(toolClass));
}
// 复杂问题:大模型会自动选择和使用合适的工具
UserMessage complexQuestion = UserMessage.from(
"北京明天天气怎么样?另外帮我计算一下 25 的平方根是多少?"
);
// ... 后续处理与之前类似
}
}
5.3 错误处理
在实际应用中,我们需要考虑工具执行可能失败的情况:
public class ErrorHandlingDemo {
@Tool("获取股票价格")
public String getStockPrice(@P("股票代码") String symbol) {
try {
// 模拟 API 调用
if ("AAPL".equals(symbol)) {
return "苹果公司当前股价:$175.32";
} else if ("GOOGL".equals(symbol)) {
return "谷歌当前股价:$142.67";
} else {
throw new RuntimeException("未找到股票代码: " + symbol);
}
} catch (Exception e) {
return "获取股票价格失败: " + e.getMessage();
}
}
public static void main(String[] args) {
// 当工具执行失败时,大模型会收到错误信息
// 并尝试用其他方式回答用户
UserMessage userMessage = UserMessage.from("TSLA 的股价是多少?");
// ... 工具调用流程
}
}
六、实际应用场景
6.1 企业内部系统集成
Tools 机制可以用于集成企业内部系统:
public class EnterpriseTools {
@Tool("查询员工信息")
public String getEmployeeInfo(
@P("员工工号") String employeeId,
@P("需要的信息类型:basic/contact/salary") String infoType
) {
// 连接企业内部 HR 系统
// 返回员工信息
return "员工 " + employeeId + " 的基本信息...";
}
@Tool("提交请假申请")
public String submitLeaveRequest(
@P("员工工号") String employeeId,
@P("开始日期,格式:YYYY-MM-DD") String startDate,
@P("结束日期,格式:YYYY-MM-DD") String endDate,
@P("请假类型:annual/sick/personal") String leaveType,
@P("请假原因") String reason
) {
// 调用请假审批系统
return "请假申请已提交,审批编号:LEAVE-2024-001";
}
}
6.2 数据库查询
public class DatabaseTools {
@Autowired
private JdbcTemplate jdbcTemplate;
@Tool("查询销售数据")
public String querySalesData(
@P("开始日期,格式:YYYY-MM-DD") String startDate,
@P("结束日期,格式:YYYY-MM-DD") String endDate,
@P("产品类别,可选:all/electronics/clothing") String category
) {
String sql = "SELECT product_name, SUM(amount) as total_sales " +
"FROM sales WHERE sale_date BETWEEN ? AND ?";
if (!"all".equals(category)) {
sql += " AND category = ?";
}
sql += " GROUP BY product_name ORDER BY total_sales DESC";
// 执行查询并返回结果
return "销售数据查询结果...";
}
}
6.3 文件操作
public class FileTools {
@Tool("读取文件内容")
public String readFile(@P("文件路径") String filePath) {
try {
return new String(Files.readAllBytes(Paths.get(filePath)));
} catch (IOException e) {
return "读取文件失败: " + e.getMessage();
}
}
@Tool("写入文件")
public String writeFile(
@P("文件路径") String filePath,
@P("要写入的内容") String content
) {
try {
Files.write(Paths.get(filePath), content.getBytes());
return "文件写入成功: " + filePath;
} catch (IOException e) {
return "写入文件失败: " + e.getMessage();
}
}
}
七、最佳实践与注意事项
7.1 工具设计原则
- 单一职责:每个工具方法应该只做一件事
- 明确描述:使用清晰的 @Tool 描述和 @P 参数说明
- 错误处理:工具方法应该有完善的异常处理
- 性能考虑:避免在工具方法中执行耗时操作
7.2 安全性考虑
public class SecurityTools
更多推荐
所有评论(0)