qinghuan
← 回到知识探索

AI 直觉 / 动手实验

工具升级多加一栏,旧助手为什么突然不会用了?

把旧请求交给新服务、把新结果交给旧调用方,分别试一次,才知道兼容究竟朝哪个方向成立。

展开本文目录
  1. 下载完整练习包
  2. 先画两支箭头,别只写“兼容”
  3. 多一个可选栏,与多一个必填栏差在哪里?
  4. 服务说得更多,旧页面也可能接不住
  5. 六行矩阵:让两个方向都露出来
  6. 写了 default,为什么还是缺字段?
  7. 升级计划应包含旧用法的退路
  8. 这张矩阵能证明到哪一步?
  9. 记住这三点
  10. 别把例子用过头
  11. 自己试一小步
  12. 查原始资料
开始前,会这些就够了

茶铺助手以前查营业状态,只交一个 shop_id。周一服务升级,新增 language;开发者觉得多一个语言选项很小,旧助手却从此每次报缺字段。另一边,新服务开始返回 PAUSED,旧页面只认识 OPEN 和 CLOSED,直接把结果当错误。模型没有变笨,两端对表格的约定变了。

已有工具契约课程练习请求与结果怎样按编号对应。这里沿着另一个方向往下走:同一工具存在新旧两版时,谁产生数据、谁接收数据,哪些旧用法仍应成立。最终产物是一张有方向的兼容矩阵,不是一次模型成功调用截图。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本;制作时实测 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • schema_compatibility.py:完整示例
  • README.md:环境、范围与独立练习
  • observed.stdout.txt:制作时的观察记录
  • verification.json:对应源码的验证范围
  • SHA256SUMS.txt:文件内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B schema_compatibility.py

解压后在含示例文件的文件夹打开 PowerShell,按命令运行。先预测结果,再与当次输出对照。 离线固定数据与应用规则,不代表真实模型、完整协议或服务联调。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

先画两支箭头,别只写“兼容”

第一支箭头是请求:旧助手产生参数,新服务按新的输入约定接收。第二支箭头是响应:新服务产生结果,旧应用按原来的结果约定读取。能通过第一支箭头,不代表第二支也能通过。发布记录应写“旧请求仍被新服务接受”,而不是只打一个笼统的勾。

这里把读取并按规则检查数据的一端叫消费者。请求方向里,服务消费参数;响应方向里,应用消费结果。Schema 是某一端用于检查数据形状的规则。每个测试都把产生方版本、消费方版本、所用规则和具体数据放在一起,读表的人才不会把方向反过来。

本文只讨论单个工具的数据契约。权限仍由服务端检查;工具成功返回也不等于模型理解正确。真实升级还可能影响延迟、错误码和旧记录恢复,下面的六个样例没有覆盖这些全部风险。

多一个可选栏,与多一个必填栏差在哪里?

旧请求是只有 shop_id 的对象。如果新版本把 language 放进 required,原来合法的请求就少了一项。若只是定义这个字段、仍允许不填,旧请求有机会继续通过;接下来还要检查不填时的业务行为是否保持原样。本文示例约定不填仍使用原来的语言。

反方向也要试:新助手已经填写 language,旧服务却规定只许出现 shop_id,它会因额外字段拒绝。这不是前一条结论的矛盾,而是另一支箭头。部署时只升级一半、回滚服务或重放旧任务,都会让不同组合真的碰面。

在 JSON Schema 中,properties 描述字段检查,required 决定必填,additionalProperties 能约束未声明字段。这些对象相关关键字针对对象起作用,单写它们不会把所有输入强制变成对象;示例所对应的完整 Schema 还应明确 type 为 object。没有出现的可选字段,与出现但值为 null,也不是同一回事。

服务说得更多,旧页面也可能接不住

假设旧页面把 status 限定为 OPEN 或 CLOSED。新服务给出 PAUSED,虽然仍是字符串,却不在旧枚举集合内。若页面有严格检查,它会拒绝;若页面跳过检查,把所有非 OPEN 都显示为“已关门”,又可能把临时暂停误说成永久关闭。

因此,输入允许更多选项与输出可能出现更多状态,影响的方向不同。新服务接受旧输入,通常是在保留原先能做的事;新服务返回旧页面不认识的值,则要求消费者有明确的未知值处理规则。能解析 JSON 远远不够,后续分支怎样解释这个值也属于契约。

