在 LLM 应用开发中,如何利用 Pydantic 实现 LLM 结构化输出,并自动生成 Tool Schema?
这个问题其实触及了 LLM 应用从“能聊天”到“能干活”的关键一步。下面我从定义模型 → 生成 Schema 约束输出 → 自动转成工具描述这条链路,把我的实践方式说清楚。
🎯 先明确要解决什么¶
LLM 本质是文本进、文本出。想让它的输出被代码可靠消费,要么写又臭又长的正则,要么手写 JSON Schema 还不一定跟数据模型同步。我的思路是:让 Pydantic 成为唯一的真相来源,Schema 和解析全由它自动派生。
📦 第一步:定义 Pydantic 模型,顺便把提示词“喂饱”¶
一切从业务模型开始,用 Field 把字段含义写清楚——这很重要,因为后面这些描述会直接交给 LLM 做约束。
from pydantic import BaseModel, Field
class UserIntent(BaseModel):
"""用户意图识别结果"""
intent: str = Field(..., description="意图类别,如 '下单','售后','咨询'")
confidence: float = Field(..., ge=0, le=1, description="置信度")
entities: list[str] = Field(default_factory=list, description="涉及的产品或人名")
💡 一个踩坑心得:Field(description=...) 就是你的 prompt engineering 素材,写得越像“对 AI 的自然语言指令”,后续输出越稳。
🔄 第二步:用 model_json_schema() 把模型变成 LLM 的“输出协议”¶
Pydantic 能直接导出标准的 JSON Schema。把它嵌入 system prompt 或作为 response_format(部分官方 API 可直接用),让 LLM 必须按这个模子输出。
schema = UserIntent.model_json_schema()
# 得到完整的 JSON Schema,包含类型、必填、字段描述等
prompt = f"""你是一个意图分析器。请严格按照以下 JSON Schema 输出(只输出 JSON,不要解释):
{schema}
用户输入:{user_input}
"""
响应回来后,一行代码完成解析+校验:
try:
intent_data = UserIntent.model_validate_json(llm_response)
except ValidationError as e:
# 可以把 e.json() 反馈给 LLM 让它自纠正,形成一个重试闭环
...
🔁 我习惯在验证失败时把 Pydantic 的报错信息(它也是 JSON)原样丢回给 LLM 做二次修复,效果出奇的好。
🧰 第三步:同一个模型,自动生成 Tool Schema(Function Calling 的灵魂)¶
做工具调用时,我们需要给 OpenAI/其他平台提供 tools 参数,里面包含函数名、描述和参数 JSON Schema。完全没必要手写,直接基于 Pydantic 模型自动生成。
-
给模型写一个 docstring(会被转成工具描述),类名转成函数名。
-
用
model_json_schema()拿参数 schema。 -
拼成标准的工具定义字典。
def pydantic_to_tool_schema(model: type[BaseModel], name: str = None):
return {
"type": "function",
"function": {
"name": name or model.__name__,
"description": model.__doc__ or "",
"parameters": model.model_json_schema()
}
}
# 定义一个可调用的工具模型
class WeatherQuery(BaseModel):
"""查询指定城市的实时天气"""
city: str = Field(..., description="城市名称,英文如 'Beijing'")
unit: str = Field(default="celsius", description="温度单位,celsius 或 fahrenheit")
tool_def = pydantic_to_tool_schema(WeatherQuery)
# 这个 tool_def 可以直接扔进 api 的 tools 列表里
📌 这样做的好处:业务模型、prompt 约束、工具定义三合一。改了字段,Schema 全自动刷新,告别手工同步的噩梦。
🛡️ 第四步:解析工具调用参数,依然用同一个模型¶
当 LLM 决定调用工具并返回参数 JSON 字符串时:
if tool_call.function.name == "WeatherQuery":
args = WeatherQuery.model_validate_json(tool_call.function.arguments)
# args 就是类型安全的 WeatherQuery 实例,直接 .city 用
weather = get_weather(args.city, args.unit)
完全不需要手写字典取值、类型转换,Pydantic 已经帮你做了强制类型和范围检查。
✨ 实战中让我觉得“真香”的几个点¶
-
字段级描述直接驱动 LLM 行为:不用在 prompt 和 schema 文档里描述两遍字段含义。
-
Pydantic 的泛型与嵌套模型:输出复杂的树状结构(比如订单含商品列表)照样一行
model_validate_json搞定。 -
版本兼容:生产环境最好锁 Pydantic v2,
model_json_schema()的生成规则更规范,对 LLM 更友好。 -
妙用
model_dump():拿到结构化数据后可直接转成字典入库、入消息队列,流转极其顺畅。
🧵 总结链路¶
Pydantic 模型定义 ➡️ model_json_schema() 自动获得约束 ➡️ 注入 Prompt / response_format 控制输出 ➡️ 同一 Schema 直接转成 Tool Definition ➡️ model_validate_json() 解析并校验结果。
模型即协议,一处定义,处处生效。 这整套做法在我近期的智能客服和 RAG 项目中很稳定,也显著降低了工程复杂度。