结构化输出控制:JSON Mode 与 Schema 约束(2026 年版)

1. 引言

大语言模型最初被设计为"自由文本生成器",其输出是非结构化的自然语言。然而在工程实践中,我们几乎总是需要结构化的输出:JSON 对象、数据库记录、API 响应、表格数据。如何可靠地从 LLM 获得结构化输出,是 Prompt 工程中最关键的实用技能之一。

2024-2026 年间,主流模型提供商相继推出了原生的结构化输出支持:OpenAI 的 Structured Outputs(JSON Schema 约束)、Anthropic 的 Tool Use、Google 的 Response Schema。本文系统化梳理各种结构化输出技术的原理、用法和最佳实践。

2. 结构化输出的三个层次

层次 1: Prompt 约束(不可靠)
  "请以 JSON 格式返回结果"
  → 模型可能返回额外文字、格式错误

层次 2: JSON Mode(基本可靠)
  response_format: { type: "json_object" }
  → 保证返回有效 JSON,但不保证 Schema

层次 3: Schema 约束(完全可靠)
  response_format: { type: "json_schema", schema: {...} }
  → 保证返回符合指定 Schema 的 JSON
层次 格式保证 Schema 保证 需要后处理 适用模型
Prompt 约束 不保证 不保证 需要解析和验证 所有模型
JSON Mode 有效 JSON 不保证 需要 Schema 验证 GPT-4o, Claude, Gemini
Schema 约束 有效 JSON 保证 不需要 GPT-4o (Structured Outputs)

3. 各平台的结构化输出方案

3.1 OpenAI Structured Outputs

2024 年 8 月,OpenAI 推出了 Structured Outputs 功能,这是目前最完善的结构化输出方案。

3.1.1 基本用法

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class CalendarEvent(BaseModel):
    name: str
    date: str
    participants: list[str]
    location: str | None = None
    is_recurring: bool = False

response = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "提取日历事件信息。"},
        {"role": "user", "content": "明天下午 3 点和张三、李四在会议室 A 开产品评审会"}
    ],
    response_format=CalendarEvent
)

event = response.choices[0].message.parsed
print(event.name)          # "产品评审会"
print(event.date)          # "2026-03-01T15:00:00"
print(event.participants)  # ["张三", "李四"]
print(event.location)      # "会议室 A"

3.1.2 JSON Schema 直接定义

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[...],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "calendar_event",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "事件名称"},
                    "date": {"type": "string", "description": "ISO 8601 格式"},
                    "participants": {
                        "type": "array",
                        "items": {"type": "string"}
                    },
                    "location": {
                        "type": ["string", "null"],
                        "description": "地点,如果未提及则为 null"
                    }
                },
                "required": ["name", "date", "participants", "location"],
                "additionalProperties": False
            }
        }
    }
)

3.1.3 Schema 约束规则

OpenAI Structured Outputs 的 strict: true 模式有以下限制:

规则 说明
additionalProperties: false 必须显式声明所有属性
所有属性 required 所有字段都必须在 required 中
可选字段用 null ["string", "null"] 而非省略
嵌套深度限制 最多 5 层嵌套
属性数量限制 最多 100 个属性
不支持 $ref 不支持 JSON Schema 引用
枚举 支持 enum

3.2 Anthropic Claude 的结构化输出

Claude 通过 Tool Use 机制实现结构化输出。

3.2.1 Tool Use 作为结构化输出

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[{
        "name": "extract_event",
        "description": "从文本中提取日历事件信息",
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "事件名称"},
                "date": {"type": "string", "description": "ISO 8601 格式"},
                "participants": {
                    "type": "array",
                    "items": {"type": "string"}
                },
                "location": {
                    "type": "string",
                    "description": "地点"
                }
            },
            "required": ["name", "date", "participants"]
        }
    }],
    tool_choice={"type": "tool", "name": "extract_event"},
    messages=[{
        "role": "user",
        "content": "明天下午 3 点和张三、李四在会议室 A 开产品评审会"
    }]
)

# 从 tool_use 内容块中提取结构化数据
for block in response.content:
    if block.type == "tool_use":
        event = block.input
        print(event)

关键参数

  • tool_choice: {"type": "tool", "name": "extract_event"}:强制模型调用指定工具
  • tool_choice: {"type": "any"}:模型必须调用某个工具,但可以自行选择

