结构化输出实战:Pydantic Schema约束LLM生成JSON

结构化输出实战: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机制

核心优势

  1. 类型安全:前端拿到的永远是符合定义的JSON,不再有解析错误。
  2. UI自动化:可以直接将JSON传给ECharts或Ant Design表格。
  3. 逻辑闭环:强制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偶尔还是会:

  1. 输出Markdown代码块标记(json ...
    )。
  2. 漏掉某个必填字段。
  3. 产生幻觉,填入不符合类型的值。

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库更宽容。

八、总结与展望

核心价值

  1. 前后端解耦:后端保证数据结构,前端专注于UI展现。
  2. 业务逻辑前置:通过Schema设计,提前规避了LLM可能产生的逻辑漏洞。
  3. 生态兼容:生成的JSON可以直接对接任何现代前端框架或数据分析工具。

后续优化

  1. 流式JSON解析:目前必须等LLM说完才能解析,未来可以尝试边说边解析。
  2. 动态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并渲染的表格组件

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!有任何问题或建议,请在评论区留言讨论。 🏃‍♂️💨

相关推荐
Dxy123931021616 小时前
Python请求方式介绍:JSON、表单及其他常见数据传输格式
数据库·python·json
肖恩想要年薪百万17 小时前
JSP中常用JSTL标签
java·开发语言·状态模式
测试_AI_一辰1 天前
AI时代,学东西的方式变了
人工智能·ai·自动化·状态模式·ai编程
北风朝向1 天前
Spring Boot 集成 Open WebUI 实现 AI 流式对话
人工智能·spring boot·状态模式
2301_780789661 天前
多层级 CC 防护体系:前端验证与后端限流的协同配置实践
运维·服务器·前端·网络安全·智能路由器·状态模式
沙振宇1 天前
【Python】使用YOLO8识别视频中的车与人物
python·yolo·音视频·状态模式·识别
薛定猫AI1 天前
【深度解析】从 Gemini 3.2、Claude 限额变化到 AI Agent:大模型工程化选型与实战评估
人工智能·状态模式
龙侠九重天1 天前
大型语言模型结构化输出:用 JSON Schema 约束大模型输出
人工智能·语言模型·自然语言处理·大模型·json
heimeiyingwang1 天前
【架构实战】API网关Spring Cloud Gateway:统一入口的艺术
架构·状态模式