问题根源:模型生成的是文本,不是数据
大模型本质上是个文本生成器,它不"理解"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 场景:
finish_reason == "length":输出被max_tokens截断,JSON 不完整------这种情况要加大max_tokens或截短输入- 模型不支持
json_schema:捕获 API 错误,降级到json_object模式 - 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