18-Tool定义和工具链接最佳实践

2 阅读27分钟

在AI Agent的世界里,Tool就是Agent的双手和眼睛。一个没有Tool的Agent只能聊天,而拥有丰富Tool的Agent可以查询数据库、调用API、发送邮件、操作文件系统。本节将深入探讨如何定义高质量的Tool,以及如何让多个Tool协同工作。

时间:30分钟 | 难度:⭐⭐⭐ | Week 3 Day 18


📋 学习目标

  • 理解Tool的本质和LangChain4J中的Tool机制
  • 掌握@Tool和@P注解的完整用法
  • 了解Tool支持的参数类型和返回值处理
  • 学会设计清晰、可维护的Tool接口
  • 掌握Tool链接和组合的最佳实践
  • 能够实现动态Tool注册和监控
  • 理解MCP协议与@Tool的关系和各自适用场景

🚀 快速入门:什么是Tool?

Tool的本质

Tool在LangChain4J中是Agent可以调用的函数。关键点:

  1. Agent决定何时调用 - 不是你强制调用,是LLM根据用户问题自主选择
  2. Agent决定调用哪个 - 从多个Tool中选择最合适的
  3. Agent决定参数值 - 从对话上下文中提取参数
用户请求 → LLM分析 → 选择Tool → 执行 → 结果返回 → LLM整合回答
    ↓           ↓          ↓         ↓        ↓           ↓
 "今天天气"  需要天气   weather    API     "晴天"    "今天北京
             数据       Tool      调用    20°C      晴天20度"

ASCII工作流程图

┌─────────────┐
│  用户输入    │ "帮我查一下北京明天的天气,如果下雨就发邮件提醒我"
└──────┬──────┘
       │
       ▼
┌─────────────────────────────────────────────────┐
│            LLM 推理引擎                          │
│  分析:需要2个步骤                               │
│  1. 查询天气 → 使用 getWeather("北京", "明天")  │
│  2. 如果下雨 → 使用 sendEmail(...)               │
└──────┬──────────────────────────────────────────┘
       │
       ├─────────────┬─────────────┐
       ▼             ▼             ▼
  ┌─────────┐  ┌─────────┐  ┌─────────┐
  │Weather  │  │Calendar │  │ Email   │
  │  Tool   │  │  Tool   │  │  Tool   │
  └────┬────┘  └─────────┘  └────┬────┘
       │                          │
       ▼                          ▼
   调用API                    发送邮件
       │                          │
       └──────────┬───────────────┘
                  ▼
          ┌──────────────┐
          │  LLM 整合答案 │
          └──────┬───────┘
                 ▼
        "明天北京有雨,已发送邮件提醒"

🔥 深度讲解

1️⃣ @Tool注解详解

@Tool是定义工具的核心注解,有以下属性:

import dev.langchain4j.agent.tool.Tool;

public class MathTools {

    // 最简单的Tool:只有描述
    @Tool("计算两个数的和")
    public double add(double a, double b) {
        return a + b;
    }

    // 完整的Tool定义
    @Tool(
        name = "multiply",  // Tool名称,默认使用方法名
        value = "计算两个数的乘积,支持整数和小数"  // 描述,帮助LLM选择Tool
    )
    public double multiply(double a, double b) {
        System.out.println("正在计算: " + a + " × " + b);
        return a * b;
    }

    // 复杂业务逻辑的Tool
    @Tool("执行数学表达式,支持 +、-、*、/、括号")
    public String evaluate(String expression) {
        try {
            // 使用表达式解析库
            double result = new ExpressionParser().parse(expression).evaluate();
            return "结果是: " + result;
        } catch (Exception e) {
            return "表达式解析错误: " + e.getMessage();
        }
    }
}

最佳实践:

  1. 描述要精确 - LLM根据描述决定是否调用这个Tool
  2. 一个Tool做一件事 - 不要创建"万能Tool"
  3. 命名要语义化 - calculateSum优于calc
// ❌ 不好的例子
@Tool("处理数据")
public String process(String data) { ... }

// ✅ 好的例子
@Tool("将JSON字符串转换为格式化的表格")
public String jsonToTable(String json) { ... }

2️⃣ @P参数注解:让LLM理解参数含义

@P注解为每个参数提供描述,这对LLM至关重要:

import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;

public class UserService {

    @Tool("搜索用户信息")
    public User findUser(
        @P("用户ID,格式为UUID,例如:123e4567-e89b-12d3-a456-426614174000")
        String userId,

        @P("是否包含详细信息(地址、订单历史等),true表示包含")
        boolean includeDetails
    ) {
        User user = database.findById(userId);
        if (includeDetails) {
            user.loadDetails();
        }
        return user;
    }

    @Tool("根据多个条件搜索用户")
    public List<User> searchUsers(
        @P("用户名,支持模糊匹配,不区分大小写")
        String name,

        @P("最小年龄,包含此年龄")
        int minAge,

        @P("最大年龄,包含此年龄")
        int maxAge,

        @P("用户状态:ACTIVE(活跃)、INACTIVE(未激活)、BANNED(已封禁)")
        String status
    ) {
        return database.search(name, minAge, maxAge, status);
    }
}

参数描述的艺术:

public class DateTools {

    @Tool("添加指定天数到日期")
    public String addDays(
        // ❌ 描述不够清晰
        // @P("日期") String date,

        // ✅ 清晰的格式说明
        @P("日期,格式必须是 yyyy-MM-dd,例如:2024-03-15")
        String date,

        // ❌ 没说明正负
        // @P("天数") int days

        // ✅ 说明范围和含义
        @P("要添加的天数,正数表示未来,负数表示过去,范围:-365到365")
        int days
    ) {
        LocalDate localDate = LocalDate.parse(date);
        return localDate.plusDays(days).toString();
    }
}

3️⃣ 支持的参数类型

LangChain4J支持丰富的参数类型:

基本类型

public class BasicTypeTools {

    @Tool("格式化文本")
    public String format(
        @P("要格式化的文本") String text,
        @P("是否转为大写") boolean uppercase,
        @P("重复次数") int repeat,
        @P("缩放因子") double scale
    ) {
        String result = uppercase ? text.toUpperCase() : text;
        result = result.repeat(repeat);
        return result;
    }
}

枚举类型

public class TaskTools {

    enum Priority {
        LOW("低优先级"),
        MEDIUM("中优先级"),
        HIGH("高优先级"),
        CRITICAL("紧急");

        private final String description;
        Priority(String description) {
            this.description = description;
        }
    }

