第7章 工具系统基础

第7章 工具系统基础

本章目标:掌握工具的定义、注册和使用,理解工具调用的执行流程


7.1 工具系统概述

7.1.1 为什么需要工具

LLM 本身只能生成文本,无法执行实际操作。工具系统让智能体能够:

┌─────────────────────────────────────────────────────────────┐
│                    工具系统的作用                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  用户: "帮我查一下北京天气"                                  │
│        │                                                    │
│        ▼                                                    │
│  ┌─────────────┐                                            │
│  │   智能体     │  "我需要调用天气查询工具..."              │
│  └──────┬──────┘                                            │
│         │                                                   │
│         ▼                                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    工具系统                          │   │
│  │  ┌───────────┐  ┌───────────┐  ┌───────────┐       │   │
│  │  │ 天气查询  │  │  发邮件   │  │  搜索引擎  │       │   │
│  │  └───────────┘  └───────────┘  └───────────┘       │   │
│  └─────────────────────────┬───────────────────────────┘   │
│                            │                                │
│                            ▼                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │            外部服务 / API / 系统                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7.1.2 工具系统特点

特点说明
注解驱动使用 @Tool@ToolParam 注解定义工具
自动 Schema 生成框架自动生成 JSON Schema 供 LLM 理解
类型安全参数和返回值都是强类型的
响应式支持支持同步、异步(Mono/Flux)和流式工具
灵活管理工具组、预设参数、动态激活等

7.2 定义工具

7.2.1 基本语法

import io.agentscope.core.tool.annotation.Tool;
import io.agentscope.core.tool.annotation.ToolParam;

/**
 * 工具类示例
 * 
 * 一个类可以包含多个工具方法
 */
public class WeatherTools {
    
    @Tool(
        name = "get_weather",
        // 工具名称
        // - LLM 看到的名称
        // - 建议使用 snake_case
        // - 要求唯一
        
        description = "获取指定城市的天气信息,包括温度、天气状况和空气质量"
        // 工具描述
        // - 帮助 LLM 理解何时使用这个工具
        // - 描述越清晰,LLM 选择越准确
    )
    public WeatherInfo getWeather(
            @ToolParam(
                name = "city",
                // 参数名(必需)
                // - Java 运行时不保留参数名,必须显式指定
                
                description = "城市名称,如:北京、上海、广州"
                // 参数描述
                // - 帮助 LLM 理解参数含义
            )
            String city,
            
            @ToolParam(
                name = "include_forecast",
                description = "是否包含未来几天的预报",
                required = false
                // required 默认为 true
                // 设为 false 表示可选参数
            )
            Boolean includeForecast
    ) {
        // 工具实现
        return weatherService.getWeather(city, includeForecast);
    }
}

7.2.2 参数类型支持

public class TypeExamples {
    
    @Tool(name = "demo_types", description = "演示支持的参数类型")
    public String demoTypes(
            // ======== 基本类型 ========
            @ToolParam(name = "text") String text,
            @ToolParam(name = "count") Integer count,
            @ToolParam(name = "price") Double price,
            @ToolParam(name = "active") Boolean active,
            
            // ======== 集合类型 ========
            @ToolParam(name = "tags") List<String> tags,
            @ToolParam(name = "scores") List<Integer> scores,
            
            // ======== 复杂对象 ========
            @ToolParam(name = "config") Config config
            // 复杂对象会被序列化为 JSON
    ) {
        return "处理完成";
    }
}

// 复杂对象定义
public class Config {
    public String mode;
    public int timeout;
    public List<String> options;
}

7.2.3 返回值类型

public class ReturnTypeExamples {
    
    // ======== 字符串返回 ========
    @Tool(name = "string_result", description = "返回字符串")
    public String stringResult(@ToolParam(name = "input") String input) {
        return "处理结果: " + input;
    }
    
