首页
看点啥
插画图片
首页 经济看点 LangChain 1.0 第七篇 - Pydantic结构化输出

LangChain 1.0 第七篇 - Pydantic结构化输出

2026-06-03 0

此前几篇文章中,Agent 的产出均为纯文本格式。但在实际开发场景下,我们常常需要 Agent 交付结构化数据,例如文本所蕴含的情感极性、代码审查的评分与改进建议,抑或用户信息的提取结果。纯文本输出方式在此类场景中显得力不从心。

LangChain 1.0 第七篇 -  Pydantic结构化输出

本篇章将系统阐述 LangChain 中实现结构化输出的完整方案:从定义 Pydantic 模型,到解析 with_structured_output() 的底层原理,再延伸至 Agent 层面结构化输出的三种策略。学完本部分后,你便能引导 Agent 返回任意所需的数据结构,并保障类型安全。

二、为什么需要结构化输出

2.1 纯文本输出的痛点

LLM 默认交付的是一段自由格式的文本。这对于人类阅读而言并无不妥,但当程序需要消费这些输出时,问题便接踵而至:

格式不可控。针对同一问题,模型可能此次返回 JSON 格式,下次却给出带有 Markdown 的纯文本。你永远无法预判模型将采用何种格式返回结果。

字段不确定。在“情感分析”的结果里,这次包含了“置信度”字段,下次该字段就可能消失了。模型输出具备不确定性,字段是否存在完全取决于模型的“状态”。

解析困难。你需要编写正则表达式或复杂的字符串处理逻辑来提取所需信息。这类解析代码既脆弱又难以维护。

  1. from langchain.chat_models import init_chat_model
    from dotenv import load_dotenv
    import os
    import json
    load_dotenv()llm = init_chat_model(
    model=os.getenv("MODEL_NAME"),
    api_key=os.getenv("API_KEY"),
    base_url=os.getenv("BASE_URL"),
    model_provider="openai",
    temperature=0,
    )prompt = """请分析以下文本的情感,并以JSON格式返回。
    JSON格式要求:
    {
    "sentiment": "positive 或 negative 或 neutral",
    "confidence": 0.95,
    "reason": "判断理由"
    }文本:这款产品的用户体验非常棒,界面简洁,功能强大!只返回JSON,不要返回其他内容。"""response = llm.invoke(prompt)
    print(response.content)

运行之后,你获得的输出可能类似这样:

  1. ‍```json
    {
    "sentiment": "positive",
    "confidence": 0.92,
    "reason": "文本使用了'非常棒'、'简洁'、'强大'等积极词汇"
    }

请注意,模型在 JSON 外层包裹了 Markdown 代码块标记。你需要手动将其剥离:

  1. content = response.content.strip()
    if content.startswith("‍```"):
    lines = content.split("n")
    content = "n".join(lines[1:-1])
    try:
    data = json.loads(content)
    print(data["sentiment"])
    except json.JSONDecodeError:
    print("解析失败,原始输出:", response.content)

这种手动解析的方式既脆弱又难以维护。随着业务复杂度提升,JSON 结构变得更为复杂,字段数量增加,手动解析的代码量将急剧膨胀,并且永远无法覆盖所有可能的异常情况。

2.2 Pydantic 的解决方案

Pydantic 是 Python 生态中最流行的数据验证库,LangChain 直接借助它来定义结构化输出的 Schema。核心思路是:你利用 Pydantic 定义一个数据模型,LangChain 将该模型转换为 JSON Schema 并发送给 LLM,LLM 依照 Schema 的约束返回结构化数据,随后 LangChain 再将返回值解析为 Pydantic 对象。整个流程完全自动化,无需手写任何解析逻辑。

下图对比了纯文本输出与结构化输出之间的差异:

三、Pydantic 模型定义

3.1 基础模型

Pydantic 模型定义即结构化输出的 Schema。你仅需继承 BaseModel,使用类型注解声明字段,LangChain 便能将该模型转换为 JSON Schema,促使模型依照此格式返回数据。