    enum TaskStatus {
        TODO, IN_PROGRESS, REVIEW, DONE, CANCELLED
    }

    @Tool("创建新任务")
    public String createTask(
        @P("任务标题") String title,
        @P("优先级:LOW、MEDIUM、HIGH、CRITICAL") Priority priority,
        @P("任务状态") TaskStatus status
    ) {
        Task task = new Task(title, priority, status);
        database.save(task);
        return "任务已创建,ID: " + task.getId();
    }

    @Tool("更新任务优先级")
    public String updatePriority(
        @P("任务ID") String taskId,
        @P("新的优先级") Priority newPriority
    ) {
        Task task = database.findTask(taskId);
        task.setPriority(newPriority);
        return String.format("任务 %s 优先级已更新为 %s",
            taskId, newPriority.description);
    }
}

POJO(Plain Old Java Object)

public class OrderTools {

    // 定义请求POJO
    public static class CreateOrderRequest {
        public String customerId;
        public List<OrderItem> items;
        public String shippingAddress;
        public PaymentMethod paymentMethod;

        public static class OrderItem {
            public String productId;
            public int quantity;
            public double price;
        }

        public enum PaymentMethod {
            CREDIT_CARD, PAYPAL, ALIPAY, WECHAT_PAY
        }
    }

    @Tool("创建新订单")
    public String createOrder(
        @P("订单详细信息,包含客户ID、商品列表、配送地址、支付方式")
        CreateOrderRequest request
    ) {
        // 验证订单
        if (request.items.isEmpty()) {
            return "错误:订单必须包含至少一个商品";
        }

        // 计算总价
        double total = request.items.stream()
            .mapToDouble(item -> item.price * item.quantity)
            .sum();

        // 保存订单
        Order order = new Order(request);
        database.save(order);

        return String.format("订单创建成功!订单号:%s,总金额:%.2f元",
            order.getId(), total);
    }
}

集合类型

public class BatchTools {

    @Tool("批量查询用户信息")
    public String batchGetUsers(
        @P("用户ID列表,多个ID用逗号分隔") List<String> userIds
    ) {
        List<User> users = database.findByIds(userIds);
        return users.stream()
            .map(User::toString)
            .collect(Collectors.joining("\n"));
    }

    @Tool("标记多个任务为完成")
    public String completeTasks(
        @P("要完成的任务ID列表") List<String> taskIds
    ) {
        int count = 0;
        for (String taskId : taskIds) {
            Task task = database.findTask(taskId);
            if (task != null) {
                task.setStatus(TaskStatus.DONE);
                count++;
            }
        }
        return String.format("成功完成 %d 个任务", count);
    }
}

Optional参数

public class SearchTools {

    @Tool("搜索文章")
    public String searchArticles(
        @P("搜索关键词,必填") String keyword,
        @P("分类,可选,不指定则搜索所有分类") Optional<String> category,
        @P("作者,可选") Optional<String> author,
        @P("最大结果数,可选,默认10") Optional<Integer> limit
    ) {
        SearchQuery query = new SearchQuery(keyword);
        category.ifPresent(query::setCategory);
        author.ifPresent(query::setAuthor);

        int maxResults = limit.orElse(10);
        List<Article> results = database.search(query, maxResults);

        return String.format("找到 %d 篇文章", results.size());
    }
}

4️⃣ Tool错误处理

Tool执行可能失败,需要优雅地处理错误:

public class DatabaseTools {

    @Tool("执行SQL查询")
    public String executeQuery(@P("SQL查询语句") String sql) {
        try {
            // 验证SQL
            if (sql.trim().toUpperCase().startsWith("DROP") ||
                sql.trim().toUpperCase().startsWith("DELETE")) {
                return "错误:不允许执行危险操作(DROP/DELETE)";
            }

            // 执行查询
            List<Map<String, Object>> results = database.query(sql);

            if (results.isEmpty()) {
                return "查询成功,但没有找到数据";
            }

            // 格式化结果
            return formatResults(results);

        } catch (SQLException e) {
            // 返回清晰的错误信息,LLM会看到并调整策略
            return "SQL错误: " + e.getMessage() +
                   "\n提示:请检查表名和列名是否正确";
        } catch (Exception e) {
            return "执行失败: " + e.getMessage();
        }
    }

    @Tool("检查数据库连接")
    public String checkConnection() {
        try {
            boolean isConnected = database.ping();
            if (isConnected) {
                return "数据库连接正常";
            } else {
                return "警告:数据库连接失败,请检查配置";
            }
        } catch (Exception e) {
            return "无法连接到数据库: " + e.getMessage();
        }
    }
}

错误处理最佳实践:

public class APITools {

    @Tool("调用第三方API")
    public String callExternalAPI(
        @P("API端点") String endpoint,
        @P("请求参数JSON") String params
    ) {
        try {
            // 1. 参数验证
            if (!endpoint.startsWith("https://")) {
                return "错误:只允许HTTPS请求";
            }

            // 2. 业务逻辑
            HttpResponse response = httpClient.post(endpoint, params);

            // 3. 结果检查
            if (response.statusCode() != 200) {
                return String.format(
                    "API调用失败:HTTP %d - %s",
                    response.statusCode(),
                    response.body()
                );
            }

            return "API调用成功: " + response.body();

        } catch (JsonParseException e) {
            // 4. 具体的错误类型
            return "JSON格式错误: " + e.getMessage() +
                   "\n正确格式示例: {\"key\": \"value\"}";

        } catch (TimeoutException e) {
            return "请求超时,请稍后重试";

        } catch (Exception e) {
            // 5. 通用错误兜底
            return "未知错误: " + e.getMessage();
        }
    }
}

5️⃣ Tool链接:多个Tool协同工作

真实场景中,经常需要多个Tool配合:

public class WorkflowTools {

    private final WeatherAPI weatherAPI;
    private final EmailService emailService;
    private final CalendarService calendarService;

    // Tool 1: 查询天气
    @Tool("查询指定城市的天气预报")
    public String getWeather(
        @P("城市名称,如:北京、上海、深圳") String city,
        @P("日期,格式 yyyy-MM-dd") String date
    ) {
        WeatherInfo weather = weatherAPI.getWeather(city, date);
        return String.format(
            "城市:%s,日期:%s,天气:%s,温度:%d°C,降雨概率:%d%%",
            city, date, weather.getCondition(),
            weather.getTemperature(), weather.getRainProbability()
        );
    }

