Structured Outputs 实战:让大模型稳定输出 JSON 的三种方案对比

问题根源:模型生成的是文本,不是数据

大模型本质上是个文本生成器,它不"理解"JSON 格式,只是模仿训练数据里见过的 JSON 样式。这意味着它随时可能:

  • 在 JSON 前后加上 ```````json```` 代码块标记
  • 输出注释(// 这里是姓名),导致 JSON 解析失败
  • 字段名用中文而你期望英文
  • 嵌套层级不对
  • 用单引号代替双引号
  • 数字字段输出成字符串
python 复制代码
# 你发送的 prompt
"提取以下文章的标题、作者、发布日期,以 JSON 格式返回"

# 模型可能返回的各种"惊喜"
"""
当然!以下是提取的信息:
```json
{
  'title': '深度学习入门',  // 单引号!
  'author': '张三',
  'date': '2025年1月1日'   // 末尾没逗号
}

"""

ini 复制代码
上面这段内容,`json.loads()` 直接报错。

接下来我们看三种解决方案,从简单到可靠依次展开。

---

## 方案一:Prompt 工程(不推荐用于生产)

最直觉的做法:在 prompt 里要求模型输出 JSON。

```python
import openai
import json

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

def extract_with_prompt(text: str) -> dict:
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {
                "role": "system",
                "content": (
                    "你是一个信息提取助手。"
                    "请严格以 JSON 格式返回结果,不要包含任何其他文字,不要使用 Markdown 代码块。"
                    "必须使用双引号,不能有注释,不能有尾随逗号。"
                )
            },
            {
                "role": "user",
                "content": f"从以下文本中提取:标题(title)、作者(author)、日期(date)\n\n{text}"
            }
        ]
    )

    content = response.choices[0].message.content.strip()

    # 还得加一堆防御性清理
    if content.startswith("```"):
        content = content.split("```")[1]
        if content.startswith("json"):
            content = content[4:]
    content = content.strip()

    return json.loads(content)

问题:即使加了再多限制,模型偶尔还是会"创意发挥"。线上跑一段时间,总会遇到解析失败的情况,而且失败率随着 prompt 复杂度上升。这种方案在测试环境看起来不错,但在生产环境会让你持续救火。


方案二:json_object 模式(基本可靠)

几乎所有主流大模型都支持 response_format: { type: "json_object" } 参数,开启后模型保证输出合法的 JSON 对象。

python 复制代码
def extract_json_object(text: str) -> dict:
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {
                "role": "system",
                "content": "你是信息提取助手,以 JSON 格式返回提取结果。"
            },
            {
                "role": "user",
                "content": f"提取 title、author、date 字段:\n\n{text}"
            }
        ],
        response_format={"type": "json_object"}  # 关键参数
    )

    content = response.choices[0].message.content
    return json.loads(content)  # 这次可以直接 parse,不会抛异常

优点 :JSON 格式本身有保证,json.loads() 不会失败。 缺点 :字段名、字段类型、是否必填------这些都没有约束。模型可能返回 {"标题": "...", "作者": "..."} 而不是 {"title": "...", "author": "..."},或者多出来几个你不需要的字段。


方案三:json_schema 模式(最可靠,推荐)

这是目前最强的方案:你提供一个 JSON Schema,模型的输出会严格符合这个 schema------包括字段名、类型、是否必填、枚举值等。

python 复制代码
def extract_with_schema(text: str) -> dict:
    schema = {
        "type": "object",
        "properties": {
            "title": {
                "type": "string",
                "description": "文章标题"
            },
            "author": {
                "type": "string",
                "description": "作者姓名"
            },
            "date": {
                "type": "string",
                "description": "发布日期,格式 YYYY-MM-DD"
            },
            "summary": {
                "type": "string",
                "description": "文章摘要,100字以内"
            },
            "tags": {
                "type": "array",
                "items": {"type": "string"},
                "description": "文章标签列表"
            }
        },
        "required": ["title", "author", "date"],
        "additionalProperties": False  # 不允许多余字段
    }

    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {"role": "system", "content": "你是信息提取助手。"},
            {"role": "user", "content": f"提取文章信息:\n\n{text}"}
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "article_info",
                "strict": True,
                "schema": schema
            }
        }
    )

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

输出结构完全可预期,字段名、类型都精确匹配 schema,后续代码直接用,不需要做任何防御性处理。


国产模型支持情况

模型 json_object json_schema 备注
DeepSeek-V3 / R1 支持 支持 strict 模式可用
Qwen-Max / Plus 支持 支持 通过 response_format 参数
Qwen-Turbo 支持 部分支持 schema 复杂时效果不稳定
GLM-4 支持 支持 需要较新版本 API
Moonshot (Kimi) 支持 开发中 目前建议用 json_object
Yi-Large 支持 部分支持

各模型对 json_schema 的支持程度参差不齐,在多模型场景下需要维护一份兼容表格。笔者在 TheRouter 中做了统一处理:网关层会自动检测目标模型的能力,将 json_schema 请求适配成对应模型支持的格式,调用方只需传一份 schema,跨模型兼容由网关透明处理。

实践建议

  • 字段结构简单(3--5个字段)→ json_object 足够
  • 有嵌套结构、枚举值、数组 → 一定用 json_schema
  • 确认目标模型支持 json_schema 再用,否则 fallback 到 json_object

实战场景一:商品信息提取

python 复制代码
from pydantic import BaseModel, Field
from typing import Optional, List
import json

# 用 Pydantic 定义数据模型
class ProductInfo(BaseModel):
    name: str = Field(description="商品名称")
    price: float = Field(description="价格,单位元")
    category: str = Field(description="商品分类")
    brand: Optional[str] = Field(default=None, description="品牌名称")
    features: List[str] = Field(description="核心功能特点列表")
    in_stock: bool = Field(description="是否有货")

# Pydantic 模型自动转换为 JSON Schema
def pydantic_to_schema(model_class) -> dict:
    schema = model_class.model_json_schema()
    # Pydantic 生成的 schema 里 $defs 需要做内联处理(简单场景不需要)
    return schema

product_schema = pydantic_to_schema(ProductInfo)

def extract_product(description: str) -> ProductInfo:
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {"role": "system", "content": "从商品描述中提取结构化信息。"},
            {"role": "user", "content": description}
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "product_info",
                "strict": True,
                "schema": product_schema
            }
        }
    )

    data = json.loads(response.choices[0].message.content)
    return ProductInfo(**data)


# 测试
desc = """
小米14 Ultra 全面发布!搭载骁龙8 Gen3处理器,
6.73寸2K AMOLED屏幕,徕卡影像系统,
5000mAh电池支持90W快充。
官方定价5999元,现货发售中。
"""

product = extract_product(desc)
print(f"商品名: {product.name}")
print(f"价格: ¥{product.price}")
print(f"分类: {product.category}")
print(f"特点: {', '.join(product.features)}")
print(f"有货: {'是' if product.in_stock else '否'}")

实战场景二:文章摘要结构化

python 复制代码
class ArticleSummary(BaseModel):
    title: str
    one_sentence_summary: str = Field(description="一句话总结,不超过50字")
    key_points: List[str] = Field(description="3-5个核心要点")
    sentiment: str = Field(description="情感倾向", pattern="^(positive|negative|neutral)$")
    difficulty_level: int = Field(description="技术难度 1-5分", ge=1, le=5)

def summarize_article(article_text: str) -> ArticleSummary:
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {
                "role": "system",
                "content": "你是专业的文章分析助手,提供客观准确的摘要分析。"
            },
            {
                "role": "user",
                "content": f"分析以下文章:\n\n{article_text}"
            }
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "article_summary",
                "strict": True,
                "schema": ArticleSummary.model_json_schema()
            }
        }
    )

    data = json.loads(response.choices[0].message.content)
    return ArticleSummary(**data)

注意 sentiment 字段用了 pattern 约束,只允许三种值。json_schema 模式会强制模型在这三个值中选一个,不会输出其他内容。


错误处理与 Fallback 策略

即使用了 json_schema,生产环境也要做防御性处理:

python 复制代码
import logging
from typing import TypeVar, Type

T = TypeVar("T", bound=BaseModel)

def safe_structured_extract(
    client: openai.OpenAI,
    model: str,
    messages: list,
    output_model: Type[T],
    fallback_value: T = None,
    max_retries: int = 2
) -> T:
    """
    带重试和 fallback 的结构化提取
    """
    schema = output_model.model_json_schema()

    for attempt in range(max_retries + 1):
        try:
            response = client.chat.completions.create(
                model=model,
                messages=messages,
                response_format={
                    "type": "json_schema",
                    "json_schema": {
                        "name": output_model.__name__.lower(),
                        "strict": True,
                        "schema": schema
                    }
                }
            )

            content = response.choices[0].message.content

            # 检查是否因 token 截断导致输出不完整
            finish_reason = response.choices[0].finish_reason
            if finish_reason == "length":
                raise ValueError("输出被截断,JSON 不完整,请增大 max_tokens")

            data = json.loads(content)
            return output_model(**data)

        except json.JSONDecodeError as e:
            logging.warning(f"JSON 解析失败 (attempt {attempt+1}): {e}")
            if attempt == max_retries:
                if fallback_value is not None:
                    return fallback_value
                raise

        except Exception as e:
            logging.error(f"结构化提取失败 (attempt {attempt+1}): {e}")
            if attempt == max_retries:
                if fallback_value is not None:
                    return fallback_value
                raise

    return fallback_value  # 理论上不会到这里

几个重要的 fallback 场景:

  1. finish_reason == "length" :输出被 max_tokens 截断,JSON 不完整------这种情况要加大 max_tokens 或截短输入
  2. 模型不支持 json_schema :捕获 API 错误,降级到 json_object 模式
  3. Pydantic 验证失败:数据解析成功但不符合业务约束,触发重试或返回默认值

Pydantic → JSON Schema 的进阶技巧

Pydantic v2 的 model_json_schema() 生成的 schema 和 OpenAI strict 模式要求的有些细微差异,需要做预处理:

python 复制代码
def prepare_schema_for_strict_mode(schema: dict) -> dict:
    """
    将 Pydantic 生成的 schema 处理成兼容 strict 模式的格式
    """
    import copy
    schema = copy.deepcopy(schema)

    def process_node(node: dict):
        # strict 模式要求所有 object 都声明 additionalProperties: false
        if node.get("type") == "object":
            node["additionalProperties"] = False
            # 确保 properties 里的每个字段都在 required 里
            if "properties" in node and "required" not in node:
                node["required"] = list(node["properties"].keys())

        # 递归处理嵌套结构
        for key in ["properties", "items", "anyOf", "allOf"]:
            if key in node:
                if isinstance(node[key], dict):
                    for v in node[key].values():
                        if isinstance(v, dict):
                            process_node(v)
                elif isinstance(node[key], list):
                    for item in node[key]:
                        if isinstance(item, dict):
                            process_node(item)

    process_node(schema)
    return schema


# 使用
raw_schema = MyModel.model_json_schema()
strict_schema = prepare_schema_for_strict_mode(raw_schema)

三种方案总结对比

方案 可靠性 字段约束 实现复杂度 适用场景
Prompt 工程 低(80-90%) 原型验证、非关键路径
json_object 高(>99%) 简单结构、快速开发
json_schema 极高(>99.9%) 生产环境、关键业务

最终建议

  • 生产环境一律使用 json_schema + Pydantic 组合
  • 用 Pydantic 定义数据模型,自动生成 schema,顺便得到类型检查和验证
  • 加重试逻辑和 fallback,不要假设大模型永远正常
  • finish_reason == "length" 是最容易被忽视的坑,一定要检查

结构化输出一旦做对了,大模型就从一个"可能返回 JSON"的黑盒,变成了一个真正可靠的数据处理组件。


作者:TheRouter 开发者,专注 AI 模型路由网关。项目主页:therouter.ai

相关推荐
Entropy-Go2 小时前
一图了解AI热门词汇 - OpenClaw/Prompt/Agent/Skill/MCP/LLM/GPU
人工智能·agent·skill·mcp·openclaw
惠惠软件2 小时前
AI 龙虾 | 对学习工作的影响和未来前瞻
人工智能·学习
是糖糖啊2 小时前
Agent 不好用?先别怪模型,试试 Harness Engineering
人工智能·设计模式
X在敲AI代码2 小时前
女娲补天系列--深度学习
人工智能·深度学习
AI精钢2 小时前
从 Prompt Engineering 到 Harness Engineering:AI 系统竞争,正在从“会写提示词”转向“会搭执行框架”
人工智能·prompt·devops·ai agent·ai engineering
大灰狼来喽2 小时前
OpenClaw 自动化工作流实战:用 Hooks + 定时任务 + Multi-MCP 构建“数字员工“
大数据·运维·人工智能·自动化·aigc·ai编程
盼小辉丶2 小时前
PyTorch实战(38)——深度学习模型可解释性
人工智能·pytorch·深度学习
你们补药再卷啦2 小时前
AgentSkills(2/4)笔记
人工智能
温九味闻醉2 小时前
Meta | HSTU:生成式推荐工业级方案
人工智能·深度学习·机器学习