14. 输出解析器 --- Pydantic 与 JSON
文章目录
- [14. 输出解析器 --- Pydantic 与 JSON](#14. 输出解析器 — Pydantic 与 JSON)
-
- [1. 输出解析器 vs with_structured_output](#1. 输出解析器 vs with_structured_output)
-
- [1.1 输出解析器 --- 独立的功能性组件](#1.1 输出解析器 — 独立的功能性组件)
- [1.2 with_structured_output --- 模型内部方法](#1.2 with_structured_output — 模型内部方法)
- [1.3 核心差异对比](#1.3 核心差异对比)
- [1.4 StrOutputParser:文本输出解析](#1.4 StrOutputParser:文本输出解析)
- [2. PydanticOutputParser:强类型结构化输出](#2. PydanticOutputParser:强类型结构化输出)
-
- [2.1 定义 Pydantic 模型](#2.1 定义 Pydantic 模型)
- [2.2 创建 PydanticOutputParser](#2.2 创建 PydanticOutputParser)
- [2.3 get_format_instructions() 的深层原理](#2.3 get_format_instructions() 的深层原理)
- [2.4 链式构建](#2.4 链式构建)
- [2.5 执行与结果验证](#2.5 执行与结果验证)
- [2.6 模板实例化预览](#2.6 模板实例化预览)
- [3. JsonOutputParser:字典形式输出](#3. JsonOutputParser:字典形式输出)
-
- [3.1 两种初始化方式](#3.1 两种初始化方式)
- [3.2 PydanticOutputParser vs JsonOutputParser](#3.2 PydanticOutputParser vs JsonOutputParser)
- [3.3 验证返回结果](#3.3 验证返回结果)
- [3.4 数据流总结](#3.4 数据流总结)
- [4. 完整代码速览](#4. 完整代码速览)
-
- [4.1 流式传输(Streaming)](#4.1 流式传输(Streaming))
- [5. 输出解析器生态总览](#5. 输出解析器生态总览)
- [6. 输出解析器最佳实践](#6. 输出解析器最佳实践)
- [7. 总结](#7. 总结)
上篇文章我们学了示例选择器:当少样本提示的例子太多、塞不进上下文窗口时,它会按长度、语义相似度、MMR 或 NGram 重叠等策略,从一大堆例子里挑出最相关的几个,再拼进提示词。不过无论怎么选示例,模型回答出来的始终是一整段文字,程序没法直接拿去用。这篇笔记就来解决这个新问题:怎么把 LLM 的原始文本输出变成程序可以直接处理的结构化数据?答案是输出解析器(Output Parser)。
输出解析器(Output Parser)是 LangChain 中将 LLM 原始文本输出转化为结构化数据 的核心组件。本文深入对比 PydanticOutputParser 与 JsonOutputParser 的用法、原理及适用场景,并展开输出解析器与with_structured_output的设计差异。
1. 输出解析器 vs with_structured_output
在之前的聊天模型-结构化输出这一小节中,我们了解到:聊天模型提供了 with_structured_output() 方法,让模型按照我们定义的 Schema 返回结构化结果。
LangChain 提供两种方式让 LLM 输出结构化数据,定位和用法截然不同。
1.1 输出解析器 --- 独立的功能性组件
输出解析器是 LangChain 中的一个独立组件,可以自由地通过 | 运算符嵌入到调用链中:
python
chain = prompt | model | parser # 链式组合
result = chain.invoke({"query": "..."})
典型代表:StrOutputParser(解析为字符串)、PydanticOutputParser(解析为 Pydantic 对象)、JsonOutputParser(解析为字典)。
1.2 with_structured_output --- 模型内部方法
with_structured_output 是聊天模型内置的结构化输出能力(详见 8.聊天模型-结构化输出):
python
structured_model = model.with_structured_output(Joke) # 封装模型
result = structured_model.invoke("讲一个笑话")
1.3 核心差异对比
| 维度 | 输出解析器 | with_structured_output |
|---|---|---|
| 定位 | 独立组件,LangChain 框架级能力 | 模型内部方法,模型级能力 |
| 组合方式 | `chain = prompt | model |
| 链式兼容 | 可嵌入任意链 | 返回独立 Runnable |
| 代码可读性 | 链定义清晰展示"模板→模型→解析"全流程 | 需单独封装调用 |
| 实现原理 | 通过提示词注入 Schema → 后处理解析 | 模型内部原生支持(函数调用/Function Calling) |
| Schema 来源 | Pydantic 模型 | Pydantic 模型 / TypedDict / JSON Schema |
选择依据:
- 需要链式组合、组件复用的 → 输出解析器
- 仅需单次结构化调用、追求简洁的 → with_structured_output
- 需要中间件(如重试、纠错)介入的 → 输出解析器 (可嵌套
OutputFixingParser)
1.4 StrOutputParser:文本输出解析
StrOutputParser 是最基础的输出解析器,它直接从 LLM 返回的 AIMessage 中提取 content 文本字符串。配合流式传输(streaming)可以逐字输出结果:
python
from langchain_core.output_parsers import StrOutputParser
chain = model | StrOutputParser()
for chunk in chain.stream("写一首夏天的诗词,50字以内。"):
print(chunk, end="|")
# 输出: 炎|夏|骄|阳|照|,|绿|树|映|蓝|天|。|...
不加 StrOutputParser 时,chain.stream() 直接返回 AIMessageChunk 块,需要手动从 .content 字段提取文本。StrOutputParser 做了这一层提取封装。
2. PydanticOutputParser:强类型结构化输出
2.1 定义 Pydantic 模型
python
from typing import Optional
from pydantic import BaseModel, Field
class Joke(BaseModel):
"""给用户讲的一个笑话"""
setup: str = Field(description="这个笑话的开头")
punchline: str = Field(description="这个笑话的妙语")
rating: Optional[int] = Field(default=None, description="从1-10分,给这个笑话评分")
关键点:
Field(description=...)中的中文描述会作为提示词的一部分暴露给 LLM,引导模型按规范填写。描述写得越精确,结构化输出的质量越高。
2.2 创建 PydanticOutputParser
python
from langchain_core.output_parsers import PydanticOutputParser
parser = PydanticOutputParser(pydantic_object=Joke)
pydantic_object 参数指定解析器绑定的 Pydantic 模型类。
2.3 get_format_instructions() 的深层原理
调用 parser.get_format_instructions() 会动态将 Pydantic 模型反向编译为 JSON Schema:
json
{
"type": "object",
"properties": {
"setup": { "type": "string", "description": "这个笑话的开头" },
"punchline": { "type": "string", "description": "这个笑话的妙语" },
"rating": { "type": "integer", "description": "从1-10分,给这个笑话评分" }
},
"required": ["setup", "punchline"]
}
这段 Schema 文本会被注入到提示词中,告诉 LLM"你应该输出这种格式"。这是 PydanticOutputParser 的核心机制 ------不依赖模型内部的 Function Calling,而是通过提示词工程 + 后处理解析实现结构化输出。
2.4 链式构建
python
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini")
prompt = PromptTemplate(
template="回复用户问题。\n返回结构说明:{format_instructions}\n用户问题:{query}\n",
partial_variables={"format_instructions": parser.get_format_instructions()},
input_variables=["query"],
)
chain = prompt | model | parser
partial_variables的作用 :format_instructions在模板创建时就已经固定(Schema 不会在每次运行中变化),通过partial_variables实现一次性填充------既避免了重复计算,又保持了模板与解析器的解耦。
2.5 执行与结果验证
python
result = chain.invoke({"query": "讲一个关于跳舞的笑话"})
print(result)
# Joke(setup="为什么舞者总是带一把扇子?", punchline="因为他们想要自己的舞步更加凉快!", rating=7)
print(type(result)) # <class '__main__.Joke'>
print(result.setup) # "为什么舞者总是带一把扇子?"
返回的是 Pydantic 模型实例,可直接通过属性访问字段,享受 IDE 类型补全。
2.6 模板实例化预览
可在执行链之前先查看模板实例化后的 message 内容,验证 format_instructions 是否正确注入:
python
msg = prompt.invoke({"query": "讲一个关于跳舞的笑话"})
print(msg)
输出效果:
回复用户问题。
返回结构说明:{"type": "object", "properties": {"setup": {"type": "string", ...}}, ...}
用户问题:讲一个关于跳舞的笑话
3. JsonOutputParser:字典形式输出
3.1 两种初始化方式
直接编写 JSON Schema 作为 LLM 的输出格式定义非常繁琐。JsonOutputParser 提供两种初始化方式:
方式一:不传 pydantic_object(无验证)
python
parser = JsonOutputParser()
此时解析器不对输出做任何校验,LLM 自行决定 JSON 的键名和结构。结果为一个自由格式的字典:
python
chain.invoke({"query": "给我讲一个关于唱歌的笑话"})
# {'joke': '为什么歌手总是带着梯子?\n因为他们想要在音乐会上达到更高的层次!'}
方式二:传入 pydantic_object(带 Schema 约束)
python
parser = JsonOutputParser(pydantic_object=Joke)
与 PydanticOutputParser 完全一样的初始化方式,仅改一行代码即可切换解析器类型:
python
from langchain_core.output_parsers import JsonOutputParser
# 仅替换这一行
parser = JsonOutputParser(pydantic_object=Joke)
# 之前的全部代码无需修改
3.2 PydanticOutputParser vs JsonOutputParser
| 维度 | PydanticOutputParser | JsonOutputParser |
|---|---|---|
| 返回类型 | Pydantic Model 实例 | dict(Python 字典) |
| 内部实现 | Pydantic 模型反序列化(校验+转换) | json.loads() 直接解析 |
| 类型安全 | 强类型,属性可 IDE 补全 | 字典键值无静态检查 |
| 性能 | 稍低(多一次模型校验) | 更高(纯 JSON 解析) |
| 适用场景 | 需要类型安全的内部处理 | 快速输出 / 仅需原始 JSON |
3.3 验证返回结果
python
result = chain.invoke({"query": "讲一个关于跳舞的笑话"})
print(type(result)) # <class 'dict'>
print(result)
# {'setup': '为什么舞者总是带一把扇子?', 'punchline': '...', 'rating': 7}
3.4 数据流总结
两种解析器遵循相同的架构流程,仅在最后一步分歧:
#mermaid-svg-V8QCnvb11htxrw1p{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-V8QCnvb11htxrw1p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V8QCnvb11htxrw1p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V8QCnvb11htxrw1p .error-icon{fill:#552222;}#mermaid-svg-V8QCnvb11htxrw1p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V8QCnvb11htxrw1p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V8QCnvb11htxrw1p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V8QCnvb11htxrw1p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V8QCnvb11htxrw1p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V8QCnvb11htxrw1p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V8QCnvb11htxrw1p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V8QCnvb11htxrw1p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V8QCnvb11htxrw1p .marker.cross{stroke:#333333;}#mermaid-svg-V8QCnvb11htxrw1p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V8QCnvb11htxrw1p p{margin:0;}#mermaid-svg-V8QCnvb11htxrw1p .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-V8QCnvb11htxrw1p .cluster-label text{fill:#333;}#mermaid-svg-V8QCnvb11htxrw1p .cluster-label span{color:#333;}#mermaid-svg-V8QCnvb11htxrw1p .cluster-label span p{background-color:transparent;}#mermaid-svg-V8QCnvb11htxrw1p .label text,#mermaid-svg-V8QCnvb11htxrw1p span{fill:#333;color:#333;}#mermaid-svg-V8QCnvb11htxrw1p .node rect,#mermaid-svg-V8QCnvb11htxrw1p .node circle,#mermaid-svg-V8QCnvb11htxrw1p .node ellipse,#mermaid-svg-V8QCnvb11htxrw1p .node polygon,#mermaid-svg-V8QCnvb11htxrw1p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-V8QCnvb11htxrw1p .rough-node .label text,#mermaid-svg-V8QCnvb11htxrw1p .node .label text,#mermaid-svg-V8QCnvb11htxrw1p .image-shape .label,#mermaid-svg-V8QCnvb11htxrw1p .icon-shape .label{text-anchor:middle;}#mermaid-svg-V8QCnvb11htxrw1p .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-V8QCnvb11htxrw1p .rough-node .label,#mermaid-svg-V8QCnvb11htxrw1p .node .label,#mermaid-svg-V8QCnvb11htxrw1p .image-shape .label,#mermaid-svg-V8QCnvb11htxrw1p .icon-shape .label{text-align:center;}#mermaid-svg-V8QCnvb11htxrw1p .node.clickable{cursor:pointer;}#mermaid-svg-V8QCnvb11htxrw1p .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-V8QCnvb11htxrw1p .arrowheadPath{fill:#333333;}#mermaid-svg-V8QCnvb11htxrw1p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-V8QCnvb11htxrw1p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-V8QCnvb11htxrw1p .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V8QCnvb11htxrw1p .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-V8QCnvb11htxrw1p .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V8QCnvb11htxrw1p .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-V8QCnvb11htxrw1p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-V8QCnvb11htxrw1p .cluster text{fill:#333;}#mermaid-svg-V8QCnvb11htxrw1p .cluster span{color:#333;}#mermaid-svg-V8QCnvb11htxrw1p 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-V8QCnvb11htxrw1p .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-V8QCnvb11htxrw1p rect.text{fill:none;stroke-width:0;}#mermaid-svg-V8QCnvb11htxrw1p .icon-shape,#mermaid-svg-V8QCnvb11htxrw1p .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V8QCnvb11htxrw1p .icon-shape p,#mermaid-svg-V8QCnvb11htxrw1p .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-V8QCnvb11htxrw1p .icon-shape .label rect,#mermaid-svg-V8QCnvb11htxrw1p .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V8QCnvb11htxrw1p .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-V8QCnvb11htxrw1p .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-V8QCnvb11htxrw1p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PydanticOutputParser
JsonOutputParser
Pydantic 模型
输出解析器
get_format_instructions
PromptTemplate
LLM 输出 JSON
选择解析器
Pydantic Model 实例
Python dict
4. 完整代码速览
python
from typing import Optional
from langchain_core.output_parsers import PydanticOutputParser, JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
# 1. 聊天模型
model = ChatOpenAI(model="gpt-4o-mini")
# 2. Pydantic 模型
class Joke(BaseModel):
"""给用户讲的一个笑话"""
setup: str = Field(description="这个笑话的开头")
punchline: str = Field(description="这个笑话的妙语")
rating: Optional[int] = Field(default=None, description="从1-10分,给这个笑话评分")
# 3. 解析器(两选一)
# parser = PydanticOutputParser(pydantic_object=Joke) # 返回 Joke 实例
parser = JsonOutputParser(pydantic_object=Joke) # 返回 dict
# 4. 提示模板
prompt = PromptTemplate(
template="回复用户问题。\n返回结构说明:{format_instructions}\n用户问题:{query}\n",
partial_variables={"format_instructions": parser.get_format_instructions()},
input_variables=["query"],
)
# 5. 链式执行
chain = prompt | model | parser
result = chain.invoke({"query": "讲一个关于跳舞的笑话"})
print(result)
执行流程:
| 步骤 | 组件 | 类型变化 |
|---|---|---|
(1) prompt |
模板实例化 | dict → BaseMessage |
(2)model |
LLM 调用 | BaseMessage → AIMessage |
(3)parser |
结构化解析 | str → Joke 实例 或 dict |
4.1 流式传输(Streaming)
输出解析器天然支持流式调用,只需将 invoke 替换为 stream:
python
# PydanticOutputParser 流式输出
for chunk in chain.stream({"query": "讲一个关于唱歌的笑话"}):
print(chunk, end="|")
# 输出字段逐步填充:setup='为什么歌手...' punchline='' → punchline='因为他们...' → rating=None
对于 PydanticOutputParser,流式传输会逐步填充模型字段(先填充 setup,再逐步拼接 punchline),最终得到完整的模型实例。
对于 JsonOutputParser,流式传输同样工作------逐步累积 JSON 键值对,最终输出完整字典。
5. 输出解析器生态总览
LangChain 在 langchain_core.output_parsers 中封装了约 20 种输出解析器:
| 解析器 | 输出类型 | 典型场景 |
|---|---|---|
StrOutputParser |
str |
纯文本提取,最基础 |
PydanticOutputParser |
Pydantic Model | 强类型结构化输出 |
JsonOutputParser |
dict |
JSON 格式输出 |
XmlOutputParser |
XML 结构 | XML 格式输出 |
YamlOutputParser |
YAML 结构 | YAML 格式输出 |
CommaSeparatedListOutputParser |
list[str] |
CSV 枚举列表 |
DatetimeOutputParser |
datetime |
日期时间提取 |
EnumOutputParser |
Enum 成员 | 枚举类型输出 |
OutputFixingParser |
包装解析器 | 自动修正格式错误的输出 |
RetryOutputParser |
包装解析器 | 输出异常时重试 LLM 调用 |
ListOutputParser |
list |
通用列表输出 |
自定义解析器
继承 BaseOutputParser,实现两个核心方法即可:
python
from langchain_core.output_parsers import BaseOutputParser
class MyParser(BaseOutputParser[dict]):
def parse(self, text: str) -> dict:
return self._custom_parse(text)
def get_format_instructions(self) -> str:
return "请按 {key: value} 格式输出"
更多解析器类型及自定义解析器详情,参考 LangChain 官方文档 --- Output Parsers。
6. 输出解析器最佳实践
-
优先考虑 PydanticOutputParser :当需要后续以类型安全的方式处理 LLM 输出时使用,字段描述(
description)写得越详细准确,LLM 输出越规范 -
JsonOutputParser 用于传输和持久化:当结果需要序列化存储或网络传输时,字典格式更轻量,且天然 JSON 可序列化
-
Partial_variables 优于硬编码 :使用
partial_variables注入 Schema 而非写死在模板字符串中,保持组件可替换性 -
链式组合优于手动调用 :
prompt | model | parser的可读性、可维护性远超独立调用各组件
7. 总结
把这篇笔记串一遍。前面几篇我们一直在解决"怎么把问题问好"------从提示词模板、少样本示例到示例选择器,但模型回答的始终是自然语言,程序没法直接当数据用。输出解析器就是放在调用链最后一步的"翻译官":它先把输出格式说明书(JSON Schema)通过 get_format_instructions() 生成出来,再用 partial_variables 注入提示词,告诉模型"你要按这个格式输出";等模型吐出原始文本后,解析器再把它翻译成程序好用的结构化数据。这就是输出解析器的核心机制------不靠模型内部的 Function Calling,而是"提示词注入加后处理解析",所以它能作为独立组件嵌进任何一条链里。
两个主力解析器怎么选?PydanticOutputParser 要求你先定义一个 Pydantic 模型,解析完返回的是模型实例,字段带类型检查,IDE 里还能自动补全,适合拿输出继续做复杂处理的场景;JsonOutputParser 直接返回字典,速度快、更轻量,适合传输和持久化。两者用法几乎一样,传不传 pydantic_object 只决定要不要按 Schema 校验,一行代码就能切换。至于它们和 with_structured_output 的区别:解析器是框架级的独立组件,能嵌入任意链,外面还能再包一层自动纠错的 OutputFixingParser;而 with_structured_output 是模型内置的方法,靠函数调用原生返回结构,单次调用更省事。
所以记住这条选型线就够了:要类型安全、要组件复用,选 PydanticOutputParser;只要原始 JSON、图快图轻,选 JsonOutputParser;只是单次结构化调用,用 with_structured_output 最简洁。一句话总结:输出解析器等于"提示词注入 Schema 加后处理解析",LangChain 里二十来种解析器,本质上都是这一个套路。