    // Tool 2: 发送邮件
    @Tool("发送邮件通知")
    public String sendEmail(
        @P("收件人邮箱地址") String to,
        @P("邮件主题") String subject,
        @P("邮件内容") String body
    ) {
        try {
            emailService.send(to, subject, body);
            return "邮件已发送到: " + to;
        } catch (Exception e) {
            return "发送失败: " + e.getMessage();
        }
    }

    // Tool 3: 添加日历事件
    @Tool("在日历中添加事件")
    public String addCalendarEvent(
        @P("事件标题") String title,
        @P("开始时间,格式 yyyy-MM-dd HH:mm") String startTime,
        @P("事件描述") String description
    ) {
        Event event = new Event(title, startTime, description);
        calendarService.addEvent(event);
        return "日历事件已添加: " + title;
    }
}

// Agent会自动链接这些Tool:
// 用户: "查下明天北京天气,如果下雨提醒我带伞,并加到日历"
//
// LLM推理过程:
// 1. 调用 getWeather("北京", "2024-03-16")
//    → 结果: "降雨概率:80%"
// 2. 判断:降雨概率高,需要提醒
// 3. 调用 sendEmail("user@example.com", "明天记得带伞", "...")
// 4. 调用 addCalendarEvent("带雨伞", "2024-03-16 08:00", "...")
// 5. 整合回答: "明天北京有雨,已发邮件提醒并添加到日历"

Tool依赖关系示例:

public class DataPipelineTools {

    @Tool("从数据库提取数据")
    public String extractData(@P("SQL查询") String sql) {
        List<Record> data = database.query(sql);
        // 返回JSON格式,供下一个Tool使用
        return JsonUtils.toJson(data);
    }

    @Tool("转换数据格式")
    public String transformData(
        @P("输入数据,JSON格式") String inputJson,
        @P("转换规则,如:rename_column, filter_rows") String rule
    ) {
        List<Record> records = JsonUtils.fromJson(inputJson);
        List<Record> transformed = applyRule(records, rule);
        return JsonUtils.toJson(transformed);
    }

    @Tool("加载数据到目标系统")
    public String loadData(
        @P("数据,JSON格式") String dataJson,
        @P("目标表名") String tableName
    ) {
        List<Record> records = JsonUtils.fromJson(dataJson);
        int count = targetDB.bulkInsert(tableName, records);
        return String.format("成功加载 %d 条记录到表 %s", count, tableName);
    }
}

// ETL流程自动链接:
// 用户: "把users表的数据导出,过滤掉已删除的,然后加载到备份库"
//
// LLM自动构建流程:
// 1. extractData("SELECT * FROM users")
// 2. transformData(上一步结果, "filter_rows: deleted = false")
// 3. loadData(上一步结果, "users_backup")

6️⃣ 动态Tool注册

有时需要在运行时动态添加Tool:

import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.agent.tool.ToolExecutionRequest;

public class DynamicToolExample {

    public void setupDynamicTools() {
        // 定义Tool规格
        ToolSpecification weatherTool = ToolSpecification.builder()
            .name("getWeather")
            .description("获取指定城市的天气信息")
            .addParameter("city", "string", "城市名称")
            .addParameter("date", "string", "日期,格式yyyy-MM-dd")
            .build();

        // 定义Tool执行器
        ToolExecutor weatherExecutor = (ToolExecutionRequest request) -> {
            String city = request.argument("city");
            String date = request.argument("date");
            // 执行实际逻辑
            return weatherAPI.get(city, date).toString();
        };

        // 注册到Agent
        AiServices<?> aiService = AiServices.builder(MyAssistant.class)
            .chatLanguageModel(model)
            .tools(weatherTool, weatherExecutor)
            .build();
    }

    // 插件化Tool系统
    public class PluginManager {
        private Map<String, ToolSpecification> toolSpecs = new HashMap<>();
        private Map<String, ToolExecutor> toolExecutors = new HashMap<>();

        public void registerPlugin(String pluginName,
                                   ToolSpecification spec,
                                   ToolExecutor executor) {
            toolSpecs.put(pluginName, spec);
            toolExecutors.put(pluginName, executor);
            System.out.println("插件已注册: " + pluginName);
        }

        public void unregisterPlugin(String pluginName) {
            toolSpecs.remove(pluginName);
            toolExecutors.remove(pluginName);
            System.out.println("插件已卸载: " + pluginName);
        }

        public AiServices<?> buildAgent() {
            return AiServices.builder(MyAssistant.class)
                .chatLanguageModel(model)
                .tools(toolSpecs.values(), toolExecutors.values())
                .build();
        }
    }
}

7️⃣ MCP:Tool的标准化协议

前面学的 @Tool 是 LangChain4J 私有的工具定义方式。如果换个框架(LangChain Python、Spring AI、Semantic Kernel),工具定义方式完全不同,工具不能复用。

MCP(Model Context Protocol) 是 Anthropic 在 2024 年底开源的工具接口标准协议,解决的就是这个问题。

MCP 的本质:给 Tool 定义一个"USB接口"

没有 MCP(现状):
  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
  │ LangChain4J │     │ Spring AI   │     │ LangChain   │
  │ @Tool注解    │     │ @Bean注入   │     │ @tool装饰器  │
  │ Java方法    │     │ Java方法    │     │ Python函数   │
  └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
         │                   │                   │
    天气Tool(Java)      天气Tool(Java)     天气Tool(Python)
    ↑ 完全不同的实现!    ↑ 又写一遍!        ↑ 再写一遍!

有了 MCP:
  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
  │ LangChain4J │     │ Spring AI   │     │ LangChain   │
  │  MCP Client │     │  MCP Client │     │  MCP Client │
  └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
         │                   │                   │
         └───────────┬───────┴───────────────────┘
                     │ 统一的 MCP 协议(JSON-RPC)
                     ▼
              ┌──────────────┐
              │  MCP Server  │  ← 天气工具只实现一次!
              │  天气服务     │     任何框架都能调用
              └──────────────┘

MCP 架构:三层分离

┌──────────────────────────────────────────────────────────┐
│  AI 应用层                                                │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐         │
│  │ Claude     │  │ GPT Agent  │  │ 你的Agent   │         │
│  │ Desktop    │  │            │  │ (LangChain4J)│         │
│  └─────┬──────┘  └─────┬──────┘  └─────┬──────┘         │
│        │               │               │                 │
│  ┌─────┴───────────────┴───────────────┴──────┐         │
│  │              MCP Client(协议层)              │         │
│  │  discover() → 发现可用工具                     │         │
│  │  call()     → 调用工具                        │         │
│  │  结果       ← 接收返回                        │         │
│  └─────┬───────────────┬───────────────┬──────┘         │
│        │               │               │                 │
└────────┼───────────────┼───────────────┼─────────────────┘
         │ JSON-RPC      │ JSON-RPC      │ JSON-RPC
         ▼               ▼               ▼
  ┌────────────┐  ┌────────────┐  ┌────────────┐
  │ MCP Server │  │ MCP Server │  │ MCP Server │
  │ 天气服务    │  │ 数据库查询  │  │ 文件系统    │
  │ (Node.js)  │  │ (Python)   │  │ (Go)       │
  └────────────┘  └────────────┘  └────────────┘
     独立进程         独立进程         独立进程

