一个编造的工单 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 的取值路径也不同,不能直接共用同一段读取代码。
官方依据:来源 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 的含义也刻意留了一步:数据可以交给业务复核,尚未自动派单。
// 教学用契约。真实 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 配置,确认预算与权限。验收时分别看字段是否合规、引用是否存在、分类是否符合业务标准,以及失败分支有没有触发写入。换模型或供应商后,用同一组样例再检查,才能知道变化出在哪里。
接口说明依据下列官方文档;本文的工单与响应均为合成数据,脚本只验证本地分支,未调用模型或测试第三方服务。