LLM 结构化输出指南:JSON Schema 与可靠解析
大模型最擅长输出「一段话」,而生产系统最需要的是一份「结构」。从模型返回的文本里抠 JSON,是所有 AI 应用工程师都经历过的噩梦:多了一个逗号、中文引号、Markdown 代码块包裹、突然多了一句「好的,以下是你要的 JSON」……解析器一炸,整条链路就断了。早期调研里,不少团队的真实解析成功率只有 90%~95%——意味着每 20 次请求就有 1 次静默失败。
好消息是,2026 年的模型和 API 已经给了我们一整套「让模型直接输出合法结构」的手段。这篇文章按可靠程度从高到低,把 JSON Schema 约束、Function Calling、类型安全解析、容错重试和流式处理全部过一遍,最后给你一份「从 95% 到 99.9%」的账单。
一、先定契约:JSON Schema 是一切的基础
不管用哪种手段,第一步都是把输出格式写成 JSON Schema。它是你和模型之间的契约,也是校验、生成示例、类型生成的唯一事实来源:
{
"type": "object",
"properties": {
"summary": {"type": "string", "description": "不超过 50 字的中文摘要"},
"tags": {"type": "array", "items": {"type": "string"}, "maxItems": 5},
"sentiment": {"enum": ["positive", "neutral", "negative"]},
"score": {"type": "number", "minimum": 0, "maximum": 100}
},
"required": ["summary", "tags", "sentiment", "score"],
"additionalProperties": false
}
写 Schema 的三个纪律:字段尽量少(超过 20 个字段,错误率明显上升);枚举给全(模型不知道你的隐含取值);每个字段写 description(约束长度、语言、格式,模型真的很吃这一套)。
二、第一手段:response_format 的 json_schema 模式
主流厂商的 OpenAI 兼容接口都支持 response_format,其中 json_schema 模式会在解码层约束输出——模型生成的 token 流被过滤器约束,根本产生不出不符合 Schema 的 JSON。这是目前可靠性最高的手段:
from openai import OpenAI
client = OpenAI(base_url="https://api.dr-ai.top/v1", api_key="sk-...")
resp = client.chat.completions.create(
model="gpt-5-mini",
response_format={
"type": "json_schema",
"json_schema": {
"name": "review_summary",
"strict": True,
"schema": REVIEW_SCHEMA,
},
},
messages=[{"role": "user", "content": "总结这条评论:……"}],
)
data = json.loads(resp.choices[0].message.content)
注意两点:strict 模式不支持可选字段和 $ref,Schema 要「扁平化」;JSON 模式不保证语义正确——结构合法了,字段值可能是错的,语义校验(枚举值合理性、范围)仍然要自己做。不同厂商对 json_schema 的实现有细微差异,用聚合网关时建议以兼容性最好的子集为准。
三、第二手段:Function Calling / 工具调用
如果你的应用本来就围绕「工具调用」设计(Agent、自动化流程),Function Calling 是天然的结构化出口:模型返回的不是自由文本,而是结构化的 function call(函数名 + 参数 JSON)。参数同样由 Schema 约束,而且和 Agent 的执行循环天然契合。相比手写 JSON 解析,它的优势是「意图」和「参数」分离:模型先决定调哪个函数,再填参数,每一步都可观测、可校验。
一个实用技巧:需要结构化输出但又不真想执行工具时,可以「声明一个叫 respond 的伪函数」,把输出结构定义在它的参数 Schema 里——很多团队用这招拿到了比 response_format 更稳定的结果(尤其在一些对 json_schema 支持不完整的模型上)。想深入了解 Agent 怎么组织工具调用,可看《2026 AI Agent 框架对比》。
四、第三手段:Pydantic 与类型安全解析
无论模型端约束多强,解析端都必须有一层类型安全的收口。用 Pydantic 定义模型,把「JSON 到对象」的转换交给验证器:
from pydantic import BaseModel, Field
from typing import Literal
class ReviewSummary(BaseModel):
summary: str = Field(..., max_length=50, description="中文摘要")
tags: list[str] = Field(..., max_length=5)
sentiment: Literal["positive", "neutral", "negative"]
score: float = Field(..., ge=0, le=100)
data = ReviewSummary.model_validate_json(raw_content)
Pydantic 的价值不止类型检查:字段名映射(模型爱用 camelCase,你爱用 snake_case)、默认值兜底、嵌套模型、错误信息定位——全都免费拿到。解析失败时,ValidationError 会精确告诉你哪个字段不对,这是后面容错重试的输入。
五、容错:解析失败不是终点
再强的约束也有漏网之鱼。生产级的解析管线必须内置三级容错:
- 清洗:去掉 Markdown 代码块围栏(```json ... ```)、裁剪首尾空白、修复常见非法字符(中文引号、尾逗号)。这一步能救回一半以上的「解析失败」。
- 修复重试:把「原始输出 + 报错信息」塞回模型,要求「只输出修正后的 JSON」。实测修复一轮的成功率在 90% 以上,最多重试两轮,再多成本不划算。
- 降级:重试仍失败就返回结构化错误(error 对象 + 原始文本),让上层走人工/兜底路径,绝不抛裸异常。
@retry(stop=stop_after_attempt(2), reraise=True)
def parse_with_repair(raw: str) -> dict:
try:
return ReviewSummary.model_validate_json(clean(raw)).model_dump()
except ValidationError as e:
repair = client.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "user", "content": raw},
{"role": "assistant", "content": "修正以下 JSON 使其合法:" + str(e.errors())},
])
return ReviewSummary.model_validate_json(repair.choices[0].message.content).model_dump()
六、流式与结构化:先流后析
流式(stream=true)场景下没有「完整响应」可解析,常见做法是先让模型输出「结构摘要 + 正文」两部分:开头一小段固定格式的 JSON 元数据(标题、要点),随后是流式正文。客户端按「首次出现分隔符」切换解析模式。更稳妥的变体是让模型先输出一个紧凑的 JSON 大纲再展开——代价是多一次调用,收益是首屏体验和结构完整性兼得。流式相关的更多细节见《LLM API 延迟优化》。
七、实测:从 95% 到 99.9% 的账单
| 手段 | 解析成功率 | 额外成本 / 延迟 |
|---|---|---|
| 裸提示词「请返回 JSON」 | ~90% | 无 |
| + JSON Schema 提示 + 清洗 | ~95% | 几乎为零 |
| + response_format json_schema(严格模式) | ~99% | 零(同 token 数) |
| + Pydantic 校验 + 修复重试 | ~99.9% | 重试轮次 × 输入 token(少量) |
看到没有:最便宜、最有效的一刀是「声明式约束」,不是重试。重试是最后一道保险,不是主力。按这个顺序叠加,绝大多数应用能把解析失败率压到千分之一以下。
八、常见坑
- Schema 太复杂:深层嵌套、大量 $ref、超长 description 都会降低模型遵守率,能平铺就平铺。
- 枚举不给全:模型会发明你没见过的值,enum 一定列全,必要时加 "other"。
- 只信结构不信语义:JSON 合法 ≠ 内容正确,数值范围、日期格式、敏感词过滤都要再过一遍。
- 中文编码问题:确保 HTTP 层 UTF-8,别在中间环节被转成 latin-1(网关和 SDK 都可能干这事)。
- 测试只测理想路径:把坏 JSON、空响应、截断流写进测试集——这是我们AI API 测试技巧里反复强调的。
结构化输出做扎实了,你的 AI 应用才配叫「系统」,而不是「聊天框」。需要多模型对比不同厂商的结构化输出表现?注册 DrAI,一个 API Key 切换 40+ 模型,同一份 Schema 直接跑对比。