给 Agent 设计工具:参数与错误返回怎么约定才好用

  created  by  鱼鱼 {{tag}}
创建于 2026年10月10日 14:44:36 最后修改于 2026年10月10日 14:44:36

做 Agent 久了会发现:模型“聪不聪明”常常不是瓶颈,工具好不好用才是。同一个模型,换一套清晰的工具契约,成功率能差一截;换一套含糊的,它就会在重试里空转,或者把半成功当成成功继续往下跑。

我踩过的坑大多长这样:工具返回一句 "failed",模型不知道是参数错了、权限不够,还是下游超时;于是它换个说法再调一次,参数其实没变。或者工具抛了未捕获异常,框架把它变成空字符串,模型以为“没结果”就去编造。

这篇文章只谈两件事:参数怎么设计,以及错误怎么返回,让工具对模型可观测、对系统可重试。

工具调用闭环:校验、执行、结构化结果与反馈

图片来源:原创示意图

参数:先让“非法输入”进不来

1. 用明确类型和枚举,少用自由文本

能用枚举就别用字符串。比如 priority: "high"|"normal"|"low",不要 priority: string 然后在文档里写“建议传 high”。模型很会发明 "urgent"、"最高"、"P0"。

日期时间也一样:约定 ISO-8601(并写明时区),比 “传个时间” 靠谱得多。金额用整数分,而不是浮点字符串。

2. 必填与选填要诚实

我见过把三个字段都标成可选,结果模型每次只填其中一个,工具内部再猜。短期好像灵活,长期全是分支地狱。原则是:

  • 完成一次有效调用的最小集合标必填;
  • 真正可选的才可选,并写默认值行为;
  • 互斥参数用 oneOf / 明确文档说明,而不是三个都可选再在代码里 if-else。

3. 给字段写“给模型看的”描述,不是给人类看的注释

user_id: 用户 ID 不够。更好的是:

user_id: 业务用户的稳定 ID,形如 u_xxx。不要传手机号或昵称。若调用方只有订单号,请先调用 find_user_by_order。

这种描述会显著减少“拿错标识符”的调用。多花 30 秒写描述,少花 30 分钟看错误日志。

4. 限制副作用范围

写操作的工具,参数里最好有显式的确认或预览模式,例如 dry_run: boolean。第一次让模型 dry_run=true 看计划,再真正执行。不是所有场景都需要,但涉及删除、转账、广播时,我几乎总会加。

返回值:成功和失败都要结构化

我对工具返回的最低要求是:永远返回机器可读的结构,不要有时 JSON、有时纯文本、有时抛异常到框架外。

一个我常用的形状(示意,不是标准):

{
  "ok": false,
  "error": {
    "code": "RATE_LIMITED",
    "retryable": true,
    "message": "上游限流,建议 2s 后重试",
    "details": {"retry_after_ms": 2000}
  },
  "request_id": "req_..."
}

成功时:

{
  "ok": true,
  "data": { "...": "..." },
  "request_id": "req_..."
}

几个关键点:

  • ok 布尔值:让模型先看这一个字段,再决定往下读。
  • error.code 稳定枚举:给程序和评估用,不要每次换措辞。
  • retryable:直接告诉模型/框架能不能重试,避免它猜。
  • message 给人看,也给模型看:简短、可执行(“缺了字段 X”“状态不允许”)。
  • request_id:排查时能把一次工具调用钉死。

错误分类:比“失败”两个字重要一万倍

我会把错误粗分成几类,并在文档和枚举里固定下来:

类型 例子 retryable 模型该怎么做
参数错误 缺字段、枚举非法 false 改参数,不要原样重试
权限/鉴权 token 过期、无权限 false(除非刷新令牌是另一工具) 换工具或上报人工
状态冲突 订单已取消还要发货 false 改计划或询问用户
限流/超时 429、上游超时 true 退避重试
下游 5xx 依赖故障 true(有上限) 退避,或降级路径
部分成功 批量里成功 3 失败 2 false(需特殊处理) 读 details,不要整批重放

