专栏:AI 全栈开发|20
一、当模型输出要进数据库,普通文本就不够用了
聊天回答多一句解释通常没关系,但当输出要写 CRM、触发退款流程或进入数据库,多一个键、少一个字段、把布尔值写成字符串,都可能变成真实业务故障。Structured Output 的目标不是让 JSON "更好看",而是把格式约束从提示词愿望升级成程序契约。
|------------------------------------------------------------------------------------------------|
| 上一篇 19《Streaming Model Output》解决的是"怎样把文本增量稳定送到浏览器"。这一篇换一个问题:当下游不再是人,而是代码,模型输出怎样才能成为程序敢直接消费的数据。 |

二、先把 JSON Mode 和 Structured Output 分清
|----------------------|---------------|---------------------|--------------|
| 方式 | 保证什么 | 不能保证什么 | 适合 |
| 提示词约定 | 什么都不硬保证 | 可能带 Markdown、废话、缺字段 | 低风险内部 Demo |
| JSON Mode | 输出是合法 JSON | 不保证字段、类型和枚举 | 只要求"能 parse" |
| JSON Schema + strict | 输出遵循给定 Schema | 不保证业务语义一定正确 | 提取、分类、路由、入库 |
本文继续沿用 F16~F19 的 Chat Completions 教学基线,所以示例使用 response_format。若项目使用 Responses API,同一概念已经移动到 text.format;两种 API 的字段位置不同,但"Schema + strict + 应用侧边缘状态处理"的思路不变。
三、Schema 管"形状",业务仍要管"意义"
结构化输出最容易被误解成"模型从此不会错"。它能显著提高结构可靠性,但不能替代业务判断:Schema 可以要求 category 只能是 billing / bug / advice,却无法保证模型把真实退款投诉一定分类成 billing。格式正确、值域合法、语义正确,是三个不同层次。
• 形状:字段是否存在、类型是否正确、是否允许多余键------由 Schema 约束。
• 值域:enum、数值上下限等可部分写进 Schema,同时仍应在应用侧校验。
• 语义:分类是否真的判断对、提取内容是否忠于原文------只能靠业务规则、评测与人工抽查。
四、最小请求:json_schema + strict 真正长什么样
Chat Completions 教学基线:response_format 里放 Schema
schema = {
"type": "object",
"properties": {
"customer_name": {"type": "string"},
"lead_level": {
"type": "string",
"enum": ["high", "mid", "low"]
}
},
"required": ["customer_name", "lead_level"],
"additionalProperties": False
}
response_format = {
"type": "json_schema",
"json_schema": {
"name": "lead_extract",
"strict": True,
"schema": schema
}
}
strict 模式的关键不是多写一个布尔值,而是 Schema 本身必须落在 Provider 支持的子集里。以 OpenAI 当前规则为例:对象要显式 additionalProperties:false,字段都必须列入 required;逻辑上的"可选值"可以用包含 null 的联合类型表达。
五、三层保障缺一不可 
图 2 请求侧、生成侧、响应侧各守一道关;strict 不是"跳过校验"的许可证
请求侧用 Pydantic/Zod/手写 JSON Schema 定义契约;生成侧由 Provider 做受限解码,尽量不让 Schema 外的 token 路径被生成;响应侧仍要显式处理拒绝、输出不完整与应用层校验。尤其是用户输入场景,安全拒绝不一定遵循你的业务 Schema,输出被 max token 截断时也不能拿半截 JSON 继续 parse。
|---------------------|-------------------------------|
| 响应状态 | 应该怎么处理 |
| 正常结构化结果 | parse → 强类型 validate → 业务规则校验 |
| refusal | 走拒绝分支,不把拒绝文本当 JSON 解析 |
| incomplete / length | 视为未完成,调整预算或重试,不消费半截对象 |
| Provider 不支持 strict | 显式降级并加强响应侧校验,不能假装仍有强约束 |
六、让代码模型成为单一事实来源
原稿采用 Pydantic 定义输出对象,这个方向应保留:业务代码面对的是 Lead 这样的强类型对象,Schema 由模型类导出,而不是再手写第二份字段表。这样字段名、枚举和值域修改时只改一处,避免"代码模型已经变了,发送给 Provider 的 Schema 还停留在旧版本"。
Pydantic:定义对象,再由 SDK/Helper 生成 Schema
from typing import Literal
from pydantic import BaseModel, Field
class Lead(BaseModel):
customer_name: str
lead_level: Literal["high", "mid", "low"]
category: Literal["billing", "bug", "advice", "other"]
urgency: int = Field(ge=1, le=5)
reply_suggestion: str
# 生产中优先使用 SDK 的 parse/helper,
# 或统一封装一个 schema adapter,避免业务代码散落手写 JSON Schema。
七、生产链路:能力探测、边缘状态、校验和有限重试 
图 3 生产路径先判断 Provider 能力,再处理 refusal / incomplete,最后才把对象交给业务层
兼容网关的最大风险不是"请求直接失败",而是某些字段被忽略后悄悄退回普通文本模式。因此 Provider Adapter 应明确记录当前端点支持 json_schema、json_object 还是仅提示词约定;每次降级都要让响应侧的校验更严格,而不是把约束丢掉。
• Schema 设计尽量清晰、扁平;复杂嵌套不是绝对不能用,但兼容端点支持度与首次 Schema 处理延迟都应测试。
• 校验失败只做有限重试,并把"失败率"当指标;如果同一 Schema 经常失败,应优先修设计,不是无限加重试。
• 用户原文只放在数据位置,不要拼进 Schema description 或系统结构;结构正确并不能阻止提示注入操控字段含义。
八、最容易把 Structured Output 用错的 7 件事
• 只写"请输出 JSON"却把下游当强类型对象使用:格式可靠性仍然押在模型自觉上。
• 把 JSON Mode 当成 JSON Schema:合法 JSON 并不等于字段、类型和枚举满足业务契约。
• strict Schema 漏掉 required 或 additionalProperties:false:强约束请求可能直接被拒绝。
• 手写 Schema 与 Pydantic/Zod 各维护一份:长期一定漂移,单一事实来源更重要。
• 拿到响应就 json.loads:先判断 refusal / incomplete 等边缘状态,再谈解析。
• 校验失败无限重试:每次重试都在花钱;有限重试后应进入人工/待审或业务降级。
• 认为 Schema 能保证内容真实:它只能把脏数据装进"形状正确"的盒子,语义仍需要评测与规则。
九、总结与下一篇
Structured Output 最值得记住的不是某个参数名,而是三层保障:请求侧定义契约,生成侧按契约约束输出,响应侧处理边缘状态并把结果校验成真正的业务对象。三层中任何一层缺失,可靠性都会重新退回"相信模型会乖乖输出"。
下一篇 F21《Model Provider 抽象层》会把 F16~F20 逐渐散落的 base_url、模型名、请求差异和错误语义收进 Provider Adapter,让业务代码只面对统一的 ModelService。