登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

AI 工具 schema 怎样为函数参数保留版本兼容

来源:17golang原创

时间:2026-10-18 11:39:29 391浏览 收藏

AI 工具上线后,最容易被忽略的是函数参数 schema 其实就是一份接口契约。今天把 include_items 改成 include_lines,或者把字符串改成数组,模型、网关和旧调用方就可能同时出现不兼容。我的经验是把版本问题拆成“工具身份、输入 schema、内部规范对象”三层:能向后兼容的变化继续留在原版本,真正改变语义的变化单独发布 v2,再由服务端适配器统一处理。

实用规则是:新增字段先做可空并保留旧字段;改名、改类型、删除字段或改变含义时,不要偷偷覆盖 v1,而是保留 v1、增加 v2,并记录旧版本调用量。

本文讨论的官方函数调用资料入口是:https://developers.openai.com/api/docs/guides/function-calling。不同模型或供应商的具体字段可能不同,下面的 schema 形状应当结合目标平台的约束调整。

先把版本放在正确的边界上

我在设计工具时会先画出三层边界。第一层是模型看到的工具身份,例如 get_order_v1 和 get_order_v2;第二层是每个工具自己的 JSON Schema;第三层是业务服务内部统一使用的 OrderQuery。版本号只放在描述文本里不够可靠,因为描述可能被模型忽略,也无法帮助服务端路由。

边界负责什么兼容策略
工具身份告诉模型可调用哪一代契约破坏性变化新增 v2 名称
参数 schema限制字段、类型和必填关系原版本只做加法或兼容映射
内部规范对象供业务代码稳定消费由 v1/v2 适配器统一转换
工具身份、参数 schema 和内部规范对象的静态边界关系
图1:结构说明图,查看工具身份、参数 schema 与内部规范对象三个边界;连线表示静态契约关系,不是运行截图。

这样做的好处是模型侧和业务侧可以分别演进。工具注册表决定暴露哪一代名称,schema 决定调用参数能否被接受,适配器则把差异收敛到一个稳定的内部字段名。图中的关系是设计说明,不代表某次真实请求已经执行。

新增参数时,用可空字段保住旧调用方

非破坏性变化通常是新增筛选项、排序选项或返回偏好。不要把旧字段从 required 中拿掉后,期待所有平台都按同一种方式处理“可选”。如果启用 strict schema,官方文档要求对象属性全部列入 required,可以把业务上的可选值表示成字符串或 null 的联合类型。

{
  "type": "object",
  "properties": {
    "schema_version": {
      "type": "string",
      "enum": ["v1"]
    },
    "order_id": {
      "type": "string"
    },
    "include_items": {
      "type": ["boolean", "null"]
    }
  },
  "required": ["schema_version", "order_id", "include_items"],
  "additionalProperties": false
}

这里的 include_items 在协议层始终出现,但值可以是 null。服务端把 null 解释为旧调用方没有提出偏好,再使用默认行为。schema_version 用枚举限制当前工具只接受 v1;当 v2 的含义发生变化时,不要仅仅把枚举扩成两个值后让一个处理器猜测差异。

新增字段还要保持旧字段的含义不变。可以新增 locale、sort 这类独立能力,但不要把旧的 include_items=true 改成“返回摘要和明细”。字段名没变而语义变了,仍然属于破坏性变化。

改名或改类型时,让 v1 和 v2 经过同一个适配器

字段改名、类型变化、枚举值删除和默认含义改变,都应该视为新的契约。以订单查询为例,v1 继续接受 include_items,v2 使用更明确的 include_lines。两边都转换成内部的 includeLines,业务层不需要知道模型调用的是哪一版。

const TOOL_VERSIONS = new Set(["v1", "v2"]);

function normalizeOrderQuery(raw) {
  // 先拒绝未知版本,避免把新字段静默当成旧语义。
  if (!raw || typeof raw !== "object" || !TOOL_VERSIONS.has(raw.schema_version)) {
    throw new Error("unsupported tool schema version");
  }

  if (raw.schema_version === "v1") {
    // v1 的字段名保留给旧调用方,内部统一使用新语义名称。
    return {
      orderId: raw.order_id,
      includeLines: raw.include_items ?? false,
      sourceVersion: "v1"
    };
  }

  // v2 已经明确表达 lines,但仍把输入转换成同一个内部对象。
  return {
    orderId: raw.order_id,
    includeLines: raw.include_lines ?? false,
    sourceVersion: "v2"
  };
}

适配器要做三件事:先校验版本,再做字段映射,最后保留 sourceVersion 供日志和迁移统计使用。不要在业务函数里散落 if (v1);否则下线 v1 时会漏掉隐藏分支。对于 v2 的 schema,工具身份和 schema_version 可以同时存在:前者负责选择契约,后者负责让服务端拒绝错配输入。

v1 与 v2 参数经过适配器归一为内部订单查询对象
图2:结构说明图,查看 v1、v2 参数与统一内部对象的关系;适配器是兼容边界,不是实际运行截图。

用迁移指标决定什么时候下线旧版本

版本发布后,不能只看新工具能否被调用。至少记录四类指标:未知版本拒绝数、schema 校验拒绝数、v1/v2 调用量,以及适配器转换后的业务失败数。若 v1 调用量仍高,说明还有调用方没有迁移;若 schema 拒绝数突然上升,优先检查工具注册版本与服务端路由是否错配。

我会把迁移分成三个阶段:先同时暴露 v1 和 v2,让新会话优先看到 v2;再在服务端保留 v1 适配器并给旧调用方发迁移提示;最后在一段可观测窗口内确认 v1 为零或低于约定阈值,再移除注册和适配代码。这个窗口不是平台保证,而是团队根据业务风险自行设定的发布策略。

  • 新增能力:原字段保持原意,新字段使用可空值,旧调用方继续走默认行为。
  • 改变语义:增加独立 v2 工具,不把两个版本混成一个模糊 schema。
  • 服务端处理:先版本校验,再适配到统一内部对象,业务层不感知外部字段差异。
  • 下线判断:结合版本调用量、拒绝数和业务失败数,而不是只看发布时间。

两个容易混淆的问题

把旧字段标成可选,是否就完成了兼容? 不一定。字段可以不再必填,但它的默认值、枚举语义和返回结果仍可能变化。兼容要同时看结构和含义。

为什么不直接在一个 schema 里接受 v1、v2 两套字段? 这样短期省去注册两个工具,长期却会让模型选择字段组合,服务端也难以判断互斥关系。只有两套字段含义高度一致、迁移周期很短时,才考虑在一个契约里做明确的联合设计。

归纳起来,schema 版本管理不是给 JSON 加一个数字,而是给工具身份、字段约束、适配入口和下线指标建立一条完整的兼容链。把破坏性变化显式化,旧调用方才有可控的迁移路径。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>