# 怎么写一个模型用得对的工具：名字、描述、参数与报错

工具定义其实是给模型的提示文本：命名与描述决定它选不选、参数 schema 决定它填什么、返回值与错误消息决定它下一步怎么走

> 工具调用 · function calling · 可靠性 · 约 8 分钟 · 10 月 13 日

## 本篇要点

1. 工具定义会被序列化注入模型上下文，占 token 也按输入 token 计费，所以它不是给程序员看的签名，而是一段驱动模型决策的提示文本。
2. 模型使用一个工具要连做五个判断——要不要调、调哪个、参数填什么、怎么读返回值、失败后怎么办，名字与描述决定前两步，schema 决定第三步，返回值与错误消息决定后两步。
3. 名字上，动词加资源比抽象名词好，参数名要点明类型（`user_id` 优于 `user`）；工具变多时用服务/资源两级命名空间前缀消歧，并合并永远连着调的多个工具，单轮工具数控制在 20 个以内。
4. 描述是影响工具表现最重要的因素，要写清做什么、何时用、何时不用和边界条件，且把最关键的规则放最前面；Anthropic 通过改描述解决了 web search 参数里被模型擅自追加年份的问题。
5. 参数结构能直接消灭非法状态：用枚举代替自由文本，不做 `toggle_light(on, off)` 这种允许非法组合的设计，不让模型填代码里已知的值，参数树尽量扁平。
6. 开启 OpenAI 的 `strict` 模式要求每个对象都写 `additionalProperties: false` 且所有属性都进 `required`，可选字段靠加 `null` 类型表达，否则请求会被拒绝。
7. 返回值要优先给高信号字段（名称、类型）而不是底层标识（uuid、mime_type）；把不可读的 ID 换成有语义的名字能减少幻觉；用 `response_format` 之类的参数让模型自选简洁或详细输出以节省 token；空结果也要返回一句可执行的提示。
8. 工具出错时应作为一次正常的结果回传（Anthropic 用 `is_error: true` 的 `tool_result`），而非抛异常终止循环；真正起作用的是错误文本的内容。
9. 好的错误消息说清哪里错了、给出合法选项、明确能否重试；同输入再试可能成功的错误应在代码内部退避重试，需要改输入才能成功的错误才交给模型。
10. 只读工具可随意重试，写工具不行——超时不等于没发生；写操作需要幂等键，或者重试前先查询操作状态，否则可能重复发信、重复扣款。

---

前几篇我们把 ReAct 循环、工具调用、记忆规划、护栏依次拆过一遍。上一篇结尾留了一句：工具描述怎么写，才能让模型更少调错工具、更少填错参数。这篇就来还这笔账。

核心判断先放最前面：**工具定义不是写给人看的函数签名，而是一段会被塞进模型上下文里的提示文本。** 你起的名字、写的描述、定的参数结构、返回的内容，模型全都当成说明书来读。所以写工具的功夫，一半在代码里，一半在提示词里。

## 模型拿到工具定义后要做的判断

