# 给 AI 智能体写工具：模型用得好的名称、描述和返回结果

> AI 智能体的工具，是模型可以请你的代码去运行的函数，而模型只能通过名称、描述、参数和返回内容来认识每个工具。围绕完整的任务做少量工具，不要每个 API 端点各做一个；像给新同事讲解那样写描述；返回简短易读的结果，出错时说明下一步该怎么做；每次改动都用真实任务检验。

Mindket 编辑部 · 发布于 2026-11-06 · 更新于 2026-11-06 · https://mindket.com/cn/learn/harness-engineering/tools-for-ai-agents

## 要点

- 先想清楚一个人会用你的 API 完成什么活，再把这件活做成一次调用。
- 工具返回的每个字段，模型都得读一遍，也都要花 token。
- 让失败的调用教会下一次调用：指出哪个输入不对，再给一个能用的例子。
- 留一组固定的、需要多步完成的难任务，每次改工具后都重跑一遍。

## 模型对你的工具知道些什么

第 1 课的 harness 示意代码用的工具，取自 Anthropic 的[客服智能体示例](https://github.com/anthropics/claude-cookbooks/blob/main/tool_use/customer_service_agent.ipynb)，下面原样列出示例里的两个替身函数。请用模型的眼光读一读。订单 O1 已经发货，可 `cancel_order("O1")` 还是返回 `True`，而这一个词就是全部信息。拿到它的模型，除了告诉客户“订单已取消”，没有别的话可说。

这个示例是为了演示 API 写的，不是用来开店的。可要是你自己的智能体也这样写，那就是缺陷，因为模型只能通过四样东西认识工具：名称、描述、输入结构，以及返回的内容。Ken Aizawa 2025 年 9 月在 Anthropic 工程博客上发表的[《Writing effective tools for agents》](https://www.anthropic.com/engineering/writing-tools-for-agents)，把工具称为“确定性系统与非确定性智能体之间的契约”。问智能体“今天要带伞吗？”，它可能调用天气工具，可能凭常识回答，也可能先问你在哪里。

一次调用和它的结果怎样在你的代码和模型之间传递，[Claude 工具调用那一课](https://mindket.com/cn/learn/claude-api/claude-tool-use)讲过，OpenAI 的做法见[函数调用那一课](https://mindket.com/cn/learn/openai-api/openai-function-calling)。决定智能体能否把任务做对的，是这些消息块里装的内容。

````python
def get_order_details(order_id):
    # Simulated order data
    orders = {
        "O1": {
            "id": "O1",
            "product": "Widget A",
            "quantity": 2,
            "price": 19.99,
            "status": "Shipped",
        },
        "O2": {
            "id": "O2",
            "product": "Gadget B",
            "quantity": 1,
            "price": 49.99,
            "status": "Processing",
        },
    }
    return orders.get(order_id, "Order not found")


def cancel_order(order_id):
    # Simulated order cancellation
    if order_id in ["O1", "O2"]:
        return True
    else:
        return False
````

## 围绕任务做工具，而不是围绕 API 端点

第一反应往往是每个端点做一个工具，而那篇工具文章说这是常见错误。普通软件逐条翻看通讯录几乎不花成本。模型读一个 `list_contacts` 的结果，却要为每一位联系人付出 token，而且上下文窗口有硬上限。所以文章建议做 `search_contacts` 或 `message_contact` 这样的工具，像人翻通讯录那样直接翻到对应的字母。

一个工具内部也可以调用好几次 API。Anthropic 举了三个这样合并的例子，列在表里，每一个都把人眼里算作一件事的几个步骤并成一次调用。工具更少、更大，上下文里的描述也更少，选错工具的机会也更少。

当好几个 MCP 服务器的工具摆在一起，名字就开始要紧了。文章建议加命名空间：按服务分，比如 `asana_search` 和 `jira_search`；按资源分，比如 `asana_projects_search` 和 `asana_users_search`。共同部分放在前面还是后面，对 Anthropic 的评估结果有不小的影响，而且哪种更好因模型而异。

| 不要做 | 改做 | 这个工具做什么 |
| --- | --- | --- |
| `list_users`、`list_events` 和 `create_event` | `schedule_event` | 找到所有人都有空的时间，并把活动订下来 |
| `read_logs` | `search_logs` | 只返回相关的日志行，每行附上一点前后文 |
| `get_customer_by_id`、`list_transactions` 和 `list_notes` | `get_customer_context` | 把一位客户近期的相关信息一次汇总返回 |

## 像给新同事讲解那样写描述

Anthropic 推出 Claude 的网页搜索工具时，Claude 总在搜索词后面加上“2025”，搜索结果因此跑偏。修好它的，是一段更好的工具描述。Anthropic 还说，正是对工具描述的精细修改，让 Claude Sonnet 3.5 在 SWE-bench Verified 上取得了当时最好的成绩。模型每次请求都会读到每一段描述，所以描述的作用就像提示词。

OpenAI 的[函数调用指南](https://developers.openai.com/api/docs/guides/function-calling)用“实习生测试”得出同样的结论：只看你给模型的那些内容，一个人能不能把这个函数用对？[函数调用那一课](https://mindket.com/cn/learn/openai-api/openai-function-calling)结合指南里关于枚举、以及代码已知参数的建议，把这个测试讲了一遍。

Bin Wu 2025 年 11 月的[高级工具使用文章](https://www.anthropic.com/engineering/advanced-tool-use)把一好一坏两个定义放在一起。坏的那个名字是一条数据库操作，描述什么也没说。好的那个名字说的是任务，描述讲清了能按什么搜索、会返回什么。

````json
// Good
{
  "name": "search_customer_orders",
  "description": "Search for customer orders by date range, status, or total amount. Returns order details including items, shipping, and payment info."
}

// Bad
{
  "name": "query_db_orders",
  "description": "Execute order query"
}
````

1. 用任务命名，而不是用操作命名。`search_customer_orders` 让模型知道什么时候该用它，`query_db_orders` 做不到。
2. 像给新同事讲解那样写描述。把团队里不言自明的查询格式、专门术语和资源之间的关系都写出来。
3. 给每个参数起一个不会被误读的名字。工具文章的例子是用 `user_id`，而不是 `user`。
4. 说明会返回什么，也说明什么情况下不该用这个工具。OpenAI 的指南建议把“什么时候不用”写进系统提示词。
5. 为复杂的输入加上示例。在 Anthropic 的内部测试中，工具调用示例把复杂参数处理的准确率从 72% 提高到了 90%。

## 工具该返回什么

下面是按这些思路改写后的 `cancel_order`。这是本课自己写的代码，没有运行过。它的描述说明了什么时候调用、会返回什么。函数返回一句模型可以直接转告客户的话，失败时返回一条错误，说明原因和下一步该怎么做。

想在第 1 课的循环里试用它，就改动代码末尾注释里点名的那一行。新 `cancel_order` 返回的这一对值会成为工具结果的 `is_error` 标记，`get_order_details` 照样能用。这样 Claude 看到的 O1 取消失败，就会被标成一次失败的调用。

````python
# The cookbook's two orders, trimmed to the fields this tool needs
ORDERS = {
    "O1": {"product": "Widget A", "quantity": 2, "status": "Shipped"},
    "O2": {"product": "Gadget B", "quantity": 1, "status": "Processing"},
}

CANCEL_ORDER = {
    "name": "cancel_order",
    "description": (
        "Cancel one order that has not shipped yet. Call it only after the customer "
        "has asked to cancel. Returns one sentence saying the order is cancelled, or "
        "an error saying why it could not be cancelled and what to do instead."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "The order ID exactly as the customer or get_order_details gives it, such as O1.",
            }
        },
        "required": ["order_id"],
    },
}


def cancel_order(order_id):
    """Returns the text the model reads and whether it is an error."""
    order = ORDERS.get(order_id)
    if order is None:
        return f"No order has the ID {order_id!r}. Order IDs look like O1. Ask the customer to check it.", True
    if order["status"] == "Shipped":
        return (
            f"Order {order_id} ({order['product']}) has already shipped and cannot be cancelled. "
            "Tell the customer. Do not call cancel_order again for this order."
        ), True
    order["status"] = "Cancelled"
    return f"Order {order_id} ({order['quantity']} x {order['product']}) is now cancelled.", False


# Lesson 1's loop has the line "output, failed = fn(**block.input), False".
# Replace it with these two lines, so the pair above sets is_error and get_order_details still works:
#     result = fn(**block.input)
#     output, failed = result if isinstance(result, tuple) else (result, False)
````

- 返回模型用得上的内容。工具文章建议不要返回 `uuid`、`256px_image_url`、`mime_type` 这类底层标识，而是返回 `name`、`image_url` 和 `file_type`。把随机 ID 换成可读的名字，哪怕换成从 0 开始编号的 ID，也减少了幻觉，让 Claude 在检索任务里更准确。
- 让模型自己选详略。加一个 `response_format` 参数，取值 `"concise"` 或 `"detailed"`，等后续调用真要用到 ID 时再返回它们。在文章的 Slack 例子里，详细回复用了 206 个 token，简洁回复用了 72 个，大约三分之一。
- 限制大小。Claude Code 默认把一次工具返回限制在 25,000 个 token。加上分页、筛选或截断，并设好合理的默认值；截断时告诉模型怎样少要一点，比如用几次窄的搜索代替一次宽泛的搜索。
- 错误信息写给模型看。看不懂的错误码或一大段堆栈，什么也教不会它。指出哪个输入不对、再给一个正确写法的错误信息，能引导下一次调用。
- 用评估来选返回格式。JSON、XML 和 Markdown 都可能在某个任务上胜出，文章没有找到一种适合所有任务和智能体的格式。

## 一次请求能放多少个工具

每个工具定义在每次请求里都占着上下文，不管模型用不用它。[什么是上下文工程](https://mindket.com/cn/learn/context-engineering/what-is-context-engineering)那一课算过：接五个 MCP 服务器，58 个工具在对话开始前就占掉大约 55K 个 token。准确率也会受影响。Anthropic 的高级工具使用文章说，最常见的失败是选错工具和填错参数，在 `notification-send-user` 和 `notification-send-channel` 这种名字相近的工具之间尤其如此。

OpenAI 的指南给出一个宽松的目标：一轮开始时可用的函数少于 20 个。函数很多或 schema 很大时，它建议把函数调用和工具搜索搭配使用：很少用到的工具延迟载入，等模型需要时再载入，这要用 `gpt-5.4` 或更新的模型。[按需载入上下文那一课](https://mindket.com/cn/learn/context-engineering/just-in-time-context)讲了延迟载入和工具搜索怎样运作。用 Claude 的 Tool Search Tool 时，延迟载入的工具不进上下文，直到 Claude 去搜索它们。

文章建议在工具定义超过 10K 个 token 或工具达到十个及以上时使用工具搜索，同时让最常用的三到五个工具始终加载。它省下的 token 和提升的准确率，按需载入上下文那一课都有数字。

## 用答案已知的任务检验工具

新写的描述或合并后的工具，在测量之前都只是猜测。工具文章的方法从基于真实使用的任务开始，每个任务配一个脚本能核对的结果，简单的可以是字符串完全匹配，复杂的可以让 Claude 来评判答案。好的任务要调用好几次工具，有时几十次。差的任务直接把查询语句交给模型，检验的只是搜索，而不是工具设计。

每个任务放进一个像第 1 课那样的简单循环里跑，一个循环一个任务，只给它你的工具。不要只看分数：文章列出的还有每次调用和每个任务的耗时、调用次数、总 token 数和工具错误。重复调用很多，说明分页或大小上限需要调整。无效参数的错误很多，问题多半出在描述上。

然后读运行记录，因为模型在自述里漏掉的东西，往往比它说出来的更要紧。Anthropic 把评估记录粘贴进 Claude Code，让它重构工具，文章里的大部分建议就来自这个循环。留出的测试任务保证了提升是真的。在这些任务上，改进后的工具胜过了 Anthropic 研究员手写的版本，也胜过了 Claude 生成的版本。

怎样从真实运行记录里搭出第一套测试集，是本路径后面 [AI 评估课程](https://mindket.com/cn/learn/ai-evals/what-are-llm-evals)的内容，其中[评估智能体那一课](https://mindket.com/cn/learn/ai-evals/evaluate-ai-agents)会给这样的完整运行打分。可以先从三个好任务和三个差任务开始，就像文章自己举的那样。

| 好的任务 | 差的任务 |
| --- | --- |
| 下周和 Jane 约个会，讨论我们最新的 Acme Corp 项目。附上上次项目规划会议的笔记，并订一间会议室。 | 下周和 jane@acme.corp 约个会。 |
| 客户 ID 9182 反映一次购买被扣了三次款。找出所有相关的日志记录，并判断有没有其他客户受到同一个问题影响。 | 在支付日志里搜索 purchase_complete 和 customer_id=9182。 |
| 客户 Sarah Chen 刚提交了取消申请。准备一个挽留方案。判断：（1）她为什么要走，（2）什么挽留方案最有吸引力，（3）提出方案前要注意哪些风险。 | 按客户 ID 45892 找到取消申请。 |

## Ryan Lopopolo 谈安静的工具，以及让智能体列出自己缺什么

Ryan Lopopolo 写了[第 1 课](https://mindket.com/cn/learn/harness-engineering/what-is-an-agent-harness)背后那篇 OpenAI Harness 工程文章，现在是 Google Cloud 的首席工程师。2026 年 3 月 7 日，他[在 X 上发帖](https://x.com/_lopopolo/status/2030357237731037528)说，prettier 和 pnpm 这类工具默认为人阅读而设计。他的办法是给 prettier 加上 `--silent`，再让智能体写个包装脚本，pnpm 工作区跑测试时只输出失败项。这相当于命令行工具版的简洁返回格式，任何带 shell 工具的智能体都能用。

3 月 26 日，他在 X 上的[一条回复](https://x.com/_lopopolo/status/2037307669464363433)里给出一句用来发现缺口的系统提示词。它要求智能体把自己犯的错记进 MISTAKES.md，把希望拥有的上下文或工具写进 DESIRES.md，把对环境的了解记进 LEARNINGS.md，再把这些文件当作遥测数据收集起来。对工具设计来说，最该读的是 DESIRES.md：每一条都是智能体自己记下的、它缺少的工具或上下文。

Anthropic 2025 年 10 月发布的视频里，Claude Relations 的 Alex Albert 与 MCP 团队的 John Welsh、Claude API 团队的 Michael Cohen 一起出镜。John Welsh 也是那篇工具文章致谢的 MCP 工程师之一。14:20 开始的一章讲怎样用 MCP 管理上下文和工具。

> prettier 和 pnpm 这类工具默认是为了让人读得懂而优化的。给 prettier 加上 --silent。
>
> Ryan Lopopolo, OpenAI Harness 工程文章的作者，现任 Google Cloud 首席工程师，引文为译文, [X](https://x.com/_lopopolo/status/2030357237731037528) (2026-11-06 查阅)

[![Ryan Lopopolo（@_lopopolo）2026 年 3 月 7 日在 X 上说：prettier 和 pnpm 这类工具默认为人类可读而优化。给 prettier 加上 --silent，再用一个包装脚本让 pnpm 工作区的测试只输出失败项。](https://mindket.com/blog/shots/x-lopopolo-silent-tool-output.webp?v=b0850f90ab5a)](https://x.com/_lopopolo/status/2030357237731037528)

Ryan Lopopolo 在 X 上，2026 年 3 月 7 日

[![Ryan Lopopolo（@_lopopolo）2026 年 3 月 26 日在 X 上回复：把这段放进系统提示词，再把文件当作遥测收集。错误记进 MISTAKES.md，想要的上下文或工具写进 DESIRES.md，对环境的了解记进 LEARNINGS.md。](https://mindket.com/blog/shots/x-lopopolo-desires-md.webp?v=b30d99c7cf65)](https://x.com/_lopopolo/status/2037307669464363433)

Ryan Lopopolo 在 X 上，2026 年 3 月 26 日

[Building with MCP and the Claude API](https://www.youtube.com/watch?v=aZLr962R6Ag)

Anthropic 的 Alex Albert、John Welsh 和 Michael Cohen 谈用 MCP 和 Claude API 构建应用，其中一章讲管理上下文和工具。

> X 官方嵌入卡片的截图，拍摄于 2026 年 11 月 6 日，时间为 UTC。X 用“Show more”截断了第二条帖子。点击截图可到 X 上打开原帖。

## 常见错误

- 把每个 API 端点都包成一个工具，让模型自己去串联。
- 模型只需要一个名字和一个状态，工具却返回 UUID 和 API 的全部字段。
- 只返回 `True`、`False` 或一个光秃秃的错误码，让模型对用户无话可说。
- 两个工具名字几乎一样，描述里却都没写清什么时候该用哪一个。
- 把每个 MCP 服务器的每个工具，都塞进每一次请求。
- 只拿几条精心挑选的提示词来评判新描述，没有留出的测试任务。

## 动手试试

下一课[长时间运行的智能体](https://mindket.com/cn/learn/harness-engineering/long-running-agents)，讲怎样借助进度文件、git 和测试，让一个智能体跨越许多个上下文窗口持续工作。

- 把改写后的 `cancel_order` 和它的 `CANCEL_ORDER` 定义放进第 1 课的循环，并采用“工具该返回什么”一节里的改动。依次要求取消 O1、O2、O3，记下模型每次对用户说了什么。
- 挑一个你已有的工具，按讲给新同事的标准重写它的描述，数一数你补了多少条信息。
- 为你自己的工具写三个好任务和三个差任务，每个都配一个脚本能核对的答案。
- 把 DESIRES.md 那句话加进某个智能体的系统提示词，用上一周，看看它想要什么。

## 自测一下

1. 智能体要在一本很大的通讯录里找一个人。Anthropic 的工具文章建议用哪个工具？

   A. list_contacts，返回全部联系人
   B. get_contact_by_uuid
   C. search_contacts

   答案：C。 搜索工具能直接找到对应条目，完整列表则会让上下文塞满模型得逐个读完的联系人。

2. 文章建议给智能体返回哪些字段？

   A. name、image_url 和 file_type
   B. uuid、256px_image_url 和 mime_type
   C. API 返回的所有字段

   答案：A。 可读的字段告诉模型下一步做什么，底层标识大多只是在花 token。

3. OpenAI 的指南为一轮开始时可用的函数建议了什么宽松上限？

   A. 少于 5 个
   B. 正好 10 个
   C. 至少 50 个
   D. 少于 20 个

   答案：D。 OpenAI 建议少于 20 个，并说明这只是宽松的建议。工具很多时，它的指南另外提供了工具搜索。

4. Anthropic 怎样确保工具的改进不只适合它自己的评估任务？

   A. 只用 Claude 写的任务
   B. 用留出的测试集检验
   C. 减少任务数量

   答案：B。 工具从没针对留出的任务调整过，所以能看出提升能否延续到新的工作上。

## 常见问题

### 怎样给 AI 智能体写好工具描述？

把它写给一位从没见过你系统的新同事。说清工具做什么、什么时候用、什么时候不用、每个参数的含义和格式，以及会返回什么。然后用真实任务衡量这次改动。

### 一个 AI 智能体能处理多少个工具？

比大多数人加载的要少。每多一个工具，每次请求都要多花 token，也多一次在相似工具之间选错的机会。最常用的几个保持加载，其余的放到工具搜索后面，Claude 和 OpenAI 较新的模型都支持这种做法。

### 工具应该返回 JSON、XML 还是 Markdown？

三种都行。Anthropic 发现最好的格式随任务和智能体而变，因为模型更擅长处理和训练数据相似的格式。在同一组评估任务上试两种格式，留下更好的那种。

### MCP 工具和函数调用的工具有什么不同？

在模型眼里它们一样，都是一个名称、一段描述和一个输入结构，所以设计建议也一样。MCP 是从独立服务器提供工具的标准方式，详见[什么是 MCP](https://mindket.com/cn/blog/what-is-mcp)，它还允许服务器标出哪些工具会做破坏性的改动。

### Claude 能替我改进工具吗？

能。把几次失败运行的记录交给它，问它是什么让智能体犯了糊涂，再让它一次改好描述和代码，让两者保持一致。只有在 Claude 没见过的任务上也站得住的改动，才值得保留。

## 资料来源

- [Anthropic Engineering: Writing effective tools for agents, with agents](https://www.anthropic.com/engineering/writing-tools-for-agents) (2026-11-06 查阅)
- [Anthropic Engineering: Introducing advanced tool use](https://www.anthropic.com/engineering/advanced-tool-use) (2026-11-06 查阅)
- [OpenAI API docs: Function calling](https://developers.openai.com/api/docs/guides/function-calling) (2026-11-06 查阅)
- [Claude Cookbooks: Creating a customer service agent with client-side tools](https://github.com/anthropics/claude-cookbooks/blob/main/tool_use/customer_service_agent.ipynb) (2026-11-06 查阅)
- [anthropic-sdk-python: ToolResultBlockParam (is_error)](https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/types/tool_result_block_param.py) (2026-11-06 查阅)
- [Ryan Lopopolo on X: pass --silent to prettier](https://x.com/_lopopolo/status/2030357237731037528) (2026-11-06 查阅)
- [Ryan Lopopolo on X: MISTAKES.md, DESIRES.md and LEARNINGS.md](https://x.com/_lopopolo/status/2037307669464363433) (2026-11-06 查阅)
- [Anthropic on YouTube: Building with MCP and the Claude API](https://www.youtube.com/watch?v=aZLr962R6Ag) (2026-11-06 查阅)

[浏览 MCP 服务器](https://mindket.com/cn/mcp-servers)
