AI 严选AIAICN / API FIELD GUIDE
结果校验

AI 返回 JSON 仍然出错?结构化输出的校验与恢复流程

JSON.parse 没报错,结果为什么还是不能入库?从工单分类的例子看字段校验、拒绝、截断与重试,附可直接运行的本地脚本。

AI 严选 · 编辑整理(AI 辅助) · 写于 · 资料核对

本文目录
  1. JSON 没坏,工单却错了
  2. 该用 JSON 模式,还是 Structured Outputs?
  3. 解析之前,先看这次回答有没有结束
  4. 用六个输入检查接收代码
  5. 失败后,别靠补括号和默认值救场
  6. 把格式检查和分类效果分开验收
直接回答

一个编造的工单 ID 也能通过 JSON 解析。结构化输出能约束字段和类型,但入库前,你仍要查这条工单是否存在、分类是否合理。

适用前提

接口配置以 OpenAI Responses 为例。换用其他模型或平台时,请核对其结构化输出支持范围。

JSON 没坏,工单却错了

假设你要把客服工单分成 normal 和 urgent 两类,并带上原工单 ID。模型返回 {"priority":"rush","evidence_id":"ticket-demo-1"}:JSON.parse 可以读它,但 rush 不在约定的分类里。把 rush 改成 urgent 后,结构检查过了;如果 ID 是编出来的,数据库仍然找不到这条工单。

即便 ID 存在,分类也可能判断错。因此,格式校验过后还要核对业务结果。下面这张表用来区分错误发生的位置,方便程序把问题交给合适的处理分支。

检查层能回答什么失败时先做什么
响应状态这次响应是否完成、是否拒绝保留独立失败状态,不解析半截内容
JSON语法字符串能否解析为JSON记录解析错误,检查是否用了错误输出字段
结构契约字段、类型、枚举是否符合约定核对Schema与客户端版本,不静默补默认值
业务约束引用对象是否存在、结果是否允许采用查原始依据与业务规则,转人工复核

该用 JSON 模式,还是 Structured Outputs?

在提示词里写“只返回 JSON”,并没有给程序一份可靠的字段约定。OpenAI 的 JSON 模式着重于输出合法 JSON;支持 Structured Outputs 的模型和配置还能约束 Schema。你的应用需要固定字段时,后者更合适,但仍要处理拒绝、未完成响应和业务错误。

工单分类的 Schema 可以很小:priority 用枚举,只接受 normal 或 urgent;evidence_id 用字符串,留给应用查询原始记录。输入不足时,也要留出“无法判断”的状态,或在应用中转人工处理,否则模型只能在不合适的选项里硬选。

OpenAI 的 strict 模式支持 JSON Schema 的一个子集,要求字段列入 required,并在对象上设置 additionalProperties:false。业务上可缺省的值,可按文档使用允许 null 的类型,读取时再处理 null。复杂 Schema 的支持情况要单独验证;平台写了“OpenAI 兼容”,并不能说明这些约束都已经实现。

官方依据:来源 1

解析之前,先看这次回答有没有结束

收到响应后,先别急着调用 JSON.parse。如果回答被截断,或者模型拒绝了请求,解析正文只会把真正的问题变成一个 JSON 报错。OpenAI 文档为拒绝和未完成输出列出了处理方式;应用应该保留这些状态,而不是统一显示“解析失败”。

下面的脚本把原始响应整理成 status、refused、text 三个字段,便于演示。这是应用自定义的数据结构,不是 SDK 的原始返回格式。接真实接口时,需要按对应版本读取输出项,同时处理空结果、传输异常和未结束的流。Chat 与 Responses 的取值路径也不同,不能直接共用同一段读取代码。

AI JSON结果校验流程:先检查响应状态,未完成或拒绝时停止;完成且未拒绝才解析JSON,解析失败检查字段或截断;结构不符时核对Schema,不补默认值;工单ID不存在时查原始依据或转人工。全部通过只进入业务复核,不自动入库或派单。
原创流程示意,与下方 accept 函数对应。工单 ID 检查只是本例的业务规则;分类是否正确仍需复核。

官方依据:来源 1

用六个输入检查接收代码

把代码保存为 validate-output.mjs,运行 node validate-output.mjs,无需安装依赖。脚本会依次打印 ready_for_review、invalid_json、invalid_shape、unknown_evidence、refused、incomplete;任一分支不符合预期就会抛错。

