跳到主要内容

API 验收

换模型后 JSON 输出坏了怎么办:结构化输出验收表与样本

按响应状态、JSON Schema 和业务事实逐层验收,附报价提取合同、10 个合成验收样本与拒答、截断的处理清单。

编写: AI Codex · 更新 · 9 分钟 · 内容方法与 AI 辅助说明

换模型后 JSON 出问题,先检查响应是否完整,再检查字段结构,最后核对业务事实。 JSON.parse 成功只说明文本能被解析;它不能证明金额、币种和订单号来自原文,也不能替你决定是否允许写入业务系统。

本文解决模型或供应商切换后的结构化输出验收,官方资料复核日期为 2026-10-08。报价和响应样本均为人工构造,没有执行付费模型比较,不提供模型准确率排名。与 供应商锁定 和 失败重试 不同,这里重点检查“返回的内容能否交给下游”。

先确认实际端点提供什么约束

将模型名、具体供应商、API 路径、SDK 版本、请求参数和 schema 版本一起记录。只把 model 改成新名字,不能证明其他条件保持一致。

方式 可以用来做什么 仍需要自己检查什么
提示词要求输出 JSON 描述期望格式 是否夹带解释、字段缺失、类型错误
JSON 模式 请求可解析的 JSON 是否符合业务 schema、内容是否完整
JSON Schema 结构化输出 在支持的接口上施加结构约束 该端点支持哪些约束、异常分支、业务事实
工具调用参数 接收待执行动作的参数 工具名、权限、参数与业务规则;生成参数不等于允许执行

OpenRouter 文档 当前明确说明,支持情况按端点区分,strict 的执行方式也因供应商而异。其结构化输出请求使用 response_format.type: "json_schema",可配合 provider.require_parameters: true 筛选支持所需参数的端点。这个筛选不能代替应用验收。

迁移时保存服务端实际发送的脱敏请求,检查 SDK 是否改写 schema、网关是否移除参数、失败后是否换到另一条路由。不支持时应明确失败或进入事先验收过的降级流程,不能静默退回普通文本后继续自动入库。

固定一份能区分缺失与零值的合同

用一个窄场景开始:从报价文本提取编号、币种、未税额、税额和总额。示例仅接受 CNY、USD,以分为单位的非负整数;退款、负数和其他币种不在本合同内。

示例原文:报价 Q-17,币种 CNY,未税额 100.00,税额 6.00,总额 106.00。

期望结果:

{
  "quote_id": "Q-17",
  "currency": "CNY",
  "subtotal_minor": 10000,
  "tax_minor": 600,
  "total_minor": 10600,
  "status": "complete"
}

应用合同要求六个键都存在。原文没有的信息用 null,并进入 needs_review,不得用零、空字符串或推算出的值补齐。这里的 status 只是模型提出的状态,下游校验器必须独立判断。

下面是应用侧 JSON Schema;完整 schema 文件 声明 Draft 7。它不是可以原样发送给每个供应商的通用请求体:API 只支持部分 schema 特性时,需要明确记录适配,并保留完整的应用侧校验。

{
  "type": "object",
  "properties": {
    "quote_id": { "type": ["string", "null"] },
    "currency": { "enum": ["CNY", "USD", null] },
    "subtotal_minor": { "type": ["integer", "null"], "minimum": 0 },
    "tax_minor": { "type": ["integer", "null"], "minimum": 0 },
    "total_minor": { "type": ["integer", "null"], "minimum": 0 },
    "status": { "enum": ["complete", "needs_review"] }
  },
  "required": ["quote_id", "currency", "subtotal_minor", "tax_minor", "total_minor", "status"],
  "additionalProperties": false
}

required 控制键是否必须存在,允许 null 控制值可否未知;两者并不冲突。JSON Schema 的对象说明 区分了缺少属性与属性值为 null,也说明单列 properties 不会自动使字段必填。

用同一组负例检查新旧链路

下载 10 个合成验收样本,先检查自己的校验器,再将对应原文作为新旧模型的同题输入。文件中的 output 是手写测试输入,不是模型已经生成过的结果。

