结构化输出实战指南:用官方答题卡终结大模型 JSON 乱象 1. 结构化输出到底在解决什么那些年被 JSON 逼疯的开发者先说一个几乎所有做过 AI 应用的人都会经历的崩溃瞬间。你调大模型认认真真在提示词里写“请只返回 JSON不要带任何多余文字”结果返回的内容开头是“好的以下是您需要的 JSON”结尾跟着一段 json 代码块标记中间偶尔还夹一行“注意这个方案仅供参考”。你的JSON.parse直接抛异常整个流程卡死。你只能再写一段正则、再加一次重试提示词、再把历史上所有翻车样例做成 few-shot 塞进去空耗 token 和时间。这就是结构化输出这个需求存在的原因——它不是银弹但它是让大模型从“聊天生成器”变成“可用 API”的关键一步。过去两年业内解决这个问题靠的是提示词规约、JSON Mode、Function Calling还有各种解析库。它们要么不稳定要么只保证“看起来像 JSON”不能保证字段类型和约束。直到 OpenAI 在 2024 年推出Structured Outputs也就是 SDK 里的withStructuredOutput才第一次把“格式确定性”从工程技巧提升到了平台能力的高度。标题里叫它“官方答题卡”本质上就是这个意思模型输出的格式不再是靠自觉而是被系统层面强制约束就像考试时必须把答案填进规定区域一样填错区域就是零分。这个能力适合谁用我个人的判断是任何在生产线里依赖大模型出数据的开发者都应该尽快迁移。不管你是做 AI Agent 的工具调用、批量信息抽取、表单自动填充还是把 LLM 当后端数据清洗器withStructuredOutput都能替你省掉一大堆脏活。如果你是刚开始做 AI 应用的新手这个东西同样值得早学——它会帮你从一开始就建立“模型输出是可校验数据”的正确心智而不是把 prompt 当许愿池。2. 从 JSON Mode 到 withStructuredOutput一次“抄作业”式迁移2.1 JSON Mode 的承诺与局限先回顾一下我们是怎么走到这一步的。2023 年底开始OpenAI 等厂商陆续提供了response_format {type: json_object}也就是所谓的 JSON Mode。它保证模型输出的文本在结构上是一个合法的 JSON 对象但注意只是“合法”不保证“符合你的目标结构”。你要求返回{name: string, age: integer}它可能给你{name: 张三, age: 28}——年龄成了字符串你的类型校验照样炸。更麻烦的是JSON Mode 要求在提示词里出现“json”这个词否则可能触发错误这个限制本身就很拧巴。后来有了 Function Calling。它的思路是给模型定义一个工具函数模型在回答时输出一个符合函数参数的 JSON 对象。这个机制比纯 JSON Mode 进了一步因为函数参数有 JSON Schema 约束模型输出的参数部分“理论上”应该符合这个 Schema。但实际跑下来你会发现它仍然可能出现字段缺失、多出字段、枚举值越界这类问题尤其是在复杂嵌套 Schema 下。而且 Function Calling 的语义是“模型决定调用哪个函数”你的使用场景如果根本不是工具调用只是想要一份结构化数据那这个设计就好像为了吃个包子去排队买了整个早餐店。2.2 withStructuredOutput 的具体用法与迁移路径Structured Outputs 的思路更直接你在请求里把目标 JSON Schema 给出来系统在解码阶段就保证输出严格符合这个 Schema而不是事后补救。在 OpenAI 官方 SDK 里体验最好的接口就是withStructuredOutput。以 Python 为例典型的调用方式长这样from openai import OpenAI from pydantic import BaseModel client OpenAI() class MovieReview(BaseModel): title: str rating: int tags: list[str] completion client.beta.chat.completions.parse( modelgpt-4o, messages[{role: user, content: 评价一下《流浪地球2》}], response_formatwithStructuredOutput(MovieReview), ) # 实际上 SDK 里直接这样传 completion client.beta.chat.completions.parse( modelgpt-4o, messages[{role: user, content: 评价一下《流浪地球2》}], response_formatMovieReview, ) review completion.choices[0].message.parsed这里有两个关键细节值得强调。第一SDK 的parse系列接口会自动把你的 Pydantic 模型转成 JSON Schema然后通过response_format里的json_schema参数传给模型拿到结果后再反序列化回 Pydantic 对象中间完全不用你手动碰 JSON。第二completion.choices[0].message.parsed直接就是类型安全的对象不用你再校验一遍。这种“定义一个类然后就一切自动”的开发体验才是它被称为“终局”的第一个原因。如果你不是 Python 生态TypeScript 侧也有对应方案用 Zod 定义 schemaSDK 同样支持把 Zod 类型直接作为withStructuredOutput的参数。Java 生态里Spring AI 也提供了BeanOutputConverter底层就是把 POJO 转成 JSON Schema 传给模型原理一致。所谓“抄作业式迁移”就是说你此前基于 JSON Mode 写的业务逻辑基本不用大改只需要把“非法 JSON 重试”这种容错代码删掉换成严格 Schema 就好。2.3 老代码改造时的三个注意点迁移不是无脑替换我实际改造过几个线上服务踩过的坑可以列一下。第一旧 Schema 往往不够严格。JSON Mode 时代我们图方便很多字段定义得很宽泛比如“description: string”到了 Structured Outputs 下你能设置minLength、pattern、enum最好一次定义到位因为模型被严格约束后宽松 Schema 反而更容易暴露数据本身的质量问题。第二枚举值要尽量给全。以前模型可能自由发挥写“unknown”现在枚举里没有这个值模型会尝试从相近语义中挑一个如果还挑不出来会返回一个特殊错误后面讲。第三嵌套层级不要太深。虽然支持嵌套对象和数组但层数太多时模型在长上下文下的稳定性会下降建议把单层字段控制在 5-8 个以内必要时拆成多次调用。3. 严格模式背后的逻辑模型是怎么被“钉死”在答题卡上的3.1 Strict Schema 的有限集合很多人以为 Structured Outputs 就是普通的 JSON Schema 校验其实不对。它的核心是“Strict Schema”这是一个刻意收窄过的 JSON Schema 子集目的只有一个让系统在解码阶段就做出强约束而不是生成完再校验。Strict Schema 有几个硬性限制我挑和日常开发最相关的说。不允许optional属性作为根级字段更准确地说Schema 里的所有字段默认都是必填你不想要某个字段就只能删掉想表示可选就得用anyOf配合null。additionalProperties必须为false也就是模型不能自由发挥往对象里塞你没定义的键。每个对象类型的属性数量是有限的官方限制是 100 个嵌套深度和$ref的使用也有严格约束。还有一个容易忽略的点字符串的pattern底层用的是 RE2 语法不是 PCRE很多正则写法和你想的不一样比如 RE2 不支持反向引用。这些限制看着烦人但它们换来的是确定性。用生活类比就是普通 JSON Schema 像“考试后老师帮你判卷告诉你哪里扣分了”Strict Schema 像“考试时直接发答题卡填错格子连机器都过不去”。判卷和高分是两码事而我们要的是机器能直接吃饭的数据。3.2 Constrained DecodingToken 层面的硬约束真正让 Structured Outputs 和传统校验拉开差距的是它底层的constrained decoding也叫受限解码。大模型生成文本是一个字一个字token 一个接一个蹦出来的。传统情况下每一步模型会按照概率从整个词表里采样受限解码则在每一步算出“当前已经生成的 token 序列 还没完成的 JSON Schema”之间的可行集合然后把概率重分配只允许采样那些能继续满足 Schema 的 token。举个例子。如果你要求rating是 1 到 5 的整数模型生成到{rating:这一步时下一个 token 的概率分布里合法的只有数字1、2、3、4、5、6以及部分符号比如}表示字段结束或,表示还有下个字段但像100或abc这类不可能出现在合法 JSON 里的 token 会被直接过滤掉。这种“走在岔路口之前就封死错误方向”的做法比“走错了再倒回来重走”不知道高到哪里去了。这也解释了为什么本地跑一个 Qwen 或 Llama 的情况下类似的 guided decoding 会慢一些——因为每生成一个 token 都要额外做一次正则/CFG 状态机匹配和 token 过滤计算。OpenAI 的服务端做了大量优化延迟增加得不明显但自部署时这个开销必须算进预算。后面讲本地化部署我会再展开。3.3 温度、采样与 Deterministic 输出Structured Outputs 出现后很多人开始追求“每次都返回一模一样的结果”。严格来说这不是 Structured Outputs 的承诺它只承诺“格式正确”。内容层面是否稳定还取决于温度参数和模型本身的随机性。不过有一个经验可以分享使用 Structured Outputs 时温度可以适当调低但不建议设为 0。设 0 在极端情况下会触发一些采样器的一致性问题反而可能让模型陷入重复或循环调到 0.1-0.3 之间的低温度配合严格格式既能保持结构稳定也能让内容在语义上保留一点灵活性。如果你做的是信息抽取类任务其实不需要太多多样性0.1 是我实测最稳的档位。4. 边界与翻车现场withStructuredOutput 不是银弹4.1 递归结构与 anyOf 的雷Structured Outputs 强归强但它没有一个模型是万能的。第一批坑就是 Schema 表达能力的限制。最典型的翻车场景是递归定义比如树形结构一个Node有children: list[Node]。在普通 JSON Schema 里这很容易写但在 Strict Schema 下OpenAI 官方文档明确不推荐带循环引用的 schema实际体验中经常报错或者表现诡异。原因也很简单无限递归会让受限解码的状态机难以静态分析系统无法预知“某个分支下到底还有多深”。我当时的解决方案是“拍平嵌套层级”限定最多三层第三层不再递归字段类型改成list[dict]。虽然类型安全性降低了但数据在实际业务里并不会无限深三层足够。第二个大雷是anyOf的位置。Strict Schema 里虽然支持anyOf但它的语义和普通 JSON Schema 有微妙差异。尤其是anyOf里包含null来表示“可空字段”的写法在 OpenAI 的实现里对default和空字符串的处理并不直观。我建议能不用anyOf就不用实在要表达可空优先用type: [string, null]这种联合类型写法。4.2 错误与拒绝模型说“我不会”的时候Structured Outputs 保证格式不保证模型一定愿意回答。当你问的问题超出安全限制或者模型认为上下文信息不足时它可能不会输出你期待的数据而是返回一个“拒绝”语义。在严格模式下这个拒绝也会套上你的 JSON Schema 外壳但内容会变成类似{ title: 抱歉我无法完成这个请求, rating: 0, tags: [refused] }如果你没有预判这种情况下游业务很可能把这条数据当真酿成事故。我在一个内容审核系统里就碰到过模型对某条用户输入拒答结果系统把tags: [refused]当成正常标签入库了。后来我们在 Schema 层面加了一个显式的refusal_reason字段并在业务代码里检查该字段是否为空。这是 Structured Outputs 的隐藏成本——你需要为“拒绝”设计一条显式的数据通路而不是假装它不存在。4.3 Token 开销与延迟不比不知道受限解码虽然能保证格式但它不是免费的午餐。实测下来相同模型、相同内容用 Structured Outputs 比纯文本生成的 token 消耗通常高出 5% 到 20%因为 JSON 的键名、引号、缩进这些结构性 token 是强制消耗。如果你还把字段名写得很长比如list_of_related_document_unique_identifierstoken 费用会肉眼可见地涨。延迟方面OpenAI 的服务端优化得很好但如果你通过 API 网关转发、或者自己部署兼容层额外开销就会变得明显。本地模型走 vLLM 或 llama.cpp 的 guided decoding 时长 Schema 下解码速度可能从 50 tokens/s 掉到 30 tokens/s 左右。所以我的实践建议是只对结构化输出需求最重的接口开启这个模式别图省事给所有对话流都套一个大 Schema。对话场景还是要速度数据场景才要确定性。4.4 多轮对话与 tool call 混合场景的处理还有一个容易忽略的边界withStructuredOutput虽然好用但它通常是针对“一次请求、一次结构化输出”设计的。如果你的场景是多轮 Agent每一轮模型不仅要输出结构化内容还要决定调用哪个工具这时把整个对话包进一个 schema 就不合适了。更合理的做法是用 Function Calling 让模型决定工具调用把withStructuredOutput只用在“最终产出数据”的那一轮。换句话说中间过程要“自由”终点要“精确”两者各有各的工具不要混为一谈。5. 终局之战的另一半选型对比与本地化部署路线5.1 不同方案的横向对比为了讲清楚“终局之战”的格局我整理了一个选型对比表覆盖目前主流的几种方案方便你按场景取用。方案格式确定性类型约束实现位置适用场景主要痛点纯提示词约束极低无应用层快速原型、自己调试频繁翻车、不可靠JSON Mode中无API 层只需要合法性类型不可控Function Calling中高弱约束API 层工具调用流程语义错配、字段易漂移withStructuredOutput / Structured Outputs高强约束API 层解码层标准化数据生产不支持递归、token 开销本地模型 guided decoding高强约束推理引擎层私有化部署、离线速度下降、算力消耗表格里最值得注意的是最后一行。Structured Outputs 虽然是 OpenAI 的提法但受限解码的思想在开源社区并不新鲜。vLLM 支持guided_decoding_backendoutlinesllama.cpp 也有类似的 JSON Schema 约束接口Ollama 的format参数同样能指定 JSON Schema。这意味着你完全可以在本地模型上获得接近withStructuredOutput的体验。5.2 本地模型的 guided decoding 实践我用 vLLM 部署 Qwen 系列模型做过一次结构化抽取服务配置大致如下from vllm import LLM, SamplingParams llm LLM(modelQwen/Qwen2.5-7B-Instruct) sampling_params SamplingParams( temperature0.1, guided_jsonjson.dumps({ type: object, properties: { title: {type: string}, rating: {type: integer}, tags: {type: array, items: {type: string}} }, required: [title, rating, tags], additionalProperties: False }) ) output llm.generate([评价一下《流浪地球2》], sampling_params)这段代码的效果和 OpenAI 的 Structured Outputs 非常接近但背后用的是 Outlines 库的有限状态机做 token 级别的约束。差别在于vLLM 的 guided decoding 对 schema 的容忍度更高比如它支持递归但速度损失也更明显。实测下来带 schema 约束的生成速度大约是自由生成速度的 60% 到 80%。如果你的业务对延迟不敏感、但数据必须私有化这条路完全可行。5.3 什么场景还得靠提示词以及我的最终建议这里要泼一点冷水。结构化输出解决了“数据形状”的问题但解决不了“数据质量”的问题。如果你的模型本身能力不足或者任务描述含糊不清即便它严格按 schema 输出填充的字段内容也可能是错的——格式正确但语义跑偏。所以我的最终建议是先用结构化输出把格式问题一键清零然后仍然要在业务层做语义校验和抽样人工 review尤其是高风险场景比如合同信息抽取、医疗数据整理、金融交易参数提取。根据我个人的工程经验一套靠谱的生产级链路是这样的第一层用withStructuredOutput或本地 guided decoding 保证格式第二层用 Pydantic 或 Zod 的反序列化逻辑兜底类型第三层业务规则校验比如金额必须大于 0、日期必须在合法范围内第四层对不确定的字段打标记交给人工复核。四层都过完这个 AI 数据才算真正能进数据库。另外一个很容易被忽略的细节给模型留一个“不知道”的出口。Schema 里所有字段都给null选项不要强迫模型胡说八道。一个诚实的null远比一个编造的值有价值——这个道理做数据工程的人应该都懂。结构化的时代确实到了。从提示词求爷爷告奶奶到 JSON Mode 半吊子再到 Function Calling 曲线救国最后到withStructuredOutput直接把格式钉死在解码层这条路我一路走来踩了不少坑。现在做 AI 应用开发比起两年前我觉得最有幸福感的变化就是终于不用再为了一个合法 JSON 去和模型讨价还价了。答题卡已经发了剩下的就是把答案填好、把校验做扎实。