Agent 工具描述怎么写:模型该调的不调、不该调的乱调怎么排查
数据截至 2026-07。本篇不给任何产品的价格、额度、上下文上限等具体数字,涉及的模型行为、框架字段名与接口口径请以你所用版本的官方最新说明为准。
模型该调的工具不调、不该调的乱调,多数时候既不是模型笨,也不是参数解析有 bug,而是你的描述里根本没写出「什么时候该选它」的判据。 模型面对工具列表时做的是一道选择题:题干只有工具名、一句摘要和参数说明这几行字,它看不到你的代码,看不到你的数据库表,也不知道你心里那条「这个只在用户明确要求导出时才用」的规矩。你没写进去的约束,对它就等于不存在。
先说清本篇和站内几篇的分工,避免你读到重复内容。给 Agent 设计工具 是从零设计一套工具时的正向总纲:模型眼里的工具长什么样、名字怎么起、描述该写哪四件事、参数 schema 和返回值怎么当第二份提示词用、框架帮得到哪一步——它回答的是「白纸一张,该怎么写」。写第一个 MCP server 管的是服务端从零搭起来、把工具注册进去跑通,属于协议与工程脚手架层;工具数量堆多了该怎么裁看 MCP 工具太多会让模型变笨吗?先分清是数量。
本篇是反过来的:工具已经上线、描述也写过一版了,模型的实际表现不对,怎么从现象倒查到该改的那一句。所以本篇的重心在三处别处没展开的东西——把现象归因的判别表、「什么时候不该调」这块负空间怎么写、以及改到什么程度该止损别再折腾文字。设计层的正向清单本篇不复述,需要时按上面的链接过去。
一、先分因:四种现象长得很像,成因完全不同
排查前先把现象分清楚,否则你会拿着改描述的锤子去砸鉴权的钉子。
现象 A:该调的不调。 用户说了「帮我查一下上周的订单」,模型直接开始编造一段解释,或者只是复述你的需求,工具调用记录里空空如也。
现象 B:调了,但选错了工具。 你有 search_orders 和 search_products,模型稳定地选后者。或者你有一个精确查询和一个模糊搜索,它永远走模糊那个。
现象 C:工具对了,参数错了。 日期传成了自然语言而不是 ISO 格式,分页参数漏填,枚举值传了个你没定义过的词,或者把用户原话整段塞进一个本该是 ID 的字段。
现象 D:不该调的乱调。 用户随口一句闲聊,它把写库的工具调了;或者一次任务里同一个工具连着调七八次,每次只换一个无关紧要的参数。
这四类的处置动作不一样。A 有可能根本不是描述问题——工具压根没注册进去、鉴权失败被静默吞掉、或者传输层断了,这些都会表现成「模型没调」。所以第一步永远是看原始的调用记录,而不是看模型说了什么。模型说「我已经帮你查过了」不能作数——它的自述是生成出来的文本,不是执行凭证,这类「谎报成功」的排查另有一篇专门讲,见 Agent 说任务已完成,实际根本没改成,怎。
怎么拿到原始记录,各家客户端不一样,但共同点是:你要看的是发给模型的那份工具列表,以及模型返回的结构化调用字段,而不是渲染在界面上的对话气泡。界面通常做过折叠和美化,参数被截断、失败的那次调用被藏起来都很常见。如果你的框架支持把整轮请求体落盘,先把落盘打开再排查,这一步省下来的时间比后面所有猜测都多。
判别表:
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 完全没有调用记录 | 工具没注册成功 / 传输层断开 / 鉴权失败被吞 | 直接看客户端的原始请求与响应,确认工具列表里到底有没有它;HTTP 层看是不是 401、403 | 先修连接和鉴权,别碰描述 |
| 有工具列表但从不选它 | 摘要没写触发场景,只写了实现细节 | 把工具名和摘要单独抄出来,自己读一遍问「什么时候用它」,答不上来就是描述的问题 | 摘要首句改成「什么时候用」,把用户可能说的原话写进去 |
| 在两个相似工具间反复选错 | 两者描述的差异点没写在首句,命名相近 | 把两条摘要并排放,看差异点是不是要读到第三行才出现 | 首句直接写差异与互斥关系,必要时改名 |
| 参数格式错 | 只写了类型没给样例,没说取值来源 | 看几次错误调用的实际入参,找共同的错法 | 每个非平凡参数补一行格式样例与取值来源 |
| 参数漏填 | 必填与默认值没写清,或参数太多 | 统计漏填的是哪几个字段,是不是排在描述末尾 | 必填项前置并标注,可选项给默认值语义 |
| 同一工具连续重复调用 | 返回结果没告诉模型「已经完整了」 | 看返回体里有没有明确的结束标志,比如总数、是否还有下一页 | 在返回结构和描述里补终止条件 |
| 闲聊场景也触发写操作 | 描述里没有负向边界 | 用几句无关的话做输入,看它调不调 | 补「什么时候不该调」段落,写操作加确认环节 |
| 返回一堆内容后模型无视 | 返回体过长或结构混乱,被截断 | 看返回长度和结构,是否有明显截断痕迹 | 精简返回字段,先给摘要再给明细 |
排到这一步,你已经知道该改哪儿了。剩下的才是写描述的手艺。
二、名字和第一句:模型是在读目录,不是在读手册
工具名是模型看到的第一个信号,权重很高。三条经验:
用「动词 + 对象」,别用团队黑话。 query_erp_v2 这种名字在你们内部一目了然,对模型是无意义字符串。search_orders、create_ticket、read_file 就直白得多。名字里出现内部系统代号,等于把选择依据藏在了一个模型无法解码的缩写里。
别让两个工具的名字只差一个词尾。 get_user 和 get_users 这种成对出现的,模型分不清也很正常,人也分不清。要么合并成一个带参数的工具,要么改成 get_user_by_id 和 list_users。同名或近名工具挂在多个服务上时冲突更明显,那种情况见 多个 MCP server 工具重名,模型老。
第一句写「什么时候用」,不写「这个工具做了什么」。 这是我见过收益最大的一处改动。对比一下:
写成实现视角,模型很难判断该不该选:
调用订单中心的查询接口,支持按时间范围和状态过滤,返回订单列表。
改成触发视角:
当用户想查看已经下过的订单(例如问「上周买了什么」「我那单发货没」)时用这个。
只查订单,不查商品目录,也不改订单状态。
后者多出来的信息是:触发场景、用户可能的原话、以及边界。这三样才是选择题的题干。工具做了什么是实现细节,放第二段。
一句话摘要建议控制在两三行以内。太长会稀释信号,而且当你的工具数量上去以后,所有描述都要一起进上下文,长描述的代价是实打实的。
三、参数说明:类型之外,还要写三件事
参数的类型定义(字符串、整数、枚举)一般由框架的结构约束负责,模型对结构约束的遵守度通常不错。真正出错的是类型之外的部分。
一是格式样例。 日期、ID、路径、版本号这类字段,只写 string 等于没写。补一行 格式:YYYY-MM-DD,例如 2026-07-15 就能挡掉绝大多数格式错。样例比文字描述更管用,模型会照着抄。
二是取值来源。 一个 order_id 是从哪来的?是用户直接给的,还是必须先调 search_orders 拿到的?如果没写清楚,模型很可能自己编一个看起来合理的 ID 传进来。写法很简单:order_id:必须来自 search_orders 的返回结果,不要自行构造。这一句能挡掉一整类幻觉式调用,相关的通用识别方法见 大模型为什么会「胡说八道」?幻觉是什么、怎么。
三是必填、默认值和互斥。 可选参数要说清不填时的行为,不填则默认查最近三十天 这种。互斥关系要明写,order_id 与 keyword 二选一,同时传时以 order_id 为准。参数之间的依赖也要写,只有 status 为已发货时 tracking_no 才有意义。
参数排序也有讲究。把必填的、决定行为分支的参数放前面,把边角的可选参数放后面。描述末尾的内容被忽略的概率明显更高。
还有一条容易被忽视的:参数说明里要写出「用户没说清时该怎么办」。比如 如果用户没有说明时间范围,不要猜,把 start_date 留空并在回复里问一句。不写这条,模型的默认行为是替你猜一个,而猜错的成本往往由线上承担。
四、写清什么时候不该调:负空间比正向描述更值钱
大多数工具描述只写正向:这个工具能干什么。缺的是负空间——什么时候明确不该用它。四类边界值得逐条写进去:
范围边界。 「只覆盖本系统内的订单,第三方渠道订单不在这里,需要转人工。」没有这句,模型会拿它去查它根本查不到的东西,然后把空结果解释成「没有订单」。
时机边界。 「必须先用 A 拿到 ID,再用 B。直接用 B 传用户原话不会有结果。」这类顺序约束光靠工具名传达不了。
代价边界。 写操作、发消息、删文件这类不可逆动作,描述里要明确写「执行前需要用户确认」,并且在实现层真的做确认,别只靠描述这一道防线。描述是软约束,模型可能不遵守;权限和确认是硬约束,这两层不能互相顶替。
重复边界。 「同一组参数在一次任务里调用一次即可,返回的 total 字段已经是全量计数。」没有终止信号,模型会因为不确定而反复重试,账单和时延都跟着上去。这条要跟返回体一起改才有效:描述里说了「已经是全量」,返回体里却既没有 total 也没有 has_more,模型仍然只能靠猜。返回值本身怎么设计成功态、失败态和分页标志,见 Agent 工具返回值怎么设计。
有个具体写法很实用:在描述末尾加一行 不要用于: 后面接两三个具体的反例。反例比抽象规则更容易被遵守,因为它给出的是可以直接匹配的模式。
五、改完要能验证:固定用例集,一次只改一句
描述属于自然语言配置,改动没有编译器帮你把关,唯一可靠的验证方式是回归。
做法不复杂。准备一份用例集,每条包含:输入的一句话、期望调用的工具名、期望的关键参数。二十到四十条就够覆盖主要分支,注意要包含负例(明确不该调工具的输入)和边界例(信息不全、该反问的输入)。
跑法是把用例批量喂给你的 Agent,只记录调用了哪个工具、参数是什么,不看最终回答。一个最小的骨架:
import json
cases = json.load(open("cases.json", encoding="utf-8"))
for c in cases:
calls = run_agent(c["input"]) # 返回 [(tool_name, args), ...]
got = [name for name, _ in calls]
ok = got[:1] == [c["expect_tool"]] if c["expect_tool"] else not got
print(("PASS" if ok else "FAIL"), c["input"], got)
关键纪律是:一次只改一处描述,跑一轮,记结果。 同时改三个工具的描述,结果变好了你也不知道是哪一处起的作用,变差了更是无从回滚。把工具定义文件纳入版本管理,每轮改动单独提交,出问题时 git diff 一眼能看出改了哪句:
git add tools/definitions.json
git commit -m "tools: search_orders 摘要改为触发视角"
还有一点要接受:同一份描述在不同模型上的表现不一样。 有的模型对负向指令遵守得好,有的更容易被工具名带偏;模型换代之后行为也会漂移。所以用例集不是一次性产物,换模型要重跑。
这里要诚实说一句可用性:部分海外厂商的模型对中国大陆有区域限制,官方渠道不支持直接开通或直接调用,具体覆盖范围以各家官网的服务条款与地区说明为准。绕开限制的路子本篇不讨论也不提供。如果你要做跨模型的横向对比,先确认每个候选模型在你所在地区、以你所在主体的身份是否合规可用,再谈评测——否则用例集跑得再漂亮,落到生产上也用不了。同一份描述在国产模型之间同样会有差异,跨模型迁移时把用例集重跑一遍是最省事的验证方式。
六、什么情况下别再折腾描述
改描述是有边际收益递减的。下面几个信号出现时,止损,换条路。
信号一:同一处描述改过三轮,用例通过率在原地打转。 通过率从 60% 到 75% 再到 72%,来回震荡,说明你已经进入了措辞玄学阶段。这时候问题多半不在文字,在工具本身的切分——两个工具的职责天然重叠,语言层面区分不开。回滚到最后一次明确变好的版本,去改工具边界。
信号二:为了让模型选对,你的描述写到了十几行。 描述长度失控本身就是设计有问题的信号。它还会连带推高每轮的上下文占用,工具一多就更明显。这时候合理的做法是合并工具,用一个必填的枚举参数分支,比如把三个查询工具合成 search,加一个 scope: orders | products | tickets。模型填枚举比在三个工具间选择要稳。
信号三:负向约束写了,模型依然会越界,而且越界代价高。 别再往描述里加感叹号了。描述是软约束,扛不住高风险动作。把这类操作从工具列表里摘出去,改成需要显式确认的流程,或者干脆从 Agent 手里收回来,走固定编排。权限收口的做法见 给 agent 全放开权限之后。
信号四:现象是间歇性的,同一条输入时对时错。 这通常不是描述问题,而是上下文里混进了历史噪声,或者工具列表在不同会话里不一致。先去查上下文里有没有残留的历史内容,别在描述上耗时间。
回滚点的定义要提前想好:用例集通过率、加上一条硬性的「负例零误触发」。哪一版改动让负例误触发出现了,哪一版就必须回。
七、避坑清单:为什么会踩,怎么避
把接口文档直接粘成工具描述。 会踩,是因为看起来省事且信息量大。但接口文档是写给调用方工程师的,讲的是字段含义和返回码,完全没有「什么时候该用」这一层。避法:粘完之后自己删掉一半,在最前面补一句触发场景。
用「智能」「自动」「支持多种场景」这类词。 会踩,是因为写的时候觉得显得能力强。实际效果是模型把它当成万能工具,什么都往里塞。避法:所有形容词换成具体的输入输出例子。
只写能干什么,不写不能干什么。 会踩,是因为写文档的习惯就是讲功能。但选择题需要排除项。避法:每个工具的描述末尾固定加一行「不要用于」。
参数只给类型不给样例。 会踩,是因为结构约束里已经声明了 string,看着挺完整。但字符串的合法格式有无数种。避法:凡是有格式要求的字段,一律给一个真实样例。
工具返回一大坨原始 JSON。 会踩,是因为服务端直接透传最省事。结果是返回体挤占上下文,模型抓不到重点,甚至被截断后当成失败重试。避法:返回体只留决策需要的字段,加上明确的计数和状态标志。
在描述里写业务规则的例外条款。 比如「VIP 用户的订单需要走另一个接口,除非是历史订单」。会踩,是因为业务确实这么复杂。但这类条件分支交给模型判断,出错率很高。避法:例外逻辑放到服务端实现里,对模型只暴露一个统一入口。
改了描述不重启、不重连。 会踩,是因为工具列表通常在会话建立时就拉取了,改了文件但客户端拿的还是旧的。避法:改完先确认客户端实际拿到的工具列表是新的,再评估效果。这类白改一轮的情况非常常见,尤其是服务端进程还挂着旧版本的时候。
用例集只放正例。 会踩,是因为写正例更符合直觉。结果是你把描述改得越来越「愿意调用」,误触发率悄悄上升。避法:负例至少占三成,且负例失败视为阻断。
八、收束
工具描述这件事的本质,是把你脑子里那套「什么时候用哪个工具」的隐性判断,用模型能读的方式外化出来。它不是文档,是选择题的题干。所以判断一段描述写得好不好,标准只有一个:把你的工具名和描述单独抄到一张白纸上,你自己能不能只凭这几行字做对选择。你都做不对,模型没道理做得对。
改之前照着这份清单过一遍:
- 有没有先确认调用记录,排除掉连接、注册、鉴权层的问题?
- 每个工具的第一句写的是触发场景,还是实现细节?
- 相似工具的差异点,有没有出现在第一句里?
- 每个非平凡参数有没有格式样例和取值来源?
- 有没有一行明确的「不要用于」?
- 不可逆操作是不是有硬约束兜底,而不是只靠描述?
- 用例集里负例占比够不够,有没有跑过回归?
- 这一轮改动是不是只动了一处,能不能单独回滚?