以情感分析为例:

  1. from pydantic import BaseModel, Field
    class SentimentAnalysis(BaseModel):
    text: str = Field(description="被分析的原始文本")
    sentiment: str = Field(description="情感极性,只能是 positive、negative 或 neutral")
    confidence: float = Field(description="情感判断的置信度,0到1之间")
    keywords: list[str] = Field(description="文本中反映情感的关键词列表")

每个字段中的 Field(description=...) 既是对开发者的说明,也是对 LLM 的指引——模型会依据这些描述来理解每个字段应填充的内容。这种双重价值正是 Pydantic 方案的核心优势。

再看一个代码审查的场景:

  1. class CodeReview(BaseModel):
    file_path: str = Field(description="被审查的文件路径")
    overall_score: int = Field(description="整体评分,1到10分")
    issues: list[str] = Field(description="发现的问题列表,每条简洁描述")
    suggestions: list[str] = Field(description="改进建议列表")
    summary: str = Field(description="一段话的审查总结")

3.2 嵌套结构

真实业务中的数据结构往往并非扁平。Pydantic 支持嵌套模型,使你能够表达任意复杂的数据层次:

  1. from typing import Optional, Literal
    from pydantic import BaseModel, Field
    class ContactInfo(BaseModel):
    phone: Optional[str] = Field(default=None, description="手机号码")
    email: Optional[str] = Field(default=None, description="邮箱地址")
    address: Optional[str] = Field(default=None, description="居住地址")
    class WorkExperience(BaseModel):
    company: str = Field(description="公司名称")
    position: str = Field(description="职位")
    duration: str = Field(description="任职时长,如 2020-2023")
    description: str = Field(description="工作内容简述")
    class PersonProfile(BaseModel):
    name: str = Field(description="姓名")
    age: Optional[int] = Field(default=None, description="年龄,如果文本中未提及则为null")
    gender: Literal["男", "女", "未知"] = Field(description="性别")
    occupation: str = Field(description="职业")
    contact: ContactInfo = Field(description="联系方式")
    experiences: list[WorkExperience] = Field(description="工作经历列表,按时间倒序排列")
    skills: list[str] = Field(description="专业技能列表")
    summary: str = Field(description="一句话概括此人背景")

Optional 表示字段可为空,Literal 用于限制字段仅能获取枚举值,list[WorkExperience] 则代表对象列表。这些类型约束最终都会转化为 JSON Schema 并传递给模型,助力其更精准地填充数据。

下图展示了 Pydantic 模型到 JSON Schema 的转换过程:

3.3 字段验证与约束

Field 不仅提供 description,还支持多种数值和字符串约束。这些约束一方面用于数据校验,另一方面也会传递给模型作为生成指引:

  1. from pydantic import BaseModel, Field
    class ProductReview(BaseModel):
    product_name: str = Field(description="产品名称")
    rating: int = Field(ge=1, le=5, description="评分,1到5分")
    price_satisfaction: int = Field(ge=1, le=10, description="价格满意度,1到10分")
    pros: list[str] = Field(
    min_length=1, max_length=5,
    description="优点列表,至少1条,最多5条"
    )
    cons: list[str] = Field(
    min_length=1, max_length=5,
    description="缺点列表,至少1条,最多5条"
    )
    recommend: bool = Field(description="是否推荐此产品")
    recommend_reason: str = Field(
    max_length=50,
    description="推荐或否定的理由,50字以内"
    )

ge(大于等于)和 le(小于等于)用于限制数值范围,min_lengthmax_length 则限制列表长度或字符串长度。尽管 LLM 不一定能 100% 遵守所有约束,但这些约束仍能显著提升输出质量。

3.4 自定义验证器

