工具调用标准:JSON Schema

P2
深入解析 · 协议与互操作

函数/工具调用标准:作为共享底层的 JSON Schema。

每个主流模型厂商的工具调用都基于同一个想法:用 JSON Schema 描述一个函数,让模型发出结构化调用,执行它,把结果回传。本文剖析这个契约、实践中真正重要的模式方言,以及厂商之间究竟在哪里有本质差异、哪里只是改了名字。

STEP 1

通用契约。

剥去厂商品牌,工具调用就是自 2023 年起就稳定的四步:

  • 声明。调用方把一组工具发给模型,每个工具有名称、自然语言描述,以及其参数的 JSON Schema。
  • 选择。模型在给定对话下,决定是否调用工具,并发出结构化调用:一个工具名加一个意在通过该工具模式校验的 JSON 参数对象。
  • 执行。你的代码——绝非模型——用这些参数运行真正的函数并产生结果。
  • 返回。结果被追加到对话中,通过一个不透明 ID 与调用关联,循环继续,直到模型不调用工具而直接作答。

模型从不执行任何东西。它只产生一个调用函数的请求。这一分离正是工具调用全部的安全与可靠性故事:模式约束模型能要求什么,你的处理器决定真正发生什么。

STEP 2

JSON Schema 是通用语——但只是它的一个子集。

OpenAI、Anthropic、Google 以及几乎所有开源模型工具调用约定的共享底层,是 JSON Schema(IETF 草案家族)。一个工具的参数是一个 JSON Schema object,带有类型化的 properties、一个 required 列表,以及 enumminimumformat、嵌套对象/数组等可选约束。

# A provider-neutral tool schema (the part everyone agrees on)
{
  "name": "create_ticket",
  "description": "Open a support ticket. Use only after the "
                 "user has confirmed the summary.",
  "parameters": {
    "type": "object",
    "properties": {
      "title":    {"type": "string", "maxLength": 120},
      "severity": {"type": "string",
                    "enum": ["low", "normal", "high", "urgent"]},
      "component": {"type": "string"},
      "steps": {
        "type": "array",
        "items": {"type": "string"},
        "description": "Ordered reproduction steps."
      }
    },
    "required": ["title", "severity"],
    "additionalProperties": false
  }
}

现实告诫:厂商只支持完整 JSON Schema 的一个子集,且各家子集并不相同。冷僻构造——$ref 递归、oneOf/allOf 组合、patternProperties、条件式 if/then——可能被静默忽略、被拒绝,或仅在"严格"模式下生效。安全且可互操作的核心是:标量类型、enum、数组、嵌套对象、required 和清晰的描述。超出这些的一切,在验证之前都视为厂商专属。

杠杆最高的单一字段是 description——工具的以及每个参数的。模型主要依据散文而非类型来选择并填充工具。一句精确描述("ISO-8601 日期;省略表示‘今天’")比任何结构约束都更能防止糟糕调用。

STEP 3

厂商差异在哪里:是信封,不是模式。

模式主体可移植。其外层包装不可移植。Field Guide 的"工具使用"一章详述了在线格式;这里给出协议层的小结:

# Anthropic Messages API — tool declaration
{
  "name": "create_ticket",
  "description": "Open a support ticket.",
  "input_schema": { /* the JSON Schema object */ }
}
# Model emits a `tool_use` content block:
#   {type:"tool_use", id:"toolu_…", name, input:{…}}
# You return a `tool_result` block keyed by tool_use_id.
# OpenAI Responses API — tool declaration
{
  "type": "function",
  "name": "create_ticket",
  "description": "Open a support ticket.",
  "parameters": { /* the JSON Schema object */ }
}
# Model emits a `function_call` item with stringified
# `arguments`; you return a `function_call_output`
# item keyed by call_id.

对互操作层真正重要的差异:

  • 键名。input_schemaparametersinput(已解析对象)对 arguments(你必须解析的 JSON 字符串)。
  • 类型判别符。OpenAI 用 "type": "function" 标记工具,因为它在用户函数之外还提供内置工具(网页搜索、文件搜索、计算机使用)。Anthropic 只有一种工具,故无判别符。
  • 严格模式。OpenAI 的 "strict": true 配合 additionalProperties: false 约束解码,使参数可证明地匹配模式。Anthropic 以不同方式强制合规,不需要此标志。严格模式也收窄了被接受的模式子集——这是用表达力换保证的有意取舍。
  • 关联 ID。tool_use_idcall_id——同一概念(把结果匹配到其调用,对并行调用至关重要),不同名字。
  • 历史模型。Anthropic 严格按角色交替的、带内容块的 messages,对 OpenAI 按 ID 关联的、有类型的扁平 input 项列表。
STEP 4

适配器很小,因为核心是共享的。

因为模式可移植、只有信封不同,所以"一个厂商无关的工具定义 + 每个厂商一层薄适配器"是标准互操作模式。你只编写每个工具一次,用中立形式,再渲染进每个厂商的包装。

# One neutral definition, rendered per provider
def to_anthropic(t):
    return {"name": t.name,
            "description": t.description,
            "input_schema": t.schema}

def to_openai(t):
    return {"type": "function",
            "name": t.name,
            "description": t.description,
            "parameters": t.schema,
            "strict": True}
# Same t.schema (the interoperable JSON Schema core)
# feeds both. Parsing the call back is the only
# other place the envelope leaks.

这正是模型上下文协议要形式化的那道接缝:与其让每个智能体作者都写这些适配器、每个工具作者按厂商重写其函数描述,MCP 把工具描述变成服务器只发布一次的协议产物。后续文章沿着这条线走。本文的要点:工具调用在核心处有一个真实、稳定的标准——JSON Schema 加四步"请求/执行/返回"循环——厂商差异是一层薄而透彻理解的翻译层,不是根本性的不兼容。

模式合法不等于安全。模式约束参数的形状,从不约束其意图。{"path": "../../etc/passwd"}{"type":"string"} 完全合法。针对业务规则与授权的参数校验发生在你的处理器里——威胁模型见"安全、对齐与智能体安全"系列。