工具Schema设计
要点
- 工具 Schema 是 LLM 和后端代码之间的「接口契约」,告诉模型这个工具叫什么、做什么、接受什么参数
- 一个工具定义由三部分组成:
name(工具名称)、description(功能描述)、inputSchema(输入参数结构) inputSchema遵循 JSON Schema 规范,用type、properties、required、description这几个关键字段描述参数- 参数描述的质量直接影响 LLM 能否正确填充参数——描述模糊,模型就会乱猜
- 枚举值(
enum)和嵌套对象是实战中用得最多的两个设计手段 - Schema 设计得越精确,模型出错的概率越低;偷懒少写几行 description,后面就要花更多时间处理模型的错误调用
1. 什么是工具 Schema
上一节讲了工具调用的整体流程:LLM 决定调用哪个工具,后端执行工具,把结果返回给 LLM。这里有一个前置条件——LLM 怎么知道有哪些工具可用、每个工具需要什么参数?
答案是工具 Schema。Schema 是一份结构化的描述,用 JSON 格式把工具的信息写清楚。你可以把它理解为工具的「说明书」,这份说明书会在每次请求时随 prompt 一起发给 LLM。模型读到说明书之后,就知道当前有哪些工具、什么时候该用、怎么传参。
先看一个最简的工具定义:
// src/tools/weather.ts
const weatherTool = {
name: 'get_weather',
description: '获取指定城市的当前天气信息',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称,例如:北京、上海'
}
},
required: ['city']
}
}发给 Anthropic API 时,这个定义放在请求体的 tools 数组里:
// src/tools/api-call.ts
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
tools: [weatherTool],
messages: [
{ role: 'user', content: '北京今天天气怎么样?' }
]
})模型如果判断需要调用工具,会返回一个 tool_use 类型的 content block,里面包含 name: 'get_weather' 和 input: { city: '北京' }。参数的结构和值,完全是模型根据 Schema 自己推断出来的。
模型推断的准确度,取决于 Schema 写得好不好。 这就是这一篇要解决的问题。
2. JSON Schema 基础
工具 Schema 的 inputSchema 字段遵循 JSON Schema 规范。工具场景下用到的子集很有限,先把核心关键字过一遍:
| 关键字 | 作用 | 示例 |
|---|---|---|
type | 指定数据类型 | 'string'、'number'、'boolean'、'object'、'array' |
properties | 定义对象的字段 | { city: { type: 'string' } } |
required | 列出必填字段 | ['city', 'unit'] |
description | 字段说明 | '温度单位,可选值:celsius、fahrenheit' |
enum | 限制取值范围 | ['celsius', 'fahrenheit'] |
items | 定义数组元素类型 | { type: 'string' } |
default | 默认值 | 'celsius' |
这些关键字组合起来,可以描述绝大多数工具的输入结构。
3. 工具定义的三个部分
3.1 name:工具的唯一标识
name 是模型在 tool_use 响应里引用的标识符。命名建议:
- 用小写字母加下划线,例如
get_weather、search_users - 名字要能体现动作,不要写
tool1、func_a - 同一组工具里名称不能重复
模型会根据名字来理解工具的大致用途。get_weather 一看就是查天气,tool_1 什么都看不出来。
3.2 description:决定模型要不要调用
description 帮模型判断「用户的问题是否需要调用这个工具」。好的 description 包含三个要素:
- 做什么:一句话说清楚功能
- 返回什么:调用后能拿到什么结果
- 什么时候该用:典型的使用场景
对比两种写法:
// 模糊
{ name: 'get_weather', description: '天气工具' }
// 清晰
{
name: 'get_weather',
description: '获取指定城市的实时天气数据,包括温度、湿度、风速。当用户询问某个地方的天气时使用。'
}第一种只给了一个名字,模型不知道能返回什么、什么场景下该调用。第二种把功能、返回值、使用场景都交代了。
3.3 inputSchema:参数的结构定义
inputSchema 描述模型调用工具时需要传入的参数。根节点始终是 type: 'object',具体字段定义在 properties 里。后面几节详细展开。
4. 参数类型与必填规则
4.1 五种基本类型
string — 最常用的类型。如果有格式要求,在 description 里写清楚:
// src/tools/date-param.ts
{
type: 'string',
description: '查询的开始日期,格式为 YYYY-MM-DD,例如 2026-01-15'
}不要只写「日期」两个字。模型可能返回 2026/1/15、Jan 15, 2026 等各种格式,后端解析时就会出问题。
number 与 integer — number 是浮点数,integer 是整数。如果业务上一定是整数(页码、数量),用 integer:
// src/tools/product-filter.ts
{
price_min: { type: 'number', description: '最低价格,单位:元' },
page: { type: 'integer', description: '页码,从 1 开始' }
}boolean — 开关类参数。在 description 里注明默认值:
{
include_archived: {
type: 'boolean',
description: '是否包含已归档的记录,默认 false'
}
}array — 用 items 定义元素类型:
{
type: 'array',
items: { type: 'string' },
description: '需要批量删除的用户 ID 列表'
}4.2 必填与可选
required 数组列出必填字段,不在里面的就是可选的:
// src/tools/search-params.ts
{
type: 'object',
properties: {
keyword: { type: 'string', description: '搜索关键词' },
category: {
type: 'string',
description: '分类筛选,可选值:tech、design、marketing'
},
page: { type: 'integer', description: '页码,默认 1' }
},
required: ['keyword']
}划分原则:如果缺少这个参数工具就无法执行,就设为必填;如果缺了也能正常工作、有合理的默认行为,就设为可选。
- 天气查询的
city:不知道查哪个城市就没法查,必填 - 搜索的
page:不传就默认第一页,可选
可选参数在 description 里写清楚默认值,模型看到「默认 20」后即使不传也知道后端会用 20。
5. 参数描述、枚举与嵌套
5.1 参数描述怎么写
模型在决定参数值时,依赖的信息来源只有两个:用户的输入和 Schema 里的 description。用户通常不会把参数格式说得很精确,所以 description 是模型理解参数的主要线索。
几个原则:
说清楚业务含义,不要只写类型:
// 不够好
{ type: 'string', description: '字符串' }
// 更好
{ type: 'string', description: '收件人的手机号,11 位数字,例如 13800138000' }给出取值范围和格式约束:
// src/tools/validation.ts
{
order_id: {
type: 'string',
description: '订单编号,格式为 ORD-XXXXXXXX,例如 ORD-20260115'
},
limit: {
type: 'integer',
description: '返回条数上限,最小 1,最大 100,默认 20'
}
}一条 description 控制在 1-2 句话。 太长会让模型注意力分散,太短起不到约束作用。如果发现一个参数需要写一大段来解释,考虑拆成多个参数。
5.2 枚举值(enum)
当参数只有固定的几个选项时,用 enum 约束。这比在 description 里写「可选值有 A、B、C」更可靠,因为 enum 是 JSON Schema 的正式约束,模型会严格遵守:
// src/tools/unit-param.ts
{
unit: {
type: 'string',
description: '温度单位',
enum: ['celsius', 'fahrenheit']
}
}有了 enum,模型只可能返回 'celsius' 或 'fahrenheit',不会返回 'C'、'℃' 或拼写错误。
枚举值的命名建议用全小写英文单词,下划线连接多个词。如果枚举值需要附带说明,在 description 里补充:
{
priority: {
type: 'integer',
description: '任务优先级,1 表示最低,5 表示最高',
enum: [1, 2, 3, 4, 5]
}
}5.3 嵌套对象
工具的输入结构比较复杂时,参数本身也可以是对象。在 properties 下再嵌套 properties:
// src/tools/nested-filter.ts
{
type: 'object',
properties: {
keyword: { type: 'string', description: '搜索关键词' },
filter: {
type: 'object',
description: '筛选条件',
properties: {
status: {
type: 'string',
description: '记录状态',
enum: ['draft', 'published', 'archived']
},
date_range: {
type: 'object',
description: '发布日期范围',
properties: {
start: { type: 'string', description: '开始日期,格式 YYYY-MM-DD' },
end: { type: 'string', description: '结束日期,格式 YYYY-MM-DD' }
},
required: ['start', 'end']
}
}
}
},
required: ['keyword']
}模型调用时返回:
// model-response.json
{
"keyword": "Hono",
"filter": {
"status": "published",
"date_range": { "start": "2026-01-01", "end": "2026-06-20" }
}
}嵌套设计的两个建议:
- 不要超过三层嵌套。 到了四层五层,模型容易搞混层级关系,填错参数的概率明显上升。如果需要很深的嵌套,考虑把结构拍平。
- 每一层对象都要写 description。 不要只给叶子节点写描述,中间层的对象也需要一句话说明:
// 不要这样——模型不知道 filter 是干什么的
filter: { type: 'object', properties: { status: { ... } } }
// 要这样
filter: {
type: 'object',
description: '可选的筛选条件,不传则返回全部结果',
properties: { status: { ... } }
}5.4 数组里放对象
当工具需要接收一组结构化数据时,数组的元素类型设为 object:
// src/tools/batch-create.ts
{
items: {
type: 'array',
description: '要创建的商品列表',
items: {
type: 'object',
properties: {
name: { type: 'string', description: '商品名称' },
price: { type: 'number', description: '价格,单位:元' },
quantity: { type: 'integer', description: '库存数量' }
},
required: ['name', 'price']
}
}
}数组内对象的 required 规则和顶层一样。这里 quantity 是可选的。
6. 一个完整的工具定义
把前面的内容组合起来,看一个贴近实战的完整示例:
// src/tools/create-order.ts
const createOrderTool = {
name: 'create_order',
description: '创建新的商品订单。当用户要求下单、购买商品时使用。需要指定商品列表和收货地址。',
inputSchema: {
type: 'object',
properties: {
items: {
type: 'array',
description: '订单中的商品列表,至少包含一个商品',
items: {
type: 'object',
properties: {
product_id: {
type: 'string',
description: '商品 ID,格式为 PRD-XXXXXX'
},
quantity: {
type: 'integer',
description: '购买数量,最小为 1',
default: 1
}
},
required: ['product_id']
}
},
shipping_address: {
type: 'object',
description: '收货地址信息',
properties: {
receiver: { type: 'string', description: '收件人姓名' },
phone: { type: 'string', description: '收件人手机号,11 位数字' },
province: { type: 'string', description: '省份' },
city: { type: 'string', description: '城市' },
detail: { type: 'string', description: '详细地址,包括街道、门牌号等' }
},
required: ['receiver', 'phone', 'province', 'city', 'detail']
},
payment_method: {
type: 'string',
description: '支付方式',
enum: ['alipay', 'wechat_pay', 'bank_card']
},
note: {
type: 'string',
description: '订单备注,例如「请在工作日配送」,最多 200 字'
}
},
required: ['items', 'shipping_address']
}
}这个定义覆盖了前面讲到的要点:
name用动词开头,语义清晰description说明了功能和典型使用场景- 枚举值用于限定支付方式
- 嵌套对象处理收货地址,数组里放对象处理商品列表
required只标记真正不可缺的字段,note和payment_method可选- 每个参数都有 description,关键参数标注了格式、长度、默认值
总结
这篇把工具 Schema 的设计拆开讲了:
- 工具定义由
name、description、inputSchema三部分组成,分别承担标识、决策依据、参数约束的职责 - JSON Schema 的子集足够覆盖工具场景,核心关键字是
type、properties、required、description、enum、items - 五种基本类型各有适用场景:
string最通用,integer和number区分整数和浮点数,boolean用于开关,array和object处理复杂结构 required的划分标准是:缺了工具还能不能正常工作- 参数 description 是影响模型调用准确度的最关键因素,写清楚业务含义、格式约束、默认值
- 枚举值用
enum正式约束,比在 description 里写可选值更可靠 - 嵌套对象不要超过三层,每一层都要写 description
Schema 设计的好坏,直接决定了模型调用工具的准确率。下一篇讲如何在 Hono 后端实现工具的注册和执行。