    // ======== 对象返回 ========
    // 自动序列化为 JSON
    @Tool(name = "object_result", description = "返回对象")
    public UserInfo objectResult(@ToolParam(name = "userId") String userId) {
        return new UserInfo("张三", 25, "北京");
    }
    
    // ======== 列表返回 ========
    @Tool(name = "list_result", description = "返回列表")
    public List<Product> listResult(@ToolParam(name = "category") String category) {
        return productService.findByCategory(category);
    }
    
    // ======== ToolResultBlock 返回 ========
    // 最灵活的方式,可以包含多种内容
    @Tool(name = "rich_result", description = "返回富内容")
    public ToolResultBlock richResult(@ToolParam(name = "query") String query) {
        return ToolResultBlock.of("id", "rich_result",
                TextBlock.builder().text("文本结果").build(),
                ImageBlock.builder()
                        .source(Base64Source.builder()
                                .data(base64Image)
                                .mediaType("image/png")
                                .build())
                        .build()
        );
    }
}

7.3 工具类型

7.3.1 同步工具

最简单的工具类型,直接返回结果:

@Tool(name = "calculate", description = "执行数学计算")
public double calculate(
        @ToolParam(name = "expression") String expression
) {
    // 同步执行,直接返回结果
    return evaluator.evaluate(expression);
}

7.3.2 异步工具

返回 MonoFlux,支持非阻塞执行:

import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

// ======== Mono 返回 ========
@Tool(name = "async_search", description = "异步搜索")
public Mono<SearchResult> asyncSearch(
        @ToolParam(name = "query") String query
) {
    // 返回 Mono,框架自动处理订阅
    return webClient.get()
            .uri("/search?q=" + query)
            .retrieve()
            .bodyToMono(SearchResult.class);
}

// ======== Flux 返回 ========
// 所有元素收集后作为列表返回给 LLM
@Tool(name = "stream_fetch", description = "流式获取数据")
public Flux<DataItem> streamFetch(
        @ToolParam(name = "source") String source
) {
    return dataService.streamFrom(source);
}

7.3.3 流式工具(ToolEmitter)

使用 ToolEmitter 发送中间进度,适合长时间任务:

import io.agentscope.core.tool.ToolEmitter;
import io.agentscope.core.message.ToolResultBlock;

@Tool(name = "process_files", description = "处理多个文件")
public ToolResultBlock processFiles(
        @ToolParam(name = "files") List<String> files,
        ToolEmitter emitter  // 自动注入,不需要 @ToolParam
) {
    int total = files.size();
    int processed = 0;
    
    for (String file : files) {
        // 处理文件
        processFile(file);
        processed++;
        
        // 发送进度更新
        // 这些更新会通过 Hook 的 ActingChunkEvent 传递
        emitter.emit(ToolResultBlock.text(
                String.format("进度: %d/%d - 正在处理 %s", 
                        processed, total, file)
        ));
    }
    
    // 返回最终结果
    return ToolResultBlock.text("全部处理完成,共 " + total + " 个文件");
}

在 Hook 中接收进度:

Hook progressHook = new Hook() {
    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof ActingChunkEvent e) {
            // 接收工具进度更新
            System.out.println("进度: " + e.getChunk().getTextContent());
        }
        return Mono.just(event);
    }
};

7.4 注册工具

7.4.1 Toolkit 基础使用

import io.agentscope.core.tool.Toolkit;

// ========================================
// 【创建和注册工具】
// ========================================

// 创建工具包
Toolkit toolkit = new Toolkit();

// 注册工具类
toolkit.registerTool(new WeatherTools());
toolkit.registerTool(new CalculatorTools());
toolkit.registerTool(new SearchTools());

// 在智能体中使用
ReActAgent agent = ReActAgent.builder()
        .name("Assistant")
        .model(model)
        .toolkit(toolkit)
        .build();

7.4.2 链式注册

// ========================================
// 【链式注册方式】
// 更灵活的配置选项
// ========================================

Toolkit toolkit = new Toolkit();

toolkit.registration()
        .tool(new WeatherTools())
        .apply();