部分成功最容易被忽略。如果工具可能部分成功,必须在契约里写清楚,并返回足够信息让上层做幂等补偿。绝不能只回一句 failed,否则模型一重试就可能重复下单。

错误类型、是否可重试与模型下一步动作对照表

图片来源:原创示意图

可重试:工具侧和 Agent 侧要分工

工具侧:

  • 对明确 retryable=true 的错误,提供 retry_after_ms 或建议退避;
  • 写操作必须幂等(幂等键由调用方传入,或工具根据业务键去重);
  • 超时要有上限,避免模型一边重试一边堆积。

Agent / 框架侧:

  • 尊重 retryable,不要对参数错误死磕三次;
  • 重试时带上同一幂等键;
  • 超过次数就升级:换策略、换工具、或人工。

我见过框架对所有失败统一重试 3 次,结果把“缺字段”也重试三遍——纯粹浪费 token,还把日志打爆。

参数校验失败,也要返回得漂亮

很多实现会在进模型前做 JSON Schema 校验,失败了直接抛给框架。更好的做法是:把校验错误变成一次“工具结果”,结构与运行时错误一致,code=INVALID_ARGUMENT,details 里列出字段级错误。

这样模型能在同一套协议下学习“怎么改参数”,而不是看到一坨堆栈或空响应。

示意:

{
  "ok": false,
  "error": {
    "code": "INVALID_ARGUMENT",
    "retryable": false,
    "message": "参数不合法",
    "details": {
      "fields": [
        {"path": "priority", "issue": "必须是 high|normal|low,收到 urgent"}
      ]
    }
  }
}

文档、示例、反例,三位一体

工具描述里我尽量固定三段:

  1. 什么时候用 / 什么时候别用;
  2. 一个成功调用的完整示例;
  3. 两个常见失败示例(参数错误 + 状态冲突)。

反例特别有用。模型很会模仿文档里的形状;你把失败示例写清楚,它更少发明奇异参数。

和可观测性的关系

约定不只是给模型看的,也是给人和系统看的:

  • 每次调用打点:tool_name、ok、error.code、耗时、request_id;
  • 评估时按 error.code 聚合,而不是按自由文本;
  • 线上告警盯 retryable=true 的比例突然升高(依赖故障),以及 INVALID_ARGUMENT 突然升高(提示词/文档漂移)。

没有稳定错误码,你的 Agent 评估集会很难做——这正好接到更深的话题:评估与回归。但那是后话。此刻只要记住:工具契约是 Agent 系统的 API,值得像对公开 API 一样认真。

一张我自己用的检查清单

发新工具前问自己:

  1. 非法参数能在 schema 层拦住吗?
  2. 成功/失败是同一套结构吗?
  3. 错误有稳定 code 和 retryable 吗?
  4. 写操作有幂等键吗?
  5. 部分成功有没有被定义?
  6. 描述里有没有“别用的时候”和失败示例?
  7. 日志里能否用 request_id 串起一次调用?

全部是“是”,才算能给模型用。否则它不是工具,是陷阱。

一个反面教材:返回 HTML 错误页

有一次下游网关故障,工具把 502 的 HTML 页面原样塞进了 data。模型居然开始“解读”那段 HTML,还总结出“服务器正在维护,建议稍后再试”——然后它真的稍后再试了二十多次。

修复很简单:HTTP 非 2xx 一律进入 ok=false 分支;body 只保留截断后的文本进 details.raw_excerpt(比如前 200 字符),并映射到 UPSTREAM_ERROR。模型不再阅读 HTML,框架按 retryable=true 做有限退避。

教训:不要让模型当 HTML 解析器,也不要把传输层失败伪装成业务数据。

版本与兼容

工具参数会演进。我倾向于:

  • 加可选字段:可以,文档写清默认行为;
  • 改枚举含义:不行,宁可加新枚举值;
  • 删除字段:先标记废弃一轮,评估集里盯旧字段使用率。

Agent 的提示词和工具 schema 经常不同步。发版时把 schema 版本号打进返回值或日志,能少抓很多头发。

和提示词工程的分工

有人把所有约束写进系统提示词:“如果工具失败,请判断是否重试……”——能用,但不稳。模型对长提示词的遵守是概率性的;对结构化字段 retryable 的遵守,可以落成代码。

