ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

大模型输出约束:从JSON Mode到Structured Output的实践指南

大模型输出约束:从JSON Mode到Structured Output的实践指南

做接口开发的这几年,凡是接过大模型输出的朋友,基本都经历过同一个噩梦:字符串解析。模型明明答得头头是道,结果一返回却是“代码块裹着JSON、JSON里藏着Markdown、Markdown里混着解释文字”,正则写了一套又一套,解析函数改了一版又一版,最后还是被一个多出来的逗号干翻。这个问题在引入JSON Mode与Structured Output之后才真正有了根治方案。这篇文章我会从实际项目出发,把这两种输出约束方式的原理、配置方法、适用场景、常见坑一次讲清楚,帮你在下一个项目里彻底告别正则地狱。

适合看的读者有两类:一类是刚接触大模型开发,每次拿到模型输出还要自己写解析逻辑的;另一类是已经用过JSON Mode,但发现格式虽对、字段却经常缺失或类型不匹配,想升级到Structured Output的。两种方式各有侧重,不存在谁完全替代谁,关键看你手头的业务场景和可控程度要求。

1. 先聊清楚:大模型输出为什么要“约束”

1.1 从一次线上事故说起

我之前维护过一个某跨平台系统,底层依赖大模型做信息抽取。上线初期,模型的温度参数设成了0.7,输出里偶尔出现一段“好的,我帮你整理如下”之类的废话。当时prompt里写了“只输出JSON”,但模型照样偶尔犯浑。最难受的一次,某图像处理Demo在凌晨批量处理数据时,大量返回里混入了```json标记,解析正则没兜住,直接导致后续流程全部失败。那次之后我意识到:靠prompt约束输出格式,就像靠口头提醒让同事写代码不加分号——大多数时候有效,但关键时候总有人不守规矩。

1.2 解析噩梦的根源

很多人以为,问题出在模型“不听话”。其实根子上是概率生成的本质:语言模型每一步都是在词汇表上按概率挑下一个token,它没有“格式校验器”在背后把关。你可以用prompt引导它,但引导不是强制。模型在生成过程中只要某个token偏离了预设的JSON结构,后续生成的内容就会顺着错误方向一路跑偏,最终产出一个残缺或非法结构。这跟人写代码一个道理:写错了语法编译期就会报错,但大模型没有“编译期”,它只有“运行时自我感觉良好”。

这种情况下,传统的缓解办法无非几条:调低温度、反复加强prompt里的格式描述、输出后用正则硬解析、解析失败就重试。这些办法不是没用,而是治标不治本。调低温度只是减少发散,不能保证结构合法;加强prompt讲究措辞和模型版本,换一个模型可能就不灵;正则解析碰上嵌套JSON非常容易写错;重试则浪费时间和成本。真正的问题在于:输出结构没有被模型侧“保证”,你只是寄希望于它“大概率给对”。

1.3 约束输出到底约束了什么

JSON Mode和Structured Output这两种方案,思路都是把“输出格式”从prompt软约束提升为“采样层硬约束”。换句话说,模型在生成每一个token时,有一个外部约束器实时检查:当前已生成的部分,加上候选的下一个token,是否仍然符合目标格式。如果不符合,这个token会被直接过滤掉。这样从数学上保证了最终输出一定是合法结构。

理解这一点很重要,因为它解释了为什么这两种方式能真正解决解析噩梦:问题从“事后解析好坏不定”变成了“事前保证格式合法”。你需要担心的不再是解析崩了怎么办,而是字段内容是否准确、类型是否符合预期、语言风格是否达标。格式本身已经被兜住了。

2. JSON Mode:把输出锁进JSON框架里

2.1 JSON Mode的原理简述

JSON Mode的核心,是让模型在推理过程中被约束为“只生成合法JSON的token序列”。它并不是简单地让模型脑子里想着JSON格式,而是在解码阶段为模型增加了一个格式约束器。这个约束器会根据当前已经生成的内容,动态计算下一个可以出现的合法token集合。凡是会导致JSON语法破坏的候选token,在采样前就被屏蔽掉。

实际效果上,启用JSON Mode后,模型返回的内容一定是可解析的JSON。注意这里说的是“合法JSON”,不代表它一定是你想要的业务结构。它能保证{"a": 1, "b": "2"}这种级别的合法性,但无法保证返回里一定有a和b这两个字段,更无法保证b的类型一定是字符串。

这个区别非常关键。我在对接某个早期项目时就碰到过:开启了JSON Mode,返回结构合法了,但prompt里要求输出的字段偶尔还是会被吞掉一两个。原因在于JSON Mode只负责“语法合法”,不负责“schema符合预期”。如果你需要严格保证字段齐全、类型匹配,请看下一章的Structured Output。

2.2 如何开启JSON Mode:具体配置

先说明一下,目前主流大模型平台基本都提供了类似参数,不同平台叫法略有差异,但思路一致。以我常用的某云平台为例,调用时在请求参数里返回格式设置成json,在消息里明确要求输出JSON对象。同时还有一个硬性要求:必须在请求内容中带上“json”这个词,否则接口会直接报错。这个设计是有意为之,用来避免模型在不该输出JSON的场景下被强制约束,导致上下文混乱。

我贴一段Python调用示例,做了简化但保留了关键步骤:

from openai import OpenAI client = OpenAI() response = client.chat.completions.create( model="gpt-4o-mini", # 仅示例 messages=[ { "role": "user", "content": "抽取下面文本中的日期、金额和商户名,以JSON格式输出:上月电费扣款128.5元" } ], response_format={"type": "json_object"}, temperature=0.2 ) print(response.choices[0].message.content)

运行后返回的内容大体是:

{ "日期": "2025-08-01", "金额": 128.5, "商户名": "某电力公司" }

这里有个细节值得注意:我在prompt里明确写了“以JSON格式输出”。前面说过,这是JSON Mode的硬性要求。我见过有同事把这句话去掉,结果接口直接报错,排查了半天还以为是SDK版本问题。如果你用的是自己封装HTTP请求的方式,也别忘了在请求体里带这个词,原因不是平台敏感,而是约束器模型本身需要这个信号来切换生成策略。

2.3 JSON Mode的适用边界

JSON Mode最大的价值是“以最小改动换最大稳定性”。它适合以下场景:字段命名不敏感、类型不敏感、输出内容本身就是给大模型做中间产物然后再喂给另一个模型的;或者说你只要求它能被json.loads直接吃掉就行的场景。

但不是所有场景都适用。比如你想做表单自动填充,要求输出包含固定的姓名、手机号、地址三个字段,且手机号必须是字符串。JSON Mode保证不了这一点,它可能出现字段缺失、字段顺序变化、手机号输出成数字等情况。这时候就需要结构约束更强的方案。

此外还涉及一个兼容性问题:JSON Mode通常要求模型本身具备原生支持能力,不同平台不同模型对JSON Mode的支持情况不一样,有些老模型即使你传了这个参数,它也只是“假装支持”,实际输出依然会跑偏。所以上线前务必针对你用的具体模型做一次格式命中率摸底测试。

3. Structured Output:从“合法”到“合格”

3.1 什么是Structured Output

Structured Output可以理解为JSON Mode的加强版。它不仅要保证输出是合法JSON,还要保证输出严格符合你预先定义的JSON Schema。所谓JSON Schema,就是一种描述“JSON长什么样”的声明式规则,包括字段名、字段类型、是否必填、取值范围、嵌套结构等。

用类比来说:JSON Mode只保证“写出来的是一篇文章”,Structured Output则保证“这篇文章的每一段都有固定标题、每个标题下字数要达标、文风统一、不能跑题”。前者管语法,后者管语义结构。

这套机制在解码阶段约束得更严密:模型每生成一个token,约束器不仅检查JSON语法合法性,还要检查当前前缀是否可能通向一个符合完整schema的最终结果。如果候选token会让后续无论如何都无法满足字段类型要求,它就同样被过滤掉。

3.2 核心优势:字段缺失和类型错乱彻底解决

我实际体验最深的一点,是Structured Output对“字段缺失”和“类型错乱”的根治效果。传统prompt方式下,你让模型输出“文章标题、作者、发布时间”,它可能偶尔漏掉发布时间;在JSON Mode下,JSON是合法了,但发布时间依然可能被遗漏。而Structured Output因为schema里声明了这三个字段都是必填,模型在解码阶段就被迫补齐,不存在“忘写”的可能。

类型问题上,schema里把金额字段声明为number,那模型就不可能输出字符串“128.5元”或者带有货币符号的玩意儿。它只能输出合法的数字。这个特性在做数据入库、调用下游API的时候极为实用,省掉了大量的类型转换和异常兜底。

再一个隐性的好处是嵌套对象的稳定性。比如你要模型输出一个用户列表,每项包含姓名和订单数组,订单数组里每项又是固定结构。这种多层嵌套结构用prompt约束很容易崩,嵌套层一深模型就迷路。但Structured Output能稳定输出正确层级,因为约束器知道每一步该出什么。

3.3 Structured Output实操配置

在目前主流平台里,Structured Output普遍通过response_format参数配合json_schema来配置。下面是一个实际可用的Python示例,目标是让模型返回某个电商场景下的订单信息:

from openai import OpenAI from pydantic import BaseModel client = OpenAI() class OrderInfo(BaseModel): order_id: str amount: float items: list[str] completion = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "user", "content": "根据以下收货短信提取订单信息:您购买的商品已发货,订单号D2025001,共3件商品,实付金额268.00元。"} ], response_format={ "type": "json_schema", "json_schema": { "name": "order_schema", "schema": OrderInfo.model_json_schema(), "strict": True } } ) print(completion.choices[0].message.content)

这里用Pydantic的model_json_schema()方法直接把Python模型类转成JSON Schema,省去手写一长串规则。strict开启后,平台侧会做严格校验。输出大概是:

{ "order_id": "D2025001", "amount": 268.0, "items": ["商品A", "商品B", "商品C"] }

如果模型试图输出"amount": "268元",这个值根本不可能出现,因为schema规定它是float。这就是Structured Output真正解决解析噩梦的地方:不仅结构合法,而且数据可直接用于业务代码,不再需要额外清洗。

3.4 平台限制与兼容性注意

实测下来Structured Output对模型版本有要求:老模型基本不支持,必须使用较新的模型。平台文档普遍会写明哪些模型支持结构输出,建议直接查文档确认,不要在旧模型上硬试。国内平台方面,有的叫“结构化输出”,有的叫“JSON Schema约束”,核心机制类似,但参数名不同,需要注意适配。

另一个容易踩的坑是:Structured Output约束的是结构,不约束枚举值和自由文本。比如你想让“性别”字段只能是“男”或“女”,虽然可以在schema里用enum限制,但如果这个约束没写,模型还是可能输出“未知”。所以schema定义得越严谨,最终交付的价值就越大。你写schema时花的每一分钟,都是在为后面减少解析和清洗的工作。

4. 一次线上改造实录:从正则地狱到结构输出

4.1 原始方案的痛点

我参与维护的某个数据清洗服务,原本的流程是:用大模型抽取非结构化文本里的实体和关系,返回自由文本,然后靠三套正则表达式做解析。实体类型有十几种,关系类型也有七八种,正则规则叠了又叠,到后期几乎没人敢动那段解析函数,谁都担心改一处崩三处。

痛点具体有三类:第一,模型输出偶尔多一个换行、缩进、或者“这里是结果:”前缀,正则匹配就会失败;第二,嵌套结构解析困难,比如“人物A是公司B的CEO,公司B位于某城市”这种关系,正则抽取很容易拆错;第三,模型升级后,输出风格变化,正则又得跟着调整,维护成本极高。

4.2 定义JSON Schema的实操过程

改造的第一步,是梳理业务上实际需要哪些字段。这一步千万别省,最好的方式是直接和下游数据消费方对齐:数据库表有哪些字段、哪些字段可空、类型分别是什么。我当时梳理出了Entity对象和Relation对象,分别定义如下:

from pydantic import BaseModel from typing import List, Optional class Entity(BaseModel): name: str category: str confidence: float class Relation(BaseModel): subject: str predicate: str object: str confidence: float class ExtractionResult(BaseModel): entities: List[Entity] relations: List[Relation]

然后把这个模型类的schema传给接口的json_schema参数。这里有个小技巧:Pydantic可以自动生成schema并校验输入输出,但为了确保模型生成的字段完全匹配,我建议你在schema之外,再在系统内部做一次Pydantic解析。也就是说,让模型输出先通过ExtractionResult.model_validate(json.loads(raw))校验一次。这样即便哪个平台对schema支持不到位,你也有内部兜底。

4.3 改造后的效果对比

改造后对比非常明显。旧方案里,解析代码大约三百多行,三套正则互相嵌套,测试用例铺了一堆。换用Structured Output之后,解析代码几乎清零,剩下的只是把返回内容直接转成Pydantic对象,再送入业务层。格式错误率从早期大约每百条有五六条异常,骤降到接近零。更关键的是,后续模型升级的时候,再也不用因为“输出风格变了”而去调整解析逻辑了。只要schema不变,模型输出就对得上。

这里并不建议完全废除正则。结构化输出保证的是“整体合法性”,但某些字段内部可能有特殊约定,比如手机号、身份证号、订单号需要二次校验格式,这些可以在Pydantic层面加正则校验。也就是说,正则从“解析主力”降级成了“字段校验工具”,身份变了,复杂度也下来了。

5. JSON Mode与Structured Output的选择策略

5.1 一张表看明白差异

很多读者可能会纠结“到底用哪个”。这里我直接放一张对比表,你在方案选型时对着看就行。

对比维度JSON ModeStructured Output
输出保证合法JSON结构合法JSON且严格符合定义的Schema
字段必填约束不保证通过required字段保证
字段类型约束不保证通过type定义保证
嵌套结构稳定性一般强
参数复杂度低,一个枚举值即可高,需要定义完整JSON Schema
模型要求需支持该平台JSON Mode通常要求更新版本模型
适用场景闲聊式抽取、中间结果、格式兜底数据入库、下游API对接、表单填充

5.2 选型建议:三个判断依据

第一个依据是“下游能不能容忍脏数据”。如果输出是直接给用户看,不走程序处理,JSON Mode就够了。但如果输出要喂给下游API或者直接入库,强烈建议上Structured Output,否则你迟早要写清洗逻辑。

第二个依据是“schema是否会频繁变化”。业务早期字段经常调整,用JSON Mode可以少维护一套schema。等字段稳定了再切成Structured Output也不迟。别在需求一周三变的时候强行上结构输出,改schema的代价会让你怀疑人生。

第三个依据是“模型能力是否匹配”。你先查一下自己用的模型是否支持structured output,如果不支持也别硬上,老老实实先用JSON Mode加一层Pydantic校验。等模型升级了再平滑切换。

5.3 混用方案:两阶段处理

还有一种更灵活的组合方案:先用JSON Mode做宽松抽取,再用Structured Output做二次精炼。两阶段串起来,既能保证第一个阶段不被schema卡死导致信息漏抽,又能在第二阶段拿到严格结构。

我实际在某个NLP项目中用过这个策略:第一阶段让模型从长文本里自由抽取“所有可能的实体”,输出JSON数组就行;第二阶段把第一阶段的实体列表作为输入,用Structured Output重新整理成统一的实体schema,补全缺失字段、纠正类型。这个流程跑下来,召回率和结构稳定性都比单阶段好不少。缺点是两次调用多一倍的推理成本和延迟,所以只建议在质量要求高的离线任务里用。

6. 常见问题与排查技巧实录

6.1 几个真实踩坑案例

先说一个小问题:有些平台要求messages里必须出现“json”字样才会启用JSON Mode,否则直接报错。我第一次遇到这个报错时,还以为是SDK版本旧,后来翻文档才发现是约束要求。解决办法很简单,在system prompt里明确写上“请始终以JSON对象格式输出”。

再说一个更隐蔽的问题:在定义JSON Schema时,我把某个字段声明成string类型,但业务上希望它输出日期。结果模型确实输出了日期,但格式五花八门:有一天“8月1日”,有一天“2025-08-01”,还有一天直接输出“昨天”。Structured Output只保证类型,不保证格式规范。所以如果你需要特定日期格式,请在schema的description里写清楚“格式必须是YYYY-MM-DD”,并在下游再校验一次。

还有一个频繁出现的问题是Strict模式下的schema限制。开启strict后,平台要求json_schema里的字段全部标记为required,不能有optional字段。如果你在Pydantic模型里写了Optional[str],某些平台会直接报错。解决办法是:要么把可选字段也设成required但允许输出空字符串,要么用一个统一的union类型包裹。这块不同平台行为有差异,建议上线前用最小demo跑通一次。

6.2 如何系统性调试输出问题

面对结构输出相关的疑难杂症,我自己习惯按下面的顺序排查:

  • 第一步,先确认接口返回的HTTP状态码和错误消息。平台报错通常很直白,比如“unsupported response format”,不要忽略这类信息。
  • 第二步,打印模型返回的原始字符串,不要经过任何解析直接看。有时候你以为它返回了非法JSON,其实只是你解析的代码写错了。
  • 第三步,把原始字符串丢给一个独立的JSON校验器验证。确认非法后再去检查schema定义,而不是盲目调prompt。
  • 第四步,检查schema本身是否合法。有些schema平台不接受,比如层级过深、字段过多,会导致请求阶段直接失败。
  • 第五步,精简复现。把任务prompt缩到最短、将schema简化到最小结构,一步一步加回去,定位是哪个字段触发了异常。

这套排查顺序帮我在多个项目里快速定位问题,比反复修改prompt试探高效得多。

6.3 还有一些平时容易被忽略的小点

温度参数对结构输出的影响没有prompt时代那么大了,因为约束器会过滤掉非法token。但温度会影响字段内容的质量,比如摘要类字段的行文流畅度。所以温度可以适当调回0到0.5之间,不必为了格式强行压到0。

token上限也和结构输出有关。当一个任务的输出比较长,比如让模型输出一整个数组的JSON,如果max_tokens设小了,输出会被截断,导致结构不完整。这一点无论哪种模式都回避不了。建议给足够余量,并在下游做截断检测。

我个人的经验是,引入Structured Output之后,最容易犯的错误反而不是模型输出不行,而是项目的“校验思维”没有跟上:很多人拿到结构输出就直接信任,不再做内部校验。但程序里应该有最后一层守卫,比如Pydantic验证、必填字段检查、枚举值检查。结构输出帮我挡掉了90%的脏数据,剩下10%要靠自己的防御性编程。两手都要硬,解析噩梦才算真正结束。

返回列表