Pydantic V2 提供了 @field_validator@model_validator 装饰器,允许你在模型层面定义复杂的校验逻辑。这在结构化输出场景中十分实用——模型返回的数据需经过二次验证方能使用:

  1. from pydantic import BaseModel, Field, field_validator, model_validator
    from typing import Optional
    class EventInfo(BaseModel):
    name: str = Field(description="活动名称")
    start_date: str = Field(description="开始日期,格式 YYYY-MM-DD")
    end_date: Optional[str] = Field(default=None, description="结束日期,格式 YYYY-MM-DD")
    location: str = Field(description="举办地点")
    capacity: int = Field(ge=1, description="最大容纳人数")
    @field_validator("start_date", "end_date")
    @classmethod
    def validate_date_format(cls, v: Optional[str]) -> Optional[str]:
    if v is None:
    return v
    from datetime import datetime
    try:
    datetime.strptime(v, "%Y-%m-%d")
    except ValueError:
    raise ValueError(f"日期格式错误: {v},应为 YYYY-MM-DD")
    return v
    @model_validator(mode="after")
    def validate_date_range(self):
    if self.end_date and self.end_date < self.start_date:
    raise ValueError("结束日期不能早于开始日期")
    return self

@field_validator 用于单个字段的校验(如日期格式检查),@model_validator(mode="after") 用于多字段联合校验(如起止日期的逻辑关系)。需要注意的是,这些验证器在 Pydantic 反序列化时执行,若 LLM 生成的数据违反约束,with_structured_output() 会抛出 ValidationError,你需要在调用层进行错误处理。

3.5 Optional 字段与 Literal 枚举的最佳实践

在信息抽取场景中,原始文本通常不会涵盖所有字段。Optionaldefault 的正确使用至关重要:

  1. from typing import Optional, Literal
    from pydantic import BaseModel, Field
    class MedicalRecord(BaseModel):
    patient_name: str = Field(description="患者姓名")
    age: Optional[int] = Field(
    default=None,
    description="患者年龄,未提及则为null"
    )
    gender: Literal["男", "女", "未知"] = Field(
    default="未知",
    description="性别,只能选 男、女、未知"
    )
    symptoms: list[str] = Field(
    default_factory=list,
    description="症状列表,至少列出已提到的症状"
    )
    severity: Literal["轻度", "中度", "重度"] = Field(
    default="轻度",
    description="严重程度,未明确标注时默认为轻度"
    )
    diagnosis: Optional[str] = Field(
    default=None,
    description="诊断结果,如果文本中未提及则为null"
    )
    medications: list[str] = Field(
    default_factory=list,
    description="提到的药物列表"
    )

关键原则:必须字段采用 strint(无 default),可选字段使用 Optional[T] 并设置 default=None。这能告知 LLM 哪些字段是必须从文本中提取的,哪些可以跳过。Literal 类型则用于限制枚举值,避免模型自创分类。

请注意!default_factory=list 用于可变默认值。如果写作 default=[] 会导致数据错误,因为 Python 的可变默认值会在线程间共享。

四、with_structured_output

4.1 基础用法

with_structured_output() 是 LangChain 提供的核心方法,它能使模型直接返回 Pydantic 对象,而非纯文本。其底层实现依托于 Tool Calling 机制——LangChain 将你的 Pydantic 模型注册为一个“工具”,让模型以调用工具的方式来返回结构化数据。

  1. # -*- encoding: utf-8 -*-
    '''
    @File : pydantic_structure.py
    @Time : 2026/04/27 10:49:59
    @Author : xcy.小相
    @Version : 1.0
    @Description : 07-Pydantic结构化输出
    '''
    from pydantic import BaseModel, Field
    from langchain.chat_models import init_chat_model
    from dotenv import load_dotenv
    import os
    load_dotenv()llm = init_chat_model(
    model=os.getenv("MODEL_NAME"),
    api_key=os.getenv("API_KEY"),
    base_url=os.getenv("BASE_URL"),
    model_provider="openai"
    )class MovieReview(BaseModel):
    title: str = Field(description="电影名称")
    director: str = Field(description="导演姓名")
    rating: float = Field(description="评分,0到10分")
    genre: list[str] = Field(description="电影类型标签")
    one_line_review: str = Field(description="一句话影评")
    structured_llm = llm.with_structured_output(MovieReview)result = structured_llm.invoke("评价一下电影《星际穿越》,导演是诺兰")
    print(type(result))
    print(result.model_dump_json(indent=2))