@Tool vs MCP:本质区别

// ===== 方式A:@Tool(紧耦合,编译时绑定)=====
// 工具代码和Agent代码在同一个JVM进程中

@Tool("查询天气")
public String getWeather(@P("城市") String city) {
    return weatherAPI.get(city);  // 直接调用,同进程
}

// 构建时绑定:
AiServices.builder(MyAgent.class)
    .tools(new WeatherTools())  // ← 工具对象直接传入
    .build();

// ===== 方式B:MCP(松耦合,运行时发现)=====
// 工具代码运行在独立进程/机器上

// MCP Server 端(可以是任何语言):
// weather-server.js
server.tool("getWeather", {
    description: "查询天气",
    parameters: {
        city: { type: "string", description: "城市" }
    }
}, async (args) => {
    return await weatherAPI.get(args.city);  // 独立进程
});

// MCP Client 端(LangChain4J中):
// 运行时动态发现并调用:
McpClient client = McpClient.connect("weather-server");
List<ToolSpecification> tools = client.listTools();  // ← 动态发现!
// Agent 使用这些 tools 就像使用 @Tool 一样

核心对比

┌─────────────┬────────────────────────┬────────────────────────┐
│             │ @Tool(LangChain4J)    │ MCP                    │
├─────────────┼────────────────────────┼────────────────────────┤
│ 绑定时机     │ 编译时(注解扫描)       │ 运行时(协议发现)       │
│ 运行位置     │ 同一个 JVM 进程        │ 独立进程(可跨机器)     │
│ 语言限制     │ 只能 Java             │ 任何语言                │
│ 工具复用     │ ❌ 框架绑定            │ ✅ 跨框架跨语言         │
│ 部署方式     │ 和应用一起打包          │ 独立部署,独立扩缩       │
│ 调用开销     │ 低(方法调用)          │ 中(进程间通信)         │
│ 适合场景     │ 简单/内部工具          │ 共享/标准化/生态工具     │
│ 类比         │ Java 本地方法调用      │ HTTP API / gRPC 调用   │
└─────────────┴────────────────────────┴────────────────────────┘

一句话:@Tool"函数调用",MCP 是"远程服务调用"

MCP 的工具描述格式