Google 的 AIP-180 把通信兼容与含义兼容分开讨论,并提醒响应枚举增加成员要考虑旧代码。本篇借用这个观察方法,不把其面向 Google API 的具体规则当作所有 Agent 框架的自动保证。

六行矩阵:让两个方向都露出来

下面的手写检查器只表达本题的对象、字符串、必填、额外字段和字符串枚举约束。fields、closed 是教学数据字段,不是 JSON Schema 标准关键字。它没有解析真正的 Schema,也不支持嵌套组合、引用、格式检查或完整类型系统;真实项目应使用与所选草案匹配的验证器。

先不运行,给六行各写一个预测。运行后只把不同于预测的行圈出来,沿箭头找到拒绝方。额外断言检查列表、null、未知语言和输入未被修改,避免“看起来都通过”其实是检查器放过了边界。这里使用固定字典,不会消耗 API 额度。

展开收起代码 · 小实验python · 59 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Offline contract fixtures; NOT a general JSON Schema implementation."""
from copy import deepcopy


def accepts(contract, value):
    # Only the object/string/required/closed-fields/string-enum fixtures below.
    # Every root contract explicitly requires an object.
    if type(value) is not dict:
        return False
    fields = contract["fields"]
    if not set(contract["required"]) <= value.keys():
        return False
    if contract["closed"] and not value.keys() <= fields.keys():
        return False
    for key, item in value.items():
        if key in fields:
            if type(item) is not str:
                return False
            allowed = fields[key].get("enum")
            if allowed is not None and item not in allowed:
                return False
    return True


v1_input = {"fields": {"shop_id": {}},
            "required": ["shop_id"], "closed": True}
v2_optional = deepcopy(v1_input)
v2_optional["fields"]["language"] = {"enum": ["zh", "en"], "default": "zh"}
v2_required = deepcopy(v2_optional)
v2_required["required"].append("language")
del v2_required["fields"]["language"]["default"]
v2_required_default = deepcopy(v2_required)
v2_required_default["fields"]["language"]["default"] = "zh"
old_request = {"shop_id": "S1"}
new_request = {"shop_id": "S1", "language": "zh"}
v1_consumer = {"fields": {"status": {"enum": ["OPEN", "CLOSED"]}},
               "required": ["status"], "closed": True}
v2_consumer = deepcopy(v1_consumer)
v2_consumer["fields"]["status"]["enum"].append("PAUSED")

cases = [
    ("old_request -> new_required_server", v2_required, old_request, False),
    ("old_request -> new_optional_server", v2_optional, old_request, True),
    ("new_request -> old_closed_server", v1_input, new_request, False),
    ("new_PAUSED_response -> old_consumer", v1_consumer, {"status": "PAUSED"}, False),
    ("old_OPEN_response -> new_consumer", v2_consumer, {"status": "OPEN"}, True),
    ("old_request -> required_with_default", v2_required_default, old_request, False),
]
for label, contract, payload, expected in cases:
    actual = accepts(contract, payload)
    assert actual is expected, label
    print(f"{label}: {actual}")

assert old_request == {"shop_id": "S1"}  # No default injection or mutation.
assert not accepts(v1_input, [])
assert not accepts(v1_input, {"shop_id": None})
assert not accepts(v2_optional, {"shop_id": "S1", "language": "fr"})
assert accepts(v2_optional, {"shop_id": "S1", "language": "en"})
print("PASS: 6 matrix cases + 5 boundary assertions")

怎样运行:保存为 schema_compatibility.py,用 Python 3.10 或更新版本运行:python schema_compatibility.py。仅用标准库,不联网、不安装依赖。 2026-10-13 已用 3.14.0 执行这份示例并通过自带断言;下方为当次输出。离线固定数据与应用规则,不代表真实模型、完整协议或服务联调。

运行后对照结果
old_request -> new_required_server: False
old_request -> new_optional_server: True
new_request -> old_closed_server: False
new_PAUSED_response -> old_consumer: False
old_OPEN_response -> new_consumer: True
old_request -> required_with_default: False
PASS: 6 matrix cases + 5 boundary assertions

写了 default,为什么还是缺字段?

Schema 里的 default 提供默认值说明,标准的校验过程不会因此自动向原数据补值。某个表单组件或框架可以选择替你补,但那是该组件的额外行为,要单独核对。所以 language 同时标成必填并写 default,不会凭空让旧请求通过。

若决定由应用做兼容转换,应明确转换发生在什么位置。例如入口先把老版本请求转换成内部命令,再由同一套业务逻辑处理。记录应能区分用户原本提供的字段与应用补出的值;不要让后来的审计误以为旧用户明确选择过这个选项。

还有一种更隐蔽的变化:字段仍叫 timeout,整数范围也没变,单位却从秒改成毫秒。任何形状检查都可能通过,业务含义已经完全不同。工具描述、示例和单位约定同样需要版本审查;换了自然语言描述,还应另测模型是否仍会按预期选工具。

升级计划应包含旧用法的退路

先收集仍被支持的真实请求形状,去除私人数据,固化成契约样例。把新服务放进去跑旧请求,再把新响应交给旧消费代码。若要并存两版工具,明确工具名或协议字段怎样选版本,以及什么时候停止接受旧版;仅在文档标题后写“v2”不会改变路由。

渐进升级可以先让服务接受旧、新两种请求,再更新调用方,最后在明确的迁移窗口后收紧规则。但每一步都要说明实际支持的组合,尤其考虑回滚时新请求是否会落回旧服务。此处是工程安排建议,不是协议提供的事务式升级。

迁移验收最好保存一次失败的原始原因:缺字段、额外字段、未知枚举或含义变化。修复方法不同,不能统一归因成“模型没按格式回答”。同时保留业务结果断言,防止通过兼容转换后,工具却查询了错误资源。

这张矩阵能证明到哪一步?

六行结果只能说明这些固定样例在这份手写约束下的接受情况。一个 True 不是“所有旧输入都兼容”的数学证明。若输入空间有长度、数值范围、嵌套对象或组合条件,还需补边界样例,必要时用真实验证器生成并测试更广的输入集合。

读原始资料时,先在 JSON Schema 对象页定位三个关键字,再看 Annotations 中 default 的说明,最后对照 AIP-180 的 Adding components 与 Semantic changes。把规范事实与本项目的迁移决定分别记下来,避免把文档里的可选能力误写成自己已经实现的保证。

记住这三点

  • 兼容必须写清数据产生方、消费方和检查规则,请求与响应分别验证。
  • 可选字段、必填字段、额外字段和枚举扩展会影响不同方向。
  • Schema 通过仍可能改变单位、默认行为和业务含义。

别把例子用过头

  • 示例是窄范围契约模型,不能作为完整 JSON Schema 验证器。
  • default 不负责标准校验中的自动补值;框架额外处理要另测。
  • 离线样例未验证模型工具选择、真实框架、灰度发布或回滚。

自己试一小步

工具下一版要增加查询日期,你能在不破坏旧用户含义的前提下设计迁移吗?

  1. 独立写出新旧请求和响应各两份,给每份标明产生方与消费方。
  2. 自行决定缺日期表示什么,并说明它与旧行为是否一致。
  3. 增加一个会让旧消费者出错的响应变化,再设计可核验的兼容处理。

做到这些,就算完成

  • 矩阵是否分别覆盖旧到新、新到旧,而不是只测同版本?
  • 是否包含缺字段、null、多余字段、边界日期和未知状态?
  • 是否把离线检查与真实工具、模型和部署验证分开记录?

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • JSON Schema:对象字段与必填项 (新标签页)↗JSON Schema · 英文

    阅读 Properties、Additional Properties、Required Properties;支持对象字段约束、必填与 null/缺失区别。

    2026-10-13 已打开并核对公开正文中的所列段落;阅读不需要账号。未据此执行真实模型或服务。

  • JSON Schema:default 等注释关键字 (新标签页)↗JSON Schema · 英文

    阅读 default 段;支持标准校验不会依此自动填补缺失值。

    2026-10-13 已打开并核对公开正文中的所列段落;阅读不需要账号。未据此执行真实模型或服务。

  • JSON Schema:枚举值 (新标签页)↗JSON Schema · 英文

    阅读 enum 定义与字符串枚举示例;支持限定可接受值集合,未提供自动迁移策略。

    2026-10-13 已打开并核对公开正文中的所列段落;阅读不需要账号。未据此执行真实模型或服务。

  • AIP-180:向后兼容 (新标签页)↗Google API Improvement Proposals · 英文

    阅读 Guidance、Adding components、Semantic changes;支持区分通信与语义兼容、避免新增必填请求字段、谨慎扩展响应枚举。其适用前提为该指南的 API 设计环境。

    2026-10-13 已打开并核对公开正文中的所列段落;阅读不需要账号。未据此执行真实模型或服务。