运行结果:

  1. <class '__main__.MovieReview'>
    {
    "title": "《星际穿越》:穿越时空的父爱与宇宙的诗意",
    "director": "克里斯托弗·诺兰",
    "rating": 9.1,
    "genre": [ "科幻", "剧情", "冒险" ],
    "one_line_review": "一部融合硬核科学与深沉情感的太空史诗,是诺兰最具哲学深度的电影之一。"
    }

输出类型直接就是 MovieReview 实例。你可以通过 .rating.genre 等属性直接访问字段,也能借助 .model_dump() 将其转为字典或使用 .model_dump_json() 转为 JSON 字符串。不再需要手写任何解析逻辑。

4.2 json_schema vs function_calling

with_structured_output()method 参数控制底层实现方式,两种主要方法各有特点:

method="function_calling"(默认值)。将 Pydantic 模型注册为一个 Function Calling 的“工具”,模型通过“调用工具”的方式返回结构化数据。这是大多数支持 Tool Calling 的模型的默认方式,效果通常最佳。

method="json_schema"。将 Pydantic 模型转换为 JSON Schema,模型在响应中遵循 JSON Schema 结构输出。此模式不会将 Schema 传递给模型,仅在模型响应后用于解析和验证输出格式。适用于不支持 Tool Calling 但支持 JSON 输出的模型。

  1. structured_llm_json = llm.with_structured_output(MovieReview, method="json_schema")
    structured_llm_func = llm.with_structured_output(MovieReview, method="function_calling")

实际选择时,绝大多数情况下使用默认的 method="function_calling" 即可。只有当模型不支持 Tool Calling 但能输出 JSON 时,才需要显式指定 method="json_schema"

两种方法的详细对比:

  1. 特性method="function_calling"method="json_schema"
    底层机制Function Calling,以“工具”形式返回JSON Schema,模型响应后解析验证
    输出纯净度高,由框架解析后直接返回 Pydantic 实例中,依赖模型自行遵循 JSON 格式
    模型兼容性需要模型显式支持 Tool Calling大部分现代模型支持
    并行输出支持同时调用多个 Function不支持并行多 Schema
    Schema 复杂度每个工具独立定义,无引用支持 $defs/$ref 复用
    输出包含思考过程部分模型会附带推理文本否,纯 JSON
    推荐场景支持 Tool Calling 的模型(默认首选)不支持 Tool Calling 的模型

在 LangGraph Agent 中,如果 Agent 同时使用了工具和结构化输出,function_calling 方式可能导致模型混淆“调工具”和“返回结构化数据”两种意图。此时,json_schema 更为安全。

4.3 错误处理与重试

模型偶尔会返回不符合 Schema 的数据,尤其是在模型能力不足或 Schema 过于复杂时。with_structured_output() 在解析失败时会抛出异常,生产环境中必须处理这种情况:

  1. from pydantic import BaseModel, Field, ValidationError
    from langchain.chat_models import init_chat_model
    from dotenv import load_dotenv
    import os
    load_dotenv()llm = init_chat_model(
    model=os.getenv("MODEL_NAME"),
    api_key=os.getenv("API_KEY"),
    base_url=os.getenv("BASE_URL"),
    model_provider="openai",
    temperature=0,
    )class ExtractionResult(BaseModel):
    company: str = Field(description="公司名称")
    position: str = Field(description="职位名称")
    salary_min: int = Field(description="最低薪资,单位元/月")
    salary_max: int = Field(description="最高薪资,单位元/月")
    structured_llm = llm.with_structured_output(ExtractionResult)text = "某互联网大厂招聘高级Python工程师,薪资范围30k-50k,要求3年以上经验。"try:
    result = structured_llm.invoke(f"从以下文本中提取招聘信息:n{text}")
    print(result.model_dump())
    except ValidationError as e:
    print(f"结构验证失败: {e}")
    fallback_result = ExtractionResult(
    company="未知",
    position="未知",
    salary_min=0,
    salary_max=0,
    )
    except Exception as e:
    print(f"调用失败: {e}")

