结构化输出实战:Pydantic Schema约束LLM生成JSON
摘要 :在AI应用中,如何让大模型稳定地输出前端可直接渲染的复杂结构(如训练计划表、数据图表)?传统的正则提取不仅脆弱,而且容易解析失败。本文基于一个真实的跑步教练AI项目,详细解析如何利用LangChain的
PydanticOutputParser实现强类型的结构化输出。我们将深入源码,结合流程图和调用链,展示如何通过Schema定义约束LLM的思维边界,以及如何设计Fallback机制处理解析异常。这套方案将前端数据对接的效率提升了50%,是构建现代化AI UI的必备技能。
一、背景:非结构化文本的"最后一公里"难题
在项目初期,Agent生成的回答都是一大段纯文本。虽然对人类阅读很友好,但对前端开发却是噩梦:
问题1:前端渲染困难
场景:用户要求生成一个"本周训练计划"。
现象:
- Agent返回:"周一跑5公里,周二休息......"
- 前端如果想把这些内容放进一个漂亮的课程表组件里,必须写复杂的正则去拆解字符串。
- 一旦Agent换了一种说法,前端解析逻辑就崩了。
问题2:数据无法二次利用
现象:
- 用户想把生成的VO2max数值存入数据库做趋势图。
- 但因为数值混杂在长文本中,提取准确率只有80%。
- 大量的数据价值被锁死在文本里。
二、解决方案:Pydantic结构化输出
为了解决上述问题,我引入了Schema驱动的结构化输出 。核心思路是:先定义数据结构,再让LLM填空。
成功
失败
重试/降级
定义Pydantic Schema
注入Prompt模板
LLM生成JSON字符串
Pydantic Parser解析
返回Typed Dict对象
Fallback机制
核心优势:
- 类型安全:前端拿到的永远是符合定义的JSON,不再有解析错误。
- UI自动化:可以直接将JSON传给ECharts或Ant Design表格。
- 逻辑闭环:强制LLM按照我们设计的逻辑框架进行思考。
三、核心实现:定义Schema
3.1 训练计划Schema
文件位置:app/schemas/training_schema.py
python
from pydantic import BaseModel, Field
from typing import List, Dict
class DayPlan(BaseModel):
"""每日训练计划"""
day: str = Field(..., description="星期几,如 Monday")
content: str = Field(..., description="具体的训练内容")
distance_km: float = Field(..., description="计划跑量(公里)")
intensity: str = Field(..., description="强度等级:Low/Medium/High")
class TrainingPlan(BaseModel):
"""完整的周训练计划"""
user_level: str = Field(..., description="用户当前水平评估")
weekly_goal: str = Field(..., description="本周核心目标")
schedule: List[DayPlan] = Field(..., description="7天的详细安排")
advice: str = Field(..., description="给用户的额外建议")
关键点:
Field(...):提供详细描述,这不仅是注释,更是给LLM的提示(Hint)。- 嵌套结构:支持List和Dict,可以表达极其复杂的业务逻辑。
四、核心实现:集成到Agent
4.1 使用PydanticOutputParser
文件位置:app/services/structured_output_service.py
python
from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
class StructuredOutputService:
def __init__(self):
self.parser = PydanticOutputParser(pydantic_object=TrainingPlan)
async def generate_plan(self, query: str, user_data: Dict) -> TrainingPlan:
# 1. 获取带有格式指令的Prompt
prompt_with_format = self.parser.get_format_instructions()
# 2. 构建完整Prompt
full_prompt = f"""
请根据用户数据生成训练计划。
用户数据:{user_data}
用户要求:{query}
{prompt_with_format}
"""
# 3. 调用LLM
result = await llm.ainvoke(full_prompt)
# 4. 解析输出
try:
return self.parser.parse(result.content)
except Exception as e:
logger.error(f"解析失败: {e}")
return self._fallback_parse(result.content)
4.2 LLM看到的Prompt长什么样?
当LLM收到请求时,它会看到类似这样的指令:
"The output should be formatted as a JSON instance that conforms to the JSON schema below...
json{"title": "TrainingPlan", "type": "object", "properties": {...}}"
这种元数据级别的约束比单纯的文字描述有效得多。
五、Fallback机制:应对解析失败
5.1 为什么需要Fallback?
即使有Schema,LLM偶尔还是会:
- 输出Markdown代码块标记(
json ...
)。 - 漏掉某个必填字段。
- 产生幻觉,填入不符合类型的值。
5.2 健壮性设计
python
def _fallback_parse(self, raw_content: str) -> TrainingPlan:
"""
当标准解析失败时的兜底策略
"""
# 1. 清理Markdown标记
cleaned = raw_content.replace("```
json", "").replace("```
", "")
# 2. 尝试手动JSON加载
try:
data = json.loads(cleaned)
return TrainingPlan(**data) # Pydantic会自动做类型转换
except:
# 3. 最后的防线:返回一个默认的空计划
logger.warning("结构化解析彻底失败,返回默认模板")
return TrainingPlan(
user_level="Unknown",
weekly_goal="保持活跃",
schedule=[],
advice="请稍后重试"
)
六、完整调用链追踪
6.1 典型场景:生成可视化课表
Frontend (React) Qwen Model Structured Service Agent API 用户 Frontend (React) Qwen Model Structured Service Agent API 用户 组装Prompt + Format Instructions 转换为Python对象 "帮我制定下周的半马备战计划" generate_plan(...) 发送请求 返回JSON字符串 parser.parse() 返回 TrainingPlan Object 返回 JSON Response 遍历 schedule 数组 渲染 Ant Design Table 显示精美的训练课表
七、踩坑记录与解决方案
坑1:Token消耗激增
现象:加上Schema后,Prompt长度增加了500 tokens。
解决方案:
- 精简Schema描述:只保留必要的Field说明。
- 复用Parser:不要在每次请求时重新创建Parser对象。
坑2:中文编码问题
现象 :LLM有时会在JSON里夹杂中文标点,导致json.loads失败。
解决方案:
- 在Prompt中明确要求:"Ensure all strings are valid UTF-8 and use standard punctuation."
- 使用Pydantic的
model_validate_json方法,它比原生JSON库更宽容。
八、总结与展望
核心价值
- 前后端解耦:后端保证数据结构,前端专注于UI展现。
- 业务逻辑前置:通过Schema设计,提前规避了LLM可能产生的逻辑漏洞。
- 生态兼容:生成的JSON可以直接对接任何现代前端框架或数据分析工具。
后续优化
- 流式JSON解析:目前必须等LLM说完才能解析,未来可以尝试边说边解析。
- 动态Schema:根据用户意图,动态组合不同的Pydantic模型。
九、完整源码
GitHub仓库 :AiRunCoachAgent
快速演示 :AiRunCoachAgent
核心文件清单:
app/
├── schemas/
│ ├── training_schema.py # 训练计划Schema
│ └── tool_schema.py # 工具调用Schema
├── services/
│ └── structured_output_service.py # 结构化输出服务
frontend/
└── src/
└── components/
└── PlanTable.tsx # 接收JSON并渲染的表格组件
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!有任何问题或建议,请在评论区留言讨论。 🏃♂️💨