【AgentScope Java新手村系列】(3)工具系统

第三章 工具系统与 @Tool 注解:让 Agent 自主决定调用哪个 Java 方法

"没有工具的 Agent 只能'纸上谈兵'。本章演示如何用 @Tool 注解把任意 Java 方法注册为 Agent 可调用的能力,Agent 在 ReAct 循环中自主决定何时调用。"

3.1 什么是工具

工具(Tool)是 Agent 与外部世界交互的能力。通过给 Agent 配备工具,它可以:

  • 查询数据库
  • 调用 API
  • 执行计算
  • 读写文件
  • 搜索网络

Agent 在推理过程中会自主决定是否需要调用工具,以及调用哪个工具。

3.2 @Tool 注解

使用 @Tool 注解将 Java 方法注册为 Agent 可调用的工具:

typescript 复制代码
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;

public class MyTools {

    @Tool(name = "get_current_time", description = "获取指定时区的当前时间")
    public String getCurrentTime(
            @ToolParam(name = "timezone", description = "时区名称,例如 'Asia/Shanghai'")
            String timezone) {
        // 实现逻辑
        return "Current time in " + timezone + ": 2024-01-01 12:00:00";
    }
}

关键点:

  • @Toolname 属性是工具的唯一标识,Agent 调用时使用此名称

  • @Tooldescription 属性描述工具的功能,Agent 根据此描述决定何时调用

  • @ToolParam 标注在方法参数上,描述参数的含义

  • 方法的返回值会作为工具的执行结果返回给 Agent

    2.0 完全兼容 1.x 的 @Tool / @ToolParam 注解与 Toolkit.registerTool(...) 注册方式。HarnessAgent 在工作区模式下还支持通过 workspace/tools.json 声明 MCP server 和工具白名单------详见第十五章。

3.3 注册工具

创建 Toolkit 实例,将工具对象注册进去:

ini 复制代码
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new MyTools());

然后把 Toolkit 传给 Agent:

scss 复制代码
import io.agentscope.core.ReActAgent;
import io.agentscope.core.tool.Toolkit;

// 纯 ReActAgent 场景
ReActAgent agent = ReActAgent.builder()
        .name("Assistant")
        .sysPrompt("你是一个可以使用工具的助手。")
        .model(model)
        .toolkit(toolkit)
        .build();

// 或 HarnessAgent 场景
HarnessAgent agent = HarnessAgent.builder()
        .name("Assistant")
        .sysPrompt("你是一个可以使用工具的助手。")
        .model(model)
        .workspace(Path.of("./workspace"))
        .toolkit(toolkit)               // 工具集可以和工作区并存
        .build();

3.4 完整示例:带工具的 Agent

ini 复制代码
package com.example;

import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.formatter.openai.OpenAIChatFormatter;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.model.OpenAIChatModel;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import io.agentscope.core.tool.Toolkit;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class ToolCallingExample {

    public static void main(String[] args) {
        String apiKey = System.getenv("DEEPSEEK_API_KEY");

        // 创建工具集并注册工具
        Toolkit toolkit = new Toolkit();
        toolkit.registerTool(new SimpleTools());

        ReActAgent agent = ReActAgent.builder()
                .name("ToolAgent")
                .sysPrompt(
                        "你是一个可以使用工具的助手。"
                        + "在需要时使用工具来准确回答问题。"
                        + "每次使用工具时请解释你在做什么。")
                .model(OpenAIChatModel.builder()
                        .apiKey(apiKey)
                        .modelName("deepseek-reasoner")
                        .baseUrl("https://api.deepseek.com")
                        .stream(true)
                        .formatter(new OpenAIChatFormatter())
                        .build())
                .toolkit(toolkit)
                .build();

        // 测试工具调用
        String reply = agent.call(new UserMessage("现在北京几点了?"), RuntimeContext.empty())
                .block()
                .getTextContent();
        System.out.println(reply);
    }

    /**
     * 工具类:每个带 @Tool 注解的方法都会被注册为一个工具
     */
    public static class SimpleTools {

        @Tool(name = "get_current_time",
              description = "获取指定时区的当前时间")
        public String getCurrentTime(
                @ToolParam(name = "timezone",
                           description = "时区名称,例如 'Asia/Shanghai'、'America/New_York'")
                String timezone) {
            try {
                ZoneId zoneId = ZoneId.of(timezone);
                LocalDateTime now = LocalDateTime.now(zoneId);
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                return String.format("Current time in %s: %s", timezone, now.format(formatter));
            } catch (Exception e) {
                return "Error: Invalid timezone. Try 'Asia/Shanghai' or 'America/New_York'";
            }
        }

        @Tool(name = "calculate",
              description = "计算简单的数学表达式")
        public String calculate(
                @ToolParam(name = "expression",
                           description = "要计算的数学表达式,例如 '123 + 456'、'10 * 20'")
                String expression) {
            try {
                expression = expression.replaceAll("\\s+", "");
                double result;
                if (expression.contains("+")) {
                    String[] parts = expression.split("\\+");
                    result = Double.parseDouble(parts[0]) + Double.parseDouble(parts[1]);
                } else if (expression.contains("-")) {
                    String[] parts = expression.split("-");
                    result = Double.parseDouble(parts[0]) - Double.parseDouble(parts[1]);
                } else if (expression.contains("*")) {
                    String[] parts = expression.split("\\*");
                    result = Double.parseDouble(parts[0]) * Double.parseDouble(parts[1]);
                } else if (expression.contains("/")) {
                    String[] parts = expression.split("/");
                    result = Double.parseDouble(parts[0]) / Double.parseDouble(parts[1]);
                } else {
                    return "Error: Unsupported operation. Use +, -, *, or /";
                }
                return String.format("%s = %.2f", expression, result);
            } catch (Exception e) {
                return "Error: Invalid expression. Example: '123 + 456'";
            }
        }

        @Tool(name = "search",
              description = "在网络上搜索信息")
        public String search(
                @ToolParam(name = "query", description = "搜索关键词")
                String query) {
            // 这里模拟搜索结果,实际项目中可以接入真实搜索 API
            return "Search results for '" + query + "':\n"
                    + "1. Result about " + query + "\n"
                    + "2. More information on " + query;
        }
    }
}