一种实用的降级策略是:结构化解析失败时,回退到纯文本模式,再用正则或简单的字符串匹配提取关键信息。虽然不尽完美,但总比直接报错要好。

五、代码实战

5.1 信息提取 Agent

下面是一个完整的“简历信息提取 Agent”,展示了嵌套 Pydantic 模型在 Agent 中的实际应用:

  1. # -*- encoding: utf-8 -*-
    '''
    @File : pydantic_structure_demo01.py
    @Time : 2026/04/27 10:49:59
    @Author : xcy.小相
    @Version : 1.0
    @Description : 07-Pydantic结构化输出
    '''
    from pydantic import BaseModel, Field
    from typing import Optional, Literal
    from langchain.chat_models import init_chat_model
    from langchain.agents import create_agent
    from langchain.tools import tool
    from dotenv import load_dotenv
    import os
    load_dotenv()class ContactInfo(BaseModel):
    phone: Optional[str] = Field(default=None, description="手机号码")
    email: Optional[str] = Field(default=None, description="邮箱地址")
    class WorkExperience(BaseModel):
    company: str = Field(description="公司名称")
    position: str = Field(description="职位")
    duration: str = Field(description="任职时长")
    description: str = Field(description="工作内容简述")
    class PersonProfile(BaseModel):
    name: str = Field(description="姓名")
    age: Optional[int] = Field(default=None, description="年龄")
    gender: Literal["男", "女", "未知"] = Field(default="未知", description="性别")
    occupation: str = Field(description="当前职业")
    contact: ContactInfo = Field(description="联系方式")
    experiences: list[WorkExperience] = Field(description="工作经历列表")
    skills: list[str] = Field(description="专业技能列表")
    summary: str = Field(description="一句话概括此人背景")
    @tool
    def search_linkedin(query: str) -> str:
    """搜索 LinkedIn 上的公开信息。"""
    mock_data = {
    "张三": "张三,高级后端工程师,曾在字节跳动(2年)和阿里巴巴(3年)工作,擅长 Python、Go 和系统设计。",
    "李四": "李四,产品经理,5年经验,专注于 AI 产品方向,曾主导过多个大模型应用的落地。",
    }
    for name, info in mock_data.items():
    if name in query:
    return info
    return f"未找到与 '{query}' 相关的公开信息。"
    llm = init_chat_model(
    model=os.getenv("MODEL_NAME"),
    api_key=os.getenv("API_KEY"),
    base_url=os.getenv("BASE_URL"),
    model_provider="openai"
    )agent = create_agent(
    model=llm,
    tools=[search_linkedin],
    system_prompt="你是一个专业的 LinkedIn 搜索助手,能够根据用户的问题搜索 LinkedIn 上的公开信息,并将结果整理为结构化的人物档案。",
    response_format=PersonProfile,
    )response = agent.invoke({"messages": [{"role": "user", "content": "搜索张三的 LinkedIn 信息"}]})
    print(response["messages"][-1].content_blocks[0].model_dump_json(indent=2, ensure_ascii=False))

运行结果示例:

  1. {
    "name": "张三",
    "age": null,
    "gender": "未知",
    "occupation": "高级后端工程师",
    "contact": {
    "phone": null,
    "email": null
    },
    "experiences": [
    {
    "company": "字节跳动",
    "position": "后端工程师",
    "duration": "2年",
    "description": "负责核心业务系统的架构设计与开发,使用Python和Go语言实现高并发服务"
    },
    {
    "company": "阿里巴巴",
    "position": "高级后端工程师",
    "duration": "3年",
    "description": "主导分布式系统开发,优化支付系统性能,提升用户体验"
    }
    ],
    "skills": [
    "Python",
    "Go",
    "系统设计",
    "分布式系统",
    "高并发架构"
    ],
    "summary": "拥有5年互联网行业经验的高级后端工程师,擅长使用Python和Go语言开发高性能系统,具备丰富的分布式架构设计经验。"
    }

5.2 自然语言信息抽取 Agent

