让模型输出 JSON,说起来简单,实际上能把人逼疯。这篇文章讲清楚三种方案的差别,以及怎么通过 TheRouter 在多个模型上统一使用。


问题:大模型输出 JSON 有多不可靠?

你让模型"以 JSON 格式输出",它可能给你:

  • 正确的 JSON —— 大多数时候
  • JSON 前后加上 Markdown 代码块(` ```json … ````)—— 经常
  • 字段名拼错,或者多了/少了字段 —— 偶尔
  • 直接用自然语言回答,完全忘了 JSON 要求 —— 偶发
  • 输出截断,JSON 不完整 —— 长内容时容易出现

这不是偶然,是模型的训练目标决定的——模型被训练成"有帮助地回答问题",而不是"严格按 schema 输出"。

解决方案有三个层次,稳定性依次递增。


方案 1:Prompt 工程

最简单的做法:在 prompt 里加一句"请以 JSON 格式输出"。

from openai import OpenAI

client = OpenAI(
    api_key="tr-...",
    base_url="https://api.therouter.ai/v1"
)

response = client.chat.completions.create(
    model="anthropic/claude-sonnet-4",
    messages=[
        {
            "role": "system",
            "content": "你是一个信息提取助手。请严格以 JSON 格式输出,不要包含任何其他内容。"
        },
        {
            "role": "user",
            "content": """从以下文章中提取关键信息,输出格式:
{
  "title": "文章标题",
  "author": "作者",
  "summary": "100字以内摘要",
  "tags": ["标签1", "标签2"]
}

文章内容:
苹果公司昨日发布了最新款 MacBook Pro,搭载 M4 Max 芯片,由首席执行官蒂姆·库克亲自发布...
"""
        }
    ]
)

import json
try:
    # 有时输出带代码块,需要清理
    content = response.choices[0].message.content.strip()
    if content.startswith("```"):
        content = content.split("```")[1]
        if content.startswith("json"):
            content = content[4:]
    result = json.loads(content)
    print(result)
except json.JSONDecodeError as e:
    print(f"解析失败: {e}")
    print(f"原始输出: {response.choices[0].message.content}")

优点:零配置,所有模型都支持。

缺点:不稳定。复杂任务时模型容易"忘记"格式要求,长输出时 JSON 可能被截断,需要写额外的清理代码。

适合场景:原型验证、对格式要求不严格的内部工具。


方案 2:json_object 模式

OpenAI 引入了 response_format 参数,设置 type: "json_object" 后,模型保证输出合法 JSON(不会有代码块、不会截断)。

response = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[
        {
            "role": "system",
            "content": "你是一个商品信息提取助手,以 JSON 格式输出提取结果。"
        },
        {
            "role": "user",
            "content": "提取以下商品信息:iPhone 16 Pro Max 256GB 黑色,原价 9999 元,现价 8999 元,库存 50 件。"
        }
    ],
    response_format={"type": "json_object"}
)

import json
result = json.loads(response.choices[0].message.content)
print(result)
# 输出一定是合法 JSON,但字段名和结构由模型决定
# {'name': 'iPhone 16 Pro Max', 'storage': '256GB', 'color': '黑色',
#  'original_price': 9999, 'current_price': 8999, 'stock': 50}

优点:输出一定是合法 JSON,不用写清理代码,json.loads() 不会抛异常。

缺点:字段结构不受控,字段名可能每次不同(比如 price vs current_price vs sale_price),字段可能缺失或多余,类型不保证(数字可能输出成字符串)。

注意:使用 json_object 时,system prompt 或 user message 里必须提到 “JSON”,否则部分模型会报错。

支持情况:GPT-4o、GPT-4.1、DeepSeek V3/R1 等支持,Claude 目前不支持此参数(通过 TheRouter 会自动 fallback 到 prompt 注入)。


方案 3:json_schema 模式(推荐)

这是 OpenAI 在 2024 年底推出的 Structured Outputs 功能,也是最可靠的方案。你提供精确的 JSON Schema,模型保证输出完全符合 schema 的结构。

response = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[
        {
            "role": "system",
            "content": "你是一个文章信息提取助手。"
        },
        {
            "role": "user",
            "content": """提取以下文章的结构化信息:

标题:《2025年AI编程工具全景报告》
作者:张伟,来自字节跳动AI实验室
发布于:2025年3月15日
正文:随着Claude 3.7、GPT-4.1等新一代模型的发布,AI编程助手正在经历新一轮革命...
标签:AI、编程、工具、2025
"""
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "article_info",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "title": {
                        "type": "string",
                        "description": "文章标题"
                    },
                    "author": {
                        "type": "string",
                        "description": "作者姓名"
                    },
                    "organization": {
                        "type": ["string", "null"],
                        "description": "作者所在机构,不明确时为 null"
                    },
                    "published_date": {
                        "type": "string",
                        "description": "发布日期,格式 YYYY-MM-DD"
                    },
                    "summary": {
                        "type": "string",
                        "description": "100字以内的文章摘要"
                    },
                    "tags": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "文章标签列表"
                    }
                },
                "required": ["title", "author", "organization", "published_date", "summary", "tags"],
                "additionalProperties": False
            }
        }
    }
)

import json
result = json.loads(response.choices[0].message.content)
# 输出保证严格符合 schema
# {
#   "title": "《2025年AI编程工具全景报告》",
#   "author": "张伟",
#   "organization": "字节跳动AI实验室",
#   "published_date": "2025-03-15",
#   "summary": "...",
#   "tags": ["AI", "编程", "工具", "2025"]
# }

注意 "strict": True"additionalProperties": False 是关键——这两个参数让模型不能输出 schema 之外的字段,也不能缺少 required 里的字段。


用 Pydantic 自动生成 JSON Schema

手写 JSON Schema 很繁琐,Pydantic 可以自动生成:

from pydantic import BaseModel, Field
from typing import Optional
import json
from openai import OpenAI

client = OpenAI(
    api_key="tr-...",
    base_url="https://api.therouter.ai/v1"
)

# 定义数据模型
class ProductInfo(BaseModel):
    name: str = Field(description="商品名称")
    brand: str = Field(description="品牌")
    price: float = Field(description="价格,单位元")
    original_price: Optional[float] = Field(None, description="原价,无折扣时为 null")
    specs: dict[str, str] = Field(description="规格参数,如 {'颜色': '黑色', '存储': '256GB'}")
    in_stock: bool = Field(description="是否有货")

class ArticleSummary(BaseModel):
    title: str
    key_points: list[str] = Field(description="3-5个核心要点")
    sentiment: str = Field(description="情感倾向:positive/negative/neutral")
    word_count_estimate: int = Field(description="预估原文字数")

def structured_extract(model_class: type[BaseModel], user_message: str, model: str = "openai/gpt-4o"):
    """通用结构化提取函数"""
    schema = model_class.model_json_schema()

    # Pydantic v2 的 schema 需要小调整以适配 OpenAI strict 模式
    # additionalProperties 必须为 False
    schema["additionalProperties"] = False

    response = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": user_message}],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": model_class.__name__,
                "strict": True,
                "schema": schema
            }
        }
    )

    return model_class.model_validate_json(response.choices[0].message.content)

# 提取商品信息
product = structured_extract(
    ProductInfo,
    "提取商品信息:AirPods Pro 第三代,苹果品牌,现价1799元原价1999元,黑色,耳机类型入耳式,有货。"
)
print(f"商品: {product.name}, 价格: {product.price}, 折扣: {product.original_price}")

# 提取文章摘要
summary = structured_extract(
    ArticleSummary,
    "分析这篇文章并提取信息:《DeepSeek R2 发布,性能超越 GPT-4.1》——DeepSeek 今日发布...",
    model="deepseek/deepseek-v3"  # 换个模型
)
print(f"要点: {summary.key_points}")

各模型 Structured Outputs 支持情况

通过 TheRouter 调用时,不同模型对 json_schema 模式的支持情况:

模型json_objectjson_schema (strict)说明
openai/gpt-4o原生支持
openai/gpt-4.1原生支持
openai/gpt-4o-mini原生支持
anthropic/claude-sonnet-4⚠️⚠️TheRouter 转换为 prompt 约束
anthropic/claude-opus-4⚠️⚠️TheRouter 转换为 prompt 约束
google/gemini-2.5-pro原生支持
deepseek/deepseek-v3原生支持
deepseek/deepseek-r1⚠️推理模型,strict 效果略差

说明:Claude 目前不原生支持 response_format 参数,TheRouter 会将 schema 注入到 system prompt 中实现类似效果,稳定性略低于原生支持的模型。对 schema 严格性要求高的场景,建议优先选 GPT-4o 或 Gemini 2.5 Pro。


错误处理与 Fallback 策略

即使用了 json_schema 模式,生产环境仍需要完善的错误处理:

from pydantic import BaseModel, ValidationError
import json
import logging

logger = logging.getLogger(__name__)

