结构化输出-让LLM返回可靠的JSON
> **本文适合谁**
结构化输出:让 LLM 返回可靠的 JSON
本文适合谁
需要让 LLM 的输出被程序处理(存入数据库、传给下一步骤、解析成对象)的开发者。从最简单的 Prompt 约束到最可靠的 JSON 模式 + pydantic 验证,本文给出渐进式的解决方案。
LLM 返回的永远是字符串。这句话写出来很简单,但真正理解它的含义需要从实际问题出发。
在 Prompt 里加上"请以 JSON 格式返回",模型的输出可能是这样的:
当然,这是你要的 JSON 数据:
```json
{"title": "...", "summary": "..."}
希望这对你有帮助!
然后 `json.loads()` 直接抛异常了。
这不是偶发问题。LLM 天生是文本生成模型,它不理解"返回 JSON"和"回复一段包含 JSON 的文字"有什么区别。结构化输出就是为了从根本上解决这个问题。
---
## 为什么需要结构化输出:先建立直觉
<pre class="mermaid" data-mermaid-index="0"></pre>
*结构化输出完整流程——从 Prompt 设计到 Pydantic 验证,每步都有明确职责*
想象两种场景:
**自由文本的麻烦**:
```python
# 这样的输出没法直接用
response = "好的,根据你的描述,这个产品的评分是 4.5 分,主要优点是性价比高,缺点是发货慢。"
# 你需要写正则表达式或者更复杂的解析逻辑才能提取出 4.5、优点列表、缺点列表
结构化输出的优势:
# 这样的输出可以直接解析
response = '{"score": 4.5, "pros": ["性价比高"], "cons": ["发货慢"]}'
import json
data = json.loads(response) # 直接完成,不需要任何解析逻辑
当 LLM 的输出需要被程序处理时,结构化输出是工程化的基础。
方式一:Prompt 约束(最简单,但不可靠)
这段代码在做什么
在 Prompt 中明确要求 JSON 格式,并给出示例结构。最简单,但模型可能在 JSON 前后加解释文字。
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com"
)
def extract_product_info(review_text: str) -> dict | None:
"""从评论文本中提取结构化信息(Prompt约束方式)"""
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=512,
temperature=0,
messages=[
{
"role": "system",
"content": "你是一个数据提取助手。从用户提供的产品评论中提取信息,只返回 JSON,不要有任何其他文字。"
},
{
"role": "user",
"content": f"""从以下评论中提取信息,返回 JSON 格式:
{{
"score": 评分(1-5的数字),
"pros": [优点列表],
"cons": [缺点列表],
"sentiment": "positive/negative/neutral"
}}
评论:{review_text}"""
}
]
)
text = response.choices[0].message.content.strip()
# 清理可能的 markdown 代码块
if text.startswith("```"):
text = text.split("```")[1]
if text.startswith("json"):
text = text[4:]
try:
return json.loads(text.strip())
except json.JSONDecodeError:
return None
result = extract_product_info("这款耳机音质很好,但充电盒做工一般,总体给4分。")
print(result)
你应该看到什么输出
{'score': 4, 'pros': ['音质很好'], 'cons': ['充电盒做工一般'], 'sentiment': 'positive'}
局限:模型有时会在 JSON 前后加解释文字,需要做清理,仍然可能失败。
方式二:JSON 模式(`response_format`,推荐)
这段代码在做什么
使用 response_format={"type": "json_object"} 参数,强制模型输出合法 JSON,不会混入其他文字。比 Prompt 约束更可靠。
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com"
)
def extract_product_info_json_mode(review_text: str) -> dict:
"""使用 JSON 模式强制输出结构化数据"""
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=512,
temperature=0,
response_format={"type": "json_object"}, # 关键:开启 JSON 模式
messages=[
{
"role": "system",
"content": "你是一个数据提取助手,只返回 JSON。" # 必须提到 JSON
},
{
"role": "user",
"content": f"""从以下评论中提取信息,返回包含以下字段的 JSON:
- score: 评分(1-5的数字)
- pros: 优点列表(字符串数组)
- cons: 缺点列表(字符串数组)
- sentiment: 情感(positive/negative/neutral)
评论:{review_text}"""
}
]
)
# JSON 模式下无需清理,直接解析——这是与方式一的最大区别
return json.loads(response.choices[0].message.content)
result = extract_product_info_json_mode("这款耳机音质很好,但充电盒做工一般,总体给4分。")
print(result)
你应该看到什么输出
{'score': 4, 'pros': ['音质很好'], 'cons': ['充电盒做工一般'], 'sentiment': 'positive'}
注意事项:
- 开启 JSON 模式时,System Prompt 或 User Prompt 中必须提及"JSON",否则部分模型会报错
- JSON 模式只保证输出是合法 JSON,不保证字段结构与你预期一致,仍需在 Prompt 中描述清楚字段
方式三:Pydantic 验证(最可靠)
这段代码在做什么
结合 pydantic 定义期望的数据结构,在解析 JSON 时自动验证类型和字段。如果字段不对或类型错误,会直接抛出异常,而不是悄悄产生错误的数据。
import os
import json
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Literal
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com"
)
# 定义期望的输出结构
class ProductReview(BaseModel):
score: float = Field(description="评分,1-5之间的数字", ge=1, le=5)
pros: List[str] = Field(description="优点列表")
cons: List[str] = Field(description="缺点列表")
sentiment: Literal["positive", "negative", "neutral"] = Field(
description="整体情感倾向"
)
def extract_review(text: str) -> ProductReview:
"""提取评论信息,用 pydantic 做类型验证"""
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=512,
temperature=0,
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": "只返回 JSON,不要其他文字。"
},
{
"role": "user",
"content": (
f"从评论中提取:score(1-5数字), pros(列表), cons(列表), "
f"sentiment(positive/negative/neutral)\n评论:{text}"
)
}
]
)
raw = response.choices[0].message.content
data = json.loads(raw)
return ProductReview(**data) # pydantic 自动验证类型
# 使用示例
review = extract_review("音质不错,但价格偏贵,给3分")
print(f"评分: {review.score}") # 3.0(float类型,自动转换)
print(f"优点: {review.pros}") # ['音质不错']
print(f"情感: {review.sentiment}") # 只能是 positive/negative/neutral
你应该看到什么输出
评分: 3.0
优点: ['音质不错']
情感: negative
为什么用 pydantic:pydantic 不只是做 JSON 解析,它会验证类型、验证字段范围(ge=1, le=5 确保评分在 1-5 之间)、验证枚举值是否合法。如果模型输出了 "sentiment": "好" 而不是合法的枚举值,pydantic 会立即报错,而不是让错误数据悄悄流入后续逻辑。
处理 JSON 解析失败
即使使用 JSON 模式,也可能偶发解析失败。加入容错处理:
import json
import re
def safe_parse_json(text: str) -> dict | None:
"""
这段代码在做什么:
多种策略尝试从模型输出中解析 JSON
先直接解析,失败了再尝试提取代码块内的 JSON
"""
text = text.strip()
# 先尝试直接解析
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# 清理 markdown 代码块
if "```json" in text:
text = text.split("```json")[1].split("```")[0].strip()
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# 尝试提取第一个 JSON 对象
match = re.search(r'\{.*\}', text, re.DOTALL)
if match:
try:
return json.loads(match.group())
except json.JSONDecodeError:
pass
return None # 所有方法都失败了
带重试的生产级实现
import os
import json
import time
from openai import OpenAI
from pydantic import BaseModel
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com"
)
def extract_with_retry(
messages: list,
model_class: type[BaseModel],
max_retries: int = 3
) -> BaseModel | None:
"""
这段代码在做什么:
带重试逻辑的结构化提取
解析失败时自动重试,最多重试3次
"""
for attempt in range(max_retries):
try:
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=1024,
temperature=0,
response_format={"type": "json_object"},
messages=messages
)
raw = response.choices[0].message.content
data = json.loads(raw)
return model_class(**data)
except (json.JSONDecodeError, Exception) as e:
if attempt < max_retries - 1:
print(f"第 {attempt + 1} 次尝试失败({e}),重试中...")
time.sleep(1)
else:
print(f"所有重试均失败:{e}")
return None
选择哪种方法
| 方法 | 可靠性 | 代码复杂度 | 适用场景 |
|---|---|---|---|
| Prompt 约束 | 中 | 简单 | 简单结构,对偶发错误容忍 |
| JSON 模式(response_format) | 高 | 中等 | 需要严格 JSON 格式的生产场景 |
| pydantic 验证 | 高(有校验) | 中等 | 需要类型安全的数据提取 |
| 带重试 | 最高 | 较复杂 | 高可靠性要求的生产场景 |
推荐组合:JSON 模式 + pydantic 验证 + 重试逻辑,是生产环境中最可靠的方案。
常见错误和解决方法
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
json.JSONDecodeError |
模型输出了非 JSON 内容 | 使用 JSON 模式 |
| JSON 模式下模型报错 | Prompt 中没有提到 "JSON" | 在 System Prompt 中明确说"以 JSON 格式输出" |
pydantic ValidationError |
模型输出了错误的字段名或类型 | 在 Prompt 中明确描述字段名和类型 |
| 字段名与预期不符 | Prompt 描述不够精确 | 给出完整的字段名示例或 JSON schema |
本篇速查卡片
好的结构化 Prompt vs 差的结构化 Prompt:
差的做法(太模糊):
请以 JSON 格式返回分析结果
好的做法(明确字段):
以 JSON 格式返回,包含以下字段:
- score: 数字(1-5)
- pros: 字符串数组(优点)
- cons: 字符串数组(缺点)
- sentiment: 字符串(positive/negative/neutral)
选择原则:原型阶段用 Prompt 约束;生产环境用 JSON 模式 + pydantic;高可靠性场景加重试逻辑。