工具描述写不好,Agent 就会乱调用:一次 Tool Schema 实验
事情是这样的。
上一篇我们把 AgentScope Java 的 Tool 拆清楚了。
Tool 不是一个普通 Java 方法。
它至少有两面。
面向模型的一面:name + description + parameters + strict
面向 Java 的一面:method invoke + runtime context + ToolResultBlock
这件事一旦理解了,后面一个问题就会变得很刺眼。
既然模型看到的不是 Java 方法体,而是一份 ToolSchema。
那这份 schema 写得好不好,会不会直接影响模型选错工具?
答案是,会。
而且这不是玄学。
在 AgentScope Java 的真实实现里,ToolSchema 就是会被 Formatter 放进模型请求里的东西。
例如 OpenAI 路径里,OpenAIChatFormatter.applyTools(...) 会把每个 ToolSchema 转成 function tool。
关键字段就是这些。
OpenAIToolFunction.builder()
.name(toolSchema.getName())
.description(toolSchema.getDescription())
.parameters(toolSchema.getParameters());
所以你在 @Tool.description 和 @ToolParam.description 里写的内容,不是普通注释。
它会进入模型上下文。
它会参与模型判断。
它会影响模型在多个工具之间怎么选。
这一篇我们就做一个小实验。
不跑真实大模型 benchmark。
先从 AgentScope Java 的真实 schema 生成链路出发,设计三组工具描述,看看为什么“同一个 Java 方法”,会因为描述不同变成三种完全不同的 Agent 行为风险。

先把真实链路摆出来
先确认一件事。
AgentScope Java 不是把 @Tool 方法体直接塞给模型。
真实链路更接近这样。
@Tool method
↓
Toolkit.registerTool(...)
↓
ReflectiveFunctionTool
↓
ToolSchemaProvider.getToolSchemas()
↓
Formatter.applyTools(...)
↓
Model request
Toolkit.registerTool(Object) 会扫描对象里的 declared methods。
如果方法上有 @Tool,就取出注解。
工具名的规则是。
String toolName =
!toolAnnotation.name().isEmpty()
? toolAnnotation.name()
: method.getName();
工具描述的规则是。
String description =
!toolAnnotation.description().isEmpty()
? toolAnnotation.description()
: "Tool: " + toolName;
这两行很关键。
如果你不写 description,AgentScope Java 会给一个兜底描述。
但这个兜底只能保证 schema 有字段。
它不能替你表达业务边界。
比如一个工具叫 query。
兜底描述会接近。
Tool: query
模型能从这里知道什么?
几乎什么都不知道。
它不知道查订单还是查用户。
它不知道什么时候该用。
它不知道查不到时应该怎么办。
它也不知道这个工具是不是会产生副作用。
所以第一个结论很简单。
在 AgentScope Java 里,description 不是可写可不写的装饰字段。
它是工具暴露给模型时的语义边界。
参数描述也会进入 schema
再看参数。
ToolSchemaGenerator.generateParameterSchema(...) 会遍历方法参数。
但它不是把所有 Java 参数都暴露出去。
它只处理带 @ToolParam 的参数。
简化成代码就是。
for (Parameter param : method.getParameters()) {
ToolParam toolParam = param.getAnnotation(ToolParam.class);
if (toolParam == null) {
continue;
}
ParameterInfo info = extractParameterInfo(param, toolParam);
properties.put(info.name, info.schema);
if (info.required) {
required.add(info.name);
}
}
参数名来自。
String paramName = toolParam.name();
参数类型来自 Java 反射类型。
JsonSchemaUtils.generateSchemaFromType(param.getParameterizedType());
如果你写了参数描述,它还会被塞进参数 schema。
if (!toolParam.description().isEmpty()) {
paramSchema.put("description", toolParam.description());
}
所以 @ToolParam.description 也不是普通注释。
它会变成模型要看的参数说明。
比如。
@ToolParam(
name = "order_id",
description = "The order id from the user's refund request")
String orderId
最后模型看到的不是 String orderId。
它看到的是类似这样的参数 schema。
{
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order id from the user's refund request"
}
},
"required": ["order_id"]
}
这就是为什么我说,Tool Schema 是一个给模型看的接口契约。
Java 方法签名只是输入。
真正影响模型行为的,是转换后的 schema。