样本变化 schema 应否通过 应用应如何处理
全部字段符合示例原文 通过 业务校验也通过后接受
删除 tax_minor 不通过 记录缺字段,不默认填 0
tax_minor: null 但标记 complete 通过 业务校验拒绝完成状态
总额写成字符串 "10600" 不通过 不静默转换类型掩盖兼容问题
多出 approved: true 不通过 不接受未知授权字段
币种为 EUR 不通过 超出本示例支持范围
总额写成 10700 通过 金额不相等且不符原文,拒绝
币种改成 USD 通过 枚举合法但原文为 CNY,拒绝
税额为负数 不通过 不符合这个非负报价合同
原文没写税额,输出 null 且待复核 通过 保留未知,进入复核,不自动发布

业务层还要检查编号非空且与原文对应、金额单位正确,以及 subtotal_minor + tax_minor = total_minor。上述等式只在三项都已知时计算。不能因为等式能反推税额,就把原文未提供的数据伪装成提取结果。

“结构正确但事实错误”必须独立计数。先用人工标注的原文与期望值验收;生产中无法可靠自动核对的事实,需要复核流程。仅让另一个模型回答“是否正确”,不能充当确定性金额校验。

拒答和截断先于字段校验处理

不要把所有响应都当作业务对象解析。先由服务适配层检查 HTTP/API 错误、拒答、结束原因、空内容和流是否完整,再抽取最终数据块交给解析器。

例如 Claude 结构化输出文档 说明,stop_reason: "refusal" 可以伴随 HTTP 200,输出未必符合 schema;max_tokens 表示内容可能被截断。这些字段名属于该接口,换供应商后需要按各自响应契约映射,不能只复制字符串判断。

响应状态 应用分支 不应做的事
API 或参数不支持错误 配置失败,停止原样重试 删除约束后冒充验收通过
明确拒答 独立拒答状态,提供适当提示 填造业务对象绕过拒答
达到输出上限 / 流中断 未完成,保留原因 给半截 JSON 补括号后写库
空内容或 JSON 无法解析 解析失败 使用上一任务的缓存当作新结果
schema 不符合 记录字段路径与规则 静默删字段、补默认值
业务事实不符合 复核或失败 仅凭模型的 complete 放行

在测试适配器时,可以构造拒答、截断和空内容响应;这些检查证明的是应用分支行为,不能推导真实模型拒答率。任何有界修复或重试都应保留原结果与修复记录,重新通过全部检查,并计入 统一重试预算。

按失败类别做迁移记录

冻结原文、schema、提示词和验收规则,一次只换一个变量。模型必须使用不同参数时,记录它为实际迁移配置,避免称作完全同条件比较。样本需覆盖正常、信息不足、矛盾字段、长输入和任务外文本;不要只选新模型容易通过的材料。

建议每次记录:样本ID / 模型与端点 / SDK与配置版本 / schema版本 / 请求ID / 结束状态 / JSON解析 / schema校验 / 事实校验 / 最终去向 / 耗时 / 已确认用量。重试后通过要另列,不能算作首次通过。

观察指标 分母与解释
首次业务验收通过数 除以全部提交样本;拒答和截断不能悄悄排除
解析与 schema 失败数 同时记录尚未进入这两步的响应状态
schema 通过但业务失败数 揭示格式正确造成的虚假安全感
待复核数、拒答数、未完成数 分开统计,按业务决定可接受比例
重试后通过与费用未知数 保留恢复成本,不能用未知费用填零

这 10 个合成样本是回归检查起点,不是统计意义上的准确率测评。上线前预先约定阻断条件,例如任何错误金额进入“自动发布”分支都阻断迁移;低风险提取场景可以保留人工复核通道。先小范围使用、保留旧配置与回退入口,再根据真实失败记录扩大范围。

如果只改变提示词,想设计一份更通用的小型对照集,可接着看十例提示词对照验收法;那篇不替代本页的 schema 和业务事实检查。

继续解决下一个问题