3.2.2 Anthropic 的 JSON Mode

Claude 也支持 Prompt 级的 JSON 输出约束:

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    system="你是一个数据提取助手。始终以 JSON 格式返回结果。",
    messages=[{
        "role": "user",
        "content": "提取事件信息并以 JSON 返回:明天下午3点开会"
    }, {
        "role": "assistant",
        "content": "{"   # 预填充 JSON 开头
    }]
)

预填充技巧:在 assistant 消息中预填充 {[ 可以引导 Claude 直接输出 JSON,减少前导文本。

3.3 Google Gemini 的结构化输出

3.3.1 Response Schema

import google.generativeai as genai

model = genai.GenerativeModel(
    "gemini-2.0-flash",
    generation_config=genai.GenerationConfig(
        response_mime_type="application/json",
        response_schema={
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "date": {"type": "string"},
                "participants": {
                    "type": "array",
                    "items": {"type": "string"}
                }
            },
            "required": ["name", "date", "participants"]
        }
    )
)

response = model.generate_content(
    "提取事件:明天下午3点和张三、李四在会议室A开产品评审会"
)

3.3.2 Enum 约束

import enum

class Sentiment(enum.Enum):
    POSITIVE = "positive"
    NEGATIVE = "negative"
    NEUTRAL = "neutral"

model = genai.GenerativeModel(
    "gemini-2.0-flash",
    generation_config=genai.GenerationConfig(
        response_mime_type="text/x.enum",
        response_schema=Sentiment
    )
)

response = model.generate_content("这个产品太棒了!")
# 保证输出是 "positive"、"negative" 或 "neutral" 之一

4. Function Calling 与结构化输出

4.1 Function Calling 的本质

Function Calling(函数调用)本质上是一种结构化输出机制:模型输出的不是自由文本,而是一个结构化的函数调用请求。

// 模型输出
{
  "tool_calls": [{
    "function": {
      "name": "get_weather",
      "arguments": "{\"city\": \"上海\", \"unit\": \"celsius\"}"
    }
  }]
}

4.2 Function Calling vs JSON Schema

维度 Function Calling JSON Schema
设计目的 调用外部工具 结构化数据输出
输出位置 tool_calls 字段 content 字段
多个输出 支持并行调用多个函数 单一 JSON 对象
Schema 严格性 可选 strict 可选 strict
适用场景 需要执行外部操作 纯数据提取/格式化

4.3 何时用 Function Calling 何时用 JSON Schema

需要模型触发外部操作?(搜索/计算/数据库查询)
├── 是 → Function Calling
└── 否 → 只需结构化数据输出
    ├── 需要 Schema 严格保证?
    │   ├── 是 → JSON Schema(strict mode)
    │   └── 否 → JSON Mode + 后处理验证
    └── 需要从多个角度提取?
        └── 是 → Function Calling(多工具并行)

5. Pydantic 与类型安全

5.1 Pydantic 模型定义

from pydantic import BaseModel, Field, validator
from typing import Optional, Literal
from datetime import datetime

class Address(BaseModel):
    street: str = Field(description="街道地址")
    city: str = Field(description="城市")
    province: str = Field(description="省/直辖市")
    postal_code: str = Field(description="邮编", pattern=r"^\d{6}$")

class OrderItem(BaseModel):
    product_name: str = Field(description="商品名称")
    quantity: int = Field(description="数量", ge=1)
    unit_price: float = Field(description="单价(元)", ge=0)

    @property
    def subtotal(self) -> float:
        return self.quantity * self.unit_price

class Order(BaseModel):
    order_id: str = Field(description="订单号")
    customer_name: str = Field(description="客户姓名")
    items: list[OrderItem] = Field(description="订单项", min_length=1)
    shipping_address: Address = Field(description="配送地址")
    status: Literal["pending", "confirmed", "shipped", "delivered"]
    notes: Optional[str] = Field(default=None, description="备注")
    created_at: datetime = Field(description="创建时间")

    @validator("order_id")
    def validate_order_id(cls, v):
        if not v.startswith("ORD-"):
            raise ValueError("订单号必须以 ORD- 开头")
        return v

5.2 Pydantic 生成 JSON Schema

schema = Order.model_json_schema()
print(json.dumps(schema, indent=2, ensure_ascii=False))

输出的 JSON Schema 可以直接用于 OpenAI Structured Outputs 或 Gemini Response Schema。

5.3 Instructor 库

Instructor 是一个轻量级库,简化了 Pydantic + LLM 的集成。

import instructor
from openai import OpenAI
from pydantic import BaseModel

client = instructor.from_openai(OpenAI())

class UserInfo(BaseModel):
    name: str
    age: int
    email: str

user = client.chat.completions.create(
    model="gpt-4o",
    response_model=UserInfo,
    messages=[{
        "role": "user",
        "content": "我叫张三,今年 28 岁,邮箱是 zhangsan@example.com"
    }]
)

print(user.name)   # "张三"
print(user.age)    # 28
print(user.email)  # "zhangsan@example.com"

Instructor 的优势

  • 自动重试:如果模型输出不符合 Schema,自动重试
  • 验证反馈:将验证错误反馈给模型让其修正
  • 多模型支持:OpenAI、Anthropic、Google、本地模型

6. 高级模式

6.1 嵌套结构与递归

class TreeNode(BaseModel):
    """组织架构中的节点"""
    name: str
    title: str
    department: str
    reports: list["TreeNode"] = Field(default_factory=list)

TreeNode.model_rebuild()  # 支持递归引用

6.2 联合类型(Union Types)

from typing import Union, Literal

class TextBlock(BaseModel):
    type: Literal["text"]
    content: str

class CodeBlock(BaseModel):
    type: Literal["code"]
    language: str
    content: str

class ImageBlock(BaseModel):
    type: Literal["image"]
    url: str
    alt_text: str

class Document(BaseModel):
    title: str
    blocks: list[Union[TextBlock, CodeBlock, ImageBlock]]

6.3 流式结构化输出

OpenAI 支持流式返回结构化输出:

from openai import OpenAI

client = OpenAI()

with client.beta.chat.completions.stream(
    model="gpt-4o",
    messages=[...],
    response_format=CalendarEvent,
) as stream:
    for event in stream:
        if event.type == "content.delta":
            print(event.parsed)  # 部分解析的 Pydantic 对象

    final = stream.get_final_completion()
    print(final.choices[0].message.parsed)

6.4 多步提取(Chain of Extraction)

对于复杂文档,分步提取比一次性提取更可靠。

# 步骤 1:提取文档的基础信息
class DocumentMeta(BaseModel):
    title: str
    author: str
    date: str
    document_type: Literal["report", "contract", "invoice", "letter"]

# 步骤 2:根据文档类型提取特定字段
class InvoiceDetails(BaseModel):
    invoice_number: str
    vendor: str
    items: list[dict]
    total_amount: float
    tax_amount: float
    due_date: str

class ContractDetails(BaseModel):
    parties: list[str]
    effective_date: str
    termination_date: str
    key_terms: list[str]

# 执行链
meta = extract(text, DocumentMeta)
if meta.document_type == "invoice":
    details = extract(text, InvoiceDetails)
elif meta.document_type == "contract":
    details = extract(text, ContractDetails)

7. 常见问题与解决方案

7.1 输出验证失败

问题 原因 解决方案
字段缺失 输入中缺少对应信息 使用 Optional + 默认值
类型错误 模型返回错误类型 使用 strict mode
枚举值错误 模型返回非枚举值 在 description 中列出所有选项
格式错误 日期/邮箱格式不对 在 description 中指定格式
数组为空 没有检测到符合条件的项 允许空数组或使用默认值

7.2 提高提取准确率的 Prompt 技巧

class ProductReview(BaseModel):
    """从电商评论中提取结构化信息。

    重要规则:
    - 如果信息在评论中未提及,对应字段设为 null
    - 不要推测或编造不存在的信息
    - 评分必须是 1-5 的整数
    """

    product_name: str = Field(
        description="商品名称。如果评论没有明确提到商品名,"
                    "使用 '未提及' 作为默认值。"
    )
    rating: int = Field(
        description="用户评分,1-5 分。如果未提及,根据情感推断。"
                    "1=非常不满, 2=不满, 3=一般, 4=满意, 5=非常满意",
        ge=1, le=5
    )
    pros: list[str] = Field(
        description="用户提到的优点,每个优点一句话概括。"
                    "如果没有提到任何优点,返回空数组。",
        default_factory=list
    )
    cons: list[str] = Field(
        description="用户提到的缺点,每个缺点一句话概括。"
                    "如果没有提到任何缺点,返回空数组。",
        default_factory=list
    )
    purchase_intent: Optional[bool] = Field(
        default=None,
        description="用户是否表达了回购意愿。"
                    "true=会回购, false=不会回购, null=未提及"
    )

7.3 处理大量数据的策略

策略 说明 适用场景
批量提取 一次提取多条记录 表格数据、列表
分页提取 分块处理长文档 长文档、多页PDF
并行提取 多个请求并行发送 大量独立文档
增量提取 先粗提取,再细化 复杂嵌套结构

8. 性能与成本优化

8.1 Token 消耗对比

方法 额外输入 Token 输出可靠性 成本影响
Prompt 约束 ~50 最小
JSON Mode ~10
Structured Outputs ~100-300 (Schema)
Function Calling ~100-500 (Tool 定义) 中-大

8.2 优化建议

建议 说明
Schema 简化 只定义必要的字段,避免过度复杂
Description 精简 字段描述简洁明了,不要写长段落
使用 Flash 模型 结构化提取用小模型足够
缓存 Schema 避免每次请求都发送完整 Schema
批量处理 合并多次小请求为一次大请求

9. 实战案例

9.1 简历解析

class Education(BaseModel):
    school: str
    degree: str
    major: str
    start_year: int
    end_year: Optional[int] = None

class WorkExperience(BaseModel):
    company: str
    title: str
    start_date: str
    end_date: Optional[str] = None
    responsibilities: list[str]

class Resume(BaseModel):
    name: str
    email: Optional[str] = None
    phone: Optional[str] = None
    summary: Optional[str] = None
    education: list[Education]
    work_experience: list[WorkExperience]
    skills: list[str]
    languages: list[str] = Field(default_factory=list)

9.2 发票信息提取

class InvoiceLineItem(BaseModel):
    description: str
    quantity: float
    unit_price: float
    amount: float
    tax_rate: Optional[float] = None

class Invoice(BaseModel):
    invoice_number: str
    invoice_date: str
    vendor_name: str
    vendor_tax_id: Optional[str] = None
    buyer_name: str
    buyer_tax_id: Optional[str] = None
    items: list[InvoiceLineItem]
    subtotal: float
    tax_total: float
    total: float
    currency: str = "CNY"
    notes: Optional[str] = None

9.3 多语言内容分类

class ContentClassification(BaseModel):
    primary_language: str = Field(
        description="内容的主要语言代码 (zh/en/ja/ko/...)"
    )
    category: Literal[
        "technology", "business", "health",
        "entertainment", "sports", "politics",
        "education", "science", "other"
    ]
    subcategory: str
    sentiment: Literal["positive", "negative", "neutral"]
    key_entities: list[str] = Field(
        description="文中提到的关键实体(人名/公司名/产品名)"
    )
    summary: str = Field(
        description="一句话摘要(使用内容的原始语言)"
    )
    is_opinion: bool = Field(
        description="是否为观点/评论(true)还是事实报道(false)"
    )

10. 趋势与展望

10.1 Schema 约束的标准化

各模型提供商的 Schema 支持正在趋同,未来可能出现统一的标准接口。

10.2 动态 Schema

根据输入内容动态生成 Schema,而非使用固定 Schema。例如,根据文档类型自动选择合适的提取模板。

10.3 结构化推理

不仅控制输出格式,还控制推理过程的结构。例如,要求模型按照特定的推理模板输出中间步骤。

10.4 多模态结构化输出

从图像、音频中直接提取结构化数据(发票图片到 JSON、语音到日历事件等)。

11. 结论

结构化输出是 LLM 应用工程化的基石。选择合适的结构化输出方案需要考虑:

  1. 可靠性要求:严格场景用 Schema 约束(Structured Outputs),宽松场景用 JSON Mode
  2. 模型兼容性:跨模型用 Instructor 库做抽象
  3. Schema 设计:好的 Schema 定义 = 好的提取效果
  4. 成本平衡:Schema 带来的额外 token 成本 vs 后处理验证的工程成本
  5. 类型安全:用 Pydantic 确保端到端的类型安全

掌握结构化输出技术,意味着你可以将 LLM 从"聊天工具"转变为"数据处理引擎",这是构建生产级 AI 应用的必经之路。


Maurice | maurice_wen@proton.me