实验场景:三个很像的工具
我们设计一个电商售后场景。
里面有三个工具。
| 工具名 | 真实业务能力 |
|---|---|
query_order |
查询订单基本信息 |
query_user |
查询用户基础资料 |
calculate_refund |
计算订单可退金额 |
用户问。
帮我看看订单 A10086 能退多少钱。
理想情况下,Agent 应该调用 calculate_refund。
因为用户问的是“能退多少钱”。
它不是要查订单详情。
也不是要查用户资料。
先写一个业务工具类。
下面这段不是仓库里的原样 Demo,而是基于 AgentScope Java 真实 @Tool / @ToolParam 机制构造的实验代码。
public class AfterSalesTools {
@Tool(
name = "query_order",
description = "Query basic order information by order id.")
public String queryOrder(
@ToolParam(
name = "order_id",
description = "The order id provided by the user")
String orderId) {
return "order_id=" + orderId + ", status=DELIVERED, total=129.00";
}
@Tool(
name = "query_user",
description = "Query basic user profile by user id.")
public String queryUser(
@ToolParam(
name = "user_id",
description = "The user id")
String userId) {
return "user_id=" + userId + ", level=VIP";
}
@Tool(
name = "calculate_refund",
description = "Calculate refundable amount for an order.")
public String calculateRefund(
@ToolParam(
name = "order_id",
description = "The order id from the user's refund request")
String orderId) {
return "order_id=" + orderId + ", refundable_amount=86.50";
}
}
如果只看这段代码,三个工具都能注册成功。
Toolkit 会扫描它们。
ToolSchemaProvider 会把它们组装成 ToolSchema。
Formatter 会把它们交给模型。
但问题在于,模型不是 Java 编译器。
模型不会因为你的方法体里返回了 refundable_amount,就自动知道这个工具更适合退款金额问题。
模型主要看名字、描述和参数 schema。
所以实验要看的不是“Java 能不能执行”。
而是“模型拿到的工具说明是否足够区分这三个工具”。
第一版:描述太短,模型只能猜
第一版我们故意写得很短。
@Tool(name = "query_order", description = "Query order.")
public String queryOrder(...)
@Tool(name = "query_user", description = "Query user.")
public String queryUser(...)
@Tool(name = "calculate_refund", description = "Calculate refund.")
public String calculateRefund(...)
这组三个描述都有意义。
但都太薄。
它们的问题不是“错”。
而是没有边界。
比如 calculate_refund。
Calculate refund.
它没有说明是“计算可退金额”。
没有说明不是“提交退款申请”。
没有说明需要订单 id。
没有说明是否会产生副作用。
它也没有告诉模型,用户只是问“能退多少钱”时应该用它,而不是 query_order。
这会导致一个常见现象。
模型可能先调用 query_order。
因为用户提到了订单。
它也可能调用 calculate_refund。
因为用户提到了退款。
如果模型不确定,还可能两个都调。
这不一定是错。
但它说明工具边界没有被写清楚。
在企业系统里,多一次无意义查询可能只是浪费。
但如果工具里混着写操作,这种“模型猜一下”的空间就危险了。
第二版:描述模糊,模型会把工具能力想大
第二版我们写得更像很多项目里的真实情况。
@Tool(
name = "calculate_refund",
description = "Handle refund related tasks for an order.")
public String calculateRefund(...)
这句话看起来比第一版更自然。
但它更危险。
因为 Handle refund related tasks 太大了。
模型可能把它理解成。
查询退款金额
提交退款申请
查询退款状态
取消退款
解释退款规则
可真实方法只做一件事。
计算可退金额。
如果用户说。
帮我给订单 A10086 申请退款。
这个工具不应该被直接当成“申请退款”工具。
但模糊描述会把模型往这个方向推。
这就是 Tool Schema 里最常见的问题。
代码能力很窄。
描述写得很宽。
模型就会以为自己拿到了更大的能力。

