结构化输出与预填充技巧
LLM 的默认输出是自由文本。但在实际应用中,经常需要模型输出 JSON(一种常见的数据格式,用大括号和键值对组织数据,例如 `{"name": "张三", "age": 25}`)、特定格式的数据,或者以特定开头开始回答。这篇讲几种让模型输出可预测格式的技术:Prompt 约束、JSON 模式(response_format)、以及 pydantic 验证。
结构化输出与 JSON 模式
LLM 的默认输出是自由文本。但在实际应用中,经常需要模型输出 JSON(一种常见的数据格式,用大括号和键值对组织数据,例如 {"name": "张三", "age": 25})、特定格式的数据,或者以特定开头开始回答。这篇讲几种让模型输出可预测格式的技术:Prompt 约束、JSON 模式(response_format)、以及 pydantic 验证。
为什么需要这一篇:当 LLM 的输出需要被程序处理时,自由文本是个障碍——你必须解析它,而解析自由文本既麻烦又脆弱。结构化输出让 LLM 成为可靠的数据处理工具,直接 json.loads() 就完事了。
为什么需要结构化输出
JSON Mode、Function Calling、预填充三种结构化输出方式的可靠性与适用场景对比
# 这样的输出没法直接用
response = "好的,根据你的描述,这个产品的评分是 4.5 分,主要优点是性价比高,缺点是发货慢。"
# 这样的输出可以直接解析
response = '{"score": 4.5, "pros": ["性价比高"], "cons": ["发货慢"]}'
当 LLM 的输出需要被程序处理(存入数据库、传给下一个步骤、渲染成特定格式)时,自由文本是个障碍。结构化输出让 LLM 成为可靠的数据处理工具。
方法一:Prompt 约束
最简单的方法:在 Prompt 中明确要求 JSON 格式,并给出示例。
import os
import json
from openai import OpenAI
client = OpenAI(
base_url="https://api.deepseek.com",
api_key=os.environ.get("DEEPSEEK_API_KEY")
)
def extract_product_info(review_text: str) -> dict:
"""从评论文本中提取结构化信息"""
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=512,
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:]
return json.loads(text)
result = extract_product_info("这款耳机音质很好,但充电盒做工一般,总体给4分。")
print(result)
# {'score': 4, 'pros': ['音质很好'], 'cons': ['充电盒做工一般'], 'sentiment': 'positive'}
局限:模型有时会在 JSON 前后加解释文字,需要做清理。
方法二:JSON 模式(response_format)
DeepSeek 支持 OpenAI 兼容的 response_format 参数,设置 type: "json_object" 后模型会强制输出合法 JSON,不会混入其他文字,比 Prompt 约束更可靠。
import os
import json
from openai import OpenAI
client = OpenAI(
base_url="https://api.deepseek.com",
api_key=os.environ.get("DEEPSEEK_API_KEY")
)
def extract_product_info_json_mode(review_text: str) -> dict:
"""使用 JSON 模式强制输出结构化数据"""
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=512,
response_format={"type": "json_object"}, # 开启 JSON 模式
messages=[
{
"role": "system",
"content": "你是一个数据提取助手,只返回 JSON。"
},
{
"role": "user",
"content": f"""从以下评论中提取信息,返回包含 score(1-5数字)、pros(优点列表)、cons(缺点列表)、sentiment(positive/negative/neutral) 的 JSON:
评论:{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(一个 Python 数据验证库,可以定义数据结构并自动检查类型是否正确),可以在解析 JSON 时自动验证结构:
import os
import json
from openai import OpenAI
from pydantic import BaseModel
from typing import List
client = OpenAI(
base_url="https://api.deepseek.com",
api_key=os.environ.get("DEEPSEEK_API_KEY")
)
class ProductReview(BaseModel):
score: float
pros: List[str]
cons: List[str]
sentiment: str
def extract_review(text: str) -> ProductReview:
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=512,
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": "只返回 JSON,不要其他文字。"
},
{
"role": "user",
"content": f"从评论中提取:score(1-5), pros(列表), cons(列表), 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(review.score) # 3.0
print(review.pros) # ['音质不错']
处理 JSON 解析失败
模型输出的 JSON 有时格式不完全正确,需要做容错处理:
import json
import re
def safe_parse_json(text: str) -> dict | None:
"""尝试从模型输出中解析 JSON"""
# 清理 markdown 代码块
text = text.strip()
if "```json" in text:
text = text.split("```json")[1].split("```")[0].strip()
elif "```" in text:
text = text.split("```")[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
使用 XML 标签的结构化输出
对于不需要程序解析的结构化输出,XML 标签比 JSON 更可靠(不需要担心引号转义等问题):
import os
from openai import OpenAI
import xml.etree.ElementTree as ET
client = OpenAI(
base_url="https://api.deepseek.com",
api_key=os.environ.get("DEEPSEEK_API_KEY")
)
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=1024,
messages=[{
"role": "user",
"content": """分析这段代码,用以下 XML 格式输出:
<analysis>
<complexity>时间复杂度</complexity>
<issues>潜在问题</issues>
<suggestions>改进建议</suggestions>
</analysis>
代码:
def find_duplicates(lst):
result = []
for i in range(len(lst)):
for j in range(i+1, len(lst)):
if lst[i] == lst[j] and lst[i] not in result:
result.append(lst[i])
return result"""
}]
)
xml_text = response.choices[0].message.content
# 提取 XML 内容
if "<analysis>" in xml_text:
xml_content = xml_text[xml_text.index("<analysis>"):xml_text.index("</analysis>")+11]
root = ET.fromstring(xml_content)
print("复杂度:", root.find("complexity").text)
print("问题:", root.find("issues").text)
选择哪种方法
| 方法 | 可靠性 | 适用场景 |
|---|---|---|
| Prompt 约束 | 中 | 简单结构,对偶发错误容忍 |
| JSON 模式(response_format) | 高 | 需要严格 JSON 格式的生产场景 |
| XML 标签 | 高 | 不需要程序解析,只需人工阅读 |
| pydantic 验证 | 高(有校验) | 需要类型安全的数据提取 |
在实际项目中,JSON 模式(response_format={"type": "json_object"})+ pydantic 验证的组合是最可靠的方案。如果解析失败,可以加一次重试逻辑。
常见错误和解决方法
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
JSON 解析失败(json.JSONDecodeError) |
模型在 JSON 前后加了解释文字 | 使用 JSON 模式(response_format) |
| JSON 模式下模型报错 | Prompt 中没有提到 "JSON" 这个词 | 在 System Prompt 或 User Prompt 中明确说"以 JSON 格式输出" |
| pydantic 验证失败 | 模型输出了预期外的字段名或类型 | 在 Prompt 中明确描述每个字段的名称和类型 |
| 模型输出了正确 JSON 但字段名不对 | Prompt 描述不够精确 | 给出完整的字段名示例,或者直接给 JSON schema |