以下是一个更为实用的完整示例——从非结构化的自然语言中批量抽取结构化数据。这种场景在实际业务中极为常见:从客户反馈中抽取产品评价、从新闻中抽取事件信息、从邮件中抽取工单数据。

  1. # -*- encoding: utf-8 -*-
    '''
    @File : pydantic_structure_demo02.py
    @Time : 2026/04/27 10:49:59
    @Author : xcy.小相
    @Version : 1.0
    @Description : 07-Pydantic结构化输出
    '''
    from pydantic import BaseModel, Field
    from typing import Optional, Literal
    from langchain.chat_models import init_chat_model
    from dotenv import load_dotenv
    import os
    load_dotenv()class ExtractedEntity(BaseModel):
    name: str = Field(description="实体名称,如人名、公司名、产品名")
    entity_type: Literal["人名", "公司", "产品", "地点", "日期", "其他"] = Field(
    description="实体类型"
    )
    description: str = Field(description="对该实体的简要描述")
    class ExtractedRelation(BaseModel):
    subject: str = Field(description="关系主体")
    predicate: str = Field(description="关系类型,如'就职于'、'开发了'、'位于'")
    object: str = Field(description="关系客体")
    class ExtractedEvent(BaseModel):
    event_type: Literal["收购", "发布", "合作", "人事变动", "其他"] = Field(
    description="事件类型"
    )
    summary: str = Field(description="事件的一句话概括")
    date: Optional[str] = Field(default=None, description="事件日期,格式 YYYY-MM-DD")
    participants: list[str] = Field(description="参与方列表")
    impact: Optional[str] = Field(default=None, description="事件影响评估")
    class NewsExtraction(BaseModel):
    entities: list[ExtractedEntity] = Field(description="文本中提到的所有关键实体")
    relations: list[ExtractedRelation] = Field(description="实体之间的关系")
    events: list[ExtractedEvent] = Field(description="文本中描述的事件")
    sentiment: Literal["正面", "负面", "中性"] = Field(description="文本整体情感倾向")
    summary: str = Field(description="文本的三句话摘要")
    llm = init_chat_model(
    model=os.getenv("MODEL_NAME"),
    api_key=os.getenv("API_KEY"),
    base_url=os.getenv("BASE_URL"),
    model_provider="openai",
    max_tokens=16384,
    )structured_llm = llm.with_structured_output(NewsExtraction)news_text = """
    2025年3月15日,智谱AI正式发布GLM-5大模型,该模型在多项基准测试中超越了GPT-4o。
    智谱AI CEO张鹏在发布会上表示,GLM-5已在多个企业客户中部署,包括字节跳动和阿里巴巴。
    分析师认为,此举将加速国内大模型产业的竞争格局变化,对OpenAI的市场份额形成压力。
    市场反应积极,智谱AI估值已突破200亿美元。
    """result = structured_llm.invoke(
    f"从以下新闻文本中抽取结构化信息:nn{news_text}"
    )print("=" * 60)
    print("实体抽取结果:")
    for entity in result.entities:
    print(f" [{entity.entity_type}] {entity.name} - {entity.description}")
    print(f"n关系抽取结果(共 {len(result.relations)} 条):")
    for rel in result.relations:
    print(f" {rel.subject} → [{rel.predicate}] → {rel.object}")
    print(f"n事件抽取结果:")
    for event in result.events:
    print(f" [{event.event_type}] {event.summary}")
    print(f" 日期: {event.date}, 参与方: {', '.join(event.participants)}")
    print(f"n情感倾向: {result.sentiment}")
    print(f"摘要: {result.summary}")

