在 LangChain 的 Model I/O 架构中,结构化输出是连接大语言模型(LLM)与下游业务系统的关键桥梁。它负责将模型生成的非结构化自然语言,转化为程序可直接消费、校验和处理的强类型数据对象。本章将深入剖析结构化输出的四种 Schema 定义模式、底层校验机制以及生产环境中的最佳实践。
一、 核心概念与架构定位
1.1 什么是结构化输出
结构化输出的核心目标是将"自然语言回答"转化为"程序可稳定消费的数据"。模型最终返回的必须是一个符合预定义结构的数据对象(如 Pydantic 模型实例、TypedDict、标准字典),而不再是无格式的自然语言文本。
工程价值:
- 易于代码处理:下游系统可直接通过属性或键名读取数据,无需编写脆弱的正则表达式或字符串分割逻辑。
- 结果高度稳定:消除"模型说法变了但意思差不多"导致的解析失败。
- 适配复杂工作流:完美支持表单抽取、分类路由、工具参数生成、Agent 状态传递等场景。
1.2 传统方式 vs 结构化输出
#mermaid-svg-N4ioKMqXvaxwbNqU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N4ioKMqXvaxwbNqU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N4ioKMqXvaxwbNqU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N4ioKMqXvaxwbNqU .error-icon{fill:#552222;}#mermaid-svg-N4ioKMqXvaxwbNqU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N4ioKMqXvaxwbNqU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N4ioKMqXvaxwbNqU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N4ioKMqXvaxwbNqU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N4ioKMqXvaxwbNqU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N4ioKMqXvaxwbNqU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N4ioKMqXvaxwbNqU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N4ioKMqXvaxwbNqU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N4ioKMqXvaxwbNqU .marker.cross{stroke:#333333;}#mermaid-svg-N4ioKMqXvaxwbNqU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N4ioKMqXvaxwbNqU p{margin:0;}#mermaid-svg-N4ioKMqXvaxwbNqU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N4ioKMqXvaxwbNqU .cluster-label text{fill:#333;}#mermaid-svg-N4ioKMqXvaxwbNqU .cluster-label span{color:#333;}#mermaid-svg-N4ioKMqXvaxwbNqU .cluster-label span p{background-color:transparent;}#mermaid-svg-N4ioKMqXvaxwbNqU .label text,#mermaid-svg-N4ioKMqXvaxwbNqU span{fill:#333;color:#333;}#mermaid-svg-N4ioKMqXvaxwbNqU .node rect,#mermaid-svg-N4ioKMqXvaxwbNqU .node circle,#mermaid-svg-N4ioKMqXvaxwbNqU .node ellipse,#mermaid-svg-N4ioKMqXvaxwbNqU .node polygon,#mermaid-svg-N4ioKMqXvaxwbNqU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N4ioKMqXvaxwbNqU .rough-node .label text,#mermaid-svg-N4ioKMqXvaxwbNqU .node .label text,#mermaid-svg-N4ioKMqXvaxwbNqU .image-shape .label,#mermaid-svg-N4ioKMqXvaxwbNqU .icon-shape .label{text-anchor:middle;}#mermaid-svg-N4ioKMqXvaxwbNqU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N4ioKMqXvaxwbNqU .rough-node .label,#mermaid-svg-N4ioKMqXvaxwbNqU .node .label,#mermaid-svg-N4ioKMqXvaxwbNqU .image-shape .label,#mermaid-svg-N4ioKMqXvaxwbNqU .icon-shape .label{text-align:center;}#mermaid-svg-N4ioKMqXvaxwbNqU .node.clickable{cursor:pointer;}#mermaid-svg-N4ioKMqXvaxwbNqU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N4ioKMqXvaxwbNqU .arrowheadPath{fill:#333333;}#mermaid-svg-N4ioKMqXvaxwbNqU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N4ioKMqXvaxwbNqU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N4ioKMqXvaxwbNqU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N4ioKMqXvaxwbNqU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N4ioKMqXvaxwbNqU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N4ioKMqXvaxwbNqU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N4ioKMqXvaxwbNqU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N4ioKMqXvaxwbNqU .cluster text{fill:#333;}#mermaid-svg-N4ioKMqXvaxwbNqU .cluster span{color:#333;}#mermaid-svg-N4ioKMqXvaxwbNqU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N4ioKMqXvaxwbNqU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N4ioKMqXvaxwbNqU rect.text{fill:none;stroke-width:0;}#mermaid-svg-N4ioKMqXvaxwbNqU .icon-shape,#mermaid-svg-N4ioKMqXvaxwbNqU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N4ioKMqXvaxwbNqU .icon-shape p,#mermaid-svg-N4ioKMqXvaxwbNqU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N4ioKMqXvaxwbNqU .icon-shape .label rect,#mermaid-svg-N4ioKMqXvaxwbNqU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N4ioKMqXvaxwbNqU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N4ioKMqXvaxwbNqU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N4ioKMqXvaxwbNqU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 结构化输出:简洁且强类型
定义 Pydantic/TypedDict Schema
with_structured_output 绑定
模型基于 Schema 约束生成
LangChain 自动解析与类型校验
直接获取强类型 Python 对象
传统方式:繁琐且脆弱
编写 Prompt 苦求返回 JSON
模型生成文本
手动 json.loads 解析
手动 try-except 捕获异常
手动校验字段类型与业务逻辑
二、 底层工作流与核心机制
2.1 with_structured_output 的核心作用
在 LangChain 1.x 中,模型对象通过调用 with_structured_output(schema) 方法绑定输出模式。该方法返回一个新的 Runnable 对象,其内部封装了 Schema 转换、Prompt 注入、结果解析与校验的完整逻辑。
2.2 结构化输出的底层工作流
大语言模型 LangChain 开发者 大语言模型 LangChain 开发者 #mermaid-svg-0UG07OMZikjw7el2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0UG07OMZikjw7el2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0UG07OMZikjw7el2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0UG07OMZikjw7el2 .error-icon{fill:#552222;}#mermaid-svg-0UG07OMZikjw7el2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0UG07OMZikjw7el2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0UG07OMZikjw7el2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0UG07OMZikjw7el2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0UG07OMZikjw7el2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0UG07OMZikjw7el2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0UG07OMZikjw7el2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0UG07OMZikjw7el2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0UG07OMZikjw7el2 .marker.cross{stroke:#333333;}#mermaid-svg-0UG07OMZikjw7el2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0UG07OMZikjw7el2 p{margin:0;}#mermaid-svg-0UG07OMZikjw7el2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0UG07OMZikjw7el2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0UG07OMZikjw7el2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0UG07OMZikjw7el2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0UG07OMZikjw7el2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0UG07OMZikjw7el2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0UG07OMZikjw7el2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0UG07OMZikjw7el2 .sequenceNumber{fill:white;}#mermaid-svg-0UG07OMZikjw7el2 #sequencenumber{fill:#333;}#mermaid-svg-0UG07OMZikjw7el2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0UG07OMZikjw7el2 .messageText{fill:#333;stroke:none;}#mermaid-svg-0UG07OMZikjw7el2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0UG07OMZikjw7el2 .labelText,#mermaid-svg-0UG07OMZikjw7el2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0UG07OMZikjw7el2 .loopText,#mermaid-svg-0UG07OMZikjw7el2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0UG07OMZikjw7el2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0UG07OMZikjw7el2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0UG07OMZikjw7el2 .noteText,#mermaid-svg-0UG07OMZikjw7el2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0UG07OMZikjw7el2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0UG07OMZikjw7el2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0UG07OMZikjw7el2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0UG07OMZikjw7el2 .actorPopupMenu{position:absolute;}#mermaid-svg-0UG07OMZikjw7el2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0UG07OMZikjw7el2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0UG07OMZikjw7el2 .actor-man circle,#mermaid-svg-0UG07OMZikjw7el2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0UG07OMZikjw7el2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 模型底层启动语法采样约束(Grammar-based sampling) 1. 定义 Schema (如 Pydantic Model)2. 调用 with_structured_output(Schema)3. 协议转换:将 Schema 转换为标准 JSON Schema4. 模型交互:将 JSON Schema 作为 Tool/Function 注入请求5. 返回符合 JSON 规范的字符串/Tool Call6. 自动解析:JSON 字符串 ->> Python 字典7. 自动校验:字典 ->> Pydantic 实例 (类型/规则校验)8. 返回强类型 Python 对象
核心约束机制:现代大模型(如 OpenAI、Anthropic)普遍支持"函数调用"或"JSON Mode"。LangChain 会将 JSON Schema 作为 Tools 传入,模型在解码生成 Token 时,会严格按照 JSON Schema 的语法树进行选择,从而在模型底层保证了输出格式绝不走样。
三、 四种 Schema 定义模式深度解析
LangChain 支持四种 Schema 定义方式。其中,只有 Pydantic 返回的是 Schema 类实例,其余三种方式返回的都是字典 ;也只有 Pydantic 在类型不匹配时会抛出异常。
3.1 模式一:Pydantic(生产环境首选)
Pydantic 提供字段校验、描述、嵌套结构,功能最丰富,是生产场景的绝对首选。
3.1.1 基本使用要素
- 必须继承
BaseModel。 - 使用 Python 类型提示(
str,int,List等)。 - 使用
Field()添加字段描述(Description),这直接充当了 Prompt 的一部分,帮助 LLM 理解字段含义。
python
from pydantic import BaseModel, Field
class MovieModel(BaseModel):
"""电影的详细信息"""
title: str = Field(description="电影标题")
year: int = Field(description="电影上映年份")
director: str = Field(description="导演")
rating: float = Field(description="电影评分,满分十分")
# 绑定并调用
structured_llm = model.with_structured_output(MovieModel)
response = structured_llm.invoke("给出盗梦空间的信息")
# response 是 MovieModel 实例,支持 IDE 自动补全
print(response.title) # 输出: 盗梦空间
3.1.2 高级特性与平台差异避坑
在实际工程中,不同模型平台(如 CloseAI/OpenAI 与 OpenRouter)对 Pydantic 高级特性的支持存在差异,需特别注意。
特性 1:可选字段(Optional)
当 LLM 无法提取某字段时,使用 Optional 可防止校验失败。
python
from typing import Optional
class Person(BaseModel):
name: str = Field(description="姓名")
age: Optional[int] = Field(description="年龄") # 缺失时返回 None,而非报错
特性 2:默认值(Default)
python
class Config(BaseModel):
timeout: Optional[int] = Field(30, description="超时时间(秒)")
retry: bool = Field(False, description="是否支持重试")
⚠️ 平台差异:CloseAI (OpenAI) 能正确应用默认值;但部分中转平台(如早期的 OpenRouter 某些模型)可能会忽略默认值,导致字段缺失或返回 0/False。
特性 3:枚举类型限制(Enum / Literal)
限制字段的可选值,防止模型"幻觉"生成非法状态。
python
from typing import Literal
from enum import Enum
class Priority(str, Enum):
LOW = "低"
MEDIUM = "中"
HIGH = "高"
class CustomerInfo(BaseModel):
issue: str = Field(description="问题描述")
# 方式 A:使用 Enum 类
urgency_enum: Priority = Field(description="紧急程度")
# 方式 B:使用 Literal 直接限定字面量(更简洁)
urgency_literal: Literal["低", "中", "高"] = Field(description="紧急程度")
特性 4:限制条件(Validation Rules)
使用 Pydantic 的约束参数(如 min_length, gt, ge)。
python
class Product(BaseModel):
name: str = Field(min_length=2)
price: float = Field(gt=0) # 必须大于 0
stock: int = Field(ge=0) # 必须大于等于 0
⚠️ 核心避坑(模型纠错能力差异) :
当 Prompt 传入非法数据(如 price=-7999)时:
- CloseAI (OpenAI) :模型具备极强的自我纠错能力,会忽略负号,返回
price=7999.0。 - OpenRouter (部分模型) :纠错能力较弱,可能会返回
price=1.0(强行满足 gt=0 的最小值)或直接报错。
3.2 模式二:TypedDict(轻量级类型约束)
TypedDict 是 Python 标准库提供的带有类型声明的字典结构。适合需要快速定义结构且无需 Pydantic 重量级校验的场景。
3.2.1 Annotated 的使用
由于 TypedDict 不支持 Field(),需使用 typing.Annotated 附加元数据(如字段描述)。
python
from typing_extensions import TypedDict, Annotated
class MovieTypedDict(TypedDict):
"""电影的详细信息"""
title: Annotated[str, "电影的正式名称"]
year: Annotated[int, "电影的公映年份,四位数字"]
director: Annotated[str, "导演全名"]
rating: Annotated[float, "10分制评分"]
structured_llm = model.with_structured_output(MovieTypedDict)
response = structured_llm.invoke("介绍下电影《星际穿越》")
# response 是标准字典 (dict)
print(response['title'])
3.2.2 特殊占位符 ... (Ellipsis) 与平台差异
在 Annotated 中使用 ... 表示该字段为必填项,不可省略。
python
class MovieDict(TypedDict):
title: Annotated[str, ..., "电影标题"] # 必填
year: Annotated[int, ..., "上映年份"] # 必填
director: Annotated[str, ..., "导演"] # 必填
rating: Annotated[float, "电影评分"] # 非必填(无 ...)
⚠️ 平台差异 :
当输入文本缺失 rating 信息时:
- CloseAI (OpenAI) :严格遵守
...约束,输出的字典中不包含rating键。 - OpenRouter :可能无法正确识别
...的必填语义,强行补全一个默认值(如rating: 0)。
3.3 模式三:JSON Schema(跨语言通用)
直接按照 JSON Schema 规范拼接 JSON 字符串。这种方式比较繁琐,且缺少 Python 层面的类型校验,不推荐作为首选,但在需要与前后端跨语言接口对齐时非常有用。
python
json_schema = {
"title": "Movie",
"type": "object",
"properties": {
"title": {"type": "string", "description": "The title of the movie"},
"year": {"type": "integer", "description": "Release year"},
"rating": {"type": "number", "description": "Rating out of 10"}
},
"required": ["title", "year", "rating"] # 必填字段
}
# 必须显式指定 method="json_schema"
structured_model = model.with_structured_output(json_schema, method="json_schema")
response = structured_model.invoke("给出盗梦空间的信息")
# response 是标准字典 (dict)
3.4 模式四:@dataclass(Python 标准库数据类)
使用 Python 标准库 dataclasses 提供的 @dataclass 装饰器。代码简洁,但不具备运行时强校验能力。
python
from dataclasses import dataclass
from pydantic import Field # 注意:dataclass 中也可借用 Field 提供描述,但校验由 LangChain 底层处理
@dataclass
class Movie:
title: str = Field(description="电影标题")
year: int = Field(description="上映年份")
rating: float = Field(description="评分")
structured_model = model.with_structured_output(Movie)
response = structured_model.invoke("给出盗梦空间的信息")
# response 是标准字典 (dict),而非 dataclass 实例
四、 运行时类型校验机制(核心避坑)
为了验证不同 Schema 在数据格式不匹配时的处理方式,我们构造一个 Fake Server,强制返回字段名不匹配的 JSON(如返回 title1 而非 title)。
4.1 四种模式的校验行为对比
| Schema 模式 | 接收到非法响应后的行为 | 返回值类型 | 工程影响 |
|---|---|---|---|
| Pydantic | 抛出 ValidationError 异常,程序中断。 |
Schema 类实例 | 最安全。能在第一时间暴露模型输出错误或网络劫持,防止脏数据流入业务逻辑。 |
| TypedDict | 静默失败,直接返回原始字典。 | dict |
存在隐患。业务代码通过 dict['title'] 取值时会抛出 KeyError。 |
| JSON Schema | 静默失败,直接返回原始字典。 | dict |
同上。 |
| @dataclass | 静默失败,直接返回原始字典。 | dict |
同上。 |
结论 :在核心业务链路中,必须使用 Pydantic 以利用其运行时强校验能力。
五、 获取结构化结果的两种范式
5.1 范式一:with_structured_output(现代标准,强烈推荐)
这是 LangChain 1.x 最简洁、最稳定的 API。
进阶用法:include_raw=True
当需要访问底层元数据(如 Token 消耗、原始响应)或处理解析错误时,可传入 include_raw=True。
python
model_with_structure = model.with_structured_output(Movie, include_raw=True)
resp = model_with_structure.invoke("介绍下电影《星际穿越》")
# resp 是一个包含三个键的字典:
# 1. 'raw': 原始的 AIMessage 对象(包含 usage_metadata 等性能指标)
# 2. 'parsed': 解析后的 Pydantic 实例(Movie 对象)
# 3. 'parsing_error': 解析错误信息(如果模型输出不符合 Schema,此处会记录异常,而非直接崩溃)
print(resp['parsed'].title)
print(resp['raw'].usage_metadata['total_tokens'])
5.2 范式二:输出解析器 JsonOutputParser(传统方式,不推荐)
这种方法依赖于在 Prompt 中明确指示模型输出 JSON 文本,然后使用解析器进行转换。
#mermaid-svg-bR30ZwM998KavY2w{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bR30ZwM998KavY2w .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bR30ZwM998KavY2w .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bR30ZwM998KavY2w .error-icon{fill:#552222;}#mermaid-svg-bR30ZwM998KavY2w .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bR30ZwM998KavY2w .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bR30ZwM998KavY2w .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bR30ZwM998KavY2w .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bR30ZwM998KavY2w .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bR30ZwM998KavY2w .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bR30ZwM998KavY2w .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bR30ZwM998KavY2w .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bR30ZwM998KavY2w .marker.cross{stroke:#333333;}#mermaid-svg-bR30ZwM998KavY2w svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bR30ZwM998KavY2w p{margin:0;}#mermaid-svg-bR30ZwM998KavY2w .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bR30ZwM998KavY2w .cluster-label text{fill:#333;}#mermaid-svg-bR30ZwM998KavY2w .cluster-label span{color:#333;}#mermaid-svg-bR30ZwM998KavY2w .cluster-label span p{background-color:transparent;}#mermaid-svg-bR30ZwM998KavY2w .label text,#mermaid-svg-bR30ZwM998KavY2w span{fill:#333;color:#333;}#mermaid-svg-bR30ZwM998KavY2w .node rect,#mermaid-svg-bR30ZwM998KavY2w .node circle,#mermaid-svg-bR30ZwM998KavY2w .node ellipse,#mermaid-svg-bR30ZwM998KavY2w .node polygon,#mermaid-svg-bR30ZwM998KavY2w .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bR30ZwM998KavY2w .rough-node .label text,#mermaid-svg-bR30ZwM998KavY2w .node .label text,#mermaid-svg-bR30ZwM998KavY2w .image-shape .label,#mermaid-svg-bR30ZwM998KavY2w .icon-shape .label{text-anchor:middle;}#mermaid-svg-bR30ZwM998KavY2w .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bR30ZwM998KavY2w .rough-node .label,#mermaid-svg-bR30ZwM998KavY2w .node .label,#mermaid-svg-bR30ZwM998KavY2w .image-shape .label,#mermaid-svg-bR30ZwM998KavY2w .icon-shape .label{text-align:center;}#mermaid-svg-bR30ZwM998KavY2w .node.clickable{cursor:pointer;}#mermaid-svg-bR30ZwM998KavY2w .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bR30ZwM998KavY2w .arrowheadPath{fill:#333333;}#mermaid-svg-bR30ZwM998KavY2w .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bR30ZwM998KavY2w .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bR30ZwM998KavY2w .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bR30ZwM998KavY2w .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bR30ZwM998KavY2w .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bR30ZwM998KavY2w .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bR30ZwM998KavY2w .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bR30ZwM998KavY2w .cluster text{fill:#333;}#mermaid-svg-bR30ZwM998KavY2w .cluster span{color:#333;}#mermaid-svg-bR30ZwM998KavY2w div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bR30ZwM998KavY2w .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bR30ZwM998KavY2w rect.text{fill:none;stroke-width:0;}#mermaid-svg-bR30ZwM998KavY2w .icon-shape,#mermaid-svg-bR30ZwM998KavY2w .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bR30ZwM998KavY2w .icon-shape p,#mermaid-svg-bR30ZwM998KavY2w .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bR30ZwM998KavY2w .icon-shape .label rect,#mermaid-svg-bR30ZwM998KavY2w .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bR30ZwM998KavY2w .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bR30ZwM998KavY2w .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bR30ZwM998KavY2w :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Prompt 指导
要求输出 JSON
模型生成
纯文本 JSON
JsonOutputParser
正则/AST 解析
Python 字典
代码范式(结合 LCEL):
python
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
# 1. 定义结构
class Movie(BaseModel):
title: str = Field(description="电影标题")
year: int = Field(description="上映年份")
# 2. 创建解析器
parser = JsonOutputParser(pydantic_object=Movie)
# 3. 创建 Prompt(必须注入格式指令)
prompt_template = ChatPromptTemplate.from_messages([
("system", "回答用户问题,必须始终输出一个包含 title 和 year 的 JSON 对象。{format_instructions}"),
("human", "问题:{question}")
])
# 注意:需要将 parser 的格式指令注入 Prompt
prompt = prompt_template.partial(format_instructions=parser.get_format_instructions())
# 4. 构建 LCEL 链
chain = prompt | model | parser
# 5. 调用(返回字典)
response = chain.invoke({"question": "介绍电影《盗梦空间》"})
为什么不推荐?
- 极度依赖 Prompt :模型可能会在 JSON 前后添加 Markdown 标记(如
json ...),导致解析失败。 - 缺乏底层约束:模型是"尽力"生成 JSON,而不是像 Function Calling 那样在底层被"强制"生成 JSON,错误率较高。
- 冗余代码:需要手动处理格式指令的注入。
六、 总结与工程决策清单
6.1 Schema 模式选型矩阵
| 场景需求 | 推荐模式 | 理由 |
|---|---|---|
| 生产环境核心业务 | Pydantic | 强类型校验、丰富的约束规则、IDE 完美支持。 |
| 轻量级脚本/快速原型 | TypedDict | 语法简洁,无需引入 Pydantic 依赖,适合简单字典结构。 |
| 跨语言/前后端接口对齐 | JSON Schema | 标准协议,可直接导出给前端或其他语言使用。 |
| 纯 Python 数据类偏好 | @dataclass | 符合 Python 原生习惯,但需注意其无强校验的缺陷。 |
6.2 生产环境最佳实践清单
- 默认使用 Pydantic :利用
Field(description="...")将业务逻辑说明直接转化为 Prompt,减少 Prompt 编写工作量。 - 谨慎处理缺失值 :对于模型可能无法提取的字段,务必使用
Optional[T]或设置合理的default值,防止ValidationError阻断主流程。 - 利用
include_raw=True做监控 :在需要统计 Token 成本或记录 LangSmith 追踪时,开启此参数以获取底层AIMessage。 - 控制嵌套层级 :LLM 处理深层嵌套 JSON 的能力有限,建议 Pydantic 模型的嵌套层级 ≤ 3 层,必要时拆分为多次调用。
- 废弃
JsonOutputParser:除非对接极老旧的不支持 Function Calling 的模型,否则全面拥抱with_structured_output。