结构化输出:让模型稳定返回 JSON
文章目录
前面几篇文章里,模型的回复都是自由文本:想从中提取书名、作者、年份,就得写正则或靠关键词匹配,模型措辞一变,代码就跟着崩。本篇介绍结构化输出:在请求中声明”请返回 JSON”,让模型按约定好的结构直接给出数据,程序拿过来就能用。
为什么需要结构化输出
自由文本对程序很不友好。以”从一段介绍里提取图书信息”为例,模型可能回答”《三体》是刘慈欣在 2008 年出版的科幻小说”,也可能回答”作者:刘慈欣,出版年份:2008 年”,格式每次都不一样。用正则去匹配,就要同时维护多种句式;漏掉一种,数据就少一个字段,解析代码永远在打补丁。
结构化输出的思路是:输出格式也是输入的一部分。我们在请求参数里声明期望的 JSON 结构,模型就按这个结构生成。程序端只需 json.loads 一次,剩下的字段访问、类型转换都由数据本身保证,代码量大幅减少,也几乎不需要为”模型换了种说法”而返工。
OpenAI 兼容接口中,实现方式主要有三种:JSON Mode、Structured Outputs 和 SDK 的 Pydantic 封装,下面逐一介绍。三种方式的最终效果都是”拿到 JSON”,区别在于结构有多大的保证、写起来多麻烦。
方式一:JSON Mode(json_object)
最轻量的一种,只需要一个参数:
1 | import json |
有两个关键限制:第一,消息里必须出现”json”字样,否则接口直接报错,这是官方要求;第二,JSON Mode 只保证”输出是合法的 JSON 对象”,不保证字段名和结构——模型可能把年份写成 year,也可能写成 publish_year,数组字段也可能时有时无。所以它适合字段少、字段名不重要的临时场景,比如只让模型”把这段文字总结成 JSON”再自己取值。
另外要注意,JSON 比自由文本更”费 token”:一层层的花括号和引号都要占 token 数。固定结构反复出现时,成本会比纯文本回复略高,批量调用时值得留意。
方式二:Structured Outputs(json_schema)
如果字段名、类型、必填项都不能含糊,就用结构化输出的正式形态:在 response_format 里提供一个 JSON Schema,并开启 strict:
1 | book_schema = { |
json_schema 字段中,name 必填,schema 是标准的 JSON Schema 定义。开启 strict: True 后模型必须严格遵守结构,但严格模式有硬性要求:**所有属性都要列入 required,且必须设置 additionalProperties: False**,否则请求会被拒绝。注意:返回内容仍然是 JSON 字符串,需要 json.loads;结构有保证,但字段值是否正确(比如年份是否真实)模型仍可能出错。
方式三:Pydantic 模型 + parse()
手写一大段 schema 容易出错,SDK 提供了更优雅的封装:定义一个 Pydantic 模型,SDK 自动把它转成 JSON Schema 发给模型,再把返回解析成模型实例:
1 | from pydantic import BaseModel |
parse() 的返回对象里,message.parsed 直接就是 Book 实例,可以 book.title 这样访问,省掉了 json.loads 和手动取值。需要先 pip install pydantic。如果模型输出无法按模型解析(比如被拒答或内容为空),parsed 会是 None,使用前记得判空。
三种方式怎么选
| 方式 | 保证程度 | 代码量 | 适用场景 |
|---|---|---|---|
json_object |
只保证合法 JSON,字段自由 | 最少 | 临时提取、字段不重要 |
json_schema |
字段、类型、必填全有保证 | 中等 | 生产环境首选 |
Pydantic parse() |
结构保证 + 自动类型转换 | 少 | 追求代码简洁 |
无论用哪种,都要先确认你的模型和服务商支持:OpenAI 官方模型对 json_schema 支持良好,但不少第三方兼容服务会忽略 response_format 参数(照常返回自由文本)或直接报错。上线前先用一个小请求验证。
常见问题
json.loads 抛异常。 输出被截断、服务商不支持该参数时,返回的可能不是合法 JSON。用 try/except 包住解析,失败后可以重试一次或降级处理。
提示词里写了要求却报 400。 json_object 模式必须让”json”字样出现在消息里;json_schema 模式则检查 schema 是否满足严格模式要求(required 齐全、additionalProperties: False、类型只用 string/integer/number/boolean/array/object)。
parsed 是 None。 说明 parse() 没能把输出解析成模型实例,常见于输出为空或被内容安全策略拦截,判空后记录日志即可。
流式输出能搭配结构化输出吗? 可以同时开启,但流式返回的是一串分片,需要先把分片拼成完整文本再 json.loads。非流式直接拿完整响应更省事。
结构对了,值却是错的。 结构化输出保证的是”长得像”,不保证”内容真实”。year 可能是模型编造的,关键数据仍需程序侧校验,必要时让模型在拿不准时返回 null 而不是硬编一个值。
结构化输出更贵吗? JSON 语法本身会占用额外的输出 token,价格随 token 计费,所以同样内容会比纯文本略贵。对高频批量场景,可以把固定结构放进提示词示例里,减少模型”摸索格式”造成的浪费。
小结
- 结构化输出通过
response_format让模型直接返回 JSON,省去脆弱的文本解析。 json_object最轻量但字段无保证;json_schema结构有强保证,是生产推荐;Pydanticparse()写法最简洁。- 无论哪种方式,返回后都要解析和校验;第三方兼容服务支持不一,先验证再用。
- 下一步我们将学习基础错误处理:超时、重试、限流与指数退避,让程序在接口不稳定时也能可靠运行。