第三章 结构化输出:让大模型只说你要的格式

第三章 结构化输出:让大模型只说你要的格式

“让模型自由发挥是艺术,让模型按格式输出是工程。”

2026 年 4 月的一份行业调研显示,在所有接入大模型 API 的国内应用中,超过 60% 的线上故障与"输出格式不符合预期"直接相关。模型返回了一段自然语言文本,但下游代码期望的是一个 JSON 对象——json.loads() 一调用,直接抛异常,整个链路崩掉。

这不是模型的问题。是你没告诉它用什么格式说话。

上一章讲的系统提示词可以"建议"模型按某种格式输出,但那只是建议——模型可能听,也可能不听。这一章要解决的是:如何强制模型按你定义的 Schema 输出数据,不是靠"请求",而是靠技术手段。

3.1 从自由文本到结构化数据

先看一个对比。假设你在做一个商品信息提取系统,用户输入一段商品描述,你需要提取出结构化信息。

不用结构化输出的结果(模型随心所欲地回复):

这个商品是一款蓝牙耳机,品牌是漫步者,型号LolliPods Pro 2,
售价299元,支持主动降噪,续航约30小时。

用了结构化输出的结果(严格按 Schema):

{
  "product_name": "LolliPods Pro 2",
  "brand": "漫步者",
  "price": 299,
  "currency": "CNY",
  "features": ["主动降噪", "蓝牙连接"],
  "battery_hours": 30
}

哪个能被下游代码直接使用?显然是后者。前者还需要你写一堆正则或者再调一次模型来解析——这就多了一次不确定性。

3.2 三种主流结构化输出方案

目前主流的结构化输出技术有三种,适用场景各不相同:

方案 原理 可靠性 适用场景
Prompt 引导 在提示词中写明 JSON 格式要求 中(偶尔格式错误) 简单场景、快速原型
JSON Mode / Response Format API 参数强制 JSON 输出 高(格式保证,内容不保证) 需要 JSON 但字段灵活
Function Calling / Tool Use 定义函数签名,模型填充参数 高(格式和字段类型都保证) 需要严格 Schema

下面分别说每种怎么用。

3.3 方案一:Prompt 引导(最简单但最不可靠)

在系统提示词或用户消息中直接写明期望的 JSON 格式:

import json
from openai import OpenAI  # 这里用 DeepSeek 兼容接口

client = OpenAI(
    api_key="your-deepseek-api-key",
    base_url="https://api.deepseek.com"
)

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": """你是一个商品信息提取器。
请从用户提供的文本中提取商品信息,必须以 JSON 格式返回:
{
  "product_name": "商品名",
  "brand": "品牌",
  "price": 数字,
  "features": ["特性1", "特性2"]
}
只返回 JSON,不要有任何其他文字。"""},
        {"role": "user", "content": "漫步者LolliPods Pro 2蓝牙耳机,299元,支持主动降噪"}
    ]
)

result = json.loads(response.choices[0].message.content)

这个方案的问题是:模型大概率会返回正确的 JSON,但偶尔会加上 ```json 的标记、多一段解释文字、或者字段名拼写不一致。在生产环境里,“大概率"不够,你需要"一定”。

3.4 方案二:JSON Mode(格式保证)

DeepSeek、通义千问等国内大模型 API 都支持 JSON Mode,通过 API 参数强制模型输出合法的 JSON:

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "提取商品信息,返回JSON格式"},
        {"role": "user", "content": "漫步者LolliPods Pro 2蓝牙耳机,299元,降噪"}
    ],
    response_format={"type": "json_object"}
)

# 这里 json.loads() 不会报错——API 保证了 JSON 格式
data = json.loads(response.choices[0].message.content)

JSON Mode 保证了语法正确(一定是合法 JSON),但不保证语义正确(字段名可能不是你要的、字段可能缺失)。这时候就需要下一步:Schema 验证。

3.5 Pydantic:Python 世界的 Schema 卫士

Pydantic 是 Python 生态中最流行的数据验证库。它可以把"模型应该输出什么结构"定义成一个 Python 类,然后自动验证模型输出是否符合要求:

from pydantic import BaseModel, Field
from typing import List, Optional

class ProductInfo(BaseModel):
    product_name: str = Field(description="商品名称")
    brand: str = Field(description="品牌名")
    price: float = Field(ge=0, description="价格,必须大于等于0")
    currency: str = Field(default="CNY", description="货币")
    features: List[str] = Field(description="商品特性列表")
    battery_hours: Optional[float] = Field(
        default=None, description="电池续航小时数"
    )

# 验证模型输出
raw_output = json.loads(response.choices[0].message.content)

try:
    product = ProductInfo(**raw_output)
    print(f"验证通过:{product.product_name}, ¥{product.price}")
except ValidationError as e:
    print(f"输出不符合 Schema:{e}")
    # 这里可以触发重试逻辑

Pydantic 帮你做了三件事:

  1. 类型检查:price 是不是数字?features 是不是列表?
  2. 约束验证:price 是不是大于 0?
  3. 默认值填充:如果模型没返回 currency,自动填 “CNY”

3.6 方案三:Function Calling(最严格的约束)

Function Calling(也叫 Tool Use)是当前最可靠的结构化输出方案。你定义一个"函数签名",模型的任务是填充函数参数——这比"请输出 JSON"的约束力强很多。

tools = [
    {
        "type": "function",
        "function": {
            "name": "extract_product_info",
            "description": "从文本中提取商品信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "product_name": {
                        "type": "string",
                        "description": "商品名称"
                    },
                    "brand": {
                        "type": "string",
                        "description": "品牌名"
                    },
                    "price": {
                        "type": "number",
                        "description": "价格"
                    },
                    "features": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "商品特性列表"
                    }
                },
                "required": ["product_name", "brand", "price"]
            }
        }
    }
]

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "漫步者LolliPods Pro 2,299元"}],
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "extract_product_info"}}
)

# 模型返回的是结构化的函数调用参数
args = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
product = ProductInfo(**args)  # 再用 Pydantic 做二次验证

Function Calling 的核心优势是模型知道自己在"填表"而不是"写作",这从认知模式上就减少了格式错误的概率。

3.7 重试机制:当格式还是错了

即使用了 JSON Mode + Function Calling + Pydantic,偶尔还是会出现验证失败的情况。这时候需要一个重试机制:

def extract_with_retry(text: str, max_retries: int = 3) -> ProductInfo:
    """带重试的结构化提取"""
    last_error = None
    
    for attempt in range(max_retries):
        try:
            response = call_llm(text, tools=tools)
            args = parse_tool_call(response)
            return ProductInfo(**args)
        except (json.JSONDecodeError, ValidationError) as e:
            last_error = e
            # 把错误信息反馈给模型,让它修正
            text = f"""上次输出有误:{str(e)}