运行结果示例:

  1. ============================================================
    实体抽取结果:
    [公司] 智谱AI - 智谱AI
    [公司] 智谱AI - 北京智谱人工智能技术有限公司
    [产品] GLM-5 - GLM-5
    [产品] GPT-4o - GPT-4o
    [人名] 张鹏 - 张鹏
    [公司] 字节跳动 - 字节跳动
    [公司] 阿里巴巴 - 阿里巴巴
    [公司] OpenAI - OpenAI
    关系抽取结果(共 8 条):
    智谱AI → [发布] → GLM-5
    市场反应 → [导致] → 估值突破200亿美元
    GLM-5发布 → [影响] → 加速产业竞争
    国内大模型产业竞争 → [影响] → 形成压力
    智谱AI → [部署] → GLM-5
    GLM-5发布 → [分析师认为] → 国内大模型产业竞争
    智谱AI → [估值] → 200亿美元
    GLM-5 → [超越] → GPT-4o
    事件抽取结果:
    [发布] 智谱AI正式发布GLM-5大模型,该模型在多项基准测试中超越了GPT-4o。
    日期: 2025年3月, 参与方: 智谱AI, GLM-5, GPT-4o
    [合作] 分析师认为GLM-5的发布将加速国内大模型产业竞争,对OpenAI市场份额形成压力。
    日期: 2025年3月(发布后), 参与方: 智谱AI, OpenAI, GLM-5
    [合作] 智谱AI CEO张鹏宣布GLM-5已在字节跳动和阿里巴巴等企业客户中部署。
    日期: 2025年3月(发布后), 参与方: 智谱AI, 字节跳动, 阿里巴巴
    [其他] 市场对GLM-5的积极反应使智谱AI估值突破200亿美元。
    日期: 2025年3月(发布后), 参与方: 智谱AI情感倾向: 正面
    摘要: 智谱科技近期推出GLM-5大模型,该模型在基准测试中超越GPT-4o,并已与字节跳动、阿里巴巴等企业达成合作部署协议。行业分析师指出,这一进展或将强化国内大模型产业的市场动能并动摇OpenAI在海外竞争者的主导地位,资本市场对此积极响应,直接推动该公司估值突破200亿美元门槛。

该示例展示了嵌套 Pydantic 模型的真正威力:NewsExtraction 包含三个嵌套模型列表(实体、关系、事件),每个都有各自的字段约束和枚举值。LLM 在一次调用中便完成了实体识别、关系抽取、事件抽取、情感分析和摘要生成五个 NLP 任务。

下图展示了信息抽取 Agent 的完整数据处理流程:

六、常见陷阱与调试

字段太多导致质量下降。当 Pydantic 模型超过 20 个字段时,LLM 的输出质量往往会下降,表现为字段为空、类型错误或内容不准确。解决方案是拆分为多个小模型,分步骤进行提取。

description 写得太模糊。模型依赖 Field(description=...) 来理解每个字段的含义。若描述模糊不清,模型的输出也会模糊不清。好的描述应当具体、明确且带有约束条件。

模型不支持 Tool Callingwith_structured_output() 底层依赖模型的 Function Calling 或 JSON Mode 能力。如果所使用的模型不支持这些能力,调用将会失败。建议使用 Qwen3 系列、GPT-4o、Claude 3.5 等明确支持 Tool Calling 的模型。

嵌套层级过深。三层以上的嵌套结构会致使模型的输出质量急剧下降。若业务确实需要深层嵌套,可考虑在 Agent 层面分步处理:先提取外层结构,再逐层填充。

temperature 设置不当。在结构化输出场景中,建议将 temperature 设为 0。较高的温度会增加模型的随机性,导致输出格式不稳定。

模型幻觉导致输出不符合 Schema。这是结构化输出中最隐蔽的陷阱。模型可能“编造” Schema 中不存在的字段,或给枚举字段填入不在枚举列表中的值。例如,你定义了 Literal["正面", "负面", "中性"],但模型返回了 "偏正面"。这类问题的根源在于:LLM 本质上是语言模型而非约束求解器,它只是“尽量”遵循 Schema,并不能 100% 保证。