我的分工是:

  • 提示词:何时调用哪个工具、业务偏好、语气;
  • 工具契约:参数形状、错误码、是否可重试、幂等;
  • 框架策略:退避、次数上限、人工升级。

能下沉到契约和框架的,就不要只写在提示词里。提示词适合表达“软偏好”,不适合当唯一的安全网。

小结

模型会犯错,工具不能火上浇油。把参数说死、把错误说清、把能否重试说明白,Agent 才会像个能协作的系统,而不是一个会打电话但听不懂忙音的人。这些约定看起来像“无聊的后端活”,但在 Agent 项目里,它们往往比换一个更贵的模型更值钱。

评论区
评论
{{comment.creator}}
{{comment.createTime}} {{comment.index}}楼
评论

给 Agent 设计工具:参数与错误返回怎么约定才好用

给 Agent 设计工具:参数与错误返回怎么约定才好用

做 Agent 久了会发现:模型“聪不聪明”常常不是瓶颈,工具好不好用才是。同一个模型,换一套清晰的工具契约,成功率能差一截;换一套含糊的,它就会在重试里空转,或者把半成功当成成功继续往下跑。

我踩过的坑大多长这样:工具返回一句 "failed",模型不知道是参数错了、权限不够,还是下游超时;于是它换个说法再调一次,参数其实没变。或者工具抛了未捕获异常,框架把它变成空字符串,模型以为“没结果”就去编造。

这篇文章只谈两件事:参数怎么设计,以及错误怎么返回,让工具对模型可观测、对系统可重试。

工具调用闭环:校验、执行、结构化结果与反馈

图片来源:原创示意图

参数:先让“非法输入”进不来

1. 用明确类型和枚举,少用自由文本

能用枚举就别用字符串。比如 priority: "high"|"normal"|"low",不要 priority: string 然后在文档里写“建议传 high”。模型很会发明 "urgent"、"最高"、"P0"。

日期时间也一样:约定 ISO-8601(并写明时区),比 “传个时间” 靠谱得多。金额用整数分,而不是浮点字符串。

2. 必填与选填要诚实

我见过把三个字段都标成可选,结果模型每次只填其中一个,工具内部再猜。短期好像灵活,长期全是分支地狱。原则是:

3. 给字段写“给模型看的”描述,不是给人类看的注释

user_id: 用户 ID 不够。更好的是:

user_id: 业务用户的稳定 ID,形如 u_xxx。不要传手机号或昵称。若调用方只有订单号,请先调用 find_user_by_order。

这种描述会显著减少“拿错标识符”的调用。多花 30 秒写描述,少花 30 分钟看错误日志。

4. 限制副作用范围

写操作的工具,参数里最好有显式的确认或预览模式,例如 dry_run: boolean。第一次让模型 dry_run=true 看计划,再真正执行。不是所有场景都需要,但涉及删除、转账、广播时,我几乎总会加。

返回值:成功和失败都要结构化

我对工具返回的最低要求是:永远返回机器可读的结构,不要有时 JSON、有时纯文本、有时抛异常到框架外。

一个我常用的形状(示意,不是标准):

{
  "ok": false,
  "error": {
    "code": "RATE_LIMITED",
    "retryable": true,
    "message": "上游限流,建议 2s 后重试",
    "details": {"retry_after_ms": 2000}
  },
  "request_id": "req_..."
}

成功时:

{
  "ok": true,
  "data": { "...": "..." },
  "request_id": "req_..."
}

几个关键点:

错误分类:比“失败”两个字重要一万倍

我会把错误粗分成几类,并在文档和枚举里固定下来:

类型 例子 retryable 模型该怎么做
参数错误 缺字段、枚举非法 false 改参数,不要原样重试
权限/鉴权 token 过期、无权限 false(除非刷新令牌是另一工具) 换工具或上报人工
状态冲突 订单已取消还要发货 false 改计划或询问用户
限流/超时 429、上游超时 true 退避重试
下游 5xx 依赖故障 true(有上限) 退避,或降级路径
部分成功 批量里成功 3 失败 2 false(需特殊处理) 读 details,不要整批重放

