LLM和Agent——专题1:大模型工具调用从入门到实战(2)

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

当用户发送一条消息,模型做的事情是:

  1. 阅读所有工具的 namedescription,判断哪些工具相关
  2. 阅读候选工具的 parameters schema,判断自己能否正确填参数
  3. 如果多个工具都匹配,选最匹配的那个
  4. 按照 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 设计要点回顾

  1. create_follow_upcustomer_id 描述里提示了前置步骤 :如果用户只有客户名,模型会知道先调 search_customer
  2. minimum / maximum 约束 :在 pagepage_size 上用了整数范围约束,防止异常值
  3. 默认值约定写在描述里 :比如 next_follow_up_date 默认 7 天后
  4. 工具职责边界清晰:查客户、创建跟进、查跟进记录三者无重叠

八、常见反模式总结

反模式 问题 正确做法
一个工具做所有事 模型不知道什么时候该用 按功能拆分为多个工具
没有 description 模型完全不知道工具的用途 每个工具写清楚 What/When/Constraint
所有参数都 required 模型被迫编造参数值 只标记真正必须的
Schema 嵌套过深 模型填充错误率飙升 控制在 2 层内,超出则拍平或拆分
参数类型用 any 模型不约束自己 始终指定具体类型
工具名近似 模型分不清 getOrderqueryOrder 用清晰有区分度的命名

九、总结

JSON Schema 不是简单的类型声明,它是你和模型之间的接口契约

  1. 类型选择 :优先用 string + enum 替代 boolean;用 integer 替代 numberarray 必须定义 items
  2. 嵌套控制:不超过 2 层,参数少时优先拍平
  3. Description 方法论:每个描述回答 What / When / Constraint 三个问题
  4. 多工具路由:工具数超过 6 个时考虑分组,超过 10 个时必须引入路由策略
  5. 强制调用 :用 tool_choice 参数控制模型的调用行为

写好工具定义是一门手艺,需要反复调试和优化。建议用 A/B 测试的方法对比不同 Schema 对调用准确率的影响。

相关推荐
沪漂阿龙1 小时前
大模型技术通俗指南:从“大力出奇迹”到AI的“格调养成”
人工智能
zhangfeng11331 小时前
ai辅助工作 agent 小龙虾 WorkBuddy vs OpenClaw 深度对比
人工智能
05候补工程师1 小时前
深度解构 ROS 2:如何手动调通 Nav2 A* 路径规划引擎
linux·人工智能·经验分享·算法·机器人
fpcc1 小时前
并行编程实战——异步编程的屏障的整体分析
人工智能·cuda
Jing_jing_X2 小时前
MCP (一)是什么?一文讲清 AI 如何连接现实世界
数据库·人工智能·oracle
好运的阿财2 小时前
OpenClaw工具拆解之host_workspace_write+host_workspace_edit
前端·javascript·人工智能·机器学习·ai编程·openclaw·openclaw工具
CodePlayer竟然被占用了2 小时前
Cursor SDK:用写代码的方式写 Agent
人工智能
eastyuxiao2 小时前
OpenClaw 自动处理流程图 + 配置清单 可应用场景
人工智能·流程图
沪漂阿龙2 小时前
从买菜做饭到大模型:一份真正看懂深度学习的硬核指南
人工智能·深度学习