很多 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 的、边界清晰的一份能力。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