部分成功最容易被忽略。如果工具可能部分成功,必须在契约里写清楚,并返回足够信息让上层做幂等补偿。绝不能只回一句 failed,否则模型一重试就可能重复下单。

错误类型、是否可重试与模型下一步动作对照表

图片来源:原创示意图

可重试:工具侧和 Agent 侧要分工

工具侧:

Agent / 框架侧:

我见过框架对所有失败统一重试 3 次,结果把“缺字段”也重试三遍——纯粹浪费 token,还把日志打爆。

参数校验失败,也要返回得漂亮

很多实现会在进模型前做 JSON Schema 校验,失败了直接抛给框架。更好的做法是:把校验错误变成一次“工具结果”,结构与运行时错误一致,code=INVALID_ARGUMENT,details 里列出字段级错误。

这样模型能在同一套协议下学习“怎么改参数”,而不是看到一坨堆栈或空响应。

示意:

{
  "ok": false,
  "error": {
    "code": "INVALID_ARGUMENT",
    "retryable": false,
    "message": "参数不合法",
    "details": {
      "fields": [
        {"path": "priority", "issue": "必须是 high|normal|low,收到 urgent"}
      ]
    }
  }
}

文档、示例、反例,三位一体

工具描述里我尽量固定三段:

  1. 什么时候用 / 什么时候别用;
  2. 一个成功调用的完整示例;
  3. 两个常见失败示例(参数错误 + 状态冲突)。

反例特别有用。模型很会模仿文档里的形状;你把失败示例写清楚,它更少发明奇异参数。

和可观测性的关系

约定不只是给模型看的,也是给人和系统看的:

没有稳定错误码,你的 Agent 评估集会很难做——这正好接到更深的话题:评估与回归。但那是后话。此刻只要记住:工具契约是 Agent 系统的 API,值得像对公开 API 一样认真。

一张我自己用的检查清单

发新工具前问自己:

  1. 非法参数能在 schema 层拦住吗?
  2. 成功/失败是同一套结构吗?
  3. 错误有稳定 code 和 retryable 吗?
  4. 写操作有幂等键吗?
  5. 部分成功有没有被定义?
  6. 描述里有没有“别用的时候”和失败示例?
  7. 日志里能否用 request_id 串起一次调用?

全部是“是”,才算能给模型用。否则它不是工具,是陷阱。

一个反面教材:返回 HTML 错误页

有一次下游网关故障,工具把 502 的 HTML 页面原样塞进了 data。模型居然开始“解读”那段 HTML,还总结出“服务器正在维护,建议稍后再试”——然后它真的稍后再试了二十多次。

修复很简单:HTTP 非 2xx 一律进入 ok=false 分支;body 只保留截断后的文本进 details.raw_excerpt(比如前 200 字符),并映射到 UPSTREAM_ERROR。模型不再阅读 HTML,框架按 retryable=true 做有限退避。

教训:不要让模型当 HTML 解析器,也不要把传输层失败伪装成业务数据。

版本与兼容

工具参数会演进。我倾向于:

Agent 的提示词和工具 schema 经常不同步。发版时把 schema 版本号打进返回值或日志,能少抓很多头发。

和提示词工程的分工

有人把所有约束写进系统提示词:“如果工具失败,请判断是否重试……”——能用,但不稳。模型对长提示词的遵守是概率性的;对结构化字段 retryable 的遵守,可以落成代码。

我的分工是:

能下沉到契约和框架的,就不要只写在提示词里。提示词适合表达“软偏好”,不适合当唯一的安全网。

小结

模型会犯错,工具不能火上浇油。把参数说死、把错误说清、把能否重试说明白,Agent 才会像个能协作的系统,而不是一个会打电话但听不懂忙音的人。这些约定看起来像“无聊的后端活”,但在 Agent 项目里,它们往往比换一个更贵的模型更值钱。


给 Agent 设计工具:参数与错误返回怎么约定才好用2026-10-10鱼鱼

{{commentTitle}}

评论   ctrl+Enter 发送评论