这里手写了两个字段的检查,不是通用 Schema 验证库。生产项目可以用受维护的验证器或 SDK 解析功能,但要让校验逻辑跟着 Schema 一起更新。ready_for_review 的含义也刻意留了一步:数据可以交给业务复核,尚未自动派单。

JavaScript · 本地合成示例,不联网、不消耗 API 额度
// 教学用契约。真实 Responses 配置放在 text.format,先核对模型支持。
const format = {type:'json_schema', name:'ticket_route', strict:true,
  schema:{type:'object', properties:{
    priority:{type:'string', enum:['normal','urgent']},
    evidence_id:{type:'string'}},
    required:['priority','evidence_id'], additionalProperties:false}};
// 这是应用自定义信封,不是 OpenAI SDK 原始 response。
function accept(envelope, knownIds) {
  if (envelope.status !== 'completed') return {kind:'incomplete'};
  if (envelope.refused) return {kind:'refused'};
  let value;
  try { value = JSON.parse(envelope.text); }
  catch { return {kind:'invalid_json'}; }
  // 只验证本例两个字段,不是通用 JSON Schema 校验器。
  if (!value || typeof value !== 'object' || Array.isArray(value) ||
      Object.keys(value).length !== 2 ||
      !Object.hasOwn(value,'priority') || !Object.hasOwn(value,'evidence_id') ||
      !format.schema.properties.priority.enum.includes(value.priority) ||
      typeof value.evidence_id !== 'string') return {kind:'invalid_shape'};
  if (!knownIds.includes(value.evidence_id)) return {kind:'unknown_evidence'};
  return {kind:'ready_for_review', value}; // 不自动派单或执行外部动作。
}
const good = JSON.stringify({priority:'normal', evidence_id:'ticket-demo-1'});
const cases = [
  [{status:'completed', text:good}, 'ready_for_review'],
  [{status:'completed', text:'{"priority":'}, 'invalid_json'],
  [{status:'completed', text:'{"priority":"rush","evidence_id":"ticket-demo-1"}'}, 'invalid_shape'],
  [{status:'completed', text:'{"priority":"urgent","evidence_id":"invented"}'}, 'unknown_evidence'],
  [{status:'completed', refused:true, text:''}, 'refused'],
  [{status:'incomplete', text:good}, 'incomplete']
];
for (const [input, expected] of cases) {
  const result = accept(input, ['ticket-demo-1']);
  if (result.kind !== expected) throw new Error('分支不符合预期:'+expected);
  console.log(result.kind);
}

失败后,别靠补括号和默认值救场

遇到 invalid_json,先看读到的是否真是正文:可能取错了输出字段,也可能拿到了半截流。遇到 invalid_shape,则检查请求里使用的 Schema、模型支持范围和客户端版本。直接删掉未知字段,或把 rush 改成默认分类,虽然能让程序继续运行,却会把错误悄悄写进业务数据。

拒绝结果应直接告诉调用方,不能靠反复改提示词强迫回答。截断也要看原因:确认是输出上限、且预算允许时,才考虑缩小任务或调整上限。来源 ID 不存在,就回查输入或交给人工;编一个能入库的 ID 没有解决任何问题。

确实需要重试时,给次数、总时限和预算设上限,每次尝试分别记录状态。入库、派单等操作放在校验和业务授权之后,并做好去重。新适配器持续失败时,切回已验证的旧版本或人工流程,不要继续消费这批无效结果。

把格式检查和分类效果分开验收

六个用例通过,说明这段接收代码能识别预设错误。它没有检验模型能否正确判断一张工单的优先级。后者需要另一组有明确预期的样例,包括信息缺失、内容矛盾和与任务无关的输入;数据应去掉私人信息。

对真实模型做测试前,记录模型版本、Schema 和 SDK 配置,确认预算与权限。验收时分别看字段是否合规、引用是否存在、分类是否符合业务标准,以及失败分支有没有触发写入。换模型或供应商后,用同一组样例再检查,才能知道变化出在哪里。

来源与适用边界

接口说明依据下列官方文档;本文的工单与响应均为合成数据,脚本只验证本地分支,未调用模型或测试第三方服务。

  1. https://developers.openai.com/api/docs/guides/structured-outputs