应对幻觉的策略:

  1. from pydantic import BaseModel, Field, ValidationError
    from langchain.chat_models import init_chat_model
    from dotenv import load_dotenv
    import os
    load_dotenv()llm = init_chat_model(
    model=os.getenv("MODEL_NAME"),
    api_key=os.getenv("API_KEY"),
    base_url=os.getenv("BASE_URL"),
    model_provider="openai",
    temperature=0,
    )class StrictSentiment(BaseModel):
    sentiment: str = Field(
    description="情感极性",
    json_schema_extra={"enum": ["正面", "负面", "中性"]},
    )
    confidence: float = Field(ge=0, le=1, description="置信度")
    structured_llm = llm.with_structured_output(StrictSentiment)text = "这个产品还行吧,不算特别好也不算特别差,中等水平。"max_retries = 3
    for attempt in range(max_retries):
    try:
    result = structured_llm.invoke(f"分析情感:{text}")
    if result.sentiment not in ["正面", "负面", "中性"]:
    raise ValueError(f"非法情感值: {result.sentiment}")
    print(f"第 {attempt + 1} 次成功: {result.sentiment} ({result.confidence})")
    break
    except (ValidationError, ValueError) as e:
    print(f"第 {attempt + 1} 次失败: {e}")
    if attempt == max_retries - 1:
    fallback = StrictSentiment(sentiment="中性", confidence=0.5)
    print(f"降级为默认值: {fallback.model_dump()}")

运行结果示例:

  1. 第 1 次成功: 中性 (0.7)

字段缺失的处理。当源文本确实不包含某字段的信息时,模型可能返回空字符串 "" 而非 null,或者编造一个看似合理的值。这是信息抽取场景中最常见的陷阱。解决方案:

  1. from typing import Optional
    from pydantic import BaseModel, Field
    class RobustExtraction(BaseModel):
    company: Optional[str] = Field(
    default=None,
    description="公司名称。如果文本中未明确提到公司名,返回null,不要猜测。"
    )
    revenue: Optional[float] = Field(
    default=None,
    description="营收金额,单位亿元。如果文本中未提及具体数字,返回null。"
    )
    source: str = Field(
    description="你所填写的每个字段值的原文依据,引用原文片段"
    )

关键技巧:在 description 中明确告知模型“如果未提及则返回 null,不要猜测”。同时增加一个 source 字段,要求模型标注信息来源——这能有效抑制幻觉,因为模型被迫提供“证据”。

list 字段返回空列表还是 null。当文本中没有相关信息时,模型可能返回 [](空列表)或 null。在 Schema 中明确使用 default_factory=list 并在描述中写清楚期望行为,可以消除此类歧义。

七、本章小结

本部分系统阐述了 LangChain 结构化输出的完整方案,核心要点在于将不可控的纯文本输出,转化为类型安全、可解析的 Pydantic 对象。

  1. 概念要点
    Pydantic 模型BaseModel + Field 定义输出 Schema
    嵌套结构OptionalLiterallist[T] 表达复杂数据
    字段约束ge/le/min_length/max_length 提升输出质量
    自定义验证器@field_validator@model_validator 做二次校验
    with_structured_output()模型级结构化输出,返回 Pydantic 实例
    method 对比function_calling(默认首选)vs json_schema(不支持 Tool Calling 时)
    Agent 结构化输出create_agent(response_format=Model)
    错误处理try/except + 重试 + 降级策略
    幻觉防御source 字段标注来源 + description 中明确 null 策略

核心原则:凡能用 Pydantic 定义之处,便无需手写解析代码;凡能使用 with_structured_output() 之处,便不要在 Prompt 中要求 JSON 格式。这是确保 LangChain 应用输出稳定、可维护的根本之道。

八、扩展阅读

  1. Pydantic V2 官方文档 — Pydantic 模型定义与验证的权威参考
  2. LangChain Structured Output 指南 — LangChain 官方结构化输出教程
  3. JSON Schema 规范 — 理解 Pydantic 模型背后的 Schema 标准
  4. OpenAI Function Calling — Tool Calling 底层机制的原始文档
喜欢(0)

上一篇

Claude Code 实战手册:从安装到每天省3小时

Claude Code 实战手册:从安装到每天省3小时

下一篇

微软发布 Web IQ:专为 AI 智能体打造的搜索 API:减少 Token 消耗并提升响应速度

微软发布 Web IQ:专为 AI 智能体打造的搜索 API:减少 Token 消耗并提升响应速度
猜你喜欢