运行后,向 Agent 提问"现在北京几点了?",Agent 会:

  1. 推理:需要获取北京时间
  2. 决定调用 get_current_time 工具,参数为 Asia/Shanghai
  3. 执行工具,获取结果
  4. 将结果整理后返回给用户

3.5 工具的返回类型

工具方法的返回值会自动转换为字符串传给 Agent。支持以下返回类型:

typescript 复制代码
// 1. 直接返回字符串
@Tool(name = "echo")
public String echo(String input) {
    return "Echo: " + input;
}

// 2. 返回对象(自动序列化为 JSON)
@Tool(name = "get_user")
public Map<String, Object> getUser(String userId) {
    return Map.of("id", userId, "name", "Alice", "age", 30);
}

// 3. 返回 void(Agent 会收到空结果)
@Tool(name = "log")
public void log(String message) {
    System.out.println("[LOG] " + message);
}

// 4. 异步返回 Mono<ToolResultBlock>
@Tool(name = "async_task")
public Mono<ToolResultBlock> asyncTask(String input) {
    return Mono.fromCallable(() -> ToolResultBlock.text("Async result: " + input));
}

3.6 工具分组

当工具较多时,可以使用工具分组(Tool Group)来管理。2.0 继续支持 1.x 的元工具机制:

arduino 复制代码
Toolkit toolkit = new Toolkit();

// 注册工具到不同分组
toolkit.registration()
        .tool(new WeatherTools())
        .group("weather")
        .apply();

toolkit.registration()
        .tool(new MathTools())
        .group("math")
        .apply();

// 创建工具分组(默认全部激活)
toolkit.createToolGroup("weather", "天气工具", true);
toolkit.createToolGroup("math", "数学工具", true);

// 注册元工具,让 Agent 可以自主切换工具组
toolkit.registerMetaTool();

注册元工具后,Agent 会获得一个 reset_equipped_tools 工具,可以自主激活/停用工具组。这在工具数量很多时非常有用,可以减少发送给 LLM 的工具描述数量。

3.7 子 Agent 作为工具(1.x 风格)

1.x 的 toolkit.registration().subAgent(...) 在 2.0 仍可用(兼容层),但新代码请用子 agent 系统 :把子 agent spec 写到 workspace/subagents/<id>.md,主 agent 就能在推理时通过 agent_spawn 委派------这是 Harness 的内置能力,不需要"把 agent 当工具注册"。详见第七章。

下面是 1.x 风格的兼容写法(不推荐新代码使用):

scss 复制代码
// 1.x:把专家 Agent 注册为主 Agent 的工具
Toolkit mainToolkit = new Toolkit();
mainToolkit.registration()
        .subAgent(() -> expertAgent)
        .apply();

下面是 2.0 推荐写法:

