AgentScope Java 的 Tool 到底是什么?它不是简单的 Java 方法
前面几篇我们已经把 AgentScope Java 的运行心智拆到一个比较清楚的位置了。
Agent 不是一次模型调用。
ReActAgent.call() 背后是一条执行链。
Msg 不是普通字符串。
Formatter 负责把统一消息结构翻译成不同模型厂商的请求。
第 5 篇又把 ReAct 拆成一个极简循环。
reasoning -> acting -> observation -> reasoning到这里,问题自然就会落到 acting 阶段。
Agent 到底怎么“行动”。
答案一般会被简化成一句话。
给 Agent 注册工具。
但这个说法太容易让 Java 开发者误会。
很多人第一次看 @Tool,会本能地想。
这不就是给 Java 方法加个注解吗?
如果只是这样,那 AgentScope Java 的 Tool System 就太薄了。
这一篇我们就专门拆这个问题。
AgentScope Java 里的 Tool 到底是什么。
它为什么不是一个普通 Java 方法。

先看一个普通 Java 方法
假设我们在一个电商系统里有退款计算逻辑。
最普通的 Java 写法可能是这样。
BigDecimal amount = refundService.calculateRefundAmount(orderId, reason);这个调用关系很确定。
调用方知道自己要调哪个方法。
调用方知道参数从哪里来。
调用方知道返回值怎么处理。
在传统后端系统里,代码路径通常是开发者写死的。
Controller -> Service -> Repository但 Agent 调工具不是这样。
Agent 的工具调用里,多了一个非常关键的参与者。
模型。
模型不是 Java 进程里的调用方。
模型也不会真的执行 Java 方法。
模型只能看到一份工具说明,然后在生成结果里表达“我想调用这个工具,并且参数是这些”。
所以 Agent 工具调用更像下面这样。
Java Method
↓
Tool Schema
↓
Model sees available tools
↓
Model emits ToolUseBlock
↓
Toolkit executes Java method
↓
ToolResultBlock goes back to context这也是 Tool 和普通方法的第一层区别。
普通方法是给代码调用的。
Tool 是先给模型看的,再由 Java Runtime 执行的。
一个最小退款工具长什么样
为了讲清楚,我们先写一个业务味更重的工具。
下面这段不是 AgentScope Java 仓库里的原样 Demo,而是基于它真实 @Tool / @ToolParam 机制写出来的最小业务示例。
public class RefundTools {
@Tool(
name = "calculate_refund_amount",
description = "Calculate refundable amount for an order. Use this only when the user asks about refund estimation.")
public String calculateRefundAmount(
@ToolParam(
name = "order_id",
description = "The order id from the user's refund request")
String orderId,
@ToolParam(
name = "refund_reason",
description = "The reason provided by the user for requesting refund")
String refundReason) {
return "order_id=" + orderId + ", refundable_amount=86.50";
}如果站在普通 Java 视角看,它确实是一个方法。
但站在 AgentScope Java 视角看,它同时承担了几件事。
部分 | 给谁看 | 作用 |
|---|---|---|
| 模型和运行时 | 让模型选择工具,也让运行时找到工具 |
| 模型 | 告诉模型什么时候应该用这个工具 |
| 模型和运行时 | 定义模型应该输出的参数名 |
| 模型 | 告诉模型参数语义、格式和约束 |
方法体 | Java 程序 | 真正执行业务逻辑 |
所以它已经不是“一个方法”。
它变成了一个双面结构。
面向模型的一面:能力说明 + 参数 schema
面向 Java 的一面:可执行方法入口这才是 Agent Tool 的核心。
@Tool 不是装饰,它会变成运行时对象
真实源码里,@Tool 在 agentscope-core 的 io.agentscope.core.tool.Tool。
它不是只有 name 和 description。
它还包含这些运行语义。
属性 | 含义 |
|---|---|
| 是否启用更严格的 schema 模式 |
| 是否只读,没有可观察副作用 |
| 是否可以和自己并发执行 |
| 是否只暴露 schema,不在本地执行 |
| 是否在调用时注入 |
| 为文件类工具补充敏感文件保护 |
| 为文件类工具补充敏感目录保护 |
| 自定义工具结果转换器 |
这几个字段已经说明,@Tool 不是一个“让方法出现在列表里”的简单注解。
它会影响工具是否可以并发、是否只读、是否本地执行、结果如何包装、是否参与状态注入和安全控制。
也就是说,@Tool 描述的是一个 Agent Runtime 能管理的工具,而不是一个裸 Java 方法。
Toolkit 注册时到底做了什么
真正把方法变成工具的是 Toolkit。
使用方式在官方示例 ToolCallingExample 里很直白。
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new SimpleTools());然后构建 ReActAgent 时把 toolkit 放进去。
ReActAgent agent =
ReActAgent.builder()
.name("ToolAgent")
.model(model)
.toolkit(toolkit)
.build();但 registerTool(...) 背后不是简单 put 一个对象。
Toolkit.registerTool(Object) 会扫描对象里的方法。
只要方法上有 @Tool,就会进入注册流程。
简化一下链路,大概是这样。
Toolkit.registerTool(toolObject)
↓
扫描 declared methods
↓
找到 @Tool 方法
↓
解析 tool name 和 description
↓
生成参数 JSON Schema
↓
创建 ReflectiveFunctionTool
↓
注册进 ToolRegistry
这里有一个关键类。
ReflectiveFunctionTool。
它是由 @Tool 注解方法生成出来的 ToolBase 子类。
源码注释里说得很清楚,它把注解驱动的注册路径桥接到 ToolBase 契约里,这样 @Tool 方法就能参与权限评估、ToolExecutor 的安全标记机制,以及 Agent 的 pending-confirmation 流程。
这句话很重要。
因为它说明了一个事实。
AgentScope Java 没有把注解方法当成普通反射调用处理。
它会把这个方法包成一个 Runtime 认识的工具对象。
Tool 的底层契约是 AgentTool
再往下看,工具的最基础接口是 AgentTool。
它要求一个工具至少提供这些能力。
String getName();
String getDescription();
Map<String, Object> getParameters();这组方法把 Tool 的本质暴露得很清楚。
方法 | 代表什么 |
|---|---|
| 工具名,用于模型选择和运行时查找 |
| 工具描述,用于模型判断何时调用 |
| 参数 JSON Schema,用于告诉模型应该给什么参数 |
| 真正执行工具,返回 |
所以 Tool 的完整形态不是。
Java 方法而是。
工具名 + 工具描述 + 参数 schema + 异步执行入口 + 结果包装如果它继承 ToolBase,还会继续拥有 readOnly、concurrencySafe、externalTool、stateInjected、权限检查等运行时语义。
这也是为什么我不建议把 Agent Tool 简单理解成“方法暴露”。
更准确的说法是。
Tool 是一个给模型选择、给 Runtime 管理、给 Java 执行的能力单元。参数不是方法签名直接暴露出来的
@ToolParam 也不是可有可无。
真实源码里的 ToolParam 注释强调了一个点。
name 是必填的。
原因也很 Java。
Java 默认并不可靠保留运行时参数名。
所以 AgentScope Java 不能赌方法参数名一定能反射出来。
它要求你显式写。
@ToolParam(name = "order_id", description = "The order id")
String orderId然后 ToolSchemaGenerator 会做一件事。
它遍历方法参数,只把带 @ToolParam 的参数放进 JSON Schema。
简化成伪代码就是。
for (Parameter param : method.getParameters()) {
ToolParam toolParam = param.getAnnotation(ToolParam.class);
if (toolParam == null) {
continue;
}
properties.put(toolParam.name(), schemaFromType(param));这意味着什么?
意味着并不是方法里的每个参数都会暴露给模型。
带 @ToolParam 的参数,是模型需要生成的输入。
不带 @ToolParam 的参数,可能是框架注入的运行时对象。
真实的 ToolMethodInvoker 里就支持自动注入这些东西。
参数类型 | 作用 |
|---|---|
| 工具流式输出进度 |
| 当前调用工具的 Agent |
| 当前 Agent 状态,需要配合 |
| 每次调用的运行上下文 |
| 旧兼容上下文 |
用户自定义 POJO | 从 |

这也是 Tool 和普通方法的第二层区别。
普通方法的参数只服务于 Java 调用。
Tool 方法的参数要分成两类。
给模型填写的参数
给框架注入的参数如果你把所有东西都暴露成 @ToolParam,模型就会看到它不该看到的运行时细节。
如果你漏掉必要的 @ToolParam,模型又不知道该如何生成参数。
这不是注解洁癖。
这是 Agent 工具调用协议的一部分。
Schema 会在 reasoning 阶段交给模型
工具注册完以后,并不是马上执行。
它会先变成 ToolSchema。
ToolSchema 里有这些字段。
private final String name;
private final String description;
private final Map<String, Object> parameters;
private final Map<String, Object> outputSchema;
private final Boolean strict;这些字段的组合,才是模型真正能看到的工具说明。
ToolSchemaProvider.getToolSchemas() 会从已注册工具里组装 schema。
如果工具属于某个 group,还会根据当前 active group 做过滤。
然后在 ReActAgent.reasoning(...) 里,工具 schema 会被拿出来。
List<Msg> modelInput =
prependSystemMsg(event.getInputMessages(), event.getSystemMessage());接下来这批 tools 会和 messages、GenerateOptions 一起进入模型调用链。
到这一刻,Tool 才完成了它面向模型的一面。
Msg 上下文
+
ToolSchema 列表
+
GenerateOptions
↓
Model
注意这里的边界。
模型看到的是 ToolSchema,不是 Java 方法体。
模型知道。
有一个叫 calculate_refund_amount 的工具。
它用于计算退款金额。
它需要 order_id 和 refund_reason。
但模型不知道这个方法里查了哪张表、调了哪个服务、用了什么缓存。
这些仍然是 Java 程序内部的事。
这就像你给模型递了一张工具说明书。
而不是把整个后端系统交给它。
真正执行发生在 acting 阶段
当模型决定调用工具时,它会在助手消息里产出工具调用结构。
在 AgentScope Java 的消息模型里,这会落到 ToolUseBlock。
前几篇我们已经讲过,ToolUseBlock 里有工具名、参数、id、metadata、state 等信息。
到了 ReActAgent.acting(...),框架会先找出还没有结果的工具调用。
List<ToolUseBlock> pendingToolCalls = extractPendingToolCalls();如果没有 pending tool call,就进入下一轮。
如果有,就执行工具。
真正执行工具的地方会走到。
toolkit.callTools(toolCalls, toolExecutionConfig, this, buildMergedRuntimeContext())再往下是 ToolExecutor.executeAll(...) 和 ToolExecutor.execute(...)。
ToolExecutor 不是简单反射调用。
它会处理一整套运行时问题。
运行时动作 | 为什么需要 |
|---|---|
根据工具名查找 | 模型只给了 tool name,需要映射到 Java 工具 |
检查工具是否存在 | 不存在时返回结构化错误 |
检查 group 是否激活 | 未激活工具不能被随便调用 |
校验参数 schema | 避免模型给错参数结构 |
合并 preset 参数 | 框架控制的参数不能被模型覆盖 |
注入运行上下文 | 让工具拿到当前 Agent、状态或业务上下文 |
处理 external tool | 外部工具不在本地执行,而是挂起 |
调度执行线程 | 默认走 Reactor |
应用 timeout / retry | 工具执行要有工程边界 |
包装 | 工具结果要回到 Agent 上下文 |

所以模型没有“调用 Java 方法”。
模型只是生成了工具调用意图。
Java Runtime 才是执行者。
这也是 Tool 和普通方法的第三层区别。
普通方法调用是代码内部控制流。
Tool 调用是模型意图和 Java Runtime 之间的结构化交接。
为什么 Tool 描述不是注释
很多 Java 开发者会低估 description。
因为在普通代码里,注释写差一点,程序照样跑。
但 Tool 的 description 不一样。
它会进入模型上下文。
它是模型判断“要不要调用这个工具”的输入之一。
比如这两个工具描述。
@Tool(name = "calculate_refund_amount", description = "Calculate amount")和。
@Tool(
name = "calculate_refund_amount",
description = "Calculate the refundable amount for an existing order. Use it only after the order id is known. It does not submit a refund request.")对 Java 编译器来说,都没区别。
对模型来说,区别很大。
第二个描述至少告诉模型三件事。
描述信息 | 对模型决策的影响 |
|---|---|
计算退款金额 | 知道工具能力边界 |
需要已知订单 id | 参数不足时应该先追问或先查订单 |
不提交退款申请 | 避免把计算工具误当写操作 |
所以在 Agent 工具系统里,description 不是写给人类代码审查的普通注释。
它更像模型决策时的 API 文档。
写不好,模型就可能乱选工具、错填参数、把查询当写入。
这就是下一篇“Tool Schema 实验”要继续展开的内容。
企业项目里不要直接暴露 Service 方法
讲到这里,可以落到企业项目了。
假设你已经有这些 Service。
orderService.queryOrder(orderId);
refundService.calculateRefundAmount(orderId);
refundService.submitRefundApply(orderId, amount);
messageService.sendRefundNotice(userId, content);最容易犯的错是。
把这些方法一股脑全部标成 @Tool。
看起来很快。
实际很危险。
因为 Service 方法是给确定性业务流程用的。
Agent Tool 是给模型选择的能力入口。
二者粒度不一定一样。
企业项目里更合理的做法,是专门设计一层 Agent Tool Adapter。
Agent
↓
RefundTools
↓
RefundApplicationService
↓
Domain Service / Repository / External APIRefundTools 不应该只是转发方法。
它应该承担这些职责。
职责 | 示例 |
|---|---|
收敛工具粒度 | 把多个内部查询组合成一个模型可理解的查询工具 |
写清楚能力边界 | 告诉模型这个工具只计算,不提交 |
隐藏敏感参数 | 用户 id、租户 id、权限信息从上下文注入,不给模型填 |
做参数兜底 | 参数缺失时返回结构化错误,而不是抛裸异常 |
标记风险等级 | 查询工具 |
控制返回内容 | 用 converter 或手工格式化,避免把敏感字段塞回模型 |

这也是我一直强调“Tool 不是方法”的原因。
如果你把 Tool 当方法,就会自然想到。
哪个 Service 有用,就暴露哪个。
如果你把 Tool 当 Agent Runtime 的能力单元,就会先问。
模型应该看到什么能力。
哪些参数应该由模型填写。
哪些上下文应该由系统注入。
哪些动作必须确认。
哪些结果不能原样返回。
这两个思路最后做出来的系统,安全性和可控性完全不同。
一个可用的 Tool 设计检查表
写 AgentScope Java 工具时,我建议先过一遍这个表。
检查项 | 问题 |
|---|---|
工具名 | 是否是模型容易理解的 snake_case 动作名 |
描述 | 是否写清楚什么时候用、什么时候不用 |
参数 | 是否只暴露模型应该填写的参数 |
上下文 | userId、tenantId、权限信息是否从 RuntimeContext 注入 |
只读性 | 查询工具是否标记 |
并发性 | 有共享状态的工具是否关闭 |
写操作 | 是否后续接权限、HITL、审计 |
返回值 | 是否过滤敏感字段,是否足够结构化 |
异常 | 是否返回模型能理解的失败信息 |
这个表不复杂。
但它能帮你避免把 Agent Tool 做成“公开 Service 方法集合”。
Agent Tool 的核心目标不是让模型拥有越多能力越好。
而是让模型在受控边界内选择正确能力。
常见误区
第一个误区,是把 @Tool 当成 RPC 注解。
它不是把方法发布成接口。
它是把方法包装成模型可选择、运行时可执行、权限系统可管理的工具。
第二个误区,是觉得 @ToolParam 可以省。
真实实现里,ToolSchemaGenerator 只会把带 @ToolParam 的参数放进 schema。
你省掉它,模型就不知道这个参数。
第三个误区,是把 description 写成“查询订单”“计算金额”这种短注释。
短不是问题。
问题是没有边界。
尤其是写操作、审批、删除、通知、付款这类工具,必须写清楚使用条件和不能做什么。
第四个误区,是让模型填写系统上下文。
比如 user_id、tenant_id、role、permission_level。
这些通常不应该由模型填。
更合理的是通过 RuntimeContext 或业务上下文注入。
第五个误区,是直接暴露底层 Service。
Service 面向确定性业务流程。
Tool 面向模型选择。
中间最好有一层专门的 Tool Adapter。
这一篇记住这几句话就够了
AgentScope Java 的 Tool 不是简单 Java 方法。
它是。
给模型看的能力说明
+
给 Runtime 管的运行约束
+
给 Java 执行的方法入口@Tool 负责把方法声明成工具,并携带只读、并发、安全、外部执行、结果转换等语义。
@ToolParam 负责明确哪些参数暴露给模型,以及这些参数该如何描述。
Toolkit 会扫描 @Tool 方法,生成 ReflectiveFunctionTool,再通过 ToolSchemaProvider 暴露成 ToolSchema。
ReActAgent 在 reasoning 阶段把 ToolSchema 给模型,在 acting 阶段根据 ToolUseBlock 让 Toolkit 真正执行工具。
企业项目里,Tool 最好是一层 Agent 能力适配器,而不是直接把所有 Service 方法贴上注解。
这样理解以后,再看下一篇就顺了。
工具描述为什么会影响 Agent 行为。
工具 schema 写得好不好,模型是真的会给你反馈的。
继续阅读
基于全文检索与主题相似度
工具描述写不好,Agent 就会乱调用:一次 Tool Schema 实验
事情是这样的。 上一篇我们把 AgentScope Java 的 Tool 拆清楚了。 Tool 不是一个普通 Java 方法。 它至少有两面。 这件事一旦理解了,后面一个问题就会变得很刺眼。 既然模型看到的不是 Java 方法体,而是一份 ToolSchema。 那这份 schema 写得好不好,...
ReAct 不神秘:我用 Java 手写了一个极简 Agent 循环
事情是这样的。 前面几篇我们一直在拆 AgentScope Java 的运行结构。 第 1 篇讲的是,Agent 不是一次模型 API 调用。 第 2 篇讲的是,agent.call(...) 背后不是一跳,而是一整条执行链。 第 3 篇讲的是,Msg 不是 String,因为 Agent 运行里要...
换模型不改 Agent?AgentScope Java 的 Formatter 层做了什么
AgentScope Java 的 Formatter 负责把统一的 Msg、ToolSchema、GenerateOptions 转成各厂商的请求结构并解析返回,使 Agent 只需改 model 字符串即可切换模型,无需修改业务代码。