@ToolLangChain4J 中的转换链:

  @Tool注解 → ToolSpecificationOpenAI Function Calling JSONJava 编译时)   (框架内部)        (发给 LLM APIMCP 的转换链:

  MCP Server 声明 → JSON SchemaLLM Function Calling JSON
  (任何语言实现)   (标准协议)     (发给 LLM API)

两者最终发给 LLM 的格式是一样的!都是 JSON Schema:
{
  "name": "getWeather",
  "description": "查询天气",
  "parameters": {
    "type": "object",
    "properties": {
      "city": { "type": "string", "description": "城市" }
    }
  }
}

所以 LLM 并不关心工具是 @Tool 定义的还是 MCP 定义的。
LLM 只看到 JSON Schema 描述 → 决定是否调用 → 返回调用请求。
区别仅在于"谁执行这个调用"@ToolJVM 内反射调用你的 Java 方法
  MCP  → 通过协议发送到外部 MCP Server

MCP 解决了什么问题

问题1:工具碎片化
  ❌ 每个框架自己的工具格式,社区无法共享
  ✅ MCP 统一格式,写一次到处用

问题2:工具与应用强绑定
  ❌ 天气工具写在你的 Java 应用里,Python 团队用不了
  ✅ MCP Server 独立运行,任何 Client 都能调用

问题3:工具生态建设
  ❌ 每个LLM应用都要自己实现一套工具
  ✅ MCP 社区共享工具库(GitHub、Slack、Jira...已有现成Server)

问题4:安全与权限
  ❌ @Tool 在同进程,工具有应用的全部权限
  ✅ MCP Server 独立进程,可以精细控制权限

LangChain4J 中使用 MCP

/**
 * LangChain4J 集成 MCP 工具
 * 工具发现和调用对 Agent 完全透明
 */
@Configuration
public class McpIntegrationConfig {

    @Bean
    public MyAgent agentWithMcpTools(ChatLanguageModel model) {
        // 1. 连接 MCP Server
        McpClient weatherServer = McpClient.builder()
            .transport(new StdioTransport("node", "weather-server.js"))
            .build();

        McpClient dbServer = McpClient.builder()
            .transport(new SseTransport("http://localhost:3001/mcp"))
            .build();

        // 2. 从 MCP Server 动态获取工具定义
        List<ToolSpecification> mcpTools = new ArrayList<>();
        mcpTools.addAll(weatherServer.listTools());  // 天气相关工具
        mcpTools.addAll(dbServer.listTools());        // 数据库相关工具

        // 3. 也可以混合使用 @Tool 和 MCP 工具
        LocalTools localTools = new LocalTools();  // 本地 @Tool 工具

        // 4. 构建 Agent(同时拥有本地工具和MCP工具)
        return AiServices.builder(MyAgent.class)
            .chatLanguageModel(model)
            .tools(localTools)            // 本地 @Tool 工具
            .tools(mcpTools)              // MCP 远程工具
            .build();
    }
}

// Agent 使用时完全无感知工具来源
// LLM 根据描述选择工具,框架自动路由到本地方法或MCP Server

什么时候用 @Tool,什么时候用 MCP?

选择 @Tool 当:
├─ 工具逻辑简单,和业务代码紧密相关
├─ 只在一个应用中使用
├─ 需要最低延迟(同进程调用)
├─ 团队统一使用 Java / LangChain4J
└─ 例:内部业务规则计算、数据格式转换

选择 MCP 当:
├─ 工具需要被多个应用 / 团队复用
├─ 工具需要独立部署和扩缩容
├─ 工具用非 Java 语言实现更方便
├─ 想利用社区现成的 MCP Server(GitHub、Slack、数据库...)
├─ 需要精细的权限控制和审计
└─ 例:公司统一的数据查询服务、第三方集成

实际项目中,两者经常混合使用:
  本地 @Tool → 业务逻辑、数据转换、简单计算
  MCP Server → 外部服务集成、跨团队共享工具、社区工具

💡 本质理解@Tool 和 MCP 不是替代关系,而是不同层次的抽象。@Tool 是代码级的工具定义("函数"),MCP 是协议级的工具接口("服务")。就像 Java 方法调用 vs REST API — 两者可以共存,各有适用场景。LLM 不关心工具来自哪里,它只看 JSON Schema 描述。

8️⃣ 四大模式对比:RAG vs @Tool vs MCP vs Fine-tuning

学到这里,我们已经接触了扩展 LLM 能力的四种核心模式。它们解决的是同一个根本问题:LLM 自身的知识和能力是有限的,如何突破这个限制?

四种模式,四种思路

┌──────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  LLM 的两大限制:                                                      │
│  1. 知识有限 — 训练数据截止到某个时间,不知道你的私有数据                      │
│  2. 能力有限 — 不能查数据库、不能发邮件、不能调API                          │
│                                                                      │
│  四种解法:                                                             │
│                                                                      │
│  ┌─────────────┐  解决知识限制                                         │
│  │ Fine-tuning  │  把知识"烧"进模型权重 ← 改变模型本身                    │
│  └─────────────┘                                                      │
│                                                                      │
│  ┌─────────────┐  解决知识限制                                         │
│  │    RAG      │  检索后注入上下文 ← 不改模型,改输入                      │
│  └─────────────┘                                                      │
│                                                                      │
│  ┌─────────────┐  解决能力限制                                         │
│  │   @Tool     │  给LLM装备函数 ← 不改模型,加工具                       │
│  └─────────────┘                                                      │
│                                                                      │
│  ┌─────────────┐  解决能力限制 + 工具标准化                              │
│  │    MCP      │  标准化工具协议 ← 不改模型,加标准化工具                   │
│  └─────────────┘                                                      │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

全维度对比

┌──────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┐
│          │ Fine-tuning     │ RAG             │ @Tool           │ MCP             │
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 一句话    │ 改造大脑         │ 给一本参考书      │ 给一套工具箱      │ 给一个工具商店    │
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 解决什么  │ 知识/风格/能力   │ 知识             │ 能力             │ 能力 + 标准化    │
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 修改对象  │ 模型权重         │ 输入Prompt       │ 输出Action       │ 输出Action      │
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 数据新鲜度│ ❌ 训练时固定     │ ✅ 实时更新      │ ✅ 实时          │ ✅ 实时         │
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 实现成本  │ 高(GPU训练)    │ 中(向量库)     │ 低(写代码)     │ 中(Server开发)│
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 运行成本  │ 低(直接生成)    │ 中(检索+生成)   │ 高(多轮调用)    │ 高(多轮+RPC)  │
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 延迟      │ 最低            │ 中              │ 高(Agent循环)  │ 较高(+网络)   │
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 可复用性  │ ❌ 模型专属      │ ❌ 应用专属      │ ❌ 框架绑定      │ ✅ 跨框架跨语言  │
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ LLM感知  │ 无感知(内化了)  │ 被动(自动注入)  │ 主动(LLM决策)  │ 主动(LLM决策) │
├──────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 可解释性  │ ❌ 黑盒          │ ✅ 可引用来源    │ ✅ 可追踪调用    │ ✅ 可追踪调用   │
└──────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┘

本质区别:知识在哪里,能力在哪里

// ===== Fine-tuning:知识在模型里 =====
// 训练后,模型"记住了"你的数据
String answer = model.generate("公司年假政策是什么?");
// → 直接回答(因为训练数据里有)
// 特点:不需要额外步骤,但知识不能更新

// ===== RAG:知识在数据库里 =====
// 每次提问,先检索再回答
List<TextSegment> docs = retriever.retrieve("年假政策");  // ← 额外步骤
String augmented = "根据以下文档回答:" + docs + "\n问题:年假政策?";
String answer = model.generate(augmented);
// → 基于检索到的文档回答
// 特点:需要向量库,但知识可实时更新

// ===== @Tool:能力在本地方法里 =====
// LLM 决定调用哪个工具
@Tool("查询员工年假余额")
public String getLeaveBalance(@P("员工ID") String empId) {
    return hrSystem.query(empId);  // ← 同JVM直接调用
}
// → LLM自主决定:"需要调用 getLeaveBalance"
// 特点:LLM主动选择,工具在同进程

// ===== MCP:能力在远程服务里 =====
// LLM 决定调用哪个工具,工具在外部Server
// MCP Server (独立部署的HR服务)
server.tool("getLeaveBalance", schema, async (args) => {
    return await hrAPI.query(args.empId);  // ← 独立进程
});
// → LLM同样自主决定,但执行在远程
// 特点:工具可跨应用共享,独立部署

流程对比图

Fine-tuning(最短路径):
  用户问题 → LLM直接回答
  ════════════════════════
  只有1步,但知识可能过时

RAG(先检索再生成):
  用户问题 → 向量化 → 检索数据库 → 注入PromptLLM回答
  ════════════════════════════════════════════════════════
  每次都检索,LLM是被动的

@ToolLLM主动调用):
  用户问题 → LLM推理 → 选择Tool → 执行(同JVM) → 观察结果 → ... → LLM回答
  ════════════════════════════════════════════════════════════════════════
  多轮循环,LLM是主动的

MCPLLM主动调用远程服务):
  用户问题 → LLM推理 → 选择ToolJSON-RPCMCP Server → 结果 → ... → LLM回答
  ════════════════════════════════════════════════════════════════════════════
  和@Tool一样的循环,但工具执行在远端

RAG vs Tool:最容易混淆的两个

关键区别:谁决定"要不要检索/调用"?

RAG:
  每次都检索,不管用户问什么
  ┌─────────────┐
  │ "你好"       │ → 也会去检索文档 → 找不到相关的 → LLM回答"你好"
  │ "年假多少天" │ → 检索文档 → 找到相关内容 → 基于文档回答
  └─────────────┘
  LLM 不需要决策,框架自动检索

Tool(@Tool / MCP):
  LLM自己决定要不要调用
  ┌─────────────┐
  │ "你好"       │ → LLM判断:不需要工具 → 直接回答
  │ "查下年假"   │ → LLM判断:需要查HR系统 → 调用getLeaveBalance
  │ "查天气+发邮件"│ → LLM判断:先调天气,再调邮件 → 多步执行
  └─────────────┘
  LLM 是决策者

