Agent 的工具设计——Tool 不是越多越好
Agent 的工具设计——Tool 不是越多越好
上个月有个同事找我 review 他的 Agent 代码,一打开我就愣住了。
他给 Agent 注册了 47 个工具。
47 个。
从发邮件到查天气,从读数据库到写文件,从调用第三方 API 到格式化字符串,全都挂在同一个 Agent 上。我问他:"你测过效果吗?"他说:"测了,但 Agent 老是用错工具,或者把简单任务搞得很复杂。"
我当时就想,这不是 Agent 的问题,这是你给 Agent 出了一道选择题,答案有 47 个。
这篇文章我想聊一个在 Agent 工程里被严重低估的问题:工具设计。很多人把精力都放在 Prompt 怎么写、模型怎么选、流程怎么编排,却忽略了工具本身的设计质量直接决定 Agent 的行为质量。
一、工具多了为什么会出问题
先说一个反直觉的结论:工具数量和 Agent 调用准确率之间,不是线性正相关,而是存在一个拐点,超过这个拐点之后,工具越多 Agent 越糊涂。
这个拐点大概在哪儿?根据我的实际测试,以 GPT-4 系列为底座,当工具数量超过 15-20 个时,调用准确率开始明显下降。到了 30 个以上,经常出现以下几种问题:
1. 工具选择模糊
用户问"帮我查一下最近的订单状态",Agent 不知道该调用 getOrderByOrderId、queryOrderList、getRecentOrders 还是 fetchOrderHistory——这些工具看起来都和"查订单"有关,但语义边界不清。
2. 工具组合爆炸
任务稍微复杂一点,Agent 会开始"创造性地"组合工具,调用路径变得出乎意料。我见过 Agent 为了回答一个简单问题,连续调用了 6 个工具,其中有 4 个是完全没必要的。
3. 幻觉工具调用
更严重的是,工具太多时 Agent 有时会"凭感觉"调用一个实际上不存在或参数用法完全错误的工具,然后把错误返回值再次传给其他工具,整个链条就雪崩了。
我专门做了一组实验数据:
| 工具数量 | 正确选择率 | 平均调用轮次 | 任务完成率 |
|---|---|---|---|
| 5 个 | 96.2% | 1.8 | 94% |
| 10 个 | 91.5% | 2.1 | 89% |
| 20 个 | 82.3% | 2.9 | 78% |
| 30 个 | 71.8% | 3.7 | 65% |
| 47 个 | 58.4% | 5.2 | 51% |
数据不骗人。47 个工具的 Agent,任务完成率还不到 60%。
二、工具粒度:太细和太粗都是病
工具设计的第一个核心问题是粒度。
粒度太细的问题
// 粒度太细的反例
@Tool("获取用户的名字")
public String getUserName(Long userId) { ... }
@Tool("获取用户的邮箱")
public String getUserEmail(Long userId) { ... }
@Tool("获取用户的手机号")
public String getUserPhone(Long userId) { ... }
@Tool("获取用户的注册时间")
public String getUserRegistrationTime(Long userId) { ... }四个工具,Agent 想要"查一下这个用户的基本信息",需要调用 4 次。每次调用都有延迟,都有失败风险,而且 Agent 还需要思考"基本信息包括哪些字段"——这本来是业务知识,不该由 Agent 自己推断。
粒度太粗的问题
// 粒度太粗的反例
@Tool("执行任何数据库操作,包括查询、插入、更新、删除")
public Object executeDatabase(String tableName, String operation,
Map<String, Object> params,
String whereClause,
boolean isTransaction) { ... }这种工具把所有数据库操作合并成一个,参数极其复杂,Agent 很难正确构造参数,而且权限边界模糊——你真的希望 Agent 有权限执行 DELETE 操作吗?
正确的粒度
工具粒度应该对应业务动作,而不是技术操作。
// 合理粒度的示例
@Tool("查询用户基本信息,返回用户的姓名、邮箱、手机号、注册时间等基础字段")
public UserBasicInfo getUserBasicInfo(Long userId) { ... }
@Tool("查询用户的历史订单列表,支持按时间范围和状态过滤")
public List<OrderSummary> getUserOrderHistory(
Long userId,
String startDate,
String endDate,
String status) { ... }一个工具做一件业务上完整的事,参数简洁,返回值有意义。
三、工具描述:比代码本身更重要
这是很多工程师最容易忽视的地方。
工具描述(description)才是 Agent 决策的依据,不是方法名,不是参数名,而是你写的那段自然语言描述。
我来对比两个设计,做同一件事——查询商品库存:
设计差的版本
@Tool("查库存")
public int checkStock(String productId) {
return inventoryService.getStock(productId);
}"查库存"——三个字,太简单了。Agent 面对"告诉我这个商品还有没有货"这类问题时,不一定能关联到这个工具。面对"帮我判断能不能下单"时,更不知道该不该用它。
设计好的版本
@Tool("""
查询指定商品的当前库存数量。
适用场景:
- 用户询问某商品是否有货、还剩多少件
- 判断商品是否可以下单
- 库存预警检查
返回值是整数,表示当前可售库存数量,0表示无货。
注意:此接口返回的是实时库存,可能与页面展示有细微差异。
""")
public int checkStock(String productId) {
return inventoryService.getStock(productId);
}两者代码完全一样,但后者的描述告诉了 Agent 三件事:
- 什么情况下调用我(适用场景)
- 返回值怎么解读(0是无货)
- 有什么注意事项(实时库存的特性)
这三个信息,帮助 Agent 在正确的时机调用正确的工具,并且正确理解返回结果。
四、完整对比:好工具 vs 坏工具
我用一个真实场景来演示,场景是一个电商智能客服 Agent。
用户问:"我昨天买的那双鞋,能退吗?"
坏的工具设计
@Service
public class BadToolDesign {
// 问题1:名字含糊,描述缺失
@Tool("处理退款")
public String handleRefund(String orderId, String reason) {
// 直接执行退款,没有任何检查
return refundService.processRefund(orderId, reason);
}
// 问题2:粒度太细,需要多次调用才能完成一件事
@Tool("获取订单ID")
public String getOrderId(Long userId, String date) {
return orderService.getOrderIdByDate(userId, date);
}
// 问题3:语义重叠,Agent会不知道该选哪个
@Tool("查询订单")
public Order queryOrder(String orderId) {
return orderService.getOrder(orderId);
}
@Tool("获取订单详情")
public OrderDetail getOrderDetail(String orderId) {
return orderService.getOrderDetail(orderId);
}
// 问题4:没有说明前置条件,Agent可能直接调用导致业务错误
@Tool("发起退货申请")
public boolean applyReturn(String orderId) {
return returnService.createReturnApplication(orderId);
}
}这套工具给 Agent 出了难题:退款和退货的区别是什么?查订单和获取订单详情有什么区别?要退货之前需不需要先查什么?这些都不清楚,Agent 会乱猜。
好的工具设计
@Service
public class GoodToolDesign {
/**
* 工具1:查询订单退货资格
* 这是整个退货流程的入口点,先判断能不能退
*/
@Tool("""
检查指定订单是否符合退货条件,返回退货资格评估结果。
请在以下情况调用此工具:
- 用户询问能否退货、退款
- 在发起退货申请之前,必须先调用此工具确认资格
返回结果包含:
- eligible: 是否可以退货(true/false)
- reason: 不可退货时的具体原因(如已超过退货期、特殊商品等)
- deadline: 退货截止日期
- refundAmount: 可退金额(元)
注意:此工具只做判断,不会实际发起任何退货操作。
""")
public ReturnEligibility checkReturnEligibility(
@P("订单ID,格式如ORD-20240115-123456") String orderId) {
return returnPolicyService.checkEligibility(orderId);
}
/**
* 工具2:查询用户最近的订单
* 当用户说"昨天买的"、"上次买的"时使用
*/
@Tool("""
查询用户最近的订单列表,支持按商品类型和时间范围筛选。
当用户提到"最近买的"、"昨天/上周/上个月买的"、没有提供具体订单号时使用此工具。
默认返回最近30天内的订单,按购买时间倒序排列。
如果用户已经提供了订单号,请直接使用其他工具,不要调用此工具。
""")
public List<OrderSummary> getRecentOrders(
@P("用户ID") Long userId,
@P("查询天数,默认30,最大90") Integer days,
@P("商品类型过滤,可选,如:服装、鞋靴、电子产品") String category) {
return orderService.getRecentOrders(userId, days, category);
}
/**
* 工具3:发起退货申请
* 必须在checkReturnEligibility确认eligible=true之后才能调用
*/
@Tool("""
为符合条件的订单发起退货申请,生成退货单号。
重要前置条件:调用此工具之前,必须先调用checkReturnEligibility确认订单可以退货。
如果尚未确认退货资格,请先调用checkReturnEligibility。
此操作会:
1. 创建退货申请记录
2. 发送确认短信给用户
3. 返回退货单号和快递上门取件时间窗口
此操作不可撤销,请在确认用户意愿后再调用。
""")
public ReturnApplication createReturnApplication(
@P("订单ID") String orderId,
@P("退货原因,从以下选择:质量问题/尺码不合适/不喜欢/描述不符/其他") String reason,
@P("用户补充说明,可选") String additionalNote) {
return returnService.createApplication(orderId, reason, additionalNote);
}
}注意几个设计细节:
- 工具3明确声明前置条件,告诉 Agent "你必须先调用工具1"
- 工具2说明了什么情况下不用调用自己(避免不必要调用)
- 每个参数都有
@P注解说明,包括格式和可选值 - 返回值结构有文字说明,Agent 知道怎么解读
五、工具数量的控制策略
说回最开始那 47 个工具的问题。
不是说工具不能多,而是要分组、分层、按需加载。
策略一:工具分组(Tool Groups)
把工具按业务域划分,不同 Agent 实例持有不同工具集:
// 订单相关 Agent,只持有订单工具
@Component
public class OrderAgent {
private final AiServices aiService;
public OrderAgent(ChatLanguageModel model) {
this.aiService = AiServices.builder(OrderAssistant.class)
.chatLanguageModel(model)
.tools(new OrderQueryTool(),
new OrderStatusTool(),
new ReturnEligibilityTool(),
new CreateReturnTool()) // 只有4个,非常聚焦
.build();
}
}
// 商品相关 Agent,只持有商品工具
@Component
public class ProductAgent {
private final AiServices aiService;
public ProductAgent(ChatLanguageModel model) {
this.aiService = AiServices.builder(ProductAssistant.class)
.chatLanguageModel(model)
.tools(new StockCheckTool(),
new ProductDetailTool(),
new PriceHistoryTool(),
new SimilarProductTool()) // 4个,聚焦商品域
.build();
}
}
// 路由 Agent,负责分发,本身不需要太多工具
@Component
public class RouterAgent {
@Autowired
private OrderAgent orderAgent;
@Autowired
private ProductAgent productAgent;
public String route(String userQuery) {
// 根据意图路由到对应 Agent
if (isOrderRelated(userQuery)) {
return orderAgent.handle(userQuery);
}
return productAgent.handle(userQuery);
}
}策略二:动态工具加载
根据上下文决定给 Agent 加载哪些工具,而不是一开始就全部挂载:
@Service
public class DynamicToolLoader {
private final Map<String, List<Object>> toolRegistry = new HashMap<>();
@PostConstruct
public void init() {
toolRegistry.put("ORDER", List.of(new OrderTool(), new ReturnTool()));
toolRegistry.put("PRODUCT", List.of(new ProductTool(), new StockTool()));
toolRegistry.put("USER", List.of(new UserProfileTool(), new PreferenceTool()));
toolRegistry.put("PAYMENT", List.of(new PaymentTool(), new InvoiceTool()));
}
public List<Object> getToolsForContext(String userQuery) {
// 用轻量级分类器判断意图,选择工具集
Set<String> intents = intentClassifier.classify(userQuery);
List<Object> tools = new ArrayList<>();
for (String intent : intents) {
tools.addAll(toolRegistry.getOrDefault(intent, List.of()));
}
// 最多返回10个工具
return tools.stream().limit(10).collect(Collectors.toList());
}
}策略三:工具合并原则
当你发现两个工具的调用场景高度重叠(超过 60%)时,考虑合并它们。
判断标准:如果用户说的同一类话,你不能直觉地知道该用哪个工具,那这两个工具的边界就不够清晰,应该合并。
六、Mermaid:工具数量与性能的关系
七、工具描述的写法模板
给你一个可以直接套用的模板:
[一句话描述工具做什么]
适用场景:
- [场景1:什么时候该调用]
- [场景2:什么时候该调用]
不适用场景(可选,但很有用):
- [什么情况下不要调用此工具]
前置条件(如有):
- [调用此工具前需要满足的条件]
返回值说明:
- [字段名]:[含义和可能的值]
注意事项:
- [副作用说明,如写操作不可撤销]
- [性能/限制说明]用这个模板写出来的工具描述,Agent 的调用准确率会有明显提升,我自己测试提升了大约 15-20%。
八、一个完整的工具设计检查清单
在写完工具之后,对着这个清单过一遍:
九、总结
回到最开始那个同事的问题。
我建议他做了三件事:
- 把 47 个工具按业务域拆分成 6 个子 Agent,每个子 Agent 最多 8 个工具
- 把所有工具描述按照模板重写,平均从 5 个字扩展到 120 个字
- 对参数都加了
@P注解,说明格式和约束
改完之后重测,任务完成率从 51% 提升到了 87%。
这件事让我重新理解了工具设计的本质:你不是在给 LLM 写 API,你是在给一个聪明但对你业务完全陌生的人写操作手册。 你写得越清楚,它犯错就越少。
工具数量少而精,描述长而准,这是 Agent 工具设计的基本法。
