文章

工具调用不是接几个 API:设计可控的 Agent Tool Layer

Agent 调工具并不难,难的是把参数、权限、重试和日志做成一套不会失控的接口。

给 Agent 接一个 API,用不了太久。麻烦通常出现在第二天:参数偶尔不合法,接口超时后重复扣款,网页返回了一句“忽略之前的指令”,或者一个查询订单的工具顺手拿到了改订单的权限。

这类问题很少能靠提示词解决。工具层得像正常的公共接口一样设计。

先把工具说清楚

模型需要知道工具做什么、接收哪些字段、会不会改数据、失败后返回什么。get_order 可以这样定义:

{
  "name": "get_order",
  "description": "按订单号读取订单当前状态,不修改订单",
  "input_schema": {
    "type": "object",
    "required": ["order_id"],
    "properties": {"order_id": {"type": "string", "pattern": "^ORD-[0-9]+$"}}
  },
  "side_effect": "read",
  "permission": "orders:read"
}

返回值也尽量结构化。直接返回 statusupdated_atitems,比写一段“订单目前正在处理中”更好用。程序可以区分空数据和接口错误,模型也少猜一次。

工具名不用追求漂亮。refund_orderhandle_customer_request 好,因为谁都看得出前者会动钱。

权限跟着动作走

通用的“数据库工具”很省开发时间,也把权限问题放大了。更稳妥的做法是拆成几个窄动作,并由运行时重新校验当前用户、订单归属和金额上限。模型选对了工具,不代表它有权执行。

我一般按后果处理:搜索和查询可以自动跑;创建草稿、生成工单这类动作要能撤销;退款、发邮件、删除数据,走到执行前停下来等人确认。

这些规则必须写在运行时里。提示词可以提醒模型,拦不住一次错误调用。

超时后到底能不能再来一次

查询接口超时,重试一两次通常没问题。创建资源就麻烦了:第一次请求可能已经成功,只是响应丢了。此时 Agent 再调一次,系统里会多出两张工单或两笔退款。

写操作要带幂等键,同一个任务重试时沿用同一个请求 ID。错误也别只扔异常文本,至少区分 TIMEOUTVALIDATION_ERRORPERMISSION_DENIEDCONFLICT。模型拿到明确错误后,才知道该改参数、换工具,还是直接告诉用户做不了。

工具结果也可能骗人

网页、上传文件和第三方接口都属于外部输入。里面出现“忽略系统指令”时,那句话仍然只是数据。工具返回值要和系统指令分开,限制长度,保留来源;浏览器类工具还要加域名和下载大小限制。

这一点容易被忽略,因为返回内容看起来是给模型读的。但 Agent 恰好会继续根据它做动作,风险也从这里传下去。

出错时要能回放

只存最终回答,排查不了一次 Agent 任务。日志里至少要有 trace_id、每轮选择的工具、脱敏参数、耗时、结果摘要、重试次数和权限判断。

用户说“刚才查错了”,你应该能找到那次查询用了什么条件、命中了哪个版本的数据。否则只能再跑一遍,然后期待这次别错。

任务本身也要有预算:最多调几次工具,最长跑多久,失败重试几次。预算用完就返回部分结果和当前状态,不要让循环在后台悄悄跑下去。

第一版做到这些就够了

先给已有工具补输入 schema 和错误码,再把只读与写入权限分开,把一次完整调用轨迹落下来。做到这里,很多“模型不听话”会露出原形:有的是参数定义含糊,有的是权限开得太大,还有的只是接口超时没有处理。

等这些问题都看得见,再考虑给 Agent 增加更多工具。