第三版:描述写清楚使用条件和禁止边界
第三版我们把边界写清楚。
@Tool(
name = "calculate_refund",
description = """
Calculate the refundable amount for an existing order.
Use this tool only when the user asks how much money can be refunded.
The order id must be known before calling this tool.
This tool does not submit, approve, cancel, or modify any refund request.
""")
public String calculateRefund(
@ToolParam(
name = "order_id",
description = "The existing order id mentioned by the user, such as A10086")
String orderId) {
return "order_id=" + orderId + ", refundable_amount=86.50";
}
这段描述比第一版长。
但它不是废话。
它给模型四类信息。
| 信息 | 作用 |
|---|---|
| 工具能做什么 | 计算已有订单的可退金额 |
| 什么时候用 | 用户问“能退多少钱”时 |
| 调用前条件 | 必须已经知道订单 id |
| 不能做什么 | 不提交、不审批、不取消、不修改退款 |
这才是 Tool description 在 Agent 系统里的正确价值。
不是把方法名翻译成英文。
而是把工具能力边界写成模型能读懂的操作说明。
如果用户说。
帮我看看订单 A10086 能退多少钱。
模型更容易选择 calculate_refund。
如果用户说。
帮我把订单 A10086 退掉。
模型至少能从描述里看到,这个工具不能提交退款申请。
它应该拒绝、追问,或者寻找另一个真正的写操作工具。
这就是描述带来的行为差异。
这不是 prompt 技巧,是运行时契约
这里容易出现一个误解。
有人会说,这不就是 prompt 写得好一点吗?
不完全是。
Tool description 确实会被模型读。
但它不是塞在系统提示词里的散文。
它位于结构化工具定义里。
在 AgentScope Java 的链路里,ToolSchemaProvider.getToolSchemas() 会组装 ToolSchema。
其中 description 来自工具对象。
ToolSchema.builder()
.name(toolName)
.description(tool.getDescription())
.parameters(registered.getExtendedParameters())
.strict(tool.getStrict())
.outputSchema(tool.getOutputSchema())
.build();
然后 Formatter 把它变成对应模型厂商的 tool 格式。
以 OpenAI 为例。
OpenAIToolFunction.builder()
.name(toolSchema.getName())
.description(toolSchema.getDescription())
.parameters(toolSchema.getParameters());
所以 description 的位置很明确。
不是 Java 注释
不是日志
不是 README
不是系统 prompt 的自由文本
而是模型工具协议的一部分
这也是为什么我更愿意把它叫做“运行时契约”。
它同时约束两端。
模型端:根据 schema 判断是否调用、如何填参
Java 端:根据同一个 schema 校验参数、执行工具、返回结果

strict 只能管格式,不能替你管语义
说到 schema,很多人会想到 strict。
AgentScope Java 的 @Tool 里确实有。
boolean strict() default false;
ReflectiveFunctionTool 会把它转成工具上的 strict 标记。
Boolean strict = annotation.strict() ? Boolean.TRUE : null;
OpenAI Formatter 里也会在 provider 支持时带上。
if (supportsStrict() && toolSchema.getStrict() != null) {
functionBuilder.strict(toolSchema.getStrict());
}
但这里要分清楚两件事。
strict 主要帮助模型更贴近参数 schema。
比如字段名、字段类型、是否需要必填参数。
它不负责判断业务语义。
如果你的 description 写成。
Handle refund related tasks.
即使 strict 开了,模型也可能认为这个工具可以处理“提交退款”。
它只是会更规整地给出参数。
不会自动知道你的方法其实只是计算金额。
所以不要把 strict 当成 description 的替代品。
一个更准确的心智模型是。
| 字段 | 主要负责 |
|---|---|
name |
工具身份和粗粒度意图 |
description |
能力边界和使用条件 |
parameters |
参数结构和类型 |
required |
参数必填约束 |
strict |
让兼容 provider 更严格遵守参数 schema |
这几个字段一起工作。
少一个,模型就少一块判断依据。
企业项目里怎么写好 description
我建议把 Tool description 写成四句话。
做什么。
什么时候用。
调用前必须具备什么条件。
明确不能做什么。
比如查询订单工具。
@Tool(
name = "query_order",
readOnly = true,
description = """
Query basic information for an existing order.
Use this tool when the user asks about order status, amount, items, or delivery state.
The order id must be known before calling this tool.
This tool does not modify the order and does not submit refund, cancel, or payment actions.
""")
public String queryOrder(...)
再比如退款金额工具。
@Tool(
name = "calculate_refund",
readOnly = true,
description = """
Calculate the refundable amount for an existing order.
Use this tool only when the user asks how much can be refunded.
The order id must be known before calling this tool.
This tool does not submit, approve, cancel, or modify any refund request.
""")
public String calculateRefund(...)
如果是写操作,就更要把边界写清楚。
@Tool(
name = "submit_refund_request",
description = """
Submit a refund request for an existing order after the user has explicitly confirmed.
Use this tool only after refund amount, reason, and order id are confirmed.
Do not call this tool for refund estimation or general refund policy questions.
This is a write operation and should be protected by permission and human confirmation.
""")
public String submitRefundRequest(...)
注意这里不只是文字问题。
查询工具可以标 readOnly = true。
写操作后面应该接权限、人工确认、审计日志。
这些是后面几篇会继续讲的东西。
但在 Tool Schema 层,description 至少要先把模型的选择边界写对。