所以:
  RAG = 被动增强(每次都做,成本固定)
  Tool = 主动调用(按需触发,成本不确定)

类比:
  RAG  = 开卷考试(每次都带参考书,不管用不用)
  Tool = 有手机的考试(需要时自己决定查什么)

组合使用:实际项目中的架构

实际项目往往不是选其中一个,而是组合使用:

┌─────────────────────────────────────────────────┐
│                   你的 AI 应用                     │
│                                                   │
│  ┌─────────────────────────────────────────────┐ │
│  │              AiServices (Agent)               │ │
│  │                                               │ │
│  │  ┌─────────┐  ┌──────────┐  ┌────────────┐  │ │
│  │  │  RAG    │  │  @Tool   │  │  MCP Tools │  │ │
│  │  │知识检索  │  │本地工具   │  │远程工具     │  │ │
│  │  └────┬────┘  └────┬─────┘  └─────┬──────┘  │ │
│  │       │            │              │          │ │
│  └───────┼────────────┼──────────────┼──────────┘ │
│          │            │              │            │
│          ▼            ▼              ▼            │
│    向量数据库     Java方法调用    MCP Server       │
│   (公司文档)    (业务逻辑)     (GitHub/Slack)     │
│                                                   │
│  + Fine-tuned 模型(如果需要特定领域风格)            │
└─────────────────────────────────────────────────────┘

常见组合:
├─ RAG + @Tool:知识库问答 + 业务操作(最常见)
├─ RAG + MCP:知识库 + 外部服务集成
├─ @Tool + MCP:本地工具 + 远程工具混合
└─ Fine-tuning + RAG:领域模型 + 实时知识(最强但最贵)

选型决策树

你的需求是什么?
│
├─ 让LLM知道私有数据(文档、知识库)
│  ├─ 数据量大,经常更新 → RAG ✅
│  ├─ 数据量小,变化少 → Fine-tuning
│  └─ 只有几篇文档 → 直接放进Prompt(长上下文)
│
├─ 让LLM执行操作(查数据库、调API、发邮件)
│  ├─ 只在一个Java应用中使用 → @Tool ✅
│  ├─ 需要跨应用/跨团队共享 → MCP ✅
│  └─ 社区已有现成实现 → MCP(用社区Server)
│
├─ 让LLM掌握特定风格/术语
│  └─ Fine-tuning ✅(唯一选择)
│
└─ 构建复杂AI应用
   └─ 通常需要组合:RAG + @Tool + MCP

💡 终极理解:这四种模式本质上回答的是同一个问题 — "如何让LLM做它原本做不到的事"。Fine-tuning 改变了模型本身;RAG 改变了模型的输入;Tool 和 MCP 改变了模型的输出(让它可以触发外部动作)。理解了这个区别,选型就变得简单了。


🎯 Tool设计原则

原则1:单一职责

// ❌ 不好:一个Tool做太多事
@Tool("处理用户请求")
public String handleUser(String action, String userId, String data) {
    if (action.equals("create")) {
        // 创建逻辑
    } else if (action.equals("update")) {
        // 更新逻辑
    } else if (action.equals("delete")) {
        // 删除逻辑
    }
    // ...
}

// ✅ 好:每个Tool做一件事
@Tool("创建新用户")
public String createUser(@P("用户信息JSON") String userJson) { ... }

@Tool("更新用户信息")
public String updateUser(@P("用户ID") String id, @P("更新数据") String data) { ... }

@Tool("删除用户")
public String deleteUser(@P("用户ID") String userId) { ... }

原则2:清晰的描述

// ❌ 描述太简略
@Tool("获取数据")
public String getData(String id) { ... }

// ✅ 描述详细准确
@Tool("根据订单ID获取订单详情,包含商品列表、价格、状态等完整信息")
public String getOrderDetails(@P("订单ID,格式为ORD-xxxxxxxx") String orderId) { ... }

原则3:合理的粒度

public class FileTools {

    // ✅ 合理粒度:基础操作
    @Tool("读取文件内容")
    public String readFile(@P("文件路径") String path) { ... }

    @Tool("写入文件")
    public String writeFile(@P("文件路径") String path, @P("内容") String content) { ... }

    @Tool("删除文件")
    public String deleteFile(@P("文件路径") String path) { ... }

    // ✅ 也可以提供高级封装
    @Tool("备份文件到指定目录")
    public String backupFile(
        @P("源文件路径") String sourcePath,
        @P("备份目录") String backupDir
    ) {
        String content = readFile(sourcePath);
        String backupPath = backupDir + "/" + getFileName(sourcePath);
        writeFile(backupPath, content);
        return "文件已备份到: " + backupPath;
    }
}

原则4:可预测的返回值

public class ConsistentTools {

    // ✅ 返回格式一致
    @Tool("查询用户")
    public String findUser(@P("用户ID") String userId) {
        User user = database.findById(userId);
        if (user == null) {
            return "错误:用户不存在,ID: " + userId;
        }
        return "用户信息: " + user.toJson();
    }

    // ✅ 状态码 + 消息
    @Tool("创建文章")
    public String createArticle(@P("文章内容") ArticleRequest request) {
        try {
            Article article = service.create(request);
            return "成功:文章已创建,ID: " + article.getId();
        } catch (ValidationException e) {
            return "验证失败:" + e.getMessage();
        } catch (Exception e) {
            return "错误:" + e.getMessage();
        }
    }
}

🚀 实战:构建多工具Agent

完整示例:个人助理Agent,整合天气、日历、邮件功能。

// 1. 定义Tool服务类
public class PersonalAssistantTools {

    private final WeatherAPI weatherAPI;
    private final GoogleCalendar calendar;
    private final EmailService emailService;
    private final TaskDatabase taskDB;

    public PersonalAssistantTools() {
        this.weatherAPI = new WeatherAPI();
        this.calendar = new GoogleCalendar();
        this.emailService = new EmailService();
        this.taskDB = new TaskDatabase();
    }

    // === 天气相关 ===

    @Tool("获取当前天气")
    public String getCurrentWeather(@P("城市名称") String city) {
        WeatherInfo weather = weatherAPI.getCurrent(city);
        return String.format(
            "【%s 当前天气】\n" +
            "天气:%s\n" +
            "温度:%d°C\n" +
            "湿度:%d%%\n" +
            "风力:%s",
            city, weather.condition, weather.temp,
            weather.humidity, weather.wind
        );
    }

