给 AI Agent 接上 Tool 之后,一个很常见的场景是这样的:
用户说:"帮我看看这个订单为什么还没发货。"
模型先调用订单查询工具,却把 order_id 填成了用户名;第二次调用时又少传了店铺 ID;好不容易查到订单,它还想直接写一条 SQL 去关联物流表。
这时候你会发现,Agent 能不能稳定工作,未必取决于模型够不够聪明。很多问题,其实在 Tool 设计阶段就已经埋下了。
Tool 太细,模型要自己拼流程;Tool 太粗,又可能失去灵活性。参数定义含糊,模型就容易猜。错误处理只返回一句 "invalid arguments",模型甚至不知道下一步该改什么。
设计 Tool,更像是在给 AI 设计一套"可操作的业务语言"。
Tool 的粒度,不是越粗越好,也不是越细越好

假设你在做一个电商客服 Agent。
一种方案是给它很多底层 Tool:
-
查询订单表
-
查询订单商品表
-
查询支付记录
-
查询物流记录
-
查询退款记录
另一种方案只有一个:
handle_customer_request()
前者看起来灵活,后者看起来简单,但两边都容易出问题。
Tool 太细时,模型必须理解数据库之间的关系,还要自己决定调用顺序。原本后端代码里确定性的业务逻辑,被转移给了一个概率模型。
比如查询"为什么订单还没发货",模型可能需要先查订单,再判断支付状态,然后查库存、仓库状态和物流信息。任何一步选错,都可能得到错误结论。
但如果 Tool 粗到 handle_customer_request() 这种程度,模型几乎失去了判断和组合能力。所有逻辑重新塞回后端,Agent 最后只剩下一个自然语言输入框。
更合适的粒度通常在业务动作这一层。
判断粒度时,可以问一个问题:
这个操作是不是一个可以被独立描述、独立授权、独立验证结果的业务动作?
如果答案是,那么它通常就是一个不错的 Tool 候选。
"查询订单"通常比"执行 SQL"更适合模型

让模型直接调用:
execute_sql(sql)
看起来非常强大。
一个 Tool 就能查询所有数据,后端也不用为几十种业务场景逐个封装接口。
问题在于,你同时把数据库结构、查询逻辑、权限边界和错误风险全部交给了模型。
用户只是问:
"我的订单发货了吗?"
如果 Tool 是 get_order(order_id),模型需要解决的问题很简单:找到订单号,然后调用。
如果 Tool 是 execute_sql(sql),它需要知道订单在哪张表、字段叫什么、物流表怎么关联、哪些状态代表已经发货,还必须确保 SQL 没有越权。
模型原本只需要理解用户意图,现在还得兼职数据库工程师。
更麻烦的是,数据库 Schema 会变化。今天字段叫 shipment_status,半年后拆成物流事件表,所有依赖 SQL 结构的 Agent 行为都可能受到影响。
业务 Tool 相当于增加了一层稳定接口:
用户语言 → 模型判断 → 业务 Tool → 数据库
这样数据库怎么变化,可以留在系统内部消化。
当然,SQL Tool 并非绝对不能存在。内部数据分析 Agent、只读数据探索、受严格权限限制的开发环境,都可能适合 SQL。
关键是不要因为 SQL "能力更强",就默认它是更好的 Tool。
Agent 设计追求的不是理论上的最大能力,而是在允许范围内稳定完成任务的能力。
模型会填错参数,所以不要把 Schema 当说明书

很多 Tool 调用失败,并不是模型选错了 Tool,而是参数出了问题。
假设你提供:
get_order(id)
这里的 id 是什么?
订单 ID、用户 ID、数据库主键,还是订单编号?
对程序员来说,也许上下文很明显;对模型来说,它看到的是多个都"说得通"的可能性。
更好的参数定义应该尽量减少猜测空间:
get_order(
order_id: string
)
并明确说明:
order_id:
用户订单编号,例如 ORD-20260818-12345,
不是用户 ID,也不是支付流水号。
枚举值也不要只给一个 status: string,而应该尽可能限制为允许值。
时间、金额、国家代码、手机号、分页大小同样如此。
这里有一个很实用的原则:
凡是可以通过 Schema 表达的约束,就不要只写在 description 里。
能用枚举,就别让模型自由填写字符串。
能定义整数范围,就别期待模型自己记住"最多 100 条"。
能指定 required,就别靠提示词告诉它"这个字段很重要"。
Tool Schema 不只是接口文档,它本身就是模型生成参数时的护栏。
缺少参数时,别逼模型猜答案

用户说:
"帮我查一下订单。"
系统需要订单号,但用户没提供。
一种糟糕的设计是让模型随便调用:
get_order(order_id="")
然后 Tool 返回错误。
更糟的是,模型自己猜一个订单号。
合理的处理方式取决于参数能否从上下文中安全获取。
如果用户上一句话已经提供订单号,模型可以直接使用。
如果系统有 list_recent_orders(user_id),并且当前用户身份已经确认,可以先查询最近订单,再让用户确认具体哪一笔。
如果参数根本无法推断,就应该向用户询问。
也就是说,缺参数并不等于 Tool 调用错误。
它可能只是意味着当前信息还不足以执行操作。
在高风险操作里尤其如此。退款金额、目标账户、删除对象这类参数,不应该靠模型"最可能的猜测"补齐。
一个成熟的 Agent 应该知道什么时候调用 Tool,也应该知道什么时候暂时不要调用。
参数错误不可避免,关键是让模型有机会修正

即使 Schema 写得很好,参数错误仍然会发生。
比如 Tool 要求:
quantity: integer
模型却传入:
"two"
或者订单号格式要求 ORD-xxxx,模型传了一个不存在的值。
这时错误响应非常重要。
只返回:
400 Bad Request
对模型几乎没有帮助。
更有效的错误应该告诉它三个信息:哪里错了、为什么错、什么样才对。
例如:
INVALID_ARGUMENT
field: quantity
received: "two"
expected: integer >= 1
如果是缺少参数:
MISSING_ARGUMENT
field: order_id
message: 查询订单必须提供订单编号。
如果用户没有提供,请先向用户询问。
如果参数格式正确,但业务上不存在:
ORDER_NOT_FOUND
order_id: ORD-123
message: 未找到该订单。
请检查订单编号,不要自动修改或猜测其他编号。
这样模型就能形成一个非常重要的恢复循环:
调用 → 校验 → 得到结构化错误 → 修正 → 再调用。
不要试图设计一个"模型永远不会填错参数"的系统。更现实的目标,是让错误变得容易发现、容易解释、容易恢复。
好的 Tool,其实是在控制模型的自由度

设计 Agent 时,人很容易被"万能 Tool"吸引。
一个 SQL Tool、一套 HTTP 请求工具、一个浏览器执行器,看起来什么都能做。
但生产系统通常需要另一种思路:模型负责理解模糊的人类意图,系统负责提供清晰、有限、可验证的动作。
Tool 的粒度尽量落在业务能力层;数据库、内部服务和实现细节留在 Tool 后面。参数用 Schema 尽量缩小选择空间,缺参数时允许模型询问,参数错误时返回机器能够理解的结构化信息。
可以把模型想象成一个刚加入公司的聪明同事。
你不会第一天就把生产数据库账号给他,然后说:"用户要什么你自己写 SQL。"
你更可能给他一套清晰的内部系统:查订单在哪里,退款从哪里发起,取消订单需要满足什么条件,操作失败会告诉他原因。
Agent Tool 设计也是一样。
真正可靠的系统,并不是让模型拥有尽可能多的自由,而是把自由留在需要理解和判断的地方,把确定性留给软件。:::
