很多团队第一次接触结构化输出时,都会有一种轻松感。因为相比纯文本生成,结构化输出看起来终于更像传统工程世界了。你可以定义字段、约束类型、要求模型返回 JSON,甚至直接挂上 schema 和函数调用接口。于是大家很自然地认为:只要模型支持这些能力,AI 系统就会更容易进入生产环境。
现实通常没有这么顺利。因为“支持结构化输出”和“结构化输出足够稳定”之间,隔着大量容易被低估的工程问题。字段可能缺失,类型可能漂移,数组顺序可能不稳定,空值语义可能前后不一,工具参数可能看起来合法但业务含义错误。更麻烦的是,这些问题很多不会以显眼的异常形式出现,它们会悄悄渗进链路下游,直到某个工作流、某个统计任务或某个审批动作出了错,团队才意识到结构早就坏了。
结构化输出的问题,不只来自模型
很多团队一看到 JSON 错误,就直觉认定“模型不行”。但在生产环境里,结构化输出的失败来源往往不只在模型。常见问题至少包括:
- Prompt 里对字段语义描述不够清楚。
- schema 本身写得过度理想化,和业务现实不匹配。
- provider 对 JSON mode 或工具调用的封装细节不同。
- 重试、回退或修复流程把原始结构进一步扭曲。
- 下游系统对“缺失字段”和“空字段”的理解不一致。
也就是说,结构化输出失败通常不是一个点坏了,而是一条链路没有形成统一约束。
最常见的五种坏法
做久了之后,我们会发现结构化输出虽然形式多样,但失败模式非常集中。最常见的大概有五类:
- 语法坏。也就是最表面的 JSON 非法、括号不闭合、引号转义错误。
- 结构坏。JSON 虽然合法,但字段层级、数组嵌套或对象形态不符合预期。
- 类型坏。应该是 number 的地方给了 string,应该是 boolean 的地方给了
"yes"。 - 语义坏。结构完全合法,但字段含义已经偏了,比如把“建议动作”填成了“已执行动作”。
- 上下文坏。结构也合法、语义也看似合理,但引用的是错误上下文或过期信息。
前两类最容易被检测出来,后三类才是真正麻烦的地方。因为它们往往会通过最基础的校验,却在业务流程里悄悄制造问题。
schema 不是越严越好
很多团队发现结构化输出不稳定后,第一反应是继续收紧 schema。这当然有帮助,但 schema 不是越严越好。过于严格的 schema,可能让模型生成失败率升高,也可能让 provider 兼容层复杂度迅速增加,甚至让回退链路全部失效。
更现实的做法,是把 schema 视作“业务需要被保障到什么程度”的表达,而不是“我们想把一切都写死”的工具。对于必须保证的字段,约束可以更强;对于允许缺省或允许降级的字段,则需要保留弹性。结构化输出真正要做的,不是把模型变成数据库,而是在生成能力和工程稳定性之间找到可持续平衡。
兼容层往往比 schema 本身更重要
只要系统开始同时接多个模型或多个 provider,兼容层就会成为结构化输出最关键的一层。因为不同供应商对 schema、JSON mode、函数调用和工具参数的支持细节经常不完全一致。某个 provider 会自动补齐空对象,另一个不会;某个模型更偏向省略可选字段,另一个则习惯返回空字符串;某个接口把数字保持为 number,另一个却可能把它序列化成 string。
如果平台没有自己的兼容层,业务方最终就会被迫自己理解这些差异。那时所谓的统一 API 其实并没有真正统一,只是把差异转移到了下游应用。
一个更成熟的兼容层通常会做这些事:
- 对不同 provider 的结构化能力做统一封装。
- 在进入业务侧前做字段归一化和默认值修正。
- 对常见类型漂移做集中修复,而不是让每个应用各写一份兜底代码。
- 记录原始输出与修复结果,方便后续追查问题来源。
只有这层兼容存在,结构化输出才不会变成一组“看起来一样、实则不同”的接口。
修复策略不能只靠一次重试
很多团队面对结构化输出错误时,最常见的兜底方式是“坏了就重试一次”。这有时有效,但只能处理最浅层问题。真正成熟的修复策略,通常至少会分层:
- 语法修复层:针对明显的 JSON 语法错误做自动修补。
- 结构修复层:根据 schema 补齐缺失层级、清理异常字段。
- 类型修复层:把常见 string/number/boolean 漂移统一收敛。
- 语义复核层:对高风险字段做额外规则验证。
为什么要分层?因为不同层的风险完全不同。语法修复通常相对安全,语义修复则很危险。如果系统把所有问题都一股脑“自动修好”,最后很容易从结构问题变成业务问题。所以修复不是越强越好,而是必须知道每一层允许修到什么程度。
结构化输出一定要配业务校验
这件事非常关键。很多系统以为 schema 校验通过,就说明结果可用了。其实对高风险业务来说,这远远不够。因为 schema 只能保证“形式上对”,无法保证“业务上对”。比如:
- 一个审批动作的
action字段是合法枚举,但当前状态根本不允许执行。 - 一个金额字段是合法 number,但超出了业务允许范围。
- 一个收件人数组结构正确,但里面包含无权限访问的对象。
这些问题必须由业务校验层处理,而不能指望模型或 schema 自己解决。成熟系统会明确把“结构校验”和“业务校验”拆开,前者负责挡掉明显无效结果,后者负责保护真实业务边界。
版本变化是结构化输出的长期风险
结构化输出还有一个常被忽视的问题:它对模型和 Prompt 版本变化极其敏感。某次看起来只是轻微的提示词调整,可能就会让字段稳定性发生变化;某个 provider 升级底层模型后,数组排序习惯和缺省值策略也可能一起变化。结果是,系统表面上没有报错,但下游统计、自动化流程和缓存命中都开始悄悄偏离。
所以真正做得稳的平台,通常会把结构化输出纳入版本治理:
- 关键 schema 变更必须走评审和回归测试。
- Prompt 变化要在核心结构任务集上做专门回归。
- provider 切换前,要做输出兼容性对比。
- 高风险结构化链路要有灰度和回滚能力。
只有把结构化输出当成正式接口来治理,而不是把它当成“模型顺便给你一个 JSON”,系统才会逐渐稳定。
观测重点要放在“坏在哪里”
结构化输出的监控不能只统计“成功率”。更有用的是把失败拆开:语法失败多少、结构失败多少、类型漂移多少、业务校验失败多少、修复后仍不可用多少。因为这些分类会直接决定优化方向。如果你只知道失败率是 4%,但不知道这 4% 到底为什么坏,就很难做真正有效的工程改进。
我们更倾向记录三层数据:
- 原始输出长什么样。
- 平台修复了什么。
- 业务最终是否接受了这次结果。
这三层一起看,团队才能判断问题到底出在模型、兼容层、修复层还是业务规则层。
结构化输出最怕的是“隐性兼容破坏”
很多普通接口一旦不兼容,会很快报错;结构化输出最麻烦的地方在于,它经常以“看起来还能用”的方式悄悄破坏兼容。例如某个可选字段过去总会返回空数组,后来变成直接省略;过去 status 是统一枚举,后来模型开始返回更自由的文本;过去顺序稳定的列表,现在会根据语义强弱重新排序。表面看起来系统仍在正常输出,实际上下游逻辑、缓存键、比较算法和统计脚本可能都已经开始偏离。
因此结构化输出不仅要做“是否有效”的校验,还要做“是否仍然兼容历史行为”的校验。对于关键链路,这类兼容性回归测试往往比普通单元测试更重要。
高风险链路里,最好把模型限制在更窄的结构任务上
生产环境里一个常见教训是:越高风险的流程,越不应该让模型承担过于开放的结构生成任务。比如在支付、审批、外呼、订单状态变更等场景中,与其让模型直接生成一大块复杂对象,不如把任务拆成更小的结构单元,让模型只负责识别意图、抽取关键参数或补充说明,其余部分交给确定性系统拼装。
这样做的核心不是“少信任模型”,而是让每个环节承担最适合自己的责任。模型擅长理解模糊输入和补齐上下文,但不一定适合直接担负最终状态对象的全部构造责任。越接近真实业务状态,越应该收紧模型的自由度。
结构化输出的发布节奏也要独立管理
很多平台把结构化能力混在普通 Prompt 改动里一起发,这很危险。因为结构化输出对下游依赖强,一次小变化就可能波及多条链路。更成熟的做法通常是:
- 为关键 schema 建立独立版本号。
- 把结构化任务单独列入回归测试集。
- 对高影响字段建立灰度和告警。
- 在 provider 切换时,优先验证结构稳定性而不是文本质量。
只有当发布流程能识别“这次改动是否影响结构化契约”,团队才有机会把这类问题从线上事故提前拉回到发布前。
真正成熟的系统,会接受“部分任务不该强上结构化”
还有一个很现实的结论:并不是所有任务都适合强行做结构化输出。有些任务本身就更偏开放生成,如果为了工程统一而硬套复杂 schema,往往会让模型表现、维护成本和失败率一起变差。对这类任务,更合理的方式可能是“结构化骨架 + 自然语言补充”,或者只对最关键字段做强约束,把其余内容保留为文本。
这不是退步,而是承认模型能力和业务边界之间需要现实折中。结构化输出的目标不是让一切都变成表格,而是让真正需要稳定进入系统的部分变得可依赖。
结语
结构化输出之所以总是坏,并不是因为模型天生靠不住,而是因为团队经常低估了它背后的工程复杂度。schema、兼容层、修复策略、业务校验、版本治理和观测体系,这几层缺任何一层,结构化输出都会在生产环境里变得脆弱。
真正做得好的系统,不是要求模型永远给出完美 JSON,而是让整条链路在面对不完美输出时依然能稳住边界。只有这样,结构化输出才会从“看起来适合生产”变成“真的能长期跑在生产里”。