    @Tool("获取未来天气预报")
    public String getWeatherForecast(
        @P("城市名称") String city,
        @P("天数,1-7天") int days
    ) {
        List<WeatherInfo> forecast = weatherAPI.getForecast(city, days);
        StringBuilder result = new StringBuilder();
        result.append(String.format("【%s 未来%d天天气】\n", city, days));

        for (WeatherInfo w : forecast) {
            result.append(String.format(
                "%s: %s, %d-%d°C, 降雨%d%%\n",
                w.date, w.condition, w.tempMin, w.tempMax, w.rainChance
            ));
        }
        return result.toString();
    }

    // === 日历相关 ===

    @Tool("查看今天的日程")
    public String getTodaySchedule() {
        LocalDate today = LocalDate.now();
        List<Event> events = calendar.getEvents(today);

        if (events.isEmpty()) {
            return "今天没有安排的日程";
        }

        StringBuilder sb = new StringBuilder("【今日日程】\n");
        for (Event event : events) {
            sb.append(String.format(
                "• %s - %s: %s\n",
                event.startTime, event.endTime, event.title
            ));
        }
        return sb.toString();
    }

    @Tool("添加日程到日历")
    public String addEvent(
        @P("日程标题") String title,
        @P("开始时间,格式:yyyy-MM-dd HH:mm") String startTime,
        @P("持续时间(分钟)") int durationMinutes,
        @P("描述,可选") String description
    ) {
        Event event = Event.builder()
            .title(title)
            .startTime(LocalDateTime.parse(startTime,
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")))
            .duration(durationMinutes)
            .description(description)
            .build();

        calendar.addEvent(event);
        return String.format("已添加日程:%s (%s)", title, startTime);
    }

    @Tool("检查时间段是否有空")
    public String checkAvailability(
        @P("日期,格式:yyyy-MM-dd") String date,
        @P("开始时间,格式:HH:mm") String startTime,
        @P("结束时间,格式:HH:mm") String endTime
    ) {
        boolean available = calendar.isAvailable(date, startTime, endTime);
        if (available) {
            return String.format("时间段 %s %s-%s 有空", date, startTime, endTime);
        } else {
            return String.format("时间段 %s %s-%s 已有安排", date, startTime, endTime);
        }
    }

    // === 邮件相关 ===

    @Tool("发送邮件")
    public String sendEmail(
        @P("收件人邮箱") String to,
        @P("邮件主题") String subject,
        @P("邮件正文") String body
    ) {
        try {
            emailService.send(Email.builder()
                .to(to)
                .subject(subject)
                .body(body)
                .build());
            return "邮件已发送到: " + to;
        } catch (Exception e) {
            return "发送失败: " + e.getMessage();
        }
    }

    @Tool("查看最新邮件")
    public String getRecentEmails(@P("邮件数量,最多20") int count) {
        List<Email> emails = emailService.getRecent(Math.min(count, 20));
        StringBuilder sb = new StringBuilder("【最新邮件】\n");

        for (Email email : emails) {
            sb.append(String.format(
                "来自:%s\n主题:%s\n时间:%s\n%s\n\n",
                email.from, email.subject, email.time,
                email.isRead ? "[已读]" : "[未读]"
            ));
        }
        return sb.toString();
    }

    // === 任务管理 ===

    @Tool("创建待办任务")
    public String createTask(
        @P("任务标题") String title,
        @P("优先级:LOW/MEDIUM/HIGH/CRITICAL") Priority priority,
        @P("截止日期,格式:yyyy-MM-dd,可选") String dueDate
    ) {
        Task task = Task.builder()
            .title(title)
            .priority(priority)
            .dueDate(dueDate != null ? LocalDate.parse(dueDate) : null)
            .status(TaskStatus.TODO)
            .build();

        taskDB.save(task);
        return String.format("任务已创建:%s [%s]", title, priority);
    }

    @Tool("查看待办任务列表")
    public String listTasks(@P("状态过滤,可选:TODO/IN_PROGRESS/DONE") String status) {
        List<Task> tasks;
        if (status != null) {
            tasks = taskDB.findByStatus(TaskStatus.valueOf(status));
        } else {
            tasks = taskDB.findAll();
        }

        if (tasks.isEmpty()) {
            return "没有找到任务";
        }

        StringBuilder sb = new StringBuilder("【任务列表】\n");
        for (Task task : tasks) {
            sb.append(String.format(
                "• [%s] %s - %s %s\n",
                task.priority, task.title, task.status,
                task.dueDate != null ? "(截止: " + task.dueDate + ")" : ""
            ));
        }
        return sb.toString();
    }

    @Tool("完成任务")
    public String completeTask(@P("任务标题或ID") String taskIdentifier) {
        Task task = taskDB.findByTitleOrId(taskIdentifier);
        if (task == null) {
            return "错误:未找到任务 " + taskIdentifier;
        }

        task.setStatus(TaskStatus.DONE);
        task.setCompletedAt(LocalDateTime.now());
        taskDB.update(task);

        return String.format("任务已完成:%s", task.title);
    }
}

// 2. 创建Agent接口
interface PersonalAssistant {
    String chat(String message);
}

// 3. 构建Agent
public class PersonalAssistantDemo {

    public static void main(String[] args) {
        // 初始化工具
        PersonalAssistantTools tools = new PersonalAssistantTools();

        // 创建语言模型
        ChatLanguageModel model = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4")
            .temperature(0.7)
            .build();

        // 构建Agent
        PersonalAssistant assistant = AiServices.builder(PersonalAssistant.class)
            .chatLanguageModel(model)
            .tools(tools)
            .chatMemory(MessageWindowChatMemory.withMaxMessages(20))
            .build();

        // 测试场景
        testScenarios(assistant);
    }

    private static void testScenarios(PersonalAssistant assistant) {
        // 场景1:天气查询 + 日程安排
        System.out.println("=== 场景1:智能日程安排 ===");
        String response1 = assistant.chat(
            "查一下明天北京的天气,如果不下雨的话," +
            "帮我在明天下午3点安排一个2小时的户外团建活动"
        );
        System.out.println(response1);

        // 场景2:任务管理 + 邮件
        System.out.println("\n=== 场景2:任务提醒 ===");
        String response2 = assistant.chat(
            "创建一个高优先级任务'完成季度报告',截止日期3月20日," +
            "然后给boss@company.com发邮件说我会按时完成"
        );
        System.out.println(response2);

        // 场景3:复杂工作流
        System.out.println("\n=== 场景3:一周工作规划 ===");
        String response3 = assistant.chat(
            "帮我规划下周的工作:" +
            "1. 查看下周天气,选择一个好天气安排客户拜访\n" +
            "2. 创建3个任务:写周报、准备演讲、代码review\n" +
            "3. 检查周三下午2-5点是否有空,没空的话找其他时间"
        );
        System.out.println(response3);
    }
}

运行效果:

=== 场景1:智能日程安排 ===
我已经查询了明天(3月16日)北京的天气,预报显示晴天,温度18-25°C,
降雨概率只有10%,非常适合户外活动!

我已经帮你在日历中添加了:
• 活动:户外团建活动
• 时间:2024-03-16 15:00-17:00
• 天气条件:晴天,适合户外

=== 场景2:任务提醒 ===
已为你完成以下操作:
1. ✅ 创建任务:完成季度报告 [HIGH] 截止:2024-03-20
2. ✅ 已发送邮件到 boss@company.com
   主题:关于季度报告的进度
   内容:您好,季度报告任务已在计划中,我会在3月20日前完成。

=== 场景3:一周工作规划 ===
下周工作已规划完成:

📅 客户拜访:安排在周二(3月19日),当天晴天,温度适宜

✅ 任务创建:
• 写周报 [MEDIUM] - 已创建
• 准备演讲 [HIGH] - 已创建
• 代码review [MEDIUM] - 已创建

⏰ 周三下午2-5点:已有会议安排(产品讨论会 14:00-16:00)
建议改为周四下午2-5点,该时段有空

📊 Tool监控和日志

生产环境需要监控Tool的执行情况:

public class MonitoredTools {

    private final MetricsCollector metrics;
    private final Logger logger = LoggerFactory.getLogger(MonitoredTools.class);

    @Tool("查询数据库")
    public String queryDatabase(@P("SQL语句") String sql) {
        // 记录开始时间
        long startTime = System.currentTimeMillis();
        String toolName = "queryDatabase";

        try {
            logger.info("开始执行Tool: {}, SQL: {}", toolName, sql);

            // 执行实际逻辑
            List<Record> results = database.query(sql);

            // 记录成功
            long duration = System.currentTimeMillis() - startTime;
            metrics.recordSuccess(toolName, duration);
            logger.info("Tool执行成功: {}, 耗时: {}ms, 结果数: {}",
                toolName, duration, results.size());

            return formatResults(results);

        } catch (Exception e) {
            // 记录失败
            long duration = System.currentTimeMillis() - startTime;
            metrics.recordFailure(toolName, duration, e);
            logger.error("Tool执行失败: {}, 耗时: {}ms, 错误: {}",
                toolName, duration, e.getMessage(), e);

            return "查询失败: " + e.getMessage();
        }
    }
}

// 指标收集器
class MetricsCollector {
    private final Map<String, ToolMetrics> metricsMap = new ConcurrentHashMap<>();

    public void recordSuccess(String toolName, long durationMs) {
        metricsMap.computeIfAbsent(toolName, k -> new ToolMetrics())
            .recordSuccess(durationMs);
    }

    public void recordFailure(String toolName, long durationMs, Exception e) {
        metricsMap.computeIfAbsent(toolName, k -> new ToolMetrics())
            .recordFailure(durationMs, e);
    }

    public void printReport() {
        System.out.println("=== Tool执行报告 ===");
        metricsMap.forEach((name, metrics) -> {
            System.out.printf(
                "Tool: %s\n" +
                "  总调用: %d 次\n" +
                "  成功率: %.2f%%\n" +
                "  平均耗时: %.2f ms\n" +
                "  最大耗时: %d ms\n",
                name, metrics.totalCalls, metrics.getSuccessRate(),
                metrics.getAvgDuration(), metrics.maxDuration
            );
        });
    }
}

// Tool指标
class ToolMetrics {
    int totalCalls = 0;
    int successCount = 0;
    int failureCount = 0;
    long totalDuration = 0;
    long maxDuration = 0;
    List<Exception> errors = new ArrayList<>();

    synchronized void recordSuccess(long duration) {
        totalCalls++;
        successCount++;
        totalDuration += duration;
        maxDuration = Math.max(maxDuration, duration);
    }

    synchronized void recordFailure(long duration, Exception e) {
        totalCalls++;
        failureCount++;
        totalDuration += duration;
        errors.add(e);
    }

    double getSuccessRate() {
        return totalCalls == 0 ? 0 : (successCount * 100.0 / totalCalls);
    }

    double getAvgDuration() {
        return totalCalls == 0 ? 0 : (totalDuration * 1.0 / totalCalls);
    }
}

📝 练习题

练习1:设计购物助手Tool

为电商网站设计一套Tool,支持:

  • 搜索商品(按关键词、分类、价格区间)
  • 查看商品详情
  • 添加到购物车
  • 查看购物车
  • 下单

要求:

  1. 每个Tool单一职责
  2. 参数描述清晰
  3. 错误处理完善

练习2:实现Tool链接

创建一个"智能行程规划"功能,整合:

  • 天气查询Tool
  • 景点推荐Tool
  • 酒店搜索Tool
  • 行程生成Tool

用户输入:"帮我规划3天北京旅游",Agent自动:

  1. 查询3天天气
  2. 根据天气推荐景点
  3. 搜索附近酒店
  4. 生成完整行程

练习3:监控和优化

为现有Tool添加:

  1. 执行时间监控
  2. 调用次数统计
  3. 错误率追踪
  4. 慢查询告警(>1s)
  5. 生成日报表

🎓 总结

本节学习了Tool的完整体系:

核心概念

  • Tool是Agent的能力扩展
  • @Tool和@P注解是基础
  • LLM根据描述自主选择和调用

最佳实践

  • 单一职责原则
  • 清晰的描述和参数说明
  • 优雅的错误处理
  • 合理的粒度划分

高级特性

  • 支持丰富的参数类型(基本类型、枚举、POJO)
  • Tool自动链接形成工作流
  • 动态Tool注册
  • 监控和日志
  • MCP标准化协议 — 跨框架跨语言工具复用

通过合理设计Tool,你的Agent可以:

  • 🔍 查询各种数据源
  • ✉️ 调用外部API和服务
  • 📝 操作文件和数据库
  • 🤖 执行复杂的业务逻辑
  • 🔗 多Tool协同完成复杂任务

🚀 下一步

恭喜完成Tool定义和最佳实践的学习!

实践建议:

  1. 为你的业务场景设计一套完整的Tool体系
  2. 实现Tool监控和告警机制
  3. 测试Tool在不同LLM上的表现(GPT-4、Claude等)
  4. 优化Tool描述,提升选择准确率

记住:好的Tool设计是优秀Agent的基础。投入时间完善Tool,Agent的能力会成倍提升!