跳到主要内容

一次结构化输出失败复盘

· 阅读需 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

这段逻辑的价值不在于语法,而在于系统终于承认“不合法就不继续往后跑”。很多结构化输出事故最后之所以变成大问题,不是因为模型第一次出错,而是因为错误第一次出现时没人愿意让它停下来。

我真正想保留的结论

一次结构化输出失败最值钱的教训通常不是“换个模型”,而是提醒我们:只要结果要进入系统流程,就必须把契约和校验当成第一优先级。