第三章 工具系统与 @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";
}
}
关键点:
-
@Tool的name属性是工具的唯一标识,Agent 调用时使用此名称 -
@Tool的description属性描述工具的功能,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 会:
- 推理:需要获取北京时间
- 决定调用
get_current_time工具,参数为Asia/Shanghai - 执行工具,获取结果
- 将结果整理后返回给用户
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 工具描述的重要性
工具的 name 和 description 是 Agent 决定是否调用工具的唯一依据。好的描述应该:
- 明确功能边界:说明工具能做什么,不能做什么
- 包含使用场景:什么时候应该调用这个工具
- 参数说明清晰:每个参数的含义、格式、取值范围
反面示例:
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 多了两条更精细的路径:
-
workspace/tools.json:声明 MCP server + 工具粒度白名单/黑名单(推荐) -
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 通过工具获取实时数据、执行操作,从而完成复杂的任务。