引言

在上一篇《Java程序员的大模型入门:ChatMemory 打造长会话记忆》中,我们介绍了 LangChain4j 的基本概念和核心组件。今天,我们将深入探讨 LangChain4j 中一个非常强大的功能——Tools(工具)机制

你是否曾问过大模型“今天是几月几号?”,却得到一个错误的答案?这是因为大模型的知识截止于其训练数据,无法感知实时信息。Tools 机制正是为了解决这类问题而生,它允许大模型调用我们编写的本地方法,从而获取实时数据、执行特定操作或访问私有系统。

本文将带你:

  1. 理解 Tools 机制的核心原理
  2. 掌握两种创建 ToolSpecification 的方法
  3. 实战构建一个城市天气预报自动客服系统
  4. 了解 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 代码解析

让我们分解一下这个示例的关键步骤:

  1. 定义工具方法:使用 @Tool 注解标记方法,并提供描述
  2. 创建 ToolSpecification:通过 ToolSpecifications.toolSpecificationFrom() 将方法转换为工具规范
  3. 发送请求:将用户消息和工具集一起发送给大模型
  4. 解析工具调用:大模型返回的不是文本,而是 ToolExecutionRequest
  5. 执行本地方法:根据请求调用对应的 Java 方法
  6. 返回结果:将执行结果封装为 ToolExecutionResultMessage 并发送给大模型
  7. 获取最终回答:大模型结合工具结果生成最终回答

三、Tools 机制完整流程

3.1 消息类型回顾

在深入之前,我们先回顾一下 LangChain4j 中的消息类型:

  • UserMessage:用户发送的消息
  • AiMessage:AI 生成的消息(可能包含工具调用请求)
  • SystemMessage:系统提示消息
  • ToolExecutionResultMessage:工具执行结果消息

3.2 完整交互流程

一个完整的 Tools 机制交互包含以下步骤:

本地工具 大模型 应用程序 用户 本地工具 大模型 应用程序 用户 发送问题 用户消息 + 工具集 返回工具调用请求 执行工具方法 返回执行结果 用户消息 + AI消息 + 工具结果 返回最终回答 显示最终回答

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 工具设计原则

  1. 单一职责:每个工具方法应该只做一件事
  2. 明确描述:使用清晰的 @Tool 描述和 @P 参数说明
  3. 错误处理:工具方法应该有完善的异常处理
  4. 性能考虑:避免在工具方法中执行耗时操作

7.2 安全性考虑

public class SecurityTools
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