title: 结构化输出实战:用 Pydantic + Function Calling 让 LLM 返回可靠数据
💡 摘要:结合 Pydantic 数据验证、JSON 格式控制和函数调用,掌握 with_structured_output() 高级 API 和手动 Chain 构建两种方案,构建可靠的 AI 结构化输出系统。
引言
在实际项目中,你是否遇到过这样的痛点:
- 让大模型提取电影信息,返回的 JSON 格式总是不一致,有时少了字段
- 天气数据提取后,需要手动处理各种异常情况(缺少字段、类型错误)
- 模型返回的 JSON 被 Markdown 代码块包裹,解析时经常报错
让 LLM 返回结构化数据是构建 AI 应用的核心需求。无论是信息提取、数据转换还是 API 响应,你都需要确保模型输出的数据格式正确、字段完整、类型合法。
本文将教你如何用 Pydantic + Function Calling 的架构,让大模型稳定返回符合预期的结构化数据,并提供完整的异常处理方案。
核心概念
为什么需要结构化输出?
大模型的输出是自由文本,但我们的程序需要的是结构化数据(如 JSON、Python 对象)。
类比:就像快递包装,自由文本是散装货物,结构化输出是标准化包装箱。有了标准化的包装,后续处理(分拣、运输、签收)才能自动化。
Pydantic 数据建模
Pydantic 是 Python 的数据验证库,通过 BaseModel 定义数据结构,自动进行类型检查和约束验证。
python
from pydantic import BaseModel, Field
from typing import List, Optional
class MovieInfo(BaseModel):
"""电影信息结构体"""
title: str = Field(..., description="电影标题")
year: int = Field(..., description="上映年份")
genre: List[str] = Field(..., description="类型标签")
director: str = Field(..., description="导演姓名")
rating: float = Field(..., ge=0, le=10, description="评分(0-10)")
suggestion: Optional[str] = Field(None, description="可选的观影建议")
关键字段约束:
| 约束 | 说明 | 示例 |
|---|---|---|
Field(...) |
必填字段 | title: str = Field(...) |
Field(None) |
可选字段 | suggestion: Optional[str] |
ge=0, le=10 |
数值范围 | rating: float = Field(..., ge=0, le=10) |
min_length=1 |
最小长度 | name: str = Field(..., min_length=1) |
pattern=r"^..." |
正则匹配 | phone: str = Field(..., pattern=r"^1\d{10}$") |
💡 为什么用 description?
description不仅是文档说明,还会被转换为 JSON Schema 传给模型,指导模型理解每个字段的含义,提高输出准确性。
原理深入
with_structured_output() 高级 API
LangChain 提供了 with_structured_output() 方法,直接利用模型的 Function Calling 能力:
python
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import List
class MovieInfo(BaseModel):
title: str = Field(..., description="电影标题")
year: int = Field(..., description="上映年份")
genre: List[str] = Field(..., description="类型标签")
director: str = Field(..., description="导演姓名")
rating: float = Field(..., ge=0, le=10, description="评分")
# 初始化模型并启用结构化输出
llm = ChatOpenAI(
api_key="your-api-key",
base_url="https://api.openai.com/v1",
model="gpt-4o-mini",
temperature=0
).with_structured_output(MovieInfo)
# 直接调用,返回 Pydantic 对象
result = llm.invoke("《盗梦空间》是 2010 年诺兰执导的科幻悬疑片,豆瓣评分 9.4")
print(type(result)) # <class '__main__.MovieInfo'>
print(result.title) # 盗梦空间
print(result.year) # 2010
print(result.genre) # ['科幻', '悬疑', '动作']
工作流程:
- LangChain 自动将
MovieInfo转换为 JSON Schema - 通过 Function Calling 协议发送给模型
- 模型返回符合 Schema 的 JSON
- LangChain 自动解析为 Pydantic 对象(包含验证)
优势:
- 自动转换,无需手动处理
- 利用模型原生 Function Calling
- 返回已验证的 Pydantic 对象
- 一行代码启用结构化输出
手动构建 Chain 方案
当需要更多控制时,可以手动构建 Prompt + Model + Parser:
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
# 定义解析器
parser = PydanticOutputParser(pydantic_object=MovieInfo)
# 构建 Prompt
prompt = ChatPromptTemplate.from_template("""
请根据以下电影描述提取结构化信息:
"{movie_description}"
请严格按照以下 JSON 格式返回:
{format_instructions}
不要有其他文字说明。
""")
# 构建 Chain
chain = prompt.partial(
format_instructions=parser.get_format_instructions()
) | llm | parser
# 执行
result = chain.invoke({
"movie_description": "《盗梦空间》是 2010 年克里斯托弗·诺兰执导的科幻悬疑片..."
})
关键步骤:
PydanticOutputParser从 Pydantic 模型生成 JSON Schema 指令prompt.partial()将指令注入到 Prompt 中- 模型返回 JSON,Parser 自动解析为 Pydantic 对象
代码示例
JSON 清理与解析
模型输出可能包含 Markdown 代码块标记,需要清理:
python
import json
import re
def extract_json_from_markdown(text: str) -> str:
"""从 Markdown 代码块中提取 JSON"""
# 移除 ```json 和 ```标记
pattern = r'```json\s*(.*?)\s*```'
match = re.search(pattern, text, re.DOTALL)
if match:
return match.group(1)
return text
# 从 AIMessage 中提取 content
if hasattr(result, 'content'):
result_text = result.content
else:
result_text = str(result)
# 清理并解析
json_text = extract_json_from_markdown(result_text)
data = json.loads(json_text)
movie = MovieInfo(**data)
Pydantic 验证异常处理
当模型返回的数据不符合约束时,Pydantic 会抛出 ValidationError:
python
from pydantic import ValidationError
try:
# 尝试创建不符合约束的对象
invalid_movie = MovieInfo(
title="测试电影",
year=2024,
genre=["动作"],
director="测试",
rating=15 # 超出 0-10 范围
)
except ValidationError as e:
print(f"验证失败:{e}")
# 输出:
# 1 validation error for MovieInfo
# rating
# Input should be less than or equal to 10
在 LCEL 中处理异常:
python
from langchain_core.exceptions import OutputParserException
try:
result = chain.invoke({"movie_description": "..."})
except OutputParserException as e:
print(f"解析失败:{e}")
# 可以尝试修复或返回错误信息
实战应用
案例 1:电影信息提取
python
from langchain_core.prompts import ChatPromptTemplate
movie_description = "《盗梦空间》是 2010 年克里斯托弗·诺兰执导的科幻悬疑片,讲述梦境入侵的故事,豆瓣评分 9.4,类型为科幻、悬疑、动作。"
prompt = ChatPromptTemplate.from_template("""
请根据以下电影描述提取结构化信息,并以 JSON 格式返回:
"{movie_description}"
返回格式:
{{
"title": "电影标题",
"year": 上映年份,
"genre": ["类型 1", "类型 2"],
"director": "导演姓名",
"rating": 评分
}}
""")
# 使用 with_structured_output 的方案
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0
).with_structured_output(MovieInfo)
result = llm.invoke(f"提取电影信息:{movie_description}")
print(f"标题:{result.title}")
print(f"年份:{result.year}")
print(f"类型:{result.genre}")
print(f"导演:{result.director}")
print(f"评分:{result.rating}")
输出:
标题:盗梦空间
年份:2010
类型:['科幻', '悬疑', '动作']
导演:克里斯托弗·诺兰
评分:9.4
案例 2:天气数据提取
python
class WeatherData(BaseModel):
"""天气数据结构体"""
city: str = Field(..., description="城市名称")
temperature: int = Field(..., description="当前温度(摄氏度)")
condition: str = Field(..., description="天气状况")
humidity: int = Field(..., description="湿度百分比")
suggestion: Optional[str] = Field(None, description="穿衣建议")
weather_description = "北京今天天气晴朗,气温 25 摄氏度,湿度 40%,适合外出活动,建议穿轻薄衣物。"
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0
).with_structured_output(WeatherData)
result = llm.invoke(f"提取天气信息:{weather_description}")
print(f"城市:{result.city}")
print(f"温度:{result.temperature}°C")
print(f"状况:{result.condition}")
print(f"湿度:{result.humidity}%")
print(f"建议:{result.suggestion}")
输出:
城市:北京
温度:25°C
状况:晴朗
湿度:40%
建议:穿轻薄衣物
最佳实践
- 优先使用
.with_structured_output():利用模型原生 Function Calling,最简洁可靠 - 定义清晰的 Schema :每个字段添加
description,指导模型理解含义 - 添加约束:数值范围、字符串长度等,提高数据质量
- 处理异常 :捕获
ValidationError和OutputParserException,提供友好的错误提示 - JSON 清理:处理 Markdown 代码块、多余空格等异常情况
- temperature=0:结构化输出时设置温度为 0,确保输出稳定性
总结
结构化输出实战的核心要点:
- Pydantic 建模 :用
BaseModel+Field定义数据结构和约束 - 高级 API :
.with_structured_output()一行代码启用 Function Calling - 手动 Chain :
PydanticOutputParser提供更多控制权 - JSON 清理:正则表达式移除 Markdown 标记
- 异常处理:捕获验证错误,确保程序稳定性
掌握了结构化输出,你就能让大模型稳定返回符合预期的数据格式,构建可靠的 AI 应用。