Tool 不是函数,而是企业的软件能力
很多 Agent 项目做到 Tool 这一层,代码会突然变得很简单。
模型识别用户意图,选择一个函数,然后把参数填进去。开发者只需要写几个注解,Agent 就可以查询订单、取消订单、申请退款,甚至修改地址。
@Tool("取消用户订单")
public void cancelOrder(String orderId) {
orderService.cancel(orderId);
}
从代码上看,这几乎没有什么值得讨论的地方。Tool 不过是一个可以被大模型调用的方法,输入是结构化参数,输出是一段执行结果。
也正因为如此,很多团队会把 Tool 设计当成一项接入工作:把现有接口包装一下,加上名称、描述和参数定义,然后交给 Agent。
这种做法在查询类场景里通常没有问题。查询天气、读取文档、搜索商品,调用错一次最多返回结果不准确。但当 Tool 开始改变业务状态时,它就不再只是函数了。
用户说:
帮我取消刚才那张订单。
模型需要做的事情似乎很简单:找到订单号,然后调用 cancelOrder。
可真正执行取消时,后台系统必须回答一连串问题。订单是不是这个用户的,当前状态是否允许取消,商品有没有出库,支付是否完成,库存是否需要释放,优惠券是否需要退回,是否已经存在售后申请,取消之后要不要发消息给商家。
这些问题不属于 Agent,也不应该通过 Prompt 解决。
它们都属于“取消订单”这项业务能力本身。
函数描述的是代码,Tool 描述的是承诺
函数通常只需要对调用方说明参数和返回值。
boolean cancelOrder(Long orderId);
从 Java 语义上看,这个定义已经完整了。传入一个订单编号,返回成功或失败。
但对于 Agent 来说,这远远不够。
它需要知道什么情况下可以调用,调用之后会发生什么,重复调用是否安全,失败以后能否重试,以及结果返回成功究竟意味着什么。
cancelOrder 返回 true,可能只是数据库中的订单状态被改成了 CANCELLED;库存释放和退款仍然在异步处理中。也可能意味着所有关联动作都已经完成,用户的钱已经原路退回。
这两种语义对 Java 编译器没有区别,对 Agent 却完全不同。
如果 Agent 把前一种结果理解成“订单已经彻底取消”,它会向用户给出错误承诺。如果它认为失败可以无条件重试,也可能让后台系统重复发送取消消息。
Tool 因此不能只描述“调用哪个方法”,还必须描述系统愿意对外承诺什么。
一个相对完整的取消能力,至少应该让调用方得到这样的结果:
{
"accepted": true,
"orderStatus": "CANCELLING",
"refundStatus": "PENDING",
"message": "取消申请已受理,退款结果将异步确认"
}
这不是为了让返回值看起来更专业,而是为了区分“请求已经接受”和“业务已经完成”。
Agent 最容易犯的错误之一,就是把接口调用成功理解成业务完成。一个稳定的 Tool 必须主动消除这种歧义。
我们真正暴露的,不应该是数据库动作
如果直接从现有后台接口包装 Tool,很容易得到下面这些能力:
updateOrderStatus
setRefundFlag
changeInventory
updateAddress
这些名字对于内部代码可能很常见,但它们并不是业务能力,只是数据修改动作。
假设 Agent 需要取消一张订单,它可能先调用:
updateOrderStatus(orderId, "CANCELLED")
然后再调用:
releaseInventory(orderId)
最后调用:
refundPayment(orderId)
看起来流程很灵活,实际上已经把业务一致性的责任交给了模型。
如果模型遗漏库存释放,订单取消了,库存却仍然被占用;如果退款成功后状态修改失败,用户收到钱,订单却仍显示已支付;如果 Agent 调整了调用顺序,系统甚至可能进入过去从未出现过的中间状态。
这类问题和模型能力没有太大关系。即使模型每次都按正确顺序调用,网络超时、消息延迟和服务失败仍然会让流程中断。
更合理的 Tool 应该是:
requestOrderCancellation
它表达的是一个完整的业务意图,而不是一组数据库动作。
Agent 只负责提出“取消这张订单”的请求。订单服务负责判断能否取消,并协调库存、支付和优惠券。至于内部使用本地事务、消息队列还是 Saga,不应该暴露给 Agent。
这其实是一个很传统的软件设计原则:让调用方表达意图,而不是告诉系统怎么修改数据。
只是 Agent 出现以后,这条原则变得更重要。过去调用方通常是开发者写的固定代码,错误路径相对可控;现在调用方是一个会动态规划、会重新尝试、也会根据自然语言改变目标的模型。接口越接近底层数据,模型能够制造的错误组合就越多。
Tool 的边界,应该比内部 Service 更窄
很多团队会直接把 Service 方法全部暴露成 Tool,认为这样可以最大化 Agent 的能力。
这种设计通常会让 Agent 看起来很聪明。它可以查询数据、修改状态、创建记录,也可以组合多个接口完成复杂任务。
但能力越多,选择空间越大,风险也越高。
一个订单服务内部可能有几十个方法:
Order queryOrder(Long orderId);
void updateStatus(Long orderId, OrderStatus status);
void releaseCoupon(Long orderId);
void unlockInventory(Long orderId);
void createRefund(Long orderId);
void appendOperationLog(Long orderId, String message);
这些方法可以在服务内部被不同业务流程复用,却不意味着它们都适合作为 Agent Tool。
Agent 不需要知道如何释放优惠券,也不需要单独写操作日志。它只需要几个边界明确的能力:
queryOrder
requestCancellation
submitRefundRequest
changeDeliveryAddress
这里有一个经常被忽略的差别:Service API 是为了支持系统内部协作,Tool API 是为了给一个不完全可信的调用方使用。
即使这个调用方来自公司自己的模型,它仍然不完全可信。模型可能误解用户意图,可能选择错误工具,也可能生成不符合预期的参数。Tool 的设计必须假设调用方会犯错,而不能依赖模型始终正确。
因此,Tool 的能力通常应该比内部 Service 更少,参数限制更严格,业务语义更完整。
这和数据库账号的最小权限原则很像。不是因为我们认定调用方恶意,而是因为任何不必要的能力,最终都会扩大错误的影响范围。
参数校验不是 Tool 的安全边界
为 Tool 增加 JSON Schema,确实可以阻止模型传入错误类型。
{
"orderId": "123456",
"reason": "用户不再需要"
}
它可以保证 orderId 是字符串,reason 不为空,却无法证明这张订单属于当前用户,也无法证明用户有权取消。
这也是很多 Agent Demo 到生产环境之间最大的断层。
Demo 关注的是模型能否正确填参数,生产系统关注的是这次操作是否被允许。
假设用户对客服 Agent 说:
把订单 123456 取消掉。
模型能够准确提取订单号,但后台仍然必须验证:
public CancellationResult requestCancellation(
UserId operator,
OrderId orderId,
CancellationReason reason) {
Order order = orderRepository.get(orderId);
if (!order.belongsTo(operator)) {
throw new PermissionDeniedException();
}
if (!order.canCancel()) {
return CancellationResult.rejected(
"当前订单状态不支持取消"
);
}
return cancellationService.submit(order, reason);
}
这里的 operator 不能由模型自由填写。它必须来自经过认证的会话上下文,由系统注入。
同样,租户编号、用户角色、数据权限和审批额度,也不能作为普通参数暴露给 Agent。
否则模型只需要“猜”出一个管理员角色,就可能绕过原本的权限体系。
Tool 的参数可以来自两部分。一部分由模型生成,例如取消原因、目标日期和用户备注;另一部分必须由平台提供,例如当前用户、租户、请求来源和权限范围。
模型参数:
- orderId
- reason
系统上下文:
- currentUserId
- tenantId
- permissionScope
- traceId
把这两类参数混在一起,是一个非常危险的设计。模型可以参与业务判断,却不能定义自己的身份和权限。
Tool 必须假设自己会被重复调用
用户点击一次按钮,通常只产生一次请求。即使页面卡顿,前端也可以禁用按钮。
Agent 不一样。
它可能因为超时而重试,也可能在重新规划后再次选择同一个 Tool。Workflow 恢复执行时,同一个节点也可能被重新调度。消息系统采用至少一次投递时,调用重复更是正常情况。
所以任何产生副作用的 Tool,都应该先回答一个问题:
同一个意图执行两次,会发生什么?
查询订单执行两次通常没有问题,创建退款执行两次就可能生成两个退款单。
最常见的解决方式,是为每次业务意图生成稳定的幂等键:
public RefundResult submitRefund(
RefundCommand command,
String idempotencyKey) {
RefundOrder existing =
refundRepository.findByIdempotencyKey(idempotencyKey);
if (existing != null) {
return RefundResult.from(existing);
}
RefundOrder refund = RefundOrder.create(
command,
idempotencyKey
);
refundRepository.save(refund);
return RefundResult.from(refund);
}
幂等键也不应该由模型随意生成。模型每次重试时可能生成不同的字符串,那样就失去了去重意义。
更稳定的做法,是由 Workflow 或 Agent 平台根据任务和步骤生成:
refund:workflow-90821:submit
只要还是同一次退款意图,无论模型调用多少次,后台都返回同一笔退款。
这也是为什么 Tool 设计无法只停留在注解和参数描述层面。一个方法加上 @Tool,并不会自动获得幂等、审计和权限能力。
注解解决的是模型如何发现函数。
生产系统关心的是函数被发现以后,怎样确保它不会把业务搞乱。
错误信息必须告诉 Agent 接下来能做什么
传统接口发生错误时,经常返回一段给开发者看的信息:
{
"code": "ORDER_STATUS_ERROR",
"message": "invalid order status"
}
对日志排查来说,这可能已经足够。对 Agent 来说,它几乎没有提供决策信息。
Agent 不知道这个错误是否可以重试,不知道需要用户补充信息,还是应该停止流程。
一个更适合 Tool 的错误结果,应该包含可执行语义:
{
"success": false,
"errorCode": "ORDER_ALREADY_SHIPPED",
"retryable": false,
"nextAction": "CREATE_AFTER_SALE_REQUEST",
"userMessage": "订单已经发货,无法直接取消,可以申请退货"
}
这里最重要的不是错误描述得更长,而是系统明确告诉 Agent:不要重复调用取消接口,应该切换到售后流程。
同样,支付服务超时时可以返回:
{
"success": false,
"errorCode": "PAYMENT_RESULT_UNKNOWN",
"retryable": false,
"nextAction": "QUERY_PAYMENT_STATUS"
}
未知状态不能直接重试支付,必须先查询结果。
如果 Tool 只返回异常文本,Agent 就只能依赖语言理解自行判断下一步。模型可能判断正确,也可能把“状态异常”理解成一次临时失败,然后继续重试。
Tool 的错误协议,本质上是在限制模型的推理空间。系统已经确定的事情,不应该再交给模型猜。
Tool 描述也属于系统设计
很多团队会认真设计 Java 接口,却随手写 Tool 描述。
@Tool("处理订单")
public Result handleOrder(String orderId) {
// ...
}
这种描述对模型几乎没有帮助。“处理订单”可能是查询、取消、退款,也可能是修改地址。模型只能根据名称和少量上下文猜测何时调用。
一个好的 Tool 描述,至少要说明用途、前置条件和不适用场景:
提交订单取消申请。
仅用于用户明确要求取消尚未发货的订单。
该工具不会立即完成退款,返回 accepted=true 仅代表申请已受理。
订单已发货时不要调用,应使用 submitAfterSaleRequest。
支付结果未知时不要重复调用。
这段描述并不是 Prompt 技巧,而是接口契约的一部分。
传统 API 文档主要写给开发者,Tool 描述写给模型。两者面对的读者不同,但承担的责任相同:让调用方知道这个能力的边界。
如果一个 Tool 很难用几句话说明清楚,通常意味着它承担了太多职责。比如“处理售后”可能同时执行退款、退货、换货和补发,这种能力即使交给人类开发者也很难正确调用,更不用说 Agent。
好的 Tool 往往不是数量少,而是语义清晰。模型看到描述后,应该能明确知道什么时候该用,什么时候不该用。
能被调用,不等于可以自动执行
并不是所有 Tool 都应该在模型选择之后立即执行。
查询类操作通常可以直接执行。修改地址、取消订单等操作可能需要用户确认。高金额退款、删除数据和批量操作,则可能需要人工审批。
可以把 Tool 按风险分成不同级别:
只读能力:
queryOrder
queryLogistics
低风险写操作:
addOrderRemark
updateContactPhone
需要用户确认:
cancelOrder
changeDeliveryAddress
需要人工审批:
largeAmountRefund
batchCloseAccount
这种分类不应该只存在于 Prompt 里,而应该成为执行平台的规则。
模型选择了 largeAmountRefund,平台不会立刻调用退款服务,而是生成审批任务。审批完成以后,再由 Workflow 继续执行。
Agent 提出动作
↓
策略引擎检查风险
↓
低风险:直接执行
高风险:用户确认
更高风险:人工审批
Agent 可以建议动作,却不应该自己决定动作是否需要审批。
这条边界越早建立,后续接入更多 Tool 时越安全。否则每增加一个业务能力,都要依赖 Prompt 提醒模型“这个操作比较危险,请谨慎”。
安全规则放在 Prompt 里,本质上只是建议。放在执行层里,才是约束。
Tool 的日志不能只记录方法调用
传统接口日志通常记录请求参数、响应结果和耗时。
Agent 场景还需要知道为什么调用。
同一个退款接口,可能是用户直接要求退款,也可能是 Agent 在处理投诉时主动规划出来的动作。出现争议以后,仅仅知道接口被调用过并不够,还需要还原当时的决策上下文。
一次完整的 Tool 审计记录,至少应该包含:
谁提出了请求
用户原始表达是什么
Agent 选择了哪个 Tool
模型提供了哪些参数
系统注入了哪些身份信息
Workflow 当时处于什么状态
Tool 返回了什么结果
是否经过用户确认或人工审批
这里不一定要保存完整的模型推理过程,但必须保存能够解释业务动作的依据。
例如:
{
"tool": "requestOrderCancellation",
"operator": "user-1024",
"orderId": "order-8891",
"workflowId": "wf-7718",
"source": "agent",
"userIntent": "用户明确要求取消订单",
"confirmation": "confirmed",
"result": "accepted"
}
企业系统迟早会遇到这样的问题:
这张订单为什么被取消?
如果答案只是“因为 Agent 调用了取消函数”,那说明系统没有真正建立责任链。
Agent 时代,后台能力需要重新包装
这并不意味着我们要重写所有 Java 服务。
大多数成熟系统已经有订单、支付、库存和售后能力。真正需要变化的,是在内部 Service 和 Agent 之间增加一层明确的能力边界。
Agent
↓
Tool / Capability API
↓
Application Service
↓
Domain Service
↓
Database / MQ
Tool 层不应该重新实现业务逻辑。它负责收窄能力、注入身份、校验策略、提供幂等键,并把内部复杂结果转换成 Agent 可以理解的语义。
例如内部退款服务可能返回十几种渠道状态,Tool 不需要把所有细节暴露出去,而是整理成几个稳定结果:
ACCEPTED
COMPLETED
REJECTED
RESULT_UNKNOWN
MANUAL_REVIEW_REQUIRED
这层转换很像早年的 BFF,也像防腐层,但它服务的调用方不是某个前端页面,而是一个会自主规划的模型。
调用方越灵活,边界越应该稳定。
Tool 设计得好不好,看 Agent 犯错时会发生什么
一个 Tool 在正常路径上能跑通,并不能说明它设计得好。
真正应该测试的是:
模型选错 Tool 会怎样。
参数不完整会怎样。
同一操作调用两次会怎样。
服务超时以后再次调用会怎样。
用户没有权限会怎样。
流程状态已经变化会怎样。
高风险动作没有确认会怎样。
如果这些情况下,系统只是抛出异常,让 Agent 自己重新判断,那么风险仍然留在模型层。
一个成熟的 Tool 应该把错误限制在自己的边界内。选错了就明确拒绝,重复调用就返回同一结果,状态不允许就给出可执行的下一步,高风险动作就进入确认或审批。
换句话说,Tool 的质量不取决于模型正确时有多顺畅,而取决于模型错误时系统会不会失控。
这也是我现在不太赞同“把所有 API 都开放给 Agent”的原因。
Agent 不是另一种前端,也不是一个更聪明的 Controller。
它是一个新的、不确定的调用方。
面对这种调用方,后台系统最重要的事情不是提供更多函数,而是把真正稳定的业务能力整理出来。
查询订单是一项能力。
申请取消是一项能力。
提交退款是一项能力。
直接修改 status 字段不是。
Tool 的名字可以是函数,参数也可以使用 JSON,但它背后承载的东西远远超过一次方法调用。它包含权限边界、业务规则、幂等语义、风险等级、错误协议和审计责任。
只有这些东西都成立,Agent 才真正获得了执行企业业务的能力。
否则,我们只是给模型递了一把可以调用 Java 方法的遥控器,然后期待它永远不会按错按钮。
Tool 从来不只是函数。它是企业愿意交给 Agent 的、边界清晰的一份能力。
更多推荐

所有评论(0)