Tool 不是函数,而是企业的软件能力

Tool 不是函数,而是企业的软件能力 很多 Agent 项目做到 Tool 这一层代码会突然变得很简单。模型识别用户意图选择一个函数然后把参数填进去。开发者只需要写几个注解Agent 就可以查询订单、取消订单、申请退款甚至修改地址。Tool(取消用户订单)publicvoidcancelOrder(StringorderId){orderService.cancel(orderId);}从代码上看这几乎没有什么值得讨论的地方。Tool 不过是一个可以被大模型调用的方法输入是结构化参数输出是一段执行结果。也正因为如此很多团队会把 Tool 设计当成一项接入工作把现有接口包装一下加上名称、描述和参数定义然后交给 Agent。这种做法在查询类场景里通常没有问题。查询天气、读取文档、搜索商品调用错一次最多返回结果不准确。但当 Tool 开始改变业务状态时它就不再只是函数了。用户说帮我取消刚才那张订单。模型需要做的事情似乎很简单找到订单号然后调用cancelOrder。可真正执行取消时后台系统必须回答一连串问题。订单是不是这个用户的当前状态是否允许取消商品有没有出库支付是否完成库存是否需要释放优惠券是否需要退回是否已经存在售后申请取消之后要不要发消息给商家。这些问题不属于 Agent也不应该通过 Prompt 解决。它们都属于“取消订单”这项业务能力本身。函数描述的是代码Tool 描述的是承诺函数通常只需要对调用方说明参数和返回值。booleancancelOrder(LongorderId);从 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 看起来很聪明。它可以查询数据、修改状态、创建记录也可以组合多个接口完成复杂任务。但能力越多选择空间越大风险也越高。一个订单服务内部可能有几十个方法OrderqueryOrder(LongorderId);voidupdateStatus(LongorderId,OrderStatusstatus);voidreleaseCoupon(LongorderId);voidunlockInventory(LongorderId);voidcreateRefund(LongorderId);voidappendOperationLog(LongorderId,Stringmessage);这些方法可以在服务内部被不同业务流程复用却不意味着它们都适合作为 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 取消掉。模型能够准确提取订单号但后台仍然必须验证publicCancellationResultrequestCancellation(UserIdoperator,OrderIdorderId,CancellationReasonreason){OrderorderorderRepository.get(orderId);if(!order.belongsTo(operator)){thrownewPermissionDeniedException();}if(!order.canCancel()){returnCancellationResult.rejected(当前订单状态不支持取消);}returncancellationService.submit(order,reason);}这里的operator不能由模型自由填写。它必须来自经过认证的会话上下文由系统注入。同样租户编号、用户角色、数据权限和审批额度也不能作为普通参数暴露给 Agent。否则模型只需要“猜”出一个管理员角色就可能绕过原本的权限体系。Tool 的参数可以来自两部分。一部分由模型生成例如取消原因、目标日期和用户备注另一部分必须由平台提供例如当前用户、租户、请求来源和权限范围。模型参数 - orderId - reason 系统上下文 - currentUserId - tenantId - permissionScope - traceId把这两类参数混在一起是一个非常危险的设计。模型可以参与业务判断却不能定义自己的身份和权限。Tool 必须假设自己会被重复调用用户点击一次按钮通常只产生一次请求。即使页面卡顿前端也可以禁用按钮。Agent 不一样。它可能因为超时而重试也可能在重新规划后再次选择同一个 Tool。Workflow 恢复执行时同一个节点也可能被重新调度。消息系统采用至少一次投递时调用重复更是正常情况。所以任何产生副作用的 Tool都应该先回答一个问题同一个意图执行两次会发生什么查询订单执行两次通常没有问题创建退款执行两次就可能生成两个退款单。最常见的解决方式是为每次业务意图生成稳定的幂等键publicRefundResultsubmitRefund(RefundCommandcommand,StringidempotencyKey){RefundOrderexistingrefundRepository.findByIdempotencyKey(idempotencyKey);if(existing!null){returnRefundResult.from(existing);}RefundOrderrefundRefundOrder.create(command,idempotencyKey);refundRepository.save(refund);returnRefundResult.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(处理订单)publicResulthandleOrder(StringorderId){// ...}这种描述对模型几乎没有帮助。“处理订单”可能是查询、取消、退款也可能是修改地址。模型只能根据名称和少量上下文猜测何时调用。一个好的 Tool 描述至少要说明用途、前置条件和不适用场景提交订单取消申请。 仅用于用户明确要求取消尚未发货的订单。 该工具不会立即完成退款返回 acceptedtrue 仅代表申请已受理。 订单已发货时不要调用应使用 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 / MQTool 层不应该重新实现业务逻辑。它负责收窄能力、注入身份、校验策略、提供幂等键并把内部复杂结果转换成 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 的、边界清晰的一份能力。