Tool Calling 的底层协议------JSON Schema 定义与函数路由
摘要:Tool Calling 的可靠性,30% 靠代码,70% 靠工具定义写得对不对。本文深入解析 JSON Schema 在工具调用中的作用,涵盖参数类型选择、嵌套结构设计、description 撰写技巧、多工具路由策略及常见反模式。适合已有 Tool Calling 基础、希望提升工具调用准确率的开发者阅读。
一、引言
上一篇文章我们讲了 Tool Calling 的基本流程。但很多开发者在实际使用时会遇到这样的问题:
- 模型要么不调用工具,要么乱调用
- 参数经常传错类型(该传 string 的传了 number)
- 工具多了以后,模型总是选错工具
- 嵌套参数模型根本不会填
这些问题几乎都源于工具定义写得不好。工具定义是模型理解「我能做什么」的唯一接口------它写得越好,模型调用得越准。本文将工具定义提升为一门手艺来讲解。
二、JSON Schema 在 Tool Calling 中的角色
2.1 不只是参数校验
JSON Schema 在工具调用中扮演三重角色:
③ 函数路由
帮助模型在多工具中
做出正确的选择
② 语义引导
通过 description 告诉模型
「这个参数应该长什么样」
① 参数验证
确保模型输出的参数
类型正确、格式合法
模型在选择工具和填充参数时,会同时参考这三个层面的信息。
2.2 模型如何处理你的 Schema
当用户发送一条消息,模型做的事情是:
- 阅读所有工具的
name和description,判断哪些工具相关 - 阅读候选工具的
parametersschema,判断自己能否正确填参数 - 如果多个工具都匹配,选最匹配的那个
- 按照 schema 约束生成结构化的参数 JSON
是
否
用户消息
① 模型阅读所有工具的
name + description
② 筛选相关候选工具
③ 阅读候选工具的
parameters schema
④ 多个工具都匹配?
选择最匹配的工具
使用唯一匹配的工具
⑤ 按 schema 约束
生成结构化参数 JSON
如果 schema 有歧义,模型会「猜」,而猜往往意味着出错。
三、参数类型:选择与陷阱
3.1 基础类型选择指南
JSON Schema 支持多种类型。以下是在 Tool Calling 场景中的具体建议:
string ------ 最常用,也是模型最容易正确填充的类型。
json
{
"type": "string",
"description": "用户邮箱地址",
"format": "email" // 可选:提示模型生成合法格式
}
number vs integer ------ 选择 integer 通常更安全。
json
// 好:明确告诉模型要整数
{ "type": "integer", "description": "每页记录数" }
// 差:模型可能返回 10.5
{ "type": "number", "description": "每页记录数" }
boolean ------ 谨慎使用。模型对布尔参数的理解有时不稳定。
json
// 建议:用 string + enum 替代 boolean
{
"type": "string",
"enum": ["asc", "desc"],
"description": "排序方向"
}
// 而不是:
{ "type": "boolean", "description": "是否升序" }
array ------ 一定要在 items 里定义元素类型。
json
// 好
{
"type": "array",
"items": { "type": "string" },
"description": "要查询的城市列表"
}
// 差:模型不知道数组里该放什么
{ "type": "array", "description": "城市列表" }
3.2 enum:限制取值范围的利器
当参数只有几个固定选项时,用 enum 而不是在 description 里用文字描述。
json
// 好:模型必须从 enum 中选择
{
"type": "string",
"enum": ["temperature", "humidity", "wind_speed", "all"],
"description": "要查询的天气指标类型"
}
// 差:模型可能返回各种奇怪的表达
{
"type": "string",
"description": "要查询的天气指标类型,可以是温度、湿度、风速或全部"
}
enum 是提升参数准确率最有效的手段之一。
3.3 required 的艺术
json
{
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名称" },
"date": { "type": "string", "description": "查询日期,默认今天" }
},
"required": ["city"] // city 必须填,date 可选
}
原则:
- 把真正必需的字段标为 required:如果缺少这个参数工具根本没法执行,就标上
- 有合理默认值的字段不要标 required :比如
date默认今天,就不要强制用户填 - required 列表太长(超过 5 个)时,模型更容易出错。考虑拆分为多个工具
四、复杂 Schema 的嵌套设计
4.1 何时需要嵌套
当你的工具参数涉及结构化数据时,需要嵌套对象。例如发送邮件工具:
json
{
"name": "send_email",
"description": "发送一封或多封邮件",
"input_schema": {
"type": "object",
"properties": {
"recipients": {
"type": "array",
"description": "收件人列表",
"items": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "收件人姓名" },
"email": {
"type": "string",
"format": "email",
"description": "收件人邮箱地址"
}
},
"required": ["email"]
}
},
"subject": { "type": "string", "description": "邮件主题" },
"body": { "type": "string", "description": "邮件正文" }
},
"required": ["recipients", "subject", "body"]
}
}
4.2 嵌套深度的建议
层级 0: 顶层 object ← 总是这里开始
层级 1: 直接子属性 string ← 绝大多数情况在这一层解决
层级 2: 子对象 ← 必要时使用
层级 3: 再嵌套一层 ← 慎用,模型出错率显著上升
层级 4+: ← 尽量避免,考虑拆分工具
核心原则:如果你的 Schema 嵌套超过 3 层,考虑是否应该拆分为多个独立的工具,或者将部分嵌套对象「拍平」为顶层参数。
层级 3: 再嵌套 ⛔ 慎用
模型出错率显著上升
层级 2: 子对象 ⚠️ 必要时使用
嵌套 object
层级 1: 直接子属性 ✅ 绝大多数情况
string, integer 等基础类型
层级 0: 顶层 object
{ }
4.3 拍平 vs 嵌套的权衡
json
// 嵌套写法:结构清晰,但模型容易填错
{
"shipping_address": {
"type": "object",
"properties": {
"province": { "type": "string" },
"city": { "type": "string" },
"detail": { "type": "string" }
}
}
}
// 拍平写法:牺牲结构清晰度,换取模型准确率
{
"shipping_province": { "type": "string", "description": "收货省份" },
"shipping_city": { "type": "string", "description": "收货城市" },
"shipping_detail": { "type": "string", "description": "收货详细地址" }
}
建议:参数总数少于 6 个时,优先拍平。超过 6 个时用嵌套分组,但嵌套不超过两层。
五、Description 撰写方法论
工具定义的 description 是整个 Tool Calling 系统中最重要的字段。Garbage in, garbage out。
5.1 三层描述法
每个工具的 description 应该回答三个问题:
① What: 这个工具做什么?
② When: 什么时候该用这个工具?
③ Constraint: 有什么限制?
示例------查询订单工具:
json
{
"name": "query_order",
"description": "根据订单号查询订单的当前状态、物流信息和金额详情。"
"当用户询问具体订单的状态、到哪了、多少钱时使用此工具。"
"仅支持已支付订单,待支付订单请使用 query_pending_order 工具。"
}
每个参数的 description 同样重要:
json
{
"order_id": {
"type": "string",
"description": "订单号,格式为 ORD-年月日-序号(如 ORD-20231201-001)。"
"注意:不是支付单号,不是物流单号。"
}
}
5.2 常见 Description 错误
错误 1:太简短
json
// 差
{ "description": "查询订单" }
// 好
{ "description": "根据订单号查询订单的当前状态(待发货/运输中/已签收)、物流公司和运单号。
当用户询问'我的订单到哪了''订单发货了吗'时使用。" }
错误 2:包含废话
json
// 差
{ "description": "查询订单。订单是用户购买商品后产生的记录。众所周知,订单查询是非常常见的需求。" }
// 好
{ "description": "根据订单号查询订单状态、物流信息和金额。当用户询问订单相关问题时使用。" }
错误 3:不说例外情况
json
// 差:用户想退款时模型也调这个
{ "description": "处理所有订单相关操作" }
// 好:明确边界
{
"description": "查询已支付订单的当前状态和物流信息。退款相关操作请使用 refund_order 工具。
待支付订单查询请使用 query_pending_order 工具。"
}
六、多工具路由策略
当你的应用有多个工具时,模型需要在它们之间做选择。以下是经过验证的路由优化策略。
6.1 工具分组
不要一次性把所有工具都传给模型。按场景分组:
python
# 按场景分组
CUSTOMER_SERVICE_TOOLS = [query_order, refund_order, send_email]
DATA_ANALYSIS_TOOLS = [query_database, generate_chart, export_csv]
ADMIN_TOOLS = [create_user, delete_user, reset_password]
def get_tools_for_context(user_role, conversation_intent):
if conversation_intent == "customer_service":
return CUSTOMER_SERVICE_TOOLS
elif conversation_intent == "data_analysis":
return DATA_ANALYSIS_TOOLS
# ...
好处:
- 模型选择准确率提升(工具池越小越准)
- Token 消耗减少(不需要每次传入全部工具定义)
- 安全性提升(普通用户不会拿到管理员工具)
6.2 工具数量与准确率的关系
经验数据(非严格统计):
| 工具数量 | 选择准确率(估算) | 建议 |
|---|---|---|
| 1-3 | ~95% | 非常可靠 |
| 4-6 | ~85% | 需要好的描述 |
| 7-10 | ~70% | 需考虑分类/分组 |
| 11+ | ~50% | 强烈建议分组或引入路由 |
6.3 强制工具调用
有时你需要模型必须使用某个工具(例如在 Agent 框架中):
OpenAI:
python
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice={"type": "function", "function": {"name": "get_weather"}},
# 强制调用指定工具
)
Anthropic:
python
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=messages,
tools=tools,
tool_choice={"type": "tool", "name": "get_weather"}, # 强制调用
)
也可以通过 tool_choice: "any"(Anthropic)告诉模型必须调用至少一个工具。
七、实战:为一个复杂的 CRM 工具集设计 Schema
假设我们要实现一个 CRM 系统的 AI 助手,包含以下工具集。下面展示完整的设计过程。
7.1 需求分析
| 操作 | 参数 | 约束 |
|---|---|---|
| 查客户 | 客户名/公司名 | 模糊搜索 |
| 创建跟进 | 客户ID、内容、日期 | 日期不能是过去 |
| 查跟进记录 | 客户ID、时间范围 | 支持分页 |
7.2 完整 Schema 设计
python
crm_tools = [
{
"type": "function",
"function": {
"name": "search_customer",
"description": "根据名称模糊搜索客户。返回匹配的客户列表,包含客户ID、公司名、联系人、电话。"
"当用户提到某个客户但不确定完整信息时使用。"
"如果搜索结果超过 20 条,会只返回前 20 条,请提示用户缩小搜索范围。",
"parameters": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "搜索关键词,可以是客户公司名或联系人姓名的一部分",
}
},
"required": ["keyword"],
},
},
},
{
"type": "function",
"function": {
"name": "create_follow_up",
"description": "为客户创建一条跟进记录。跟进记录包括沟通内容、下次跟进日期等。"
"当用户说'帮我记录一下''创建一条跟进''写个备注'时使用。",
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "客户ID,格式如 CUST-00001。如果用户只提供了客户名,请先使用 search_customer 工具查找客户ID",
},
"content": {
"type": "string",
"description": "跟进内容,包括沟通要点和结果",
},
"next_follow_up_date": {
"type": "string",
"description": "下次跟进日期,格式 YYYY-MM-DD。必须是今天或未来的日期。如果不确定,默认取 7 天后",
},
},
"required": ["customer_id", "content"],
},
},
},
{
"type": "function",
"function": {
"name": "list_follow_ups",
"description": "查询指定客户的历史跟进记录。按时间倒序排列。"
"当用户询问'之前跟这个客户聊了什么''上次跟进是什么时候'时使用。"
"支持分页,每页默认 10 条。",
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "客户ID,格式如 CUST-00001",
},
"start_date": {
"type": "string",
"description": "查询起始日期,格式 YYYY-MM-DD。不填则默认最近 30 天",
},
"end_date": {
"type": "string",
"description": "查询结束日期,格式 YYYY-MM-DD。不填则默认今天",
},
"page": {
"type": "integer",
"description": "页码,从 1 开始。默认 1",
"minimum": 1,
},
"page_size": {
"type": "integer",
"description": "每页条数。默认 10,最大 50",
"minimum": 1,
"maximum": 50,
},
},
"required": ["customer_id"],
},
},
},
]
7.3 设计要点回顾
create_follow_up的customer_id描述里提示了前置步骤 :如果用户只有客户名,模型会知道先调search_customerminimum/maximum约束 :在page和page_size上用了整数范围约束,防止异常值- 默认值约定写在描述里 :比如
next_follow_up_date默认 7 天后 - 工具职责边界清晰:查客户、创建跟进、查跟进记录三者无重叠
八、常见反模式总结
| 反模式 | 问题 | 正确做法 |
|---|---|---|
| 一个工具做所有事 | 模型不知道什么时候该用 | 按功能拆分为多个工具 |
| 没有 description | 模型完全不知道工具的用途 | 每个工具写清楚 What/When/Constraint |
| 所有参数都 required | 模型被迫编造参数值 | 只标记真正必须的 |
| Schema 嵌套过深 | 模型填充错误率飙升 | 控制在 2 层内,超出则拍平或拆分 |
参数类型用 any |
模型不约束自己 | 始终指定具体类型 |
| 工具名近似 | 模型分不清 getOrder 和 queryOrder |
用清晰有区分度的命名 |
九、总结
JSON Schema 不是简单的类型声明,它是你和模型之间的接口契约:
- 类型选择 :优先用
string+enum替代boolean;用integer替代number;array必须定义items - 嵌套控制:不超过 2 层,参数少时优先拍平
- Description 方法论:每个描述回答 What / When / Constraint 三个问题
- 多工具路由:工具数超过 6 个时考虑分组,超过 10 个时必须引入路由策略
- 强制调用 :用
tool_choice参数控制模型的调用行为
写好工具定义是一门手艺,需要反复调试和优化。建议用 A/B 测试的方法对比不同 Schema 对调用准确率的影响。