toolkit.registration()
        .tool(new SearchTools())
        .group("search")  // 分配到工具组
        .apply();

toolkit.registration()
        .tool(new AdminTools())
        .presetParameters(Map.of(
                "admin_command", Map.of("apiKey", System.getenv("ADMIN_KEY"))
        ))
        .apply();

7.4.3 查看注册的工具

// 获取所有工具信息
List<ToolInfo> tools = toolkit.getToolInfos();

for (ToolInfo tool : tools) {
    System.out.println("工具: " + tool.getName());
    System.out.println("描述: " + tool.getDescription());
    System.out.println("参数: " + tool.getParameters());
    System.out.println("---");
}

7.5 工具执行流程

7.5.1 执行流程图

┌─────────────────────────────────────────────────────────────┐
│                    工具执行流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. LLM 推理                                               │
│      │                                                      │
│      ▼                                                      │
│   ┌─────────────────────────────────────────┐              │
│   │ 生成 ToolUseBlock                        │              │
│   │ {                                        │              │
│   │   "name": "get_weather",                 │              │
│   │   "input": {"city": "北京"}              │              │
│   │ }                                        │              │
│   └─────────────────────────────────────────┘              │
│      │                                                      │
│      ▼                                                      │
│   2. 框架处理                                               │
│      │                                                      │
│      ├─── PreActingEvent (Hook)                            │
│      │                                                      │
│      ▼                                                      │
│   ┌─────────────────────────────────────────┐              │
│   │ Toolkit 查找并执行工具                    │              │
│   │ - 参数反序列化                            │              │
│   │ - 调用工具方法                            │              │
│   │ - 结果序列化                              │              │
│   └─────────────────────────────────────────┘              │
│      │                                                      │
│      ├─── PostActingEvent (Hook)                           │
│      │                                                      │
│      ▼                                                      │
│   ┌─────────────────────────────────────────┐              │
│   │ 生成 ToolResultBlock                     │              │
│   │ {                                        │              │
│   │   "name": "get_weather",                 │              │
│   │   "output": "北京:晴,25℃"              │              │
│   │ }                                        │              │
│   └─────────────────────────────────────────┘              │
│      │                                                      │
│      ▼                                                      │
│   3. 结果返回 LLM,继续推理                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7.5.2 并行执行

当 LLM 一次请求多个工具时,框架默认并行执行:

// LLM 可能一次请求多个工具:
// 1. get_weather(city="北京")
// 2. get_weather(city="上海")
// 3. get_time(timezone="Asia/Shanghai")

// 框架会并行执行这些工具,提高效率

7.6 错误处理

7.6.1 工具内部错误处理

@Tool(name = "risky_operation", description = "可能失败的操作")
public String riskyOperation(
        @ToolParam(name = "input") String input
) {
    try {
        return performOperation(input);
    } catch (BusinessException e) {
        // 返回友好的错误信息
        // LLM 会看到这个信息并做出相应处理
        return "操作失败: " + e.getMessage();
    } catch (Exception e) {
        // 记录日志
        log.error("工具执行异常", e);
        return "系统错误,请稍后重试";
    }
}

7.6.2 返回错误结果

@Tool(name = "validate_input", description = "验证输入")
public ToolResultBlock validateInput(
        @ToolParam(name = "data") String data
) {
    if (!isValid(data)) {
        // 返回标记为错误的结果
        return ToolResultBlock.builder()
                .id("tool-id")
                .name("validate_input")
                .isError(true)  // 标记为错误
                .output(List.of(TextBlock.builder()
                        .text("输入格式不正确: " + getErrorDetails(data))
                        .build()))
                .build();
    }
    
    return ToolResultBlock.text("验证通过");
}

7.7 完整示例

package com.example.tools;

import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.annotation.Tool;
import io.agentscope.core.tool.annotation.ToolParam;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;

/**
 * 工具系统完整示例
 */
public class ToolSystemExample {
    
