先看结论与判断条件
- schemaVersion 必须位于响应顶层并使用稳定语义,不能从字段是否出现或模型名称猜测版本。
- 新增可选字段通常可以向后兼容,新增必填字段、收紧枚举或改变字段含义需要新版本与明确迁移。
- 未知字段策略、缺失字段策略和类型错误策略要分别定义,不能用一个宽松 try-catch 吞掉所有协议问题。
- Prompt 模板、system instruction、输入 schema、模型配置和工具声明要作为同一配置资产绑定版本。
- 远程配置只选择已打包且已验证的兼容档位,获取失败、尚未激活或版本越界时采用应用内安全默认值。
- 旧版 App 的降级不是静默丢字段,而是返回可解释终态,并保留模板、协议、解析器和候选版本回执。
先把 AI JSON 当成跨版本协议
结构化输出一旦被移动端用于渲染卡片、驱动流程或构造工具候选,就具备协议属性。模型并不保证每次都精确复现示例,Prompt 文案也可能远程变化,因此客户端不能只判断字符串能否被 JSON.parse。真正的契约包括顶层版本、字段类型、必填性、枚举、空值、数组上限、嵌套结构和失败终态。
schemaVersion 应在生成要求、响应 schema 和客户端数据类中同时出现,并由解析器在读取业务字段前验证。版本不能由模型标识、发布日期或某个新字段推断,因为同一模型可能服务多个模板,旧模板也可能继续运行。顶层显式版本让日志、回执和兼容矩阵拥有共同主键,减少“看起来像新版”的模糊判断。
版本语义要面向解析兼容,不必追随每次文案调整。修正提示词但不改变输出契约,可以保持协议版本并更新 templateVersion;改变必填字段、枚举解释或嵌套含义时,应升级协议版本。这样产品内容迭代与客户端协议迁移分开,避免一个远程文案变更迫使所有旧 App 同时升级。
| 版本字段 | 负责对象 | 何时变化 | 旧客户端用途 |
|---|---|---|---|
| schemaVersion | JSON 契约 | 字段或语义不兼容变化 | 选择解析器 |
| templateVersion | Prompt 配置 | 模板内容或模型配置变化 | 定位生成来源 |
| parserVersion | 客户端实现 | 解析与降级逻辑变化 | 复现本地行为 |
| appVersion | 安装候选 | 应用发布变化 | 匹配兼容矩阵 |
| policyVersion | 权限与输出策略 | 安全边界变化 | 决定允许动作 |
字段演进要先判断向后与向前兼容
新增可选字段通常对忽略未知字段的旧解析器较友好,但仍要考虑字段名冲突、对象大小和下游透传。若旧客户端会把原始 JSON 转发给另一个组件,未知字段可能在那里获得新含义。协议评审应跟踪字段从生成、解析、持久化、缓存到工具网关的完整路径,不能只检查 Kotlin 数据类能否反序列化。
新增必填字段会让旧响应无法满足新解析器,也会让旧解析器无法承担新业务流程。更稳妥的迁移是先在新版本中把字段设为可选并提供确定性默认,确认服务端和客户端覆盖后,再在后续协议版本提升必填性。默认值必须代表安全终态,不能用空字符串触发隐含的全量查询或默认高权限动作。
枚举变化比新增普通字段更容易造成崩溃。Kotlin enum 若遇到未知字符串,常见配置可能直接失败;把未知值统一映射为 OTHER 也可能掩盖需要阻止的状态。协议应明确哪些枚举允许 unknown 分支,哪些涉及付款、权限或外部动作必须拒绝,并把原始未知值保留在受控诊断字段中。
| 变化 | 旧客户端风险 | 推荐策略 | 失败终态 |
|---|---|---|---|
| 新增可选展示字段 | 通常可忽略 | 验证未知字段策略 | 继续旧视图 |
| 新增必填字段 | 缺失或无法表达 | 升级 schemaVersion | 拒绝不兼容响应 |
| 新增枚举值 | 枚举解析失败 | 显式 unknown 或版本升级 | 降级或拒绝 |
| 改变字段类型 | 类型转换错误 | 新字段名与迁移期 | 拒绝解析 |
| 改变字段含义 | 静默语义错误 | 新版本且禁用旧模板 | 返回协议不匹配 |
未知、缺失和错误类型要有不同终态
解析器首先验证 JSON 根类型和 schemaVersion,再选择对应版本适配器。版本已知后,缺少必填字段说明生成结果不完整;出现未知字段说明服务端可能提前发布;字段类型错误说明生成偏离契约或中间层破坏数据。三类问题的责任和回滚动作不同,不能都落到一个 generic parse error。
对于纯展示字段,未知字段可以被忽略,但要计数并绑定模板版本,观察是否发生服务端先行发布。对于控制流程、工具参数、数据范围和权限判断字段,应使用 allowlist;未知字段不得透传到工具调用或本地数据库。缺失字段若有默认值,默认逻辑必须写进版本适配器并接受测试,不由 UI 层临时猜测。
类型宽松转换会造成隐蔽问题。例如字符串数字自动转为整数、单对象自动包装成数组,可能让错误输出通过并在后续版本形成事实标准。生产解析器应坚持 schema 声明,只在版本迁移器中实施有限、可记录的转换。转换失败返回明确的 protocol_mismatch,不尝试把原文交给另一个更宽松解析器碰运气。
| 异常类别 | 检测位置 | 可否默认 | 记录字段 |
|---|---|---|---|
| 未知版本 | 读取业务字段前 | 否 | schemaVersion、appVersion |
| 缺失必填字段 | 版本适配器 | 仅协议明示时 | field、templateVersion |
| 未知可选字段 | 对象解码 | 按版本策略 | field、parserVersion |
| 未知高影响枚举 | 枚举适配器 | 通常否 | rawValue、policyVersion |
| 字段类型错误 | schema 校验 | 否 | expected、actual |
| 多余工具字段 | 工具网关前 | 否 | field、requestId |
模板、system instruction 与 schema 必须同版审查
服务端 Prompt 模板可能同时包含模型参数、system instructions、输入 schema、工具声明和模板正文。只给正文做版本控制会遗漏影响输出结构的其他字段。配置资产应有不可变摘要,记录 templateVersion、目标 schemaVersion、允许模型、工具集合和适用 App 范围;发布时一次性审核和切换。
system instruction 可以引导模型输出指定结构,却不能替代客户端校验。模型可能遗漏字段、输出未知枚举或在 JSON 外增加文字,网络与缓存层也可能返回旧内容。应用必须把响应视为不可信输入,先执行语法与 schema 校验,再决定渲染、缓存或构造工具候选,不能因为模板由自己管理就跳过解析边界。
模板回滚也要检查协议方向。若新 App 已依赖新版必填字段,把模板回滚到旧 schema 会让新解析器失败;若模板前滚但旧 App 仍在线,旧解析器可能收到未知版本。控制面应按 appVersion 或 capability 集合选择模板,而不是只有一个全局开关,并保留每次选择的版本回执。
远程配置只能选择客户端已知档位
Android 客户端需要随安装包提供应用内默认配置,远程参数在 fetch 和 activate 完成后才可能生效。协议开关的默认值应指向该 App 已打包、已测试的解析器和模板组合。首次启动、离线、获取失败或尚未激活时继续使用默认档位,不能因为远程值为空而选择最新 schema。
远程参数不应直接携带任意 schema 或 Prompt 文本。更安全的模式是传递有限 rolloutKey,客户端用 allowlist 映射到已知 capability;未知 key、目标版本高于客户端能力或签发时间不合规时拒绝采用。服务端仍需按请求声明的能力选择输出,不能指望客户端远程开关补救一个已经发出的不兼容响应。
activate 后也要记录实际采用值与来源,区分 bundled_default、remote_active 和 remote_rejected。Remote Config 不是秘密存储,参数最终可被客户端观察,因此不能放入密钥或把它当作强制授权。协议档位只决定兼容选择,工具权限和数据授权继续由服务端安全边界控制。
| 状态 | 客户端动作 | 服务端动作 | 回执 |
|---|---|---|---|
| bundled_default | 使用内置兼容档位 | 返回声明能力内版本 | 默认版本与应用版本 |
| remote_active | 采用 allowlist 中档位 | 匹配 capability | 参数版本与激活时间 |
| remote_unknown | 拒绝远程值 | 保持旧协议 | 未知 key |
| remote_too_new | 拒绝越界版本 | 不得发送新版 | 客户端最大版本 |
| fetch_failed | 继续默认值 | 保持兼容输出 | 失败类别 |
兼容矩阵要覆盖生成、解析、缓存和降级
兼容矩阵的行是仍在服务期的 App 与 parserVersion,列是 schemaVersion 与 templateVersion 组合。每个交叉点至少验证正常响应、未知可选字段、缺失必填字段、未知枚举、错误类型、JSON 外文本和旧缓存回放。结果区分 pass、expected_reject 和 not_covered,不能只记录“没有崩溃”。
缓存是协议升级最常见的遗漏。磁盘或数据库可能保存旧 JSON,新 App 升级后会先读缓存;服务端也可能按错误键复用另一个模板版本结果。缓存记录应携带 schemaVersion、templateVersion 和创建时的解析策略,新解析器读取时走迁移器或丢弃。不能在缓存层剥掉版本字段后只保存业务对象。
instrumented test 用于验证 Android 序列化配置、进程重建、升级安装、离线回放和 UI 降级。设备端结果仍需绑定候选 APK、系统和测试数据摘要,单一设备不能代表完整矩阵。服务端模板和模型输出用契约测试覆盖,二者回执在同一发布清单中关联但不互相替代。
用兼容检查器验证版本、字段和降级结果
可以把兼容样本组织为公开安全 JSON:顶层声明客户端允许版本、已知字段、必填字段、允许枚举和未知字段策略,cases 数组保存脱敏响应与期望终态。检查器不调用模型,不执行工具,也不读取真实账号数据,只验证协议关系,因此适合放进 CI 解释为什么某次模板升级被拒绝。
下面的 Python 代码从命令行读取矩阵文件。输入缺失时直接非零退出;随后拒绝未声明版本、缺少必填字段、未知枚举、禁止的未知字段,以及实际解析结果不等于 expectedResult 的样本。它要求每个样本明确 compatible、fallback 或 reject,避免异常被吞掉后仍登记通过。
示例只覆盖单层对象,真实项目要扩展嵌套结构、数组上限、空值、数值范围和工具字段 allowlist。扩展时仍保持同一原则:规则来自版本化 schema,样本来自可复核测试,失败结果不可被自动改写为兼容。模型输出是被测输入,不是证明协议正确的证据。
import json
import sys
from pathlib import Path
RESULTS = {"compatible", "fallback", "reject"}
def stop(message):
print(f"schema gate failed: {message}", file=sys.stderr)
raise SystemExit(2)
def load_matrix(path_text):
path = Path(path_text)
if not path.is_file():
print("schema gate failed: matrix file is missing", file=sys.stderr)
raise SystemExit(2)
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
stop(f"cannot parse matrix: {exc}")
def classify(case, policy):
payload = case.get("payload")
if not isinstance(payload, dict):
return "reject"
version = payload.get("schemaVersion")
if version not in policy.get("acceptedVersions", []):
return "fallback" if version in policy.get("fallbackVersions", []) else "reject"
required = set(policy.get("requiredFields", {}).get(version, []))
if not required.issubset(payload):
return "reject"
known = set(policy.get("knownFields", {}).get(version, []))
unknown = set(payload).difference(known)
if unknown and policy.get("unknownFieldPolicy") == "reject":
return "reject"
allowed_status = set(policy.get("statusEnums", {}).get(version, []))
if payload.get("status") not in allowed_status:
return "reject"
return "compatible"
def validate(data):
policy = data.get("policy")
cases = data.get("cases")
if not isinstance(policy, dict) or not isinstance(cases, list) or not cases:
stop("policy and non-empty cases are required")
for index, case in enumerate(cases):
expected = case.get("expectedResult")
if expected not in RESULTS:
stop(f"case {index} has no valid expected result")
actual = classify(case, policy)
if actual != expected:
stop(f"case {index} expected {expected} but got {actual}")
print(f"schema gate passed: {len(cases)} cases")
if __name__ == "__main__":
if len(sys.argv) != 2:
stop("usage: check_schema_matrix.py matrix.json")
validate(load_matrix(sys.argv[1]))发布顺序要让旧客户端先获得安全选择
协议升级先发布能理解新旧版本的服务端选择逻辑和新客户端解析器,但仍让旧模板作为默认。新客户端进入可观察范围后,再按 capability 小范围启用新版模板;确认 expected_reject、fallback 和 compatible 回执符合矩阵,才扩大范围。过程中保留一键回到旧模板的路径,同时确保新客户端仍能解析旧响应。
任何模板、schema、远程档位、序列化配置或候选 App 变化都应产生新的证据绑定。安全开发记录包括来源、配置摘要、构建身份、矩阵结果、设备回执和回滚条件。没有这些项目证据时,只能说明设计采用了版本化方法,不能声称线上旧版 App 已无解析失败。
站内的 AI 输出日志隐私文章可继续检查协议错误回执的脱敏边界;准备评估商业 App 的结构化 AI 输出、远程模板和加固范围时,可通过页面行动按钮进入御盾中央平台提交 schema、兼容矩阵与候选包。提交只启动评估,兼容结论仍以实际版本组合和同候选回执为准。
- 响应顶层显式携带 schemaVersion。
- 必填、未知字段、枚举和类型错误分别定义终态。
- 模板版本绑定模型配置、system instruction、schema 与工具声明。
- 远程参数只选择客户端 allowlist 内兼容档位。
- 缓存记录保留 schemaVersion 与 templateVersion。
- 新旧 App、模板、缓存和降级组合都有回执。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Kotlin 序列化依赖生成的序列化器、描述符、字段名和格式配置。 | Kotlin serialization 说明插件、序列化器和格式化使用方式。 | 官方 Kotlin 用法不证明第三方库具有相同未知字段和枚举行为。 |
| Prompt 模板中的模型配置、系统指令、输入 schema、工具声明和正文应同版审查。 | Firebase server prompt template syntax 展示这些字段可以共同构成服务端模板。 | 模板语法不能保证模型稳定输出,也不替代客户端与服务端授权。 |
| system instruction 不能替代结构化输出的确定性解析和工具权限检查。 | Firebase AI Logic system instructions 说明系统指令用于影响模型行为。 | 影响行为不等于每次满足 schema,也不能完全阻止越权或异常输出。 |
| Android 远程参数采用存在默认、fetch、activate 和失败状态。 | Firebase Remote Config for Android 说明应用内默认值及获取激活流程。 | 远程配置不是秘密存储或强制安全控制,协议值仍需客户端 allowlist。 |
| 升级安装、缓存回放和 Android 序列化行为应在设备环境回归。 | Android instrumented tests 说明设备端测试可访问应用上下文和平台 API。 | 单一设备结果不能代表全部系统、厂商和版本组合。 |
| 协议升级需要保留来源、构建、验证和变更证据。 | NIST SP 800-218 SSDF 提供安全开发与供应链治理实践。 | 组织级框架不定义具体 schema,也不证明某个 App 兼容矩阵通过。 |
| 未知高影响枚举或版本越界应失败关闭,而不是静默映射。 | 工程判断:无法确认语义时继续驱动业务动作会扩大协议错误影响。 | 纯展示字段可按版本策略降级,具体分类需要业务风险评审。 |
| 当前没有项目证据证明旧版 App 已能解析目标线上模板。 | 项目证据尚未接入;需要模板摘要、App 候选、矩阵样本和设备回执。 | 文章提供检查方法,不提供未经执行的兼容或线上效果结论。 |
工程常见问题
只要模型返回合法 JSON,旧版 App 就能安全解析吗?
不能。合法 JSON 只通过语法层,字段版本、必填性、枚举、类型和未知字段仍可能不兼容,必须按 schemaVersion 选择适配器。
新增一个可选字段是否一定向后兼容?
通常较容易,但要确认旧解析器的未知字段策略、缓存透传和下游工具。若字段可能在其他组件获得含义,仍需矩阵测试。
未知枚举统一映射为 OTHER 是否最稳妥?
只适合允许降级的展示状态。涉及权限、付款、数据范围或外部动作时,未知语义应拒绝,不能用 OTHER 继续执行。
Remote Config 能否直接下发新的 JSON schema?
不建议让旧客户端执行任意新 schema。远程值应只选择安装包已知的 rolloutKey,越过客户端能力时拒绝并采用内置默认。
为什么缓存也必须保存 schemaVersion?
升级后的 App 可能先读取旧缓存。没有版本就无法选择迁移器、判断模板来源或安全丢弃,容易把旧语义当作当前协议。
加固会影响结构化输出兼容验证吗?
加固可能影响序列化器、反射保留、网络模型和异常路径。候选包变化后应重跑解析、缓存、降级和设备矩阵,旧回执不能代签。