03 · Structured Output#
LLM 输出的是自然语言,但程序需要的是结构化数据(JSON、字典、对象)。 如何可靠地从 LLM 获取结构化输出,是生产环境中最常见的挑战之一。
本章介绍三种方案,从简单到严格:
Prompt 约束法 — 用提示词要求输出 JSON
JSON Mode — API 级别强制输出合法 JSON
Function Calling — 定义 schema,模型按 schema 填写
每种方案都会展示如何用 Pydantic 验证输出。
import json
import litellm
import os
from dotenv import load_dotenv
from utils.llm_client import chat
load_dotenv()
True
1. Prompt 约束法#
最简单的方式:在 prompt 里要求模型输出 JSON,并给出 schema 示例。
优点: 所有模型通用,无需特殊 API 缺点: 模型可能在 JSON 前后加多余文字,需要额外解析
def extract_json(text: str) -> dict:
"""从模型输出中提取 JSON,容忍前后有多余文字。"""
# 找第一个 { 到最后一个 }
start = text.find("{")
end = text.rfind("}") + 1
if start == -1 or end == 0:
raise ValueError(f"No JSON found in: {text!r}")
return json.loads(text[start:end])
review = "这款耳机音质非常好,低音有力,佩戴也很舒适,就是价格略贵,充电仓做工一般。总体推荐。"
prompt = f"""分析以下产品评论,以 JSON 格式输出,字段说明如下:
- sentiment: 情感倾向(正面/负面/中性)
- score: 评分 1-5
- pros: 优点列表
- cons: 缺点列表
- recommend: 是否推荐(true/false)
只输出 JSON,不要其他文字。
评论:{review}"""
raw = chat(prompt, temperature=0)
print("原始输出:")
print(raw.strip())
data = extract_json(raw)
print("\n解析结果:")
print(json.dumps(data, ensure_ascii=False, indent=2))
原始输出:
{
"sentiment": "正面",
"score": 4,
"pros": [
"音质非常好",
"低音有力",
"佩戴舒适"
],
"cons": [
"价格略贵",
"充电仓做工一般"
],
"recommend": true
}
解析结果:
{
"sentiment": "正面",
"score": 4,
"pros": [
"音质非常好",
"低音有力",
"佩戴舒适"
],
"cons": [
"价格略贵",
"充电仓做工一般"
],
"recommend": true
}
2. JSON Mode#
OpenAI、部分其他 provider 支持 response_format={"type": "json_object"},
在 API 层面保证输出是合法 JSON(不会有多余文字)。
注意: JSON Mode 只保证合法 JSON,不保证字段完整——仍需 Pydantic 验证。
articles = [
"OpenAI 于 2024 年发布了 GPT-4o,这是一个多模态模型,支持文字、图像和音频输入。",
"苹果公司在 WWDC 2024 上发布了 Apple Intelligence,将 AI 功能集成到 iOS 18 中。",
"谷歌 DeepMind 的 AlphaFold 3 能够预测几乎所有生物分子的结构,2024年荣获诺贝尔化学奖。",
]
system_prompt = """从新闻文章中提取结构化信息。
输出包含以下字段的 JSON:
- company: 公司名称
- product: 产品或技术名称
- year: 年份(整数)
- category: 类别(如:大语言模型/多模态/生物AI/其他)
- summary: 一句话摘要"""
for article in articles:
response = litellm.completion(
model=os.getenv("LLM_MODEL"),
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": article},
],
response_format={"type": "json_object"},
temperature=0,
)
data = json.loads(response.choices[0].message.content)
print(json.dumps(data, ensure_ascii=False, indent=2))
print()
{
"company": "OpenAI",
"product": "GPT-4o",
"year": 2024,
"category": "多模态",
"summary": "OpenAI 于 2024 年发布 GPT-4o,这是一个多模态模型,支持文字、图像和音频输入。"
}
{
"company": "苹果公司",
"product": "Apple Intelligence",
"year": 2024,
"category": "多模态",
"summary": "苹果公司在 WWDC 2024 推出 Apple Intelligence,将多模态 AI 功能集成到 iOS 18 中。"
}
{
"company": "谷歌 DeepMind",
"product": "AlphaFold 3",
"year": 2024,
"category": "生物AI",
"summary": "AlphaFold 3 能够预测几乎所有生物分子的结构,并在2024年因该成果荣获诺贝尔化学奖。"
}
3. Function Calling#
Function Calling(也叫 Tool Use)是目前最严格的结构化输出方式:
你定义一个 JSON Schema 描述数据结构
模型按 schema 填写,字段类型有保证
适合需要精确控制输出格式的生产场景
# 定义 schema:从简历文本中提取结构化信息
resume_schema = {
"type": "function",
"function": {
"name": "extract_resume",
"description": "从简历文本中提取结构化信息",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "姓名"},
"email": {"type": "string", "description": "邮箱"},
"years_exp": {"type": "number", "description": "工作年限"},
"skills": {"type": "array", "items": {"type": "string"}, "description": "技能列表"},
"education": {"type": "string", "description": "最高学历"},
"languages": {"type": "array", "items": {"type": "string"}, "description": "编程语言"},
},
"required": ["name", "years_exp", "skills"],
},
},
}
resume_text = """
张伟,毕业于上海交通大学计算机系(本科),邮箱:zhangwei@example.com
5年后端开发经验,熟悉 Python、Go、Java,
擅长分布式系统、微服务架构、Kubernetes 容器编排。
有丰富的 LLM 应用开发经验,做过 RAG 系统和 AI Agent。
""".strip()
response = litellm.completion(
model=os.getenv("LLM_MODEL"),
messages=[{"role": "user", "content": resume_text}],
tools=[resume_schema],
tool_choice={"type": "function", "function": {"name": "extract_resume"}},
temperature=0,
)
tool_call = response.choices[0].message.tool_calls[0]
extracted = json.loads(tool_call.function.arguments)
print(json.dumps(extracted, ensure_ascii=False, indent=2))
{
"name": "张伟",
"email": "zhangwei@example.com",
"years_exp": 5,
"skills": [
"分布式系统",
"微服务架构",
"Kubernetes",
"RAG 系统",
"AI Agent",
"LLM 应用开发",
"Python",
"Go",
"Java"
],
"education": "上海交通大学 计算机系 本科",
"languages": [
"Python",
"Go",
"Java"
]
}
4. Pydantic 验证#
无论用哪种方法获取 JSON,都应该用 Pydantic 验证:
字段类型检查
缺失字段检测
自动类型转换
清晰的错误信息
from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional
class Resume(BaseModel):
name: str
email: Optional[str] = None
years_exp: float = Field(ge=0, description="工作年限,必须 >= 0")
skills: List[str]
education: Optional[str] = None
languages: List[str] = []
# 验证 Function Calling 的输出
resume = Resume(**extracted)
print("验证通过!")
print(f"姓名: {resume.name}")
print(f"工作年限: {resume.years_exp} 年")
print(f"技能: {', '.join(resume.skills)}")
print(f"编程语言: {', '.join(resume.languages)}")
print(f"\nPydantic model dict:")
print(resume.model_dump_json(indent=2))
验证通过!
姓名: 张伟
工作年限: 5.0 年
技能: 分布式系统, 微服务架构, Kubernetes, RAG 系统, AI Agent, LLM 应用开发, Python, Go, Java
编程语言: Python, Go, Java
Pydantic model dict:
{
"name": "张伟",
"email": "zhangwei@example.com",
"years_exp": 5.0,
"skills": [
"分布式系统",
"微服务架构",
"Kubernetes",
"RAG 系统",
"AI Agent",
"LLM 应用开发",
"Python",
"Go",
"Java"
],
"education": "上海交通大学 计算机系 本科",
"languages": [
"Python",
"Go",
"Java"
]
}
# Pydantic 会捕获错误
from pydantic import ValidationError
bad_data = [
{"name": "张伟", "years_exp": -1, "skills": []}, # years_exp < 0
{"years_exp": 3, "skills": ["Python"]}, # 缺少 name
{"name": "李四", "years_exp": "五年", "skills": []}, # 类型错误(字符串→数字)
]
for data in bad_data:
try:
Resume(**data)
except ValidationError as e:
errors = e.errors()
print(f"数据: {data}")
print(f"错误: {errors[0]['loc']} - {errors[0]['msg']}\n")
数据: {'name': '张伟', 'years_exp': -1, 'skills': []}
错误: ('years_exp',) - Input should be greater than or equal to 0
数据: {'years_exp': 3, 'skills': ['Python']}
错误: ('name',) - Field required
数据: {'name': '李四', 'years_exp': '五年', 'skills': []}
错误: ('years_exp',) - Input should be a valid number, unable to parse string as a number
5. 端到端示例:结构化信息提取流水线#
把以上所有技术组合成一个可复用的提取函数。
from pydantic import BaseModel
from typing import List, Optional, Type, TypeVar
T = TypeVar("T", bound=BaseModel)
def extract_structured(
text: str,
schema: Type[T],
instructions: str = "",
) -> T:
"""
从文本中提取结构化数据,用 Pydantic schema 验证。
自动生成 JSON 格式要求,通过 JSON Mode 获取输出。
"""
# 根据 Pydantic schema 生成字段说明
fields = schema.model_fields
field_desc = "\n".join(
f"- {name}: {field.description or field.annotation}"
for name, field in fields.items()
)
system = f"""从文本中提取信息,以 JSON 格式输出。
{instructions}
字段说明:
{field_desc}"""
response = litellm.completion(
model=os.getenv("LLM_MODEL"),
messages=[
{"role": "system", "content": system},
{"role": "user", "content": text},
],
response_format={"type": "json_object"},
temperature=0,
)
raw = json.loads(response.choices[0].message.content)
return schema(**raw)
# 示例:从新闻提取事件信息
class NewsEvent(BaseModel):
headline: str = Field(description="新闻标题(一句话)")
who: str = Field(description="主要人物或组织")
what: str = Field(description="发生了什么")
when: Optional[str] = Field(None, description="时间")
where: Optional[str] = Field(None, description="地点")
impact: str = Field(description="潜在影响")
news = """
2024年12月,谷歌 DeepMind 发布了新一代 AI 模型 Gemini 2.0,
该模型支持原生多模态输入输出,包括文字、图像、音频和视频。
这是谷歌在 AI 领域对抗 OpenAI 的最新举措,
预计将对企业 AI 采购决策产生重大影响。
""".strip()
event = extract_structured(news, NewsEvent)
print(event.model_dump_json(indent=2, ensure_ascii=False))
{
"headline": "谷歌 DeepMind 发布支持原生多模态的 Gemini 2.0 AI 模型",
"who": "谷歌 DeepMind(与 OpenAI 的竞争背景)",
"what": "发布新一代 AI 模型 Gemini 2.0,支持原生多模态输入输出(文字、图像、音频、视频),作为对抗 OpenAI 的最新举措。",
"when": "2024年12月",
"where": "未具体说明(由谷歌 DeepMind 对外发布,面向全球)",
"impact": "预计将显著影响企业 AI 采购决策,推动多模态应用部署并加剧谷歌与 OpenAI 之间的竞争,可能重塑 AI 产品与服务市场格局。"
}
小结#
方案 |
可靠性 |
通用性 |
适用场景 |
|---|---|---|---|
Prompt 约束 |
⭐⭐ |
✅ 所有模型 |
原型验证、简单提取 |
JSON Mode |
⭐⭐⭐ |
✅ 主流模型 |
生产环境通用方案 |
Function Calling |
⭐⭐⭐⭐ |
⚠️ 需支持 tool use |
精确 schema 控制 |
Pydantic 验证 |
— |
✅ |
永远都要加,不管用哪种方案 |
最佳实践: JSON Mode + Pydantic 是目前生产环境最稳健的组合。
Module 2 · Prompting 完结 🎉
下一模块 → Module 3: API 实战:Streaming、Function Calling 深度使用、Vision 多模态