前面几篇文章里,模型的回复都是自由文本:想从中提取书名、作者、年份,就得写正则或靠关键词匹配,模型措辞一变,代码就跟着崩。本篇介绍结构化输出:在请求中声明”请返回 JSON”,让模型按约定好的结构直接给出数据,程序拿过来就能用。

为什么需要结构化输出

自由文本对程序很不友好。以”从一段介绍里提取图书信息”为例,模型可能回答”《三体》是刘慈欣在 2008 年出版的科幻小说”,也可能回答”作者:刘慈欣,出版年份:2008 年”,格式每次都不一样。用正则去匹配,就要同时维护多种句式;漏掉一种,数据就少一个字段,解析代码永远在打补丁。

结构化输出的思路是:输出格式也是输入的一部分。我们在请求参数里声明期望的 JSON 结构,模型就按这个结构生成。程序端只需 json.loads 一次,剩下的字段访问、类型转换都由数据本身保证,代码量大幅减少,也几乎不需要为”模型换了种说法”而返工。

OpenAI 兼容接口中,实现方式主要有三种:JSON Mode、Structured Outputs 和 SDK 的 Pydantic 封装,下面逐一介绍。三种方式的最终效果都是”拿到 JSON”,区别在于结构有多大的保证、写起来多麻烦。

方式一:JSON Mode(json_object)

最轻量的一种,只需要一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import json
import os

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
model_name = os.getenv("MODEL_NAME", "gpt-4o-mini")

resp = client.chat.completions.create(
model=model_name,
messages=[
{"role": "user", "content": "《三体》是刘慈欣创作的科幻小说,2008 年由重庆出版社出版。请用 JSON 格式返回书名、作者和出版年份。"},
],
response_format={"type": "json_object"},
)

data = json.loads(resp.choices[0].message.content)
print(data["title"], data["author"], data["year"])

有两个关键限制:第一,消息里必须出现”json”字样,否则接口直接报错,这是官方要求;第二,JSON Mode 只保证”输出是合法的 JSON 对象”,不保证字段名和结构——模型可能把年份写成 year,也可能写成 publish_year,数组字段也可能时有时无。所以它适合字段少、字段名不重要的临时场景,比如只让模型”把这段文字总结成 JSON”再自己取值。

另外要注意,JSON 比自由文本更”费 token”:一层层的花括号和引号都要占 token 数。固定结构反复出现时,成本会比纯文本回复略高,批量调用时值得留意。

方式二:Structured Outputs(json_schema)

如果字段名、类型、必填项都不能含糊,就用结构化输出的正式形态:在 response_format 里提供一个 JSON Schema,并开启 strict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
book_schema = {
"name": "book_info",
"description": "从文本中提取图书信息",
"schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"author": {"type": "string"},
"year": {"type": "integer"},
"tags": {"type": "array", "items": {"type": "string"}},
},
"required": ["title", "author", "year", "tags"],
"additionalProperties": False,
},
"strict": True,
}

resp = client.chat.completions.create(
model=model_name,
messages=[{"role": "user", "content": "《三体》是刘慈欣的科幻小说,2008 年出版,主题涉及外星文明与人性。"}],
response_format={"type": "json_schema", "json_schema": book_schema},
)

data = json.loads(resp.choices[0].message.content)
print(data["title"], data["year"], data["tags"])

json_schema 字段中,name 必填,schema 是标准的 JSON Schema 定义。开启 strict: True 后模型必须严格遵守结构,但严格模式有硬性要求:**所有属性都要列入 required,且必须设置 additionalProperties: False**,否则请求会被拒绝。注意:返回内容仍然是 JSON 字符串,需要 json.loads;结构有保证,但字段值是否正确(比如年份是否真实)模型仍可能出错。

方式三:Pydantic 模型 + parse()

手写一大段 schema 容易出错,SDK 提供了更优雅的封装:定义一个 Pydantic 模型,SDK 自动把它转成 JSON Schema 发给模型,再把返回解析成模型实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pydantic import BaseModel

class Book(BaseModel):
title: str
author: str
year: int
tags: list[str]

resp = client.beta.chat.completions.parse(
model=model_name,
messages=[{"role": "user", "content": "《三体》是刘慈欣的科幻小说,2008 年出版。"}],
response_format=Book,
)

book = resp.choices[0].message.parsed
print(book.title, book.year, book.tags)

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)。

parsedNone 说明 parse() 没能把输出解析成模型实例,常见于输出为空或被内容安全策略拦截,判空后记录日志即可。

流式输出能搭配结构化输出吗? 可以同时开启,但流式返回的是一串分片,需要先把分片拼成完整文本再 json.loads。非流式直接拿完整响应更省事。

结构对了,值却是错的。 结构化输出保证的是”长得像”,不保证”内容真实”。year 可能是模型编造的,关键数据仍需程序侧校验,必要时让模型在拿不准时返回 null 而不是硬编一个值。

结构化输出更贵吗? JSON 语法本身会占用额外的输出 token,价格随 token 计费,所以同样内容会比纯文本略贵。对高频批量场景,可以把固定结构放进提示词示例里,减少模型”摸索格式”造成的浪费。

小结

  • 结构化输出通过 response_format 让模型直接返回 JSON,省去脆弱的文本解析。
  • json_object 最轻量但字段无保证;json_schema 结构有强保证,是生产推荐;Pydantic parse() 写法最简洁。
  • 无论哪种方式,返回后都要解析和校验;第三方兼容服务支持不一,先验证再用。
  • 下一步我们将学习基础错误处理:超时、重试、限流与指数退避,让程序在接口不稳定时也能可靠运行。