模型并不能“调用”你的函数。请求里的 `tools` 参数会被序列化后注入 system message，用模型训练时见过的格式呈现，因此它既占用上下文长度，也按输入 token 计费[[1]](#ref1)。读着这段文本，模型要连做五个判断：这一轮该不该用工具、要用的话用哪一个、参数各填什么值、读完返回值下一步做什么、出错了怎么补救。

```mermaid
flowchart LR
  A["用户请求"] --> B{"要调工具吗"}
  B -->|"不调"| C["直接回答"]
  B -->|"调"| D["选哪个工具<br/>名字+描述决定"]
  D --> E["参数填什么<br/>schema+参数说明决定"]
  E --> F["执行工具"]
  F --> G["读返回值<br/>决定下一步"]
  F -->|"失败"| H["读错误消息<br/>修正/重试/放弃"]
  G --> I["下一轮"]
  H --> I
```

名字、描述、参数 schema、返回值、错误消息分别负责其中一环。哪一环没写清，模型就在那一环上猜，而猜错的结果会直接变成一次真实调用。

## 名字：最便宜的一次消歧

名字是模型读描述之前就先扫到的东西。`get_current_weather` 比 `weather` 更不容易被误用[[5]](#ref5)；参数名同理，`user_id` 比 `user` 好，因为它直接点明需要的是标识符，而不是一个用户对象[[4]](#ref4)。

工具一多，撞名就成了主要问题。Anthropic 的建议是按命名空间分组：先按服务分（`asana_search`、`jira_search`），再按资源分（`asana_projects_search`、`asana_users_search`）[[4]](#ref4)。这是在帮模型做二分查找，而不是在一堆含义模糊的名字里硬猜。

另一个常被忽略的做法是合并：如果 `create_pr`、`review_pr`、`merge_pr` 永远按固定顺序连着调，不如合成一个带 `action` 参数的工具。工具少一个，选择歧义就少一层[[3]](#ref3)。OpenAI 给的经验值是单轮可用工具尽量控制在 20 个以内[[1]](#ref1)——这只是软建议，但方向明确：工具囤多了，选择准确率会掉。

## 描述：写清“什么时候用”和“什么时候别用”

Anthropic 把话讲得很直白：描述是影响工具表现最重要的因素，每个工具至少写 3–4 句[[3]](#ref3)。好的描述回答四件事：做什么、什么时候该用、什么时候不该用、输入输出各代表什么。

看一个具体写法。OpenAI 的 o 系列指南给 `file_create` 举的例子，除“创建文件”本身，还写了：只在需要持久保存且文件尚不存在时调用；先调 `directory_check` 确认目录存在；不要覆盖已有文件，要替换就先删后建[[2]](#ref2)。这些不是给别人看的文档细节，而是直接替模型划好了决策边界。该指南还提到一个反直觉的细节：把最重要的规则放在描述最前面、少写不具约束力的铺垫话，在同一套评测里工具调用准确率差了 6%[[2]](#ref2)。

描述能修的问题有多隐蔽，Anthropic 讲过一个自家案例：他们上线 web search 工具后发现，模型总在 `query` 参数里自作主张追加 “2025”，把搜索结果带偏。修的办法不是改代码，是改工具描述[[4]](#ref4)。

反过来说，“这个工具用来处理数据”这种描述等于没写——模型无法从抽象名词里判断该不该用。

## 参数：用 schema 消灭“非法状态”

参数结构能替你挡掉一大类错误：它可以规定什么组合合法。

- **能用枚举就别开自由文本。** `units` 写成 `["celsius", "fahrenheit"]`，模型就没机会编出 `centigrade`[[1]](#ref1)。
- **不要把非法组合做成可选项。** OpenAI 举的反例是 `toggle_light(on: bool, off: bool)`：两个参数能同时为真，这种设计把非法状态写进了接口本身[[1]](#ref1)。
- **别让模型填你已经知道的东西。** 如果代码里已经有 `order_id`，就不要把它做成参数让模型生成，而是定义成无参数的 `submit_refund()`，由代码自己传入 id[[1]](#ref1)。
- **扁平优先于深层嵌套。** 嵌套越多，模型越容易漏填子字段或填错同名层级，OpenAI 明确建议参数树尽量做平[[2]](#ref2)。
- **`strict` 模式值得开，但要清楚代价。** 开启后参数会严格符合 schema，而不是尽力而为。代价是两条硬要求：每个对象都要写 `additionalProperties: false`，并且 `properties` 里的所有字段都必须出现在 `required` 里；真正的可选字段靠给它加上 `null` 类型来表达[[1]](#ref1)。不满足这两条，请求会被直接拒绝。

OpenAI 还给了个很好用的自检方式——“实习生测试”：把一个完全不了解你系统的实习生放到这份 schema 前面，他能不能正确调用？如果他反问你，那些问题的答案就该补进描述或参数说明里[[1]](#ref1)。

## 返回值：这是模型的输入，不是给人看的日志

写接口的人习惯把字段全返回，反正调用方自己挑。但对模型来说，返回值是它下一步推理的直接依据，噪声会稀释信号。

Anthropic 建议优先返回高信号字段：`name`、`image_url`、`file_type` 这类能直接支撑下一步动作的信息，而不是 `uuid`、`256px_image_url`、`mime_type` 这类底层标识[[4]](#ref4)。他们还观察到，把随机的字母数字 UUID 换成更有语义的名字（哪怕只是换成从 0 开始的编号），模型在检索类任务上的准确率会明显提升，幻觉减少[[4]](#ref4)。

但后续工具调用有时确实需要这些 ID。折中做法是加一个 `response_format` 之类的枚举参数，让模型自己选 `concise`（只给内容）还是 `detailed`（带 ID）；在 Anthropic 举的 Slack 线程例子里，`concise` 模式的 token 消耗大约是 `detailed` 的三分之一[[4]](#ref4)。

还有两件事属于返回值设计：可能返回大量内容的工具要内建分页、范围选择或截断，并给合理默认值；空结果也要说人话。返回 `[]` 时模型常误以为工具坏了，而返回“没有匹配结果，试试更宽的关键词或换个筛选条件”，它就知道该怎么办[[6]](#ref6)。

## 报错：错误消息是一条写给模型的指令

这是最容易做错的一环。工具抛出异常、整个智能体循环停在那里，这不是“错误处理”，是崩溃。

正确做法是把失败也当成一次正常的工具返回：在 Anthropic 的 Messages API 里，就是回一个 `tool_result` 块，配上对应的 `tool_use_id`，把 `is_error` 设为 `true`，里面写一段人话[[5]](#ref5)[[6]](#ref6)。模型读到它，自己决定修参数、换工具还是放弃。关键是：`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]](#ref6)。

还有一个分工问题要想清楚：哪些重试该在代码里做，哪些该交给模型。标准很简单——如果同样的输入再试一次有可能成功（网络抖动、限流），就该在工具函数内部用退避重试解决，模型根本不需要知道；如果必须换一个输入才可能成功（参数写错、换工具、问用户），就把错误交回给模型[[6]](#ref6)。让模型去处理网络抖动，只是把一件它无能为力的事情塞进上下文。

## 副作用：写操作千万别盲重试

这一点直接接上上一篇的护栏。

只读工具（`search`、`get`、`list`）随便重试都安全，跑两次不改变任何东西。写工具不一样：**超时并不等于没发生**——请求可能已经在服务端成功执行，只是响应没回来。这时模型若按“错误 → 重试”再来一次，就可能发出两封邮件、扣两次款。

标准解法是幂等键（idempotency key）：调用方给这次操作附上唯一标识，服务端保证相同的键只产生一次效果。这样重试就安全了——第一次若已生效，第二次是个空操作，直接返回原结果[[6]](#ref6)。如果你用的下游 API 不支持，那必须在重试前先查一次操作状态，或者把这份不确定性上报给人，而不是赌一把。

## 写在最后

把上面这些收成一份可以贴在桌上的自查表：

- 名字能不能一眼看出它做什么、作用在哪个资源上？
- 描述里写清什么时候该用、什么时候不该用、有哪些坑了吗？
- 参数里有没有可以同时成立的非法组合？有没有让模型填你已知的值？
- 返回的是高信号信息，还是一堆模型用不上的内部字段？
- 出错时返回给模型的那句话，能不能让它做出正确的下一步？
- 这个工具是只读还是写？写的话，重试安全吗？

一句话总结：工具定义是模型能看到的唯一文档。写工具时想的应该是“怎么把脑子里那份心照不宣的上下文说清楚”，而不是“怎么把函数签名写规范”。工具一多，就冒出新的问题：怎么组织整个工具集，才不会让模型淹在里面。这一条留到下一篇。

## 术语表

- 工具定义（tool definition）：随请求一起发给模型的、描述某个工具名字、用途和参数的 JSON 结构，会被注入上下文，模型据此决定调不调、调哪个、怎么填参数。
- strict 模式：要求模型输出的工具参数严格符合 schema 的设置；代价是 schema 必须满足 `additionalProperties: false` 和全字段进 `required` 两条硬约束。
- 命名空间（namespacing）：给工具名加服务或资源前缀（如 `github_list_prs`、`jira_search`），让模型在工具变多时仍能快速定位该用哪个。
- tool_result 与 `is_error`：Anthropic API 里把工具执行结果回传给模型的块；把 `is_error` 设为 `true` 表示这次调用失败，但真正引导模型下一步的是 `content` 里的文字。
- 幂等键（idempotency key）：调用方为一次写操作附上的唯一标识，服务端保证相同键只生效一次；有了它，超时后的重试才是安全的。

## 来源

1. [OpenAI Function calling 指南 — 工具定义字段、strict 模式的两条硬约束、描述与参数命名最佳实践、工具数量建议](https://developers.openai.com/api/docs/guides/function-calling)
2. [OpenAI o 系列函数调用指南 — file_create 描述示例、关键规则前置带来的准确率差异、扁平参数结构建议](https://developers.openai.com/cookbook/examples/o-series/o3o4-mini_prompting_guide)
3. [Anthropic Define tools — 描述应涵盖的四件事、合并相关操作与命名空间建议](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/implement-tool-use)
4. [Anthropic Engineering: Writing effective tools for agents — 高信号返回值、UUID 语义化、response_format 省 token，以及通过改描述修复 web search 参数偏置的案例](https://www.anthropic.com/engineering/writing-tools-for-agents)
5. [Anthropic tool-use-concepts — is_error 的用法与具体工具名优于模糊名的建议](https://github.com/anthropics/skills/blob/main/skills/claude-api/shared/tool-use-concepts.md)
6. [Returning Tool Errors to an LLM for Graceful Recovery — 错误措辞对照表、代码重试与模型重试的分工、幂等键与空结果处理](https://ai-tldr.dev/learn/llm-apis/function-calling/handling-tool-errors)

---

原文：https://pangzhengboyin.com/articles/writing-tools-agents-can-use-correctly-acd4ac79

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
