结构化输出控制
在实际应用中,LLM 的输出通常需要被程序解析和处理——而不仅仅是给人阅读。本节教你如何精确控制 LLM 的输出格式,实现可靠的结构化输出。
为什么结构化输出重要?
graph LR
A[LLM 输出] --> B{输出类型?}
B -->|非结构化| C["自然语言文本
(人阅读)"] B -->|结构化| D["JSON/表格/代码
(程序解析)"] C --> C1["适用:聊天、写作
报告、解释"] D --> D1["适用:API响应、数据抽取
表单填充、工作流"] style A fill:#ede7f6,stroke:#5e35b1,stroke-width:3px style C fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style D fill:#c8e6c9,stroke:#43a047,stroke-width:2px
(人阅读)"] B -->|结构化| D["JSON/表格/代码
(程序解析)"] C --> C1["适用:聊天、写作
报告、解释"] D --> D1["适用:API响应、数据抽取
表单填充、工作流"] style A fill:#ede7f6,stroke:#5e35b1,stroke-width:3px style C fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style D fill:#c8e6c9,stroke:#43a047,stroke-width:2px
在生产环境中,至少 60-70% 的 LLM 调用需要结构化输出。如果模型返回的 JSON 格式不正确,整个应用流程就会中断。
方法1:提示词约束法
最基本的方法——在提示词中明确要求输出格式。
JSON 输出
from openai import OpenAI
import json
client = OpenAI()
prompt = """
分析以下产品评论,提取关键信息。
评论:
"用了三个月了,总体还不错。电池续航给力,一天一充完全够用。
但是相机在夜景模式下有点模糊,希望后续能通过软件更新改善。
价格在3000元左右,性价比不错。"
请严格按照以下 JSON 格式输出(不要包含任何其他文字):
{
"sentiment": "正面/负面/中性",
"overall_score": 1到10的整数,
"pros": ["优点1", "优点2"],
"cons": ["缺点1"],
"price_mentioned": true或false,
"price_range": "价格描述或null",
"key_features": ["提到的功能1", "功能2"],
"purchase_intent": "推荐购买/观望/不推荐"
}
"""
response = client.chat.completions.create(
model="gpt-4o",
temperature=0,
messages=[{"role": "user", "content": prompt}]
)
# 解析 JSON
result = json.loads(response.choices[0].message.content)
print(json.dumps(result, ensure_ascii=False, indent=2))
提高 JSON 输出可靠性的技巧:
# 技巧1:在System Prompt中强调格式
system_prompt = """你是一个数据处理助手。
你只输出合法的JSON格式,不包含任何解释性文字、Markdown标记或代码围栏。
确保JSON可以被 json.loads() 直接解析。"""
# 技巧2:给出完整的JSON Schema
schema_prompt = """
按以下 JSON Schema 输出:
{
"type": "object",
"properties": {
"name": {"type": "string", "description": "产品名称"},
"score": {"type": "integer", "minimum": 1, "maximum": 10},
"tags": {"type": "array", "items": {"type": "string"}}
},
"required": ["name", "score", "tags"]
}
"""
# 技巧3:防御性解析
def safe_parse_json(text: str) -> dict:
"""安全地从LLM输出中解析JSON"""
# 移除可能的Markdown代码围栏
text = text.strip()
if text.startswith("```json"):
text = text[7:]
if text.startswith("```"):
text = text[3:]
if text.endswith("```"):
text = text[:-3]
return json.loads(text.strip())
方法2:OpenAI Structured Outputs(原生支持)
从 2024 年起,OpenAI 提供了原生的结构化输出能力——模型层面保证输出符合指定的 JSON Schema。
from openai import OpenAI
from pydantic import BaseModel
from typing import Optional
client = OpenAI()
# 使用 Pydantic 定义输出结构
class ProductReview(BaseModel):
sentiment: str
overall_score: int
pros: list[str]
cons: list[str]
price_mentioned: bool
price_range: Optional[str]
summary: str
# 使用 response_format 参数
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06", # 需要支持 Structured Outputs 的模型版本
messages=[
{
"role": "system",
"content": "你是一个产品评论分析助手。"
},
{
"role": "user",
"content": "分析评论:用了三个月,电池续航好,相机夜景模式差,价格3000元性价比不错。"
}
],
response_format=ProductReview # 直接传入 Pydantic 模型
)
review = response.choices[0].message.parsed
print(f"情感: {review.sentiment}")
print(f"评分: {review.overall_score}")
print(f"优点: {review.pros}")
Structured Outputs vs 提示词约束
| 维度 | 提示词约束 | Structured Outputs |
|---|---|---|
| 可靠性 | ~90-95%(可能偶尔格式错误) | 100%(模型层保证) |
| 复杂 Schema | 复杂场景容易出错 | 完美支持嵌套结构 |
| 兼容性 | 所有模型通用 | 仅 OpenAI 特定版本支持 |
| 灵活性 | 可自定义任意格式 | 仅限 JSON Schema |
| 额外成本 | 无 | 可能略多 Token |
方法3:多种输出格式控制
Markdown 表格
table_prompt = """
比较以下三种 Python Web 框架,以 Markdown 表格格式输出。
框架:FastAPI、Django、Flask
比较维度:
| 维度 | FastAPI | Django | Flask |
|------|---------|--------|-------|
| 性能 | | | |
| 学习曲线 | | | |
| 生态系统 | | | |
| 适用场景 | | | |
| 异步支持 | | | |
每个单元格用简短的描述(不超过15字),可使用 ⭐ 评分。
"""
XML 格式
xml_prompt = """
将以下用户信息转换为 XML 格式。
用户信息:
- 姓名: 张三
- 年龄: 28
- 邮箱: zhangsan@example.com
- 技能: Python, JavaScript, SQL
请输出如下格式的 XML:
<user>
<name>...</name>
<age>...</age>
<email>...</email>
<skills>
<skill>...</skill>
</skills>
</user>
"""
YAML 格式
yaml_prompt = """
为以下微服务生成 Kubernetes Deployment 配置文件(YAML格式)。
服务信息:
- 服务名: user-service
- 镜像: myregistry/user-service:v1.2.0
- 副本数: 3
- 端口: 8080
- 资源限制: CPU 500m, 内存 512Mi
- 环境变量: DATABASE_URL, REDIS_URL
- 健康检查路径: /health
请输出完整可用的 YAML 配置。
"""
结构化输出的高级技巧
1. 处理复杂嵌套结构
complex_schema_prompt = """
分析以下技术文章,提取结构化信息。按如下嵌套 JSON 格式输出:
{
"article": {
"title": "文章标题",
"author": "作者",
"published_date": "YYYY-MM-DD",
"category": "技术分类",
"difficulty": "入门/中级/高级"
},
"content_analysis": {
"main_topic": "核心主题",
"key_concepts": [
{
"concept": "概念名称",
"definition": "简短定义",
"importance": "高/中/低"
}
],
"technologies_mentioned": ["技术1", "技术2"],
"code_languages": ["语言1"]
},
"quality_assessment": {
"clarity_score": 1-10,
"depth_score": 1-10,
"practical_value": 1-10,
"overall_rating": 1-10,
"strengths": ["优点1"],
"improvements": ["改进点1"]
},
"metadata": {
"word_count_estimate": 数字,
"reading_time_minutes": 数字,
"target_audience": "目标读者描述"
}
}
文章内容如下:
---
{article_text}
---
"""
2. 条件格式输出
根据输入的不同,输出不同的结构:
conditional_prompt = """
分析用户的技术问题并分类处理。
根据问题类型,使用不同的输出格式:
如果是 Bug 报告:
{
"type": "bug",
"severity": "critical/high/medium/low",
"component": "受影响的组件",
"steps_to_reproduce": ["步骤1", "步骤2"],
"expected_behavior": "预期行为",
"actual_behavior": "实际行为",
"suggested_fix": "建议修复方案"
}
如果是功能请求:
{
"type": "feature_request",
"priority": "P0/P1/P2/P3",
"description": "功能描述",
"user_story": "作为X,我想要Y,以便Z",
"acceptance_criteria": ["标准1", "标准2"],
"estimated_effort": "small/medium/large"
}
如果是使用咨询:
{
"type": "question",
"topic": "问题领域",
"answer": "简明回答",
"related_docs": ["相关文档链接"],
"follow_up_needed": true/false
}
用户问题:{user_input}
"""
3. 批量处理与表格输出
batch_prompt = """
请分析以下5条客户反馈,以JSON数组格式返回每条的分析结果。
反馈列表:
1. "配送太慢了,等了5天"
2. "产品质量不错,很满意"
3. "客服态度很好,解决了我的问题"
4. "价格涨了,不太划算了"
5. "包装有损坏,但产品没问题"
输出格式:
[
{
"id": 1,
"text": "原始文本",
"sentiment": "positive/negative/neutral",
"category": "配送/产品/客服/价格/包装",
"urgency": "high/medium/low",
"action_required": "需要采取的行动"
}
]
确保返回正好5条分析结果。
"""
验证与错误处理
在生产环境中,永远不要假设 LLM 的输出格式是正确的。
from pydantic import BaseModel, ValidationError
from typing import Optional
import json
class AnalysisResult(BaseModel):
"""定义预期的输出结构"""
sentiment: str
score: int
summary: str
tags: list[str]
def robust_structured_call(
prompt: str,
max_retries: int = 3
) -> Optional[AnalysisResult]:
"""
带重试和验证的结构化LLM调用。
Args:
prompt: 提示词
max_retries: 最大重试次数
Returns:
验证通过的结果,或 None
"""
for attempt in range(max_retries):
try:
response = client.chat.completions.create(
model="gpt-4o-mini",
temperature=0,
messages=[
{
"role": "system",
"content": "你只输出合法的JSON,不包含其他文字。"
},
{"role": "user", "content": prompt}
]
)
raw_text = response.choices[0].message.content
parsed = safe_parse_json(raw_text)
result = AnalysisResult(**parsed) # Pydantic 验证
return result
except json.JSONDecodeError as e:
print(f"第{attempt+1}次尝试:JSON解析失败 - {e}")
except ValidationError as e:
print(f"第{attempt+1}次尝试:数据验证失败 - {e}")
except Exception as e:
print(f"第{attempt+1}次尝试:未知错误 - {e}")
print("所有重试均失败")
return None
# 使用
result = robust_structured_call(
"分析评论:'这个手机拍照很棒但电池不耐用',JSON格式输出sentiment, score(1-10), summary, tags"
)
if result:
print(f"情感: {result.sentiment}, 评分: {result.score}")
动手练习
练习1:设计数据抽取提示词
设计一个提示词,从一段招聘JD(职位描述)中抽取以下结构化信息: - 职位名称、公司名称、工作地点 - 薪资范围 - 技术要求(数组) - 经验要求 - 学历要求
要求输出可被程序直接解析的JSON。
练习2:构建验证流水线
基于 robust_structured_call 函数,添加以下功能:
- 当JSON解析失败时,自动将上次的错误信息反馈给模型重试
- 记录每次调用的 Token 消耗
- 统计成功率
本章要点
- ✅ 生产环境中大部分 LLM 调用需要结构化输出
- ✅ 提示词约束法通用但不100%可靠,需要防御性解析
- ✅ OpenAI Structured Outputs 能从模型层面保证格式正确
- ✅ 除了 JSON,还可以控制 Markdown 表格、XML、YAML 等格式输出
- ✅ 复杂场景可以使用条件格式和批量处理
- ✅ 生产代码必须加入验证和重试机制,不要信任原始输出
下一步:提示链与多步任务分解 🚀