前几篇我们把 ReAct 循环、工具调用、记忆规划、护栏依次拆过一遍。上一篇结尾留了一句:工具描述怎么写,才能让模型更少调错工具、更少填错参数。这篇就来还这笔账。
核心判断先放最前面:工具定义不是写给人看的函数签名,而是一段会被塞进模型上下文里的提示文本。 你起的名字、写的描述、定的参数结构、返回的内容,模型全都当成说明书来读。所以写工具的功夫,一半在代码里,一半在提示词里。
模型拿到工具定义后要做的判断
模型并不能“调用”你的函数。请求里的 tools 参数会被序列化后注入 system message,用模型训练时见过的格式呈现,因此它既占用上下文长度,也按输入 token 计费1。读着这段文本,模型要连做五个判断:这一轮该不该用工具、要用的话用哪一个、参数各填什么值、读完返回值下一步做什么、出错了怎么补救。
名字、描述、参数 schema、返回值、错误消息分别负责其中一环。哪一环没写清,模型就在那一环上猜,而猜错的结果会直接变成一次真实调用。
名字:最便宜的一次消歧
名字是模型读描述之前就先扫到的东西。get_current_weather 比 weather 更不容易被误用5;参数名同理,user_id 比 user 好,因为它直接点明需要的是标识符,而不是一个用户对象4。
工具一多,撞名就成了主要问题。Anthropic 的建议是按命名空间分组:先按服务分(asana_search、jira_search),再按资源分(asana_projects_search、asana_users_search)4。这是在帮模型做二分查找,而不是在一堆含义模糊的名字里硬猜。
另一个常被忽略的做法是合并:如果 create_pr、review_pr、merge_pr 永远按固定顺序连着调,不如合成一个带 action 参数的工具。工具少一个,选择歧义就少一层3。OpenAI 给的经验值是单轮可用工具尽量控制在 20 个以内1——这只是软建议,但方向明确:工具囤多了,选择准确率会掉。
描述:写清“什么时候用”和“什么时候别用”
Anthropic 把话讲得很直白:描述是影响工具表现最重要的因素,每个工具至少写 3–4 句3。好的描述回答四件事:做什么、什么时候该用、什么时候不该用、输入输出各代表什么。
看一个具体写法。OpenAI 的 o 系列指南给 file_create 举的例子,除“创建文件”本身,还写了:只在需要持久保存且文件尚不存在时调用;先调 directory_check 确认目录存在;不要覆盖已有文件,要替换就先删后建2。这些不是给别人看的文档细节,而是直接替模型划好了决策边界。该指南还提到一个反直觉的细节:把最重要的规则放在描述最前面、少写不具约束力的铺垫话,在同一套评测里工具调用准确率差了 6%2。
描述能修的问题有多隐蔽,Anthropic 讲过一个自家案例:他们上线 web search 工具后发现,模型总在 query 参数里自作主张追加 “2025”,把搜索结果带偏。修的办法不是改代码,是改工具描述4。
反过来说,“这个工具用来处理数据”这种描述等于没写——模型无法从抽象名词里判断该不该用。
参数:用 schema 消灭“非法状态”
参数结构能替你挡掉一大类错误:它可以规定什么组合合法。
- 能用枚举就别开自由文本。
units写成["celsius", "fahrenheit"],模型就没机会编出centigrade1。 - 不要把非法组合做成可选项。 OpenAI 举的反例是
toggle_light(on: bool, off: bool):两个参数能同时为真,这种设计把非法状态写进了接口本身1。 - 别让模型填你已经知道的东西。 如果代码里已经有
order_id,就不要把它做成参数让模型生成,而是定义成无参数的submit_refund(),由代码自己传入 id1。 - 扁平优先于深层嵌套。 嵌套越多,模型越容易漏填子字段或填错同名层级,OpenAI 明确建议参数树尽量做平2。
strict模式值得开,但要清楚代价。 开启后参数会严格符合 schema,而不是尽力而为。代价是两条硬要求:每个对象都要写additionalProperties: false,并且properties里的所有字段都必须出现在required里;真正的可选字段靠给它加上null类型来表达1。不满足这两条,请求会被直接拒绝。
OpenAI 还给了个很好用的自检方式——“实习生测试”:把一个完全不了解你系统的实习生放到这份 schema 前面,他能不能正确调用?如果他反问你,那些问题的答案就该补进描述或参数说明里1。
返回值:这是模型的输入,不是给人看的日志
写接口的人习惯把字段全返回,反正调用方自己挑。但对模型来说,返回值是它下一步推理的直接依据,噪声会稀释信号。
Anthropic 建议优先返回高信号字段:name、image_url、file_type 这类能直接支撑下一步动作的信息,而不是 uuid、256px_image_url、mime_type 这类底层标识4。他们还观察到,把随机的字母数字 UUID 换成更有语义的名字(哪怕只是换成从 0 开始的编号),模型在检索类任务上的准确率会明显提升,幻觉减少4。
但后续工具调用有时确实需要这些 ID。折中做法是加一个 response_format 之类的枚举参数,让模型自己选 concise(只给内容)还是 detailed(带 ID);在 Anthropic 举的 Slack 线程例子里,concise 模式的 token 消耗大约是 detailed 的三分之一4。
还有两件事属于返回值设计:可能返回大量内容的工具要内建分页、范围选择或截断,并给合理默认值;空结果也要说人话。返回 [] 时模型常误以为工具坏了,而返回“没有匹配结果,试试更宽的关键词或换个筛选条件”,它就知道该怎么办6。
报错:错误消息是一条写给模型的指令
这是最容易做错的一环。工具抛出异常、整个智能体循环停在那里,这不是“错误处理”,是崩溃。
正确做法是把失败也当成一次正常的工具返回:在 Anthropic 的 Messages API 里,就是回一个 tool_result 块,配上对应的 tool_use_id,把 is_error 设为 true,里面写一段人话56。模型读到它,自己决定修参数、换工具还是放弃。关键是:is_error 只是个提示位,真正驱动模型下一步行动的是 content 里的那句话,所以那句话才是你要认真写的。
对比一下措辞:
| 你写的错误 | 模型能做对什么 |
|---|---|
400 Bad Request | 几乎什么都做不了,只能瞎猜 |
Field 'start_date' must use YYYY-MM-DD. You passed 'next Friday'. | 把日期转成正确格式再重试 |
The search API timed out. This is temporary — retry once with a narrower query. | 缩小查询范围重试一次 |
Order 99 does not exist. Do not retry; ask the user to confirm the order number. | 停止重试,去问用户 |
规律是:说清哪里错了、给出合法选项、明确说这次能不能重试。“temporary”“try again”“do not retry”这些词模型会照字面理解,所以要刻意使用6。
还有一个分工问题要想清楚:哪些重试该在代码里做,哪些该交给模型。标准很简单——如果同样的输入再试一次有可能成功(网络抖动、限流),就该在工具函数内部用退避重试解决,模型根本不需要知道;如果必须换一个输入才可能成功(参数写错、换工具、问用户),就把错误交回给模型6。让模型去处理网络抖动,只是把一件它无能为力的事情塞进上下文。
副作用:写操作千万别盲重试
这一点直接接上上一篇的护栏。
只读工具(search、get、list)随便重试都安全,跑两次不改变任何东西。写工具不一样:超时并不等于没发生——请求可能已经在服务端成功执行,只是响应没回来。这时模型若按“错误 → 重试”再来一次,就可能发出两封邮件、扣两次款。
标准解法是幂等键(idempotency key):调用方给这次操作附上唯一标识,服务端保证相同的键只产生一次效果。这样重试就安全了——第一次若已生效,第二次是个空操作,直接返回原结果6。如果你用的下游 API 不支持,那必须在重试前先查一次操作状态,或者把这份不确定性上报给人,而不是赌一把。
写在最后
把上面这些收成一份可以贴在桌上的自查表:
- 名字能不能一眼看出它做什么、作用在哪个资源上?
- 描述里写清什么时候该用、什么时候不该用、有哪些坑了吗?
- 参数里有没有可以同时成立的非法组合?有没有让模型填你已知的值?
- 返回的是高信号信息,还是一堆模型用不上的内部字段?
- 出错时返回给模型的那句话,能不能让它做出正确的下一步?
- 这个工具是只读还是写?写的话,重试安全吗?
一句话总结:工具定义是模型能看到的唯一文档。写工具时想的应该是“怎么把脑子里那份心照不宣的上下文说清楚”,而不是“怎么把函数签名写规范”。工具一多,就冒出新的问题:怎么组织整个工具集,才不会让模型淹在里面。这一条留到下一篇。