请重新从以下文本提取信息:{text}"""
    
    raise RuntimeError(f"重试 {max_retries} 次仍然失败:{last_error}")

关键技巧:重试时把错误信息告诉模型。模型看到"price 应该是数字但你给了字符串"这样的反馈,第二次大概率就能修正。

3.8 实战决策树

面对一个新场景,怎么选择结构化输出方案?

输出需要结构化吗?
├── 不需要 → 自由文本即可
└── 需要
    ├── 字段固定、类型严格?
    │   ├── 是 → Function Calling + Pydantic 验证
    │   └── 否 → JSON Mode + 宽松解析
    └── 输出可能很长(>2000 tokens)?
        ├── 是 → 分段提取 + 合并
        └── 否 → 单次调用即可

一个务实的建议:先用 Prompt 引导做原型验证想法,确认可行后切换到 Function Calling + Pydantic 做生产版本。不要一上来就搞最复杂的方案。


扩展阅读

如果你想在本章主题上走得更深,推荐以下资源:

  • 📖 《Pydantic V2 官方文档》— 数据验证的标准参考,理解 Field、Validator、自定义类型
  • 📖 《Building LLM Apps》(Valentino Gagliardi)— 第 5 章系统讲解了 Function Calling 的工程实践
  • 🛠️ Instructor 库(jxnl/instructor)— 基于 Pydantic 的结构化输出库,支持自动重试和流式输出

本章小结

本章的核心是:结构化输出是把大模型从"随心所欲的作家"变成"严格按规格交付的工程师"的关键一步。

你在本章学到了:

  • 三种结构化输出方案的原理和适用场景:Prompt 引导、JSON Mode、Function Calling
  • Pydantic 是 Python 中做 Schema 验证的标准选择,能在模型输出后做二次把关
  • 重试机制要把错误信息反馈给模型,而不是简单地重跑同样的请求
  • 选择方案的决策树:从场景出发,不要过度设计

下一步行动:在接下来的 24 小时内,把你项目中一个用自由文本输出的 LLM 调用改成 Function Calling + Pydantic 验证,跑 20 次看通过率。

结构化输出解决了"格式"问题,但如果你需要更复杂的约束——比如"输出中不能包含竞争对手名称"或"必须引用知识库中的内容"——就需要更重的武器了。下一章,我们来看 Guardrails 框架,给 AI 装上真正的护栏。


本章末:4 个立刻可以用的 DeepSeek 提示词

提示词 1:为业务场景设计 Pydantic Schema

我的业务场景是 [描述场景,如:从用户评论中提取情感和关键词]。

请帮我设计一个 Pydantic V2 的 BaseModel 类,包含:
1. 所有需要提取的字段(合理的类型和约束)
2. 每个字段的 Field description
3. 必要的自定义 validator
4. 一个使用示例

输出完整的 Python 代码。

使用场景:快速为新的结构化提取任务设计数据模型。


提示词 2:生成 Function Calling 的 Tool 定义

我需要一个 Function Calling 的工具定义,功能是 [描述功能]。

输入参数包括:
[列出参数名、类型、是否必填、说明]

请生成符合 OpenAI/DeepSeek API 格式的 tools JSON 定义,
并附一段调用示例代码(Python)。

使用场景:从业务需求快速生成标准的 Tool 定义。


提示词 3:调试结构化输出错误

我用 Function Calling 提取信息,但遇到了以下错误:

Schema 定义:
[粘贴你的 Pydantic 或 JSON Schema]

模型实际输出:
[粘贴模型的原始输出]

错误信息:
[粘贴 ValidationError 或 JSONDecodeError]

请分析错误原因,并给出:
1. 修复 Schema 的建议(如果是 Schema 问题)
2. 修复 Prompt 的建议(如果是引导不足)
3. 添加兜底逻辑的代码

使用场景:结构化输出调试不通时的诊断工具。


提示词 4:批量测试结构化输出的稳定性

以下是我的结构化输出 Schema:
[粘贴 Schema]

请帮我生成 10 个不同复杂度的测试输入文本:
- 3 个简单文本(信息完整、格式清晰)
- 3 个中等文本(部分信息缺失或模糊)
- 2 个复杂文本(信息散乱、有干扰信息)
- 2 个边界文本(极短/极长/多语言混合)

每个测试输入附带期望的输出 JSON。

使用场景:系统上线前,批量测试结构化输出的鲁棒性。