14.输出解析器-Pydantic与JSON

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 模板实例化 dictBaseMessage
(2)model LLM 调用 BaseMessageAIMessage
(3)parser 结构化解析 strJoke 实例 或 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. 输出解析器最佳实践

  1. 优先考虑 PydanticOutputParser :当需要后续以类型安全的方式处理 LLM 输出时使用,字段描述(description)写得越详细准确,LLM 输出越规范

  2. JsonOutputParser 用于传输和持久化:当结果需要序列化存储或网络传输时,字典格式更轻量,且天然 JSON 可序列化

  3. Partial_variables 优于硬编码 :使用 partial_variables 注入 Schema 而非写死在模板字符串中,保持组件可替换性

  4. 链式组合优于手动调用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 里二十来种解析器,本质上都是这一个套路。

相关推荐
ReleaseU1 小时前
Claude Code 两天三版本、Cursor 把仓库搬进编辑器:AI 编程工具在卷什么?
人工智能·大模型
存在morning1 小时前
【PySpark 学习笔记 四】DataFrame 进阶:窗口函数、高级聚合与复杂类型
笔记·学习
tachibana21 小时前
初识智能体
人工智能·ai·大模型·llm·agent
找方案1 小时前
Seedance 2.5突破30秒视频生成,AI视频赛道进入工业化时代
大数据·人工智能·字节跳动·可灵·ai视频生成·seedance2.5·工业化时代
IT_陈寒1 小时前
Python的列表拷贝坑得我原地打转
前端·人工智能·后端
deepdata_cn1 小时前
从弱AI到强AI:符号推理是实现通用人工智能AGI的关键钥匙
人工智能·agi·符号推理
richard_first1 小时前
第5章 Attention 的思想
人工智能·深度学习·transformer
AI英德西牛仔1 小时前
秘塔 导出word指令之外:AI导出鸭电脑版的底层逻辑与批量交付哲学
人工智能·word·excel·deepseek·ai导出鸭
山甫aa1 小时前
日志技术 Logback + Slf4j —— 从零开始的 Web 后端学习
java·后端·学习·web·logback