不要把 description 写成内部实现说明
还有一个反方向的坑。
有人会把 description 写成这样。
Call RefundService.calculateRefundAmount and join order_refund_rule table.
这对模型并没有太大帮助。
模型不需要知道你内部调哪个 Service。
模型需要知道它什么时候该用这个工具,以及这个工具能不能满足用户意图。
更好的写法是。
Calculate the refundable amount for an existing order.
Use this tool only when the user asks how much money can be refunded.
This tool does not submit or approve a refund request.
也就是说,description 面向的是模型决策,而不是 Java 代码审计。
内部实现细节应该留在代码、注释、日志和设计文档里。
工具描述应该表达外部能力边界。
一个可复用的 Tool Schema 检查表
每次写 AgentScope Java 工具前,我会过一下这个表。
| 检查项 | 好问题 |
|---|---|
| 工具名 | 模型只看 name 时,能不能大致猜到用途 |
| 正向能力 | description 是否说清楚工具到底做什么 |
| 使用场景 | 是否写了“when the user asks...”这类触发条件 |
| 前置条件 | 是否写清楚必须已有订单 id、用户 id、文件路径等 |
| 禁止边界 | 是否明确不做提交、删除、付款、通知等动作 |
| 参数描述 | 每个 @ToolParam 是否说明来源、格式、例子 |
| required | 必填参数是否真的应该由模型提供 |
| 系统上下文 | user_id、tenant_id、权限信息是否避免让模型填写 |
| 只读/写入 | 查询工具是否标 readOnly,写工具是否准备权限和 HITL |
| 返回值 | 是否返回模型能继续推理的结构化结果 |
这里最值得强调的是“禁止边界”。
很多人会写工具能做什么。
很少人写工具不能做什么。
但在 Agent 系统里,“不能做什么”非常重要。
因为模型会根据用户问题和工具说明做匹配。
如果工具描述把能力写宽了,模型就可能把它用到不该用的场景里。
这篇文章的结论
AgentScope Java 的工具描述不是普通注释。
它会进入 ToolSchema。
ToolSchema 会进入 Formatter。
Formatter 会把它交给模型。
模型再根据这些结构化工具说明决定要不要调用工具、调用哪个工具、参数怎么填。
所以。
description 写得短,模型只能猜。
description 写得宽,模型会把工具能力想大。
description 写清楚边界,Agent 行为才更可控。
strict 可以帮助约束参数格式。
但它不能替你表达业务语义。
真正让 Agent 少乱调用的,是清楚的工具名、准确的工具描述、明确的参数 schema、以及不要把系统上下文交给模型填写。
如果你在企业项目里接 AgentScope Java,我建议从今天开始把 Tool description 当成 API 契约写。
不是给人类看一眼。
而是给模型做选择。
下一篇我们就顺着这个链路继续往下走。
模型真的输出了 Tool Call 之后,AgentScope Java 如何把它变成一次真正的 Java 方法调用。
继续阅读
基于全文检索与主题相似度
AgentScope Java 的 Tool 到底是什么?它不是简单的 Java 方法
事情是这样的。 前面几篇我们已经把 AgentScope Java 的运行心智拆到一个比较清楚的位置了。 Agent 不是一次模型调用。 ReActAgent.call() 背后是一条执行链。 Msg 不是普通字符串。 Formatter 负责把统一消息结构翻译成不同模型厂商的请求。 第 5 篇又把...
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 字符串即可切换模型,无需修改业务代码。