    // ========================================
    // 【工具类定义】
    // ========================================
    
    public static class UtilityTools {
        
        @Tool(
            name = "get_current_datetime",
            description = "获取当前的日期和时间"
        )
        public String getCurrentDatetime() {
            return LocalDateTime.now()
                    .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        }
        
        @Tool(
            name = "calculate",
            description = "执行基本数学运算(加减乘除)"
        )
        public double calculate(
                @ToolParam(name = "a", description = "第一个数") double a,
                @ToolParam(name = "operation", description = "运算符:+, -, *, /") String operation,
                @ToolParam(name = "b", description = "第二个数") double b
        ) {
            return switch (operation) {
                case "+" -> a + b;
                case "-" -> a - b;
                case "*" -> a * b;
                case "/" -> {
                    if (b == 0) throw new IllegalArgumentException("除数不能为零");
                    yield a / b;
                }
                default -> throw new IllegalArgumentException("不支持的运算: " + operation);
            };
        }
        
        @Tool(
            name = "format_list",
            description = "将列表格式化为编号列表"
        )
        public String formatList(
                @ToolParam(name = "items", description = "要格式化的项目列表") 
                List<String> items,
                @ToolParam(name = "style", description = "样式:numbered 或 bullet", required = false)
                String style
        ) {
            if (style == null) style = "numbered";
            
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < items.size(); i++) {
                if ("bullet".equals(style)) {
                    sb.append("• ").append(items.get(i)).append("\n");
                } else {
                    sb.append(i + 1).append(". ").append(items.get(i)).append("\n");
                }
            }
            return sb.toString().trim();
        }
    }
    
    // ========================================
    // 【主程序】
    // ========================================
    
    public static void main(String[] args) {
        String apiKey = System.getenv("DASHSCOPE_API_KEY");
        
        // 1. 创建模型
        DashScopeChatModel model = DashScopeChatModel.builder()
                .apiKey(apiKey)
                .modelName("qwen-max")
                .stream(true)
                .build();
        
        // 2. 创建并注册工具
        Toolkit toolkit = new Toolkit();
        toolkit.registerTool(new UtilityTools());
        
        // 3. 创建智能体
        ReActAgent agent = ReActAgent.builder()
                .name("UtilityAssistant")
                .sysPrompt("""
                    你是一个实用工具助手,可以:
                    1. 查询当前时间
                    2. 进行数学计算
                    3. 格式化列表
                    
                    请根据用户需求使用合适的工具。
                    """)
                .model(model)
                .toolkit(toolkit)
                .build();
        
        // 4. 测试工具调用
        String[] testQueries = {
                "现在几点了?",
                "计算 123 乘以 456 等于多少",
                "把这些内容做成列表:苹果、香蕉、橙子、葡萄"
        };
        
        for (String query : testQueries) {
            System.out.println("问题: " + query);
            Msg response = agent.call(
                    Msg.builder().textContent(query).build()
            ).block();
            System.out.println("回答: " + response.getTextContent());
            System.out.println("---");
        }
    }
}

7.8 本章小结

本章我们学习了:

  1. 工具定义

- @Tool 注解:name、description - @ToolParam 注解:name(必需)、description、required - 支持的参数和返回类型

  1. 工具类型

- 同步工具:直接返回结果 - 异步工具:返回 Mono/Flux - 流式工具:使用 ToolEmitter

  1. 工具注册

- Toolkit 基础使用 - 链式注册方式 - 查看注册的工具

  1. 执行流程

- LLM 生成 ToolUseBlock - 框架执行工具 - 返回 ToolResultBlock

  1. 错误处理

- 工具内部异常处理 - 返回错误结果


练习

  1. 实现天气工具:对接真实的天气 API
  2. 异步搜索工具:使用 WebClient 实现异步 HTTP 请求
  3. 进度报告工具:使用 ToolEmitter 报告长任务进度
  4. 错误处理:测试各种错误情况的处理

下一章 → 第8章 工具高级特性

← 返回目录