AgentScope Java 的 Tool 到底是什么?它不是简单的 Java 方法

AgentScope Java 原理与企业级智能体实战34 次阅读27 分钟

前面几篇我们已经把 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 方法。

Lucas 把普通 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 视角看,它同时承担了几件事。

部分

给谁看

作用

name

模型和运行时

让模型选择工具,也让运行时找到工具

description

模型

告诉模型什么时候应该用这个工具

@ToolParam.name

模型和运行时

定义模型应该输出的参数名

@ToolParam.description

模型

告诉模型参数语义、格式和约束

方法体

Java 程序

真正执行业务逻辑

所以它已经不是“一个方法”。

它变成了一个双面结构。

面向模型的一面:能力说明 + 参数 schema
面向 Java 的一面:可执行方法入口

这才是 Agent Tool 的核心。

@Tool 不是装饰,它会变成运行时对象

真实源码里,@Toolagentscope-coreio.agentscope.core.tool.Tool

它不是只有 namedescription

它还包含这些运行语义。

属性

含义

strict

是否启用更严格的 schema 模式

readOnly

是否只读,没有可观察副作用

concurrencySafe

是否可以和自己并发执行

externalTool

是否只暴露 schema,不在本地执行

stateInjected

是否在调用时注入 AgentState

dangerousFiles

为文件类工具补充敏感文件保护

dangerousDirectories

为文件类工具补充敏感目录保护

converter

自定义工具结果转换器

这几个字段已经说明,@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
Lucas 用放大镜扫描 @Tool 方法,把它们装进 Toolkit 工具箱

这里有一个关键类。

ReflectiveFunctionTool

它是由 @Tool 注解方法生成出来的 ToolBase 子类。

源码注释里说得很清楚,它把注解驱动的注册路径桥接到 ToolBase 契约里,这样 @Tool 方法就能参与权限评估、ToolExecutor 的安全标记机制,以及 Agent 的 pending-confirmation 流程。

这句话很重要。

因为它说明了一个事实。

AgentScope Java 没有把注解方法当成普通反射调用处理。

它会把这个方法包成一个 Runtime 认识的工具对象。

Tool 的底层契约是 AgentTool

再往下看,工具的最基础接口是 AgentTool

它要求一个工具至少提供这些能力。

String getName();
String getDescription();
Map<String, Object> getParameters();

这组方法把 Tool 的本质暴露得很清楚。

方法

代表什么

getName()

工具名,用于模型选择和运行时查找

getDescription()

工具描述,用于模型判断何时调用

getParameters()

参数 JSON Schema,用于告诉模型应该给什么参数

callAsync(...)

真正执行工具,返回 ToolResultBlock

所以 Tool 的完整形态不是。

Java 方法

而是。

工具名 + 工具描述 + 参数 schema + 异步执行入口 + 结果包装

如果它继承 ToolBase,还会继续拥有 readOnlyconcurrencySafeexternalToolstateInjected、权限检查等运行时语义。

这也是为什么我不建议把 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 里就支持自动注入这些东西。

参数类型

作用

ToolEmitter

工具流式输出进度

Agent

当前调用工具的 Agent

AgentState

当前 Agent 状态,需要配合 stateInjected=true

RuntimeContext

每次调用的运行上下文

ToolExecutionContext

旧兼容上下文

用户自定义 POJO

RuntimeContext 按类型取出的业务上下文

Lucas 把 @ToolParam 参数贴成 schema 标签,未贴标签的上下文线留给运行时注入

这也是 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 会和 messagesGenerateOptions 一起进入模型调用链。

到这一刻,Tool 才完成了它面向模型的一面。

Msg 上下文
  +
ToolSchema 列表
  +
GenerateOptions
  ↓
Model
Lucas 把工具 schema 卡片递进模型黑盒,模型只看到说明书看不到 Java 方法体

注意这里的边界。

模型看到的是 ToolSchema,不是 Java 方法体。

模型知道。

有一个叫 calculate_refund_amount 的工具。

它用于计算退款金额。

它需要 order_idrefund_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 不是简单反射调用。

它会处理一整套运行时问题。

运行时动作

为什么需要

根据工具名查找 AgentTool

模型只给了 tool name,需要映射到 Java 工具

检查工具是否存在

不存在时返回结构化错误

检查 group 是否激活

未激活工具不能被随便调用

校验参数 schema

避免模型给错参数结构

合并 preset 参数

框架控制的参数不能被模型覆盖