yaml 复制代码
<!-- workspace/subagents/data-analyst.md -->
---
description: 数据分析专家。当用户要做统计分析、可视化、数据清洗时使用。
model: openai:gpt-4o-mini
---

你是一个数据分析专家。请按以下流程工作:
1. 先用 read_file / grep_files 收集数据
2. 做必要的统计与可视化
3. 给出业务结论

主 agent 在推理时直接 agent_spawn agent_id="data-analyst" task="...",框架自动加载并执行子 agent,结果以 TOOL_RESULT 块回给主 agent。

3.8 工具描述的重要性

工具的 namedescription 是 Agent 决定是否调用工具的唯一依据。好的描述应该:

  1. 明确功能边界:说明工具能做什么,不能做什么
  2. 包含使用场景:什么时候应该调用这个工具
  3. 参数说明清晰:每个参数的含义、格式、取值范围

反面示例:

typescript 复制代码
@Tool(name = "do_stuff", description = "做事情")
public String doStuff(String input) { ... }

正面示例:

less 复制代码
@Tool(name = "get_weather",
      description = "获取指定城市的当前天气信息。"
              + "返回温度、湿度和天气状况。"
              + "当用户询问某个地点的天气时使用此工具。")
public String getWeather(
        @ToolParam(name = "city",
                   description = "城市英文名称,例如 'Beijing'、'New York'")
        String city) { ... }

3.9 工具执行配置

可以为工具执行配置超时和重试:

scss 复制代码
import io.agentscope.core.model.ExecutionConfig;

ReActAgent agent = ReActAgent.builder()
        .name("Assistant")
        .model(model)
        .toolkit(toolkit)
        .toolExecutionConfig(ExecutionConfig.builder()
                .timeout(Duration.ofSeconds(30))
                .maxRetries(2)
                .build())
        .build();

3.10 2.0 增量:工具在 Harness 中的可见性

HarnessAgent 默认会把 Toolkit 里的所有工具暴露给 LLM------和 1.x 行为一致。如果你想做工具粒度的允许/拒绝,2.0 多了两条更精细的路径:

  1. workspace/tools.json:声明 MCP server + 工具粒度白名单/黑名单(推荐)

  2. Middleware :在 onActing / onModelCall 钩子里改写工具列表

    // workspace/tools.json { "mcpServers": { "name": "github", "command": "npx", "args": \["-y", "@modelcontextprotocol/server-github", "env": { "GITHUB_TOKEN": "${env:GITHUB_TOKEN}" }, "enabled": true } ], "toolFilter": { "mode": "allowlist", "tools": "read_file", "write_file", "grep_files", "github__\*" } }

RC2 新增ToolCallParam.builder(original) 可以从已有参数创建副本并覆盖特定字段:

ini 复制代码
ToolCallParam param = ToolCallParam.builder(original)
        .input(newInput)
        .build();

这在 Middleware 或 Hook 中需要拦截并修改工具调用参数时非常有用------不用从头构建整个 ToolCallParam

3.11 实际应用场景

工具系统可以实现各种能力:

场景

工具示例

信息查询

天气查询、股票查询、知识库搜索

数据处理

数据库查询、文件读写、格式转换

外部交互

发送邮件、创建日程、调用第三方 API

计算推理

数学计算、统计分析、数据可视化

系统操作

创建文件、执行命令、管理进程

在实际项目中,工具通常是与业务系统集成的桥梁。Agent 通过工具获取实时数据、执行操作,从而完成复杂的任务。

相关推荐
吴声子夜歌1 小时前
Java——多线程编程技巧
java·多线程
AI行业学习1 小时前
CC-Switch v3.16.1 官方下载 | 安装配置详细教程【2026.6.10】
java·开发语言·vue.js·python·mysql·eclipse·html
云烟成雨TD2 小时前
Spring AI 1.x 系列【59】容器化开发支持:Docker Compose 与 Testcontainers
人工智能·spring·docker
不负岁月无痕2 小时前
C++ 模板核心内容与高频面试题汇总
java·开发语言·c++
Flittly2 小时前
【AgentScope Java新手村系列】(2)第一个Agent-基础对话
java·spring boot·spring·ai
摇滚侠2 小时前
Spring MVC 不是一个单独的框架,是 Spring 框架的一个模块
java·spring·mvc
阿正的梦工坊2 小时前
【Rust】04-借用、引用与切片
java·数据库·rust
小二·2 小时前
Spring Boot 3 + Vue 3 全栈开发实战
vue.js·spring boot·后端
devilnumber2 小时前
静态代理 & 动态代理:实战运用 + 场景区别 + 怎么选
java·开发语言·代理模式