一次结构化输出失败复盘
· 阅读需 3 分钟
这次失败并不复杂,但特别典型。模型表面上已经按 JSON 返回了结果,接口也没有报错,前端甚至还能正常展示。但真正把结果往下游流程里送时,系统才发现有一个关键字段类型不稳定,有时候是字符串,有时候是数组。
这种问题最麻烦的地方就在于,它不像完全报错那样容易被发现,而是会先以“偶发脏数据”的形式悄悄混进链路。
最早暴露问题的,不是模型层,而是下游解析层
最早暴露问题的,不是模型调用层,而是后面的业务解析层。平时测试样本都过了,线上少量真实请求一进来,就开始出现解析分支越来越多、补丁越来越多的情况。
这类问题最危险的地方在于,它看起来不像“模型失败”,更像“系统偶尔脏一点”。可一旦放任这种脏结果继续往后流,后面的每一层都会开始长兼容补丁。
我后来更确定的一点:JSON 只是外观,Schema 才是契约
这次复盘让我更确定一点:结构化输出的问题,很多时候不是“模型会不会返回 JSON”,而是“Schema 是否真的被定义清楚、校验是否真的执行到位”。
如果没有强校验,系统会默认把“差不多对”当成“可以接收”,后面就会越来越难收拾。
我后来做的,不是换模型,而是把入口收紧
这次之后,我会优先做两件事:
- 模型返回后立刻做严格校验
- 校验失败就 fallback 或重试,不让脏结果继续往后流
把问题卡在入口处,比在下游到处补救省心得多。
如果把这个动作再写得具体一点,它应该更像下面这样:
const parsed = OutputSchema.safeParse(rawResult)
if (!parsed.success) {
await saveFailureSample({
scene: 'ticket-triage',
reason: parsed.error.flatten(),
})
return fallbackToHuman()
}
return parsed.data
这段逻辑的价值不在于语法,而在于系统终于承认“不合法就不继续往后跑”。很多结构化输出事故最后之所以变成大问题,不是因为模型第一次出错,而是因为错误第一次出现时没人愿意让它停下来。
我真正想保留的结论
一次结构化输出失败最值钱的教训通常不是“换个模型”,而是提醒我们:只要结果要进入系统流程,就必须把契约和校验当成第一优先级。