注入运行上下文

让工具拿到当前 Agent、状态或业务上下文

处理 external tool

外部工具不在本地执行,而是挂起

调度执行线程

默认走 Reactor boundedElastic

应用 timeout / retry

工具执行要有工程边界

包装 ToolResultBlock

工具结果要回到 Agent 上下文

Lucas 把模型吐出的 ToolUseBlock 送进 ToolExecutor 机器

所以模型没有“调用 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 API

RefundTools 不应该只是转发方法。

它应该承担这些职责。

职责

示例

收敛工具粒度

把多个内部查询组合成一个模型可理解的查询工具

写清楚能力边界

告诉模型这个工具只计算,不提交

隐藏敏感参数

用户 id、租户 id、权限信息从上下文注入,不给模型填

做参数兜底

参数缺失时返回结构化错误,而不是抛裸异常

标记风险等级

查询工具 readOnly=true,写工具后续接人工确认

控制返回内容

用 converter 或手工格式化,避免把敏感字段塞回模型

Lucas 在网关门前给查询工具贴绿标,给写操作工具上红色权限锁

这也是我一直强调“Tool 不是方法”的原因。

如果你把 Tool 当方法,就会自然想到。

哪个 Service 有用,就暴露哪个。

如果你把 Tool 当 Agent Runtime 的能力单元,就会先问。

模型应该看到什么能力。

哪些参数应该由模型填写。

哪些上下文应该由系统注入。

哪些动作必须确认。

哪些结果不能原样返回。

这两个思路最后做出来的系统,安全性和可控性完全不同。

一个可用的 Tool 设计检查表

写 AgentScope Java 工具时,我建议先过一遍这个表。

检查项

问题

工具名

是否是模型容易理解的 snake_case 动作名

描述

是否写清楚什么时候用、什么时候不用

参数

是否只暴露模型应该填写的参数

上下文

userId、tenantId、权限信息是否从 RuntimeContext 注入

只读性

查询工具是否标记 readOnly=true

并发性

有共享状态的工具是否关闭 concurrencySafe

写操作

是否后续接权限、HITL、审计

返回值

是否过滤敏感字段,是否足够结构化

异常

是否返回模型能理解的失败信息

这个表不复杂。

但它能帮你避免把 Agent Tool 做成“公开 Service 方法集合”。

Agent Tool 的核心目标不是让模型拥有越多能力越好。

而是让模型在受控边界内选择正确能力。

常见误区

第一个误区,是把 @Tool 当成 RPC 注解。

它不是把方法发布成接口。

它是把方法包装成模型可选择、运行时可执行、权限系统可管理的工具。

第二个误区,是觉得 @ToolParam 可以省。

真实实现里,ToolSchemaGenerator 只会把带 @ToolParam 的参数放进 schema。

你省掉它,模型就不知道这个参数。

第三个误区,是把 description 写成“查询订单”“计算金额”这种短注释。

短不是问题。

问题是没有边界。

尤其是写操作、审批、删除、通知、付款这类工具,必须写清楚使用条件和不能做什么。

第四个误区,是让模型填写系统上下文。

比如 user_idtenant_idrolepermission_level

这些通常不应该由模型填。

更合理的是通过 RuntimeContext 或业务上下文注入。

第五个误区,是直接暴露底层 Service。

Service 面向确定性业务流程。

Tool 面向模型选择。

中间最好有一层专门的 Tool Adapter。

这一篇记住这几句话就够了

AgentScope Java 的 Tool 不是简单 Java 方法。

它是。

给模型看的能力说明
+
给 Runtime 管的运行约束
+
给 Java 执行的方法入口

@Tool 负责把方法声明成工具,并携带只读、并发、安全、外部执行、结果转换等语义。

@ToolParam 负责明确哪些参数暴露给模型,以及这些参数该如何描述。

Toolkit 会扫描 @Tool 方法,生成 ReflectiveFunctionTool,再通过 ToolSchemaProvider 暴露成 ToolSchema

ReActAgent 在 reasoning 阶段把 ToolSchema 给模型,在 acting 阶段根据 ToolUseBlockToolkit 真正执行工具。

企业项目里,Tool 最好是一层 Agent 能力适配器,而不是直接把所有 Service 方法贴上注解。

这样理解以后,再看下一篇就顺了。

工具描述为什么会影响 Agent 行为。

工具 schema 写得好不好,模型是真的会给你反馈的。

继续阅读

基于全文检索与主题相似度