TL;DR
- 模型选不选对工具,主要取决于工具名、描述和参数 schema 这三样“给模型看的文档”,而不是你的后端实现有多优雅。
- 工具描述是写给模型看的 prompt,不是写给同事看的注释:要写清“什么时候用”,而不只是“这是什么”。
- 参数 schema 越宽松,模型越容易编造参数;用枚举、必填项和字段级描述把自由度收窄到正确范围。
- 合并工具减少选择成本,拆分工具减少参数歧义——判断依据是“调用意图是否同构”,而不是代码是否复用。
- 工具数量与描述长度会直接占用上下文(context),十几个工具就能吃掉几千 token,粒度设计就是上下文预算设计。
前置知识
- 了解 Agent 的基本循环:模型输出工具调用 → 你的代码执行 → 把结果回填给模型。如果还没建立这个心智模型,先读同路径的《Agent 的四个基本构件》。
- 会写 JSON,能看懂 JSON Schema 的
type/properties/required这些字段。 - 用过至少一家模型厂商的 function calling(工具调用)接口,知道工具定义大概长什么样。
- 不需要:不需要训练或微调模型,不需要读模型源码,也不需要先有一套完整的 Agent 框架——本文的示例用几十行原生代码就能验证。
零基础?先读小白故事线《如果 Agent 是新来的实习生》。
正文
工具不是 API:中间隔着一层“翻译”
大多数人的第一版工具,是把后端已有的 REST 接口原样搬过来:接口叫 /api/v1/order/query,工具就叫 queryOrderV1;接口有 12 个 query 参数,工具 schema 就抄 12 个参数。
结果通常是模型选错工具、漏传必填参数,或者把 page_size 填成 "ten"。
问题不在模型笨,而在于你交付的是一份给人类工程师看的接口文档,而模型需要的是一份能指导决策的说明书。这两者的差别在于:
- 人类看接口文档时,脑子里有业务上下文,知道“查订单”和“查物流”是两件事;
- 模型看到的只有工具名、描述、参数 schema 这三段文本,它必须在完全没有业务背景的情况下,仅凭这三段文本判断“现在该调哪个、参数怎么填”。
所以工具设计本质是一次面向模型的翻译:把 API 的能力,翻译成模型能正确选择和正确填参的形式。下面三节分别对应翻译的三个落点:命名、描述、参数 schema。
命名:让工具名本身携带选择信号
工具名是模型最先看到的 token,也是最省上下文的信号。好的工具名应当满足两条:
- 动词 + 对象,语义完整:
search_orders优于orders,orders又优于getData。 - 同族工具共享前缀,差异体现在后缀:
search_orders/search_customers/search_products,模型一眼能看出这是同一类操作作用在不同对象上。
反面模式是内部代号式命名:doQuery、handleReq、proc2。这类名字对人类是历史包袱,对模型则是纯噪声——它无法从名字推断出任何调用时机。
一个常见的坑是版本号进名字:queryOrderV2。模型不知道 V1 和 V2 的业务差异,只会在两个几乎同名的工具之间随机摇摆。版本差异应该收敛在实现里,或者用描述说明,而不是让模型去做版本决策。
命名阶段的自检问题很简单:如果只看名字不看描述,模型能不能猜出这个工具大概什么时候用? 猜不出,就该改名。
描述:写“何时用”,而不是“是什么”
工具描述(description)是整份工具定义里最被低估的字段。它实际是一段 prompt,作用是帮模型在多个候选工具之间做选择。
对比两种写法:
# 写法 A(是什么)
查询订单信息。
# 写法 B(何时用 + 边界)
根据订单号或用户邮箱查询订单状态、金额与物流信息。
当用户询问“我的订单到哪了”“这笔订单退款了吗”时使用。
不适用于查询商品库存,库存请用 search_products。
写法 B 多出来的两句话,分别解决了模型的两类错误:该用没用(不知道触发场景)和不该用却用(不知道边界)。
写描述时值得固定的几个要素:
- 触发场景:用户在问什么问题时该调用它。用自然语言写,接近用户真实说法。
- 输入来源:参数通常从哪里来(用户口述、上一次工具返回的 ID 等)。
- 负向边界:明确写出“不适用于什么”,尤其是存在近邻工具时。
- 返回内容:返回的是列表还是单条、是否分页。这会影响模型后续怎么用结果。
描述长度需要权衡:太短不足以区分近邻工具,太长则每个工具都吃掉几百 token。经验做法是近邻工具之间必须能互相区分,与谁都不像的工具可以写得简短。
参数 schema:把自由度收窄到正确范围
参数 schema 决定了模型能填出什么。宽松的 schema 把校验责任推给了运行时,而模型编造参数时你是被动的一方。
三个最有效的收窄手段:
其一,能枚举就枚举。 状态筛选不要写成自由字符串:
{
"name": "status",
"type": "string",
"enum": ["pending", "paid", "shipped", "refunded"],
"description": "订单状态筛选,不传则返回全部状态"
}
枚举同时解决了拼写错误和语义歧义:模型不会再把 shipped 写成 delivered。
其二,必填项要真必填。 很多实现为了让调用“更容易成功”,把所有参数都设成可选,然后在后端猜默认值。这会让模型在信息不足时也硬着头皮调用,返回一堆无关结果。信息不足时让模型反问用户,比让它猜参数更好——把真正必需的参数放进 required。
其三,每个字段都写 description。 字段级描述是模型填参时的唯一线索。user_id 和 order_id 都是字符串,模型只能靠描述区分“这是下单用户的 ID”还是“这是订单编号”。日期字段尤其要写清格式与时区,否则模型会在 2026-09-12、20260912、Sep 12, 2026 之间随机选择。
一个容易忽略的点是参数之间的依赖关系。如果“按邮箱查”和“按订单号查”二选一,要么拆成两个工具,要么在描述里写明“二者至少提供一个”。JSON Schema 的 oneOf / anyOf 能表达这类约束,但要注意并非所有厂商的实现都完整支持——截至 2026-09-12,各家的 schema 支持范围仍有差异,用之前先查对应厂商文档。
合并还是拆分:看调用意图是否同构
这是工具设计里最常被问的问题:我有一堆相似接口,该合成一个工具还是拆成多个?
判断标准不是代码是否复用,而是调用意图是否同构——模型在决定调用时,脑子里想的是不是同一件事。
该合并的信号:
- 多个操作共享同一组参数,只是过滤条件不同。例如“按 ID 查订单”“按邮箱查订单”“按手机号查订单”,意图都是“查一个订单”,合并成
search_orders加一个by参数或几个可选参数更清晰。 - 操作总是成对出现,且模型几乎不会只用一个。例如“创建草稿”和“提交草稿”,如果业务上不允许只创建不提交,合并成带
submit布尔参数的工具能减少一次往返。
该拆分的信号:
- 参数集合几乎不重叠。硬合并会导致 schema 里一半字段对当前调用无意义,模型容易填错。
- 副作用性质不同。读操作和写操作混在一个工具里,会让“是否安全重试”这件事变得无法判断。
- 权限或确认策略不同。需要用户二次确认的写操作,应该独立成工具,方便在调用层插入确认。
一个实用的检验方法:把候选工具名和描述念一遍,看模型是否需要在两个工具之间做“业务判断”。如果需要,说明拆分是对的;如果两个工具的区别只是参数形式,合并更省上下文。
粒度与上下文成本:工具定义是要花钱的
工具定义不是免费的。每个工具的 name、description、参数 schema 都会作为输入 token 进入每一次请求。粗算一下:一个描述写充分的工具,定义部分通常在 100–300 token;15 个工具就是 1500–4500 token 的固定开销,每一轮对话都要重新付一次。
这意味着工具粒度设计同时是上下文预算设计:
- 工具越多,选择准确率越低。 候选集变大后,近邻工具之间的混淆概率上升。实践中的常见做法是把常用工具保持在十几个以内,更多的能力通过“先选类别再选具体工具”的两级结构暴露。
- 描述越长,固定成本越高。 但砍描述会直接损害选择准确率,所以更该砍的是冗余工具,而不是描述本身。
- 返回结果也是上下文。 工具返回一大坨 JSON 会挤占后续推理空间。让工具支持分页、字段裁剪,或者在返回前做一次摘要,往往比优化描述收益更大。
如果工具集合已经很大,可以考虑用 MCP(Model Context Protocol)这类协议把工具按需加载,而不是一次性全量注入。截至 2026-09-12,MCP 仍是较新的规范,接入前建议先读官方文档确认当前能力边界。
一个最小可验证的骨架
下面这段代码演示了“工具定义”和“实际执行”是两份东西:模型只看到 tools 里的定义,执行逻辑由你在本地分发。把描述改差,你会直接观察到模型选错工具。
const tools = [{
type: "function",
function: {
name: "search_orders",
description: "根据订单号或用户邮箱查询订单状态与物流。用户询问订单进度、退款状态时使用。不用于查询商品库存。",
parameters: {
type: "object",
properties: {
order_id: { type: "string", description: "订单编号,形如 ORD-20260912-001" },
email: { type: "string", description: "下单用户邮箱,与 order_id 至少提供一个" }
},
required: []
}
}
}];
// 模型返回 tool_call 后,由你按 name 分发到真实实现
function dispatch(call) {
const args = JSON.parse(call.function.arguments);
if (call.function.name === "search_orders") return searchOrders(args);
throw new Error(`unknown tool: ${call.function.name}`);
}
完整的多工具定义、错误回填与重试策略见官方文档。
延伸阅读
- OpenAI Function Calling 指南:工具定义字段、
strict模式与并行调用的官方说明,是参数 schema 写法的第一手依据。 - Anthropic Tool use:从模型侧解释工具描述如何影响选择,附有描述写法的对比示例。
- JSON Schema 官方教程:
enum、required、oneOf等约束的准确定义,写参数 schema 时用来查语法。 - Model Context Protocol 官方文档:工具数量增长后按需加载工具的协议方案与当前能力边界。
- Anthropic《Building effective agents》:把工具设计放回 Agent 整体架构中讨论,说明工具粒度如何影响循环复杂度。