def safe_structured_extract(
    client,
    model_class: type[BaseModel],
    messages: list[dict],
    model: str = "openai/gpt-4o",
    fallback_model: str = "openai/gpt-4o-mini",
    max_retries: int = 2
) -> BaseModel | None:
    """带重试和 fallback 的结构化提取"""

    schema = model_class.model_json_schema()
    schema["additionalProperties"] = False

    for attempt in range(max_retries):
        current_model = model if attempt == 0 else fallback_model
        try:
            response = client.chat.completions.create(
                model=current_model,
                messages=messages,
                response_format={
                    "type": "json_schema",
                    "json_schema": {
                        "name": model_class.__name__,
                        "strict": True,
                        "schema": schema
                    }
                }
            )

            # 检查 finish_reason,length 说明被截断
            finish_reason = response.choices[0].finish_reason
            if finish_reason == "length":
                logger.warning(f"输出被截断 (model={current_model},attempt={attempt})")
                continue

            raw = response.choices[0].message.content
            result = model_class.model_validate_json(raw)
            return result

        except json.JSONDecodeError as e:
            logger.warning(f"JSON 解析失败 attempt={attempt}: {e}")
        except ValidationError as e:
            # Schema 校验失败——说明模型没有严格遵守 schema
            logger.warning(f"Schema 校验失败 attempt={attempt}: {e}")
        except Exception as e:
            logger.error(f"请求失败 attempt={attempt}: {e}")

    logger.error(f"所有重试均失败,model={model}")
    return None


# 使用示例
class OrderInfo(BaseModel):
    order_id: str
    customer_name: str
    items: list[str]
    total_amount: float
    status: str

result = safe_structured_extract(
    client,
    OrderInfo,
    messages=[{
        "role": "user",
        "content": "提取订单信息:订单号 ORD-2025-001,客户张三,购买了MacBook Pro和AirPods,总计15798元,已发货。"
    }]
)

if result:
    print(f"订单 {result.order_id},客户 {result.customer_name},金额 {result.total_amount}")
else:
    print("提取失败,需要人工处理")

实际场景:API 响应标准化

一个实际用途:让大模型把非结构化的用户反馈转成标准化的结构,便于下游处理:

from pydantic import BaseModel
from typing import Literal
from enum import Enum

class FeedbackCategory(str, Enum):
    BUG = "bug"
    FEATURE_REQUEST = "feature_request"
    COMPLAINT = "complaint"
    PRAISE = "praise"
    QUESTION = "question"

class ParsedFeedback(BaseModel):
    category: FeedbackCategory
    priority: Literal["low", "medium", "high", "critical"]
    product_area: str
    summary: str
    action_required: bool
    suggested_assignee: Literal["engineering", "product", "support", "none"]

def parse_user_feedback(feedback_text: str) -> ParsedFeedback:
    schema = ParsedFeedback.model_json_schema()
    schema["additionalProperties"] = False

    response = client.chat.completions.create(
        model="openai/gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "你是一个客服工单分类助手,将用户反馈分类为标准化格式。"
            },
            {"role": "user", "content": f"分类以下用户反馈:\n\n{feedback_text}"}
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "ParsedFeedback",
                "strict": True,
                "schema": schema
            }
        }
    )

    return ParsedFeedback.model_validate_json(response.choices[0].message.content)

# 测试
feedback = parse_user_feedback(
    "我的账单页面一直报错500,已经持续两天了,严重影响我的业务运营,急需处理!!"
)
print(f"类别: {feedback.category.value}")      # bug
print(f"优先级: {feedback.priority}")           # critical
print(f"需要行动: {feedback.action_required}")  # True
print(f"分配给: {feedback.suggested_assignee}") # engineering

三种方案对比总结

维度Prompt 工程json_objectjson_schema (strict)
配置复杂度最低
输出稳定性
字段结构保证严格保证
类型安全保证
模型兼容性全部大部分GPT/Gemini/DeepSeek
适用场景原型/内部工具简单集成生产环境

生产环境的建议:用 Pydantic 定义数据模型 + json_schema strict 模式 + 错误重试,对于不支持原生 json_schema 的模型(如 Claude),通过 TheRouter 的 prompt 转换也能达到不错的稳定性。

需要在多个模型之间切换时,TheRouter 的统一接口让你不用为每个模型写不同的适配代码——同一套 Pydantic 模型,同一个 API 调用,换个 model 参数就行。


  • 注册地址:therouter.ai
  • API Base:https://api.therouter.ai/v1
  • 支持国内直连,不需要代理

更多推荐