从纯文本到经过验证的对象:选择LangChain输出解析器
比较 LangChain 链路中的 StrOutputParser、JsonOutputParser、StructuredOutputParser 和 PydanticOutputParser,了解每种解析器实际上能保证多少结构信息。
聊天模型会返回文本,而文本形式便于屏幕阅读。但一旦响应需要用于生成新的提示、存储到数据库或驱动用户界面组件,就需要更可预测的数据格式:经过校验的纯字符串、JSON对象或类型化记录。LangChain通过输出解析器填补了这一空白,并提供了多种具有不同保障级别的解析器。本指南将使用四种解析器构建同一个小型太阳系项目,逐行解释每条代码逻辑,最后给出选择合适解析器的实用准则。
如果您想更全面地了解解析器如何与LCEL、可执行组件及内存协同工作,请参阅LangChain中从原始文本到处理管道的流程。而本文的重点更为具体:每个解析器实际上能实现什么功能,以及其功能的边界在哪里。
为何仅靠原始模型文本不够
向模型询问关于太阳系的问题,您可能会得到类似以下的回答。虽然读起来不错,但程序若不先判断出某句话的结束和下一句的开始位置,就无法从中提取具体的事实信息。
The Solar System consists of the Sun and the objects that orbit it.
It contains eight planets along with moons, asteroids, and comets.
处理此类回答的程序更希望获得可直接引用的命名值,例如一个每个事实对应一个键的对象:
{
"fact_1": "The Solar System contains eight planets.",
"fact_2": "The Sun is at the center of the Solar System.",
"fact_3": "The Solar System also contains moons, asteroids, and comets."
}
输出解析器是连接这两种形式的组件。它接收模型生成的任何内容,然后直接返回应用程序可直接使用的值,无需额外的字符串处理。不同解析器的功能范围有所差异:有些仅负责解包文本,有些则用于解析JSON,而最严格的解析器还会根据模式对结果进行验证。
Raw LLM Response
↓
Output Parser
↓
Parsed Output
请记住这种三阶段的结构。下面的每个示例都是它的变体,随着需求越来越严格,中间环节会添加更多处理逻辑。
四种解析器,四个结构层级
此处介绍的四种解析器构成了一个阶梯结构,每一层都能对结果提供更强的控制:
StrOutputParser将模型的输出转换为普通的Python字符串。JsonOutputParser将响应解析为与JSON兼容的值,如字典或列表。
StructuredOutputParser允许你指定模型应返回的字段名称。PydanticOutputParser通过Pydantic模型描述期望的数据结构,并据此验证输出。无论选择哪种方式,数据流程都是相同的:
LLM Response
↓
Output Parser
↓
Parsed Output
不同之处在于最终输出的内容以及对其可信度的判断。
实际示例:先生成报告,再生成摘要
第一个项目是一个两步流程。主题“太阳系”会被传递给一个模型,该模型会生成一份长篇报告。这份报告随后会被输入到另一个提示词中,要求其输出五行摘要。关键在于数据传递:第一次模型调用的输出会成为下一次提示词的输入。
Solar System
↓
LLM
↓
Detailed Report
↓
LLM
↓
5-Line Summary
如果没有解析器,第一步会返回一个消息对象而非报告文本,因此需要在构建第二条提示语之前先将其解包。而中间使用的解析器会将这一解包操作作为处理流程的一部分,从而让两个步骤能够顺畅地组合在一起。
StrOutputParser:当只需要文本时
StrOutputParser是LangChain提供的最简单的解析器。聊天模型会返回一个AIMessage对象,该解析器会提取其内容并给出普通的字符串形式。当后续处理者是人类、另一条提示语或任何仅需文本内容且不需要JSON或架构的对象时,这就是最合适的工具。
将其集成到报告生成流程中
在“先生成报告再总结”项目中,处理步骤如下:
- 第一条提示语要求模型针对该主题生成一份详细的报告。
每次模型调用之后都会有一个StrOutputParser,因此每次数据传递都是以普通字符串的形式进行。完整的组件序列如下:
Solar System
↓
Prompt 1
↓
LLM
↓
StrOutputParser
↓
Detailed Report
↓
Prompt 2
↓
LLM
↓
StrOutputParser
↓
5-Line Summary
OpenAI版本
这是使用ChatOpenAI的完整流程。请先从头到尾阅读一遍,之后我们会详细讲解关键部分。
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
load_dotenv()
model = ChatOpenAI()
# 1st prompt -> detailed report
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
# 2nd prompt -> summary
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
parser = StrOutputParser()
chain = template1 | model | parser | template2 | model | parser
result = chain.invoke({'topic': 'Solar System'})
print(result)
模型是使用默认设置创建的。其上方的load_dotenv()函数会从本地的.env文件中读取API密钥,因此脚本中不会存储任何凭证信息。
model = ChatOpenAI()
第一个模板仅接受一个变量topic,并要求生成关于该主题的详细报告。
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
第二个模板需要一个名为text的变量,该变量将用于存储报告内容,同时还需要一个五行的报告摘要。
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
如果你复制这段代码,有一个小错误需要修正:模板字符串中包含/n,这其实是字母n后面的斜杠,并非换行符。如果你希望报告从新的一行开始,应使用\n。模型通常两种写法都能处理,但你认为要发送的提示语应当就是实际发送的内容。
接下来是解析器。由于它在多次调用之间不会保留任何状态,因此单个实例可以在处理链的多个环节中被重复使用。
parser = StrOutputParser()
将所有部分连接起来的就是处理链的定义:
chain = template1 | model | parser | template2 | model | parser
管道运算符属于LCEL(LangChain表达式语言)的组合方式:每个组件的输出会成为下一个组件的输入。若按垂直顺序排列,执行顺序如下:
template1
↓
model
↓
parser
↓
template2
↓
model
↓
parser
注意第一个解析器能为你做什么。在首次调用模型后,解析器会以字符串形式返回结果,而这个字符串就会填充第二个模板中的{text}。第二个解析器会对最终响应执行相同操作,因此整个链路的输出是总结内容本身,而非消息对象。
你可以通过传递一个键与第一个模板输入变量对应的字典来启动这个链路:
result = chain.invoke({'topic': 'Solar System'})
然后展示结果:
print(result)
链路返回的内容
最后输出的数值是从生成的报告里提取的五行摘要。由于最后一个组件是StrOutputParser,因此结果为普通的Python str类型:
print(result)
典型的运行结果大致如下:
1. The Solar System consists of the Sun and all objects that orbit it.
2. It includes eight planets, along with dwarf planets, moons, asteroids, and comets.
3. The four inner planets are rocky, while the outer planets are mostly gas or ice giants.
4. The Sun contains most of the Solar System's mass and provides the energy that drives many processes.
5. The Solar System is located in the Milky Way galaxy.
请将其视为结构示例,而非固定答案。不同运行次数及不同模型下,表述方式会有所差异。
使用Hugging Face模型的相同流程
“先生成报告再提取摘要”的流程也可应用于开源模型。该版本使用指向google/gemma-2-2b-it的HuggingFaceEndpoint,并通过ChatHuggingFace将其封装,使其具备聊天模型的功能:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
load_dotenv()
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
# 1st prompt -> detailed report
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
# 2nd prompt -> summary
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
prompt1 = template1.invoke({'topic': 'Solar System'})
result = model.invoke(prompt1)
prompt2 = template2.invoke({'text': result.content})
result1 = model.invoke(prompt2)
print(result1.content)
这里有一个重要的区别。这个版本从不使用StrOutputParser,也不会构建管道链。它通过.invoke()手动格式化每个提示词,调用模型,然后从返回的消息中读取.content后再继续处理。虽然这种方式可行,但恰恰就是解析器要解决的手动处理问题。
两种方法对比如下:使用 OpenAI 和解析器时:
OpenAI
Prompt
↓
ChatOpenAI
↓
StrOutputParser
↓
String
使用 Hugging Face 且手动访问时:
Hugging Face
Prompt
↓
ChatHuggingFace
↓
result.content
↓
String
两者都以字符串形式结束。OpenAI 的脚本展示了解析器在处理流程中的作用,而 Hugging Face 的脚本则使用了相同的逻辑,只是调用了不同的服务提供商且没有使用解析器。其实你也可以用 Hugging Face 的模型写出 template1 | model | parser | template2 | model | parser 这样的结构;解析器并不关心消息是由哪个服务提供商生成的。
这一层的要点是:当应用只需要文本形式的响应时,就使用 StrOutputParser。
JsonOutputParser:无契约约束的 JSON
JsonOutputParser 是更进一步的解决方案。它会要求模型输出 JSON 格式的数据,然后将回复转换为 Python 数据结构,这在代码需要通过键来提取数值而非阅读文本时非常有用。
它不会强制要求特定的数据结构。在没有架构定义的情况下,虽然会指示模型以 JSON 格式回复,但并未规定必须包含哪些键或这些键的取值应为何类型。使用相同提示进行两次调用完全可能得到结构不同的输出,因此代码必须能够应对这种情况。
各部分的功能对应
这个项目要求模型提供关于太阳系的五个事实。具体步骤如下:
- 创建一个
JsonOutputParser。 - 使用
get_format_instructions()请求格式说明。 - 将这些说明插入提示语中。
- 将处理后的提示语发送给模型。
- 让解析器将模型的回复转换为 Python 值。
Solar System
↓
PromptTemplate
↓
Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON Object
OpenAI 版本
整个流程相当简单:
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
load_dotenv()
# Define Model
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
parser = JsonOutputParser()
template = PromptTemplate(
template="Give me 5 facts about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
chain = template | model | parser
result = chain.invoke({"topic": "Solar System"})
print(result)
该模型配置为使用 gpt-4.1-mini,温度值为 0,这样就能在模型允许的范围内让输出结果尽可能可重复。它就是负责生成五条事实的组件。
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
解析器是在没有参数的情况下创建的,正因如此它才没有可用于强制执行的架构:
parser = JsonOutputParser()
格式说明才是关键
在模型运行之前,解析器可以描述它期望的格式。这一描述来自一个方法调用:
parser.get_format_instructions()
它返回一段文本,指示模型以 JSON 格式回复。这不是用于对模型执行的代码,而是提示文本。你通过 partial_variables 将其注入模板中,这样模板变量只会在模板定义时填充一次,而不会在每次调用时都重新填充:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
生成的模板包含两个占位符:
template = PromptTemplate(
template="Give me 5 facts about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic}在调用时提供,用于指定需要获取的信息内容。{format_instruction}则预先填入解析器规定的格式说明。
因此,当使用该输入调用处理链时,模型会以JSON格式同时收到主题与回答指令:
{"topic": "Solar System"}
组装与运行处理链
该处理链本身仅有三个阶段:
chain = template | model | parser
执行顺序为:
PromptTemplate
↓
ChatOpenAI
↓
JsonOutputParser
↓
Parsed JSON
模板会生成最终的提示语,ChatOpenAI负责回答,而JsonOutputParser则将回复解析为Python数据。该解析器还能容忍模型的一些常见习惯,比如JSON被包裹在Markdown代码块中,它会在解析前先去除这些包裹。
使用该主题来调用它:
result = chain.invoke({"topic": "Solar System"})
然后打印返回的内容:
print(result)
返回内容
结果以 JSON 格式包含五条事实。脚本仅打印数值,因此没有需要引用的标准输出;一个可能的响应如下:
{
"facts": [
"The Solar System is centered around the Sun.",
"There are eight recognized planets in the Solar System.",
"The four inner planets are rocky planets.",
"The outer planets include gas giants and ice giants.",
"The Solar System is located in the Milky Way galaxy."
]
}
这种结构,即单个 facts 键包含一个列表,是模型可能采用的几种格式之一。另一次运行可能会返回 fact_1 到 fact_5,或者直接返回一个列表。如果后续代码通过 result["facts"] 来访问数据,那么一旦模型选择不同的格式,该代码就会出错。
流程追踪
与 StrOutputParser 相比,新版本的不同之处在于解析器会参与两次处理:一次是在模型调用之前,负责提供指令;另一次则是在之后,负责解析数据。
Prompt
↓
JSON Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON
这些指令是由解析器本身生成的:
parser.get_format_instructions()
它们通过部分变量传递到提示框中:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
模型根据这些指令给出回答,随后解析器将回答转换为 Python 值。整个流程就是这样:
Solar System
↓
PromptTemplate
↓
JSON Format Instructions
↓
ChatOpenAI
↓
JsonOutputParser
↓
JSON Object
与上一级的对比可以用两行文字概括。StrOutputParser的输出为:
StrOutputParser
↓
Plain String
而JsonOutputParser的输出为:
JsonOutputParser
↓
JSON-compatible Structured Data
需记住一点:目前仍然没有固定的架构。虽然输出的是 JSON,但字段及其嵌套结构由模型决定。顺便提一下,当前版本的JsonOutputParser还支持一个可选的pydantic_object参数,该参数可以为格式指令添加架构定义;但在当前这种不带参数的形式下,它仅要求输入有效的 JSON。
Hugging Face版本
JSON工作流可直接用于Gemma模型。模型设置会发生变化,但解析器、格式说明以及处理流程保持不变:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
load_dotenv()
# Define the model
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
parser = JsonOutputParser()
template = PromptTemplate(
template='Give me 5 facts about {topic} \n {format_instruction}',
input_variables=['topic'],
partial_variables={
'format_instruction': parser.get_format_instructions()
}
)
chain = template | model | parser
result = chain.invoke({'topic': 'Solar System'})
print(result)
除了模型部分外,整个处理流程完全相同:
PromptTemplate
↓
Hugging Face Model
↓
JsonOutputParser
↓
JSON Object
将解析功能独立成组件所带来的实际好处在于:更换提供方时不会影响解析逻辑。不过需要注意的是,较小的开源模型对格式说明的遵循程度不如大型托管模型。如果响应内容中包含JSON之外的文字或结尾逗号,解析器会抛出OutputParserException异常,因此生产代码应捕获该异常并尝试重试或采取备用方案。
StructuredOutputParser:为所需字段命名
StructuredOutputParser会根据您预先定义的字段列表来提取 JSON 数据。而 JsonOutputParser仅要求“以 JSON 格式输出”,此解析器则明确要求“使用这些键以 JSON 格式输出”。
字段通过 ResponseSchema进行定义,每个字段都有name和description,描述部分会告知模型该字段应包含哪些内容。这样一来,就能更精确地控制响应的格式。
三事实项目
该项目要求提供关于太阳系的三个事实,每个事实对应一个字段:
fact_1用于存储与主题相关的第一个事实。fact_2用于存储第二个事实。fact_3用于存储第三个事实。
解析器会将这些定义转换为指令,然后依据这些指令来解析返回的响应:
Solar System
↓
PromptTemplate
↓
Predefined Field Schema
↓
LLM
↓
StructuredOutputParser
↓
Structured JSON
OpenAI版本
以下是完整脚本:
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
load_dotenv()
# Define Model
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
schema = [
ResponseSchema(name="fact_1", description="Fact 1 about the topic"),
ResponseSchema(name="fact_2", description="Fact 2 about the topic"),
ResponseSchema(name="fact_3", description="Fact 3 about the topic"),
]
parser = StructuredOutputParser.from_response_schemas(schema)
template = PromptTemplate(
template="Give 3 fact about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
chain = template | model | parser
result = chain.invoke({"topic": "Solar System"})
print(result)
该模型的配置仍为之前的 gpt-4.1-mini:
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
真正的差异体现在模式列表上:
schema = [
ResponseSchema(name="fact_1", description="Fact 1 about the topic"),
ResponseSchema(name="fact_2", description="Fact 2 about the topic"),
ResponseSchema(name="fact_3", description="Fact 3 about the topic"),
]
每个 ResponseSchema 都会提供两样内容:
name会成为最终字典中的键。description则用于指定该键应包含的内容。
三个模式会生成三个必填键:fact_1、fact_2 和 fact_3。
无需直接实例化此解析器,有一个类方法会根据模式列表来构建它:
parser = StructuredOutputParser.from_response_schemas(schema)
与JSON解析器类似,格式相关指令也由解析器提供:
parser.get_format_instructions()
这次的指令更为丰富。它们包含一个JSON框架,列出了每个字段名称及其描述,并要求模型将答案封装在带边框的json代码块中。它们的连接方式与之前相同:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
提示模板包含两个常见的占位符:
template = PromptTemplate(
template="Give 3 fact about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic}会在调用时被填充,而{format_instruction}则包含根据架构生成的字段列表。使用这些输入调用时,两者都会被发送给模型:
{"topic": "Solar System"}
执行流程
该流程仍分为与之前相同的三个阶段:
chain = template | model | parser
解析器位于最后一步:
PromptTemplate
↓
ChatOpenAI
↓
StructuredOutputParser
↓
Structured JSON
模板用于构建提示语,模型负责生成答案,而StructuredOutputParser则从回复中提取已声明的字段。
result = chain.invoke({"topic": "Solar System"})
print(result)
输出结果的样子
结果是一个字典,其中包含根据您定义的键所对应的三个值。脚本会将其打印出来:
print(result)
一个典型的输出结果如下:
{
"fact_1": "The Solar System is centered around the Sun.",
"fact_2": "There are eight recognized planets in the Solar System.",
"fact_3": "The Solar System is located in the Milky Way galaxy."
}
具体的值会有所不同,但键的集合必须保持不变:
fact_1
fact_2
fact_3
这正是它相比普通JSON解析的优势:字段名称由应用程序决定,而非模型。如果模型遗漏了某个已声明的键,解析器会抛出错误,而不会默默地返回结构不同的结果,这样处理起来远比在三个函数之后遇到KeyError要容易得多。
流程追踪
从主题到带键的结果的完整路径:
Solar System
↓
PromptTemplate
↓
ResponseSchema
↓
Format Instructions
↓
ChatOpenAI
↓
StructuredOutputParser
↓
{
fact_1: ...,
fact_2: ...,
fact_3: ...
}
一切始于字段定义:
ResponseSchema(
name="fact_1",
description="Fact 1 about the topic"
)
解析器会将这些内容转换为格式指令,这些指令会被放入提示语中,模型随后作出响应,解析器再提取出已声明的字段。与之前的流程相比:
JsonOutputParser
↓
JSON output
↓
Structure can vary
StructuredOutputParser
↓
Predefined fields
↓
More controlled structure
简而言之,JsonOutputParser的作用是获取JSON数据,而StructuredOutputParser则旨在根据用户指定的键来获取JSON数据。
有一个需要明确说明的限制:ResponseSchema拥有一个默认值为string的type属性,但它仅会改变指令的表述方式。解析器会检查键是否存在,而不会验证值类型或范围。如果要求age必须是高于某个阈值的整数,此解析器无法实现这一约束。
还需根据您使用的 LangChain 版本检查导入路径。示例中是从 langchain.output_parsers 导入的,而在较新的版本中,这个旧版解析器已被移出核心包,因此可能需要修改导入路径。
Hugging Face 版本
Gemma 版本使用相同的三种架构以及相同的解析器构建方式:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
load_dotenv()
# Define the model
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
schema = [
ResponseSchema(name='fact_1', description='Fact 1 about the topic'),
ResponseSchema(name='fact_2', description='Fact 2 about the topic'),
ResponseSchema(name='fact_3', description='Fact 3 about the topic'),
]
parser = StructuredOutputParser.from_response_schemas(schema)
template = PromptTemplate(
template='Give 3 fact about {topic} \n {format_instruction}',
input_variables=['topic'],
partial_variables={
'format_instruction': parser.get_format_instructions()
}
)
chain = template | model | parser
result = chain.invoke({'topic': 'Solar System'})
print(result)
并且流程也相同:
PromptTemplate
↓
ChatHuggingFace
↓
StructuredOutputParser
↓
Structured JSON
只有模型提供方有所不同,架构和解析器保持不变。
PydanticOutputParser:结构与验证
PydanticOutputParser 是这四种中最严格的。它通过 Pydantic 模型来描述预期的响应,因此输出的定义同时也等同于有效内容的定义。
这已经超越了简单的解析功能。字段会携带真实的 Python 类型,而且 Field() 可以添加约束条件,例如整数必须大于某个最小值。当模型的输出不满足这些条件时,会出现异常而非错误数据。
为何值得额外设置
- 模式强制:响应必须符合明确定义的结构。
- 类型安全:字段使用 Python 类型如
str、int和float,数值会据此被转换或拒绝。 - 验证:Pydantic 会检查你声明的所有约束条件。
- 链式集成:它能像其他解析器一样与提示词、模型及 LCEL 链路完美集成。
虚构人物项目
此示例要求模型根据给定地点(此处为“印度”)创造一个人物形象,需包含三个字段:
name,该人物的姓名。age,该人物的年龄。city,他们居住的城市。
age字段还有附加约束:其值必须大于18。
Input
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
LLM
↓
PydanticOutputParser
↓
Validated Pydantic Object
OpenAI版本
完整脚本如下:
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
load_dotenv()
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
class Person(BaseModel):
name: str = Field(description="Name of the person")
age: int = Field(gt=18, description="Age of the person")
city: str = Field(description="Name of the city of the person")
parser = PydanticOutputParser(pydantic_object=Person)
template = PromptTemplate(
template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
input_variables=["place"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
chain = template | model | parser
final_result = chain.invoke({"place": "Indian"})
print(final_result)
其结构与之前相同:定义一个Person模型,将其传递给PydanticOutputParser,再将解析器与提示词及模型相连。
模型配置保持不变:
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
核心部分是Pydantic类:
class Person(BaseModel):
name: str = Field(description="Name of the person")
age: int = Field(gt=18, description="Age of the person")
city: str = Field(description="Name of the city of the person")
这是响应的格式要求:
name必须是字符串类型。
age 必须是严格大于 18 的整数。city 必须为字符串类型。Field() 既会添加一段可供人类阅读的描述(该描述会出现在提示中),也会设置约束条件(在解析后会进行验证)。受约束的字段本身:
age: int = Field(gt=18, description="Age of the person")
gt=18 表示“大于 18”,因此年龄恰好为 18 时无法通过验证。如果想要表示“18 岁或以上”,请使用 ge=18。
解析器是通过传递类而非实例来创建的:
parser = PydanticOutputParser(pydantic_object=Person)
这样就能指定在生成指令以及验证回复时使用哪种模型。
格式说明仍来自之前的同一方法:
parser.get_format_instructions()
对于这个解析器,它们包含从Pydantic模型生成的JSON Schema,其中包含字段描述以及age字段的exclusiveMinimum约束。这些内容会像往常一样通过partial变量插入:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
提示模板:
template = PromptTemplate(
template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
input_variables=["place"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
{place}用于控制模型应生成的人物类型。使用该输入会要求提供一名虚构的印度人的姓名、年龄和所在城市:
{"place": "Indian"}
执行处理流程
整个处理流程仍保持常见的三阶段结构:
chain = template | model | parser
这次最终阶段会返回一个模型实例:
PromptTemplate
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Pydantic Object
模板负责构建提示语,模型负责给出回答,而PydanticOutputParser则负责解析JSON并将其验证为Person对象。
final_result = chain.invoke({"place": "Indian"})
print(final_result)
输出结果的样子
返回的是一个 Person 对象,而非字典。打印后会显示 Pydantic 的默认表示形式:
name='Rahul Sharma' age=28 city='Mumbai'
不同运行次数的结果可能有所不同,但这些特性是始终不变的:
name → string
age → integer (> 18)
city → string
这就是各解析器差异最明显的地方。JsonOutputParser仅要求输出 JSON 格式,StructuredOutputParser会为字段命名,而 PydanticOutputParser则将整个契约表示为一个真实的类。你可以利用编辑器的自动补全功能访问 final_result.age,并确信它是一个大于 18 的 int 类型,因为其他类型在进入你的代码之前就会引发验证错误。
流程追踪
从输入到经过验证的对象:
"Indian"
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Person Object
其结构如下所示,为便于阅读省略了相关描述与限制条件:
class Person(BaseModel):
name: str
age: int
city: str
该类会被传递给解析器:
PydanticOutputParser(pydantic_object=Person)
解析器根据模型生成指令,这些指令会被包含在提示词中,模型随后作出响应,解析器再将回复解析为Person对象,并对其执行Pydantic验证。从概念上讲:
Pydantic Model
↓
Defines Structure + Types + Constraints
↓
LLM Response
↓
PydanticOutputParser
↓
Validated Pydantic Object
最终得到的Python对象其数据必然符合既定规则,这比任何JSON字典都能提供的保障更为严格。
一个实际后果是:验证失败会以OutputParserException异常的形式在处理链中体现出来。此时需要决定应如何处理——常见的方案包括重新尝试调用、通过LangChain的OutputFixingParser将错误反馈给模型,或是记录日志后返回安全的默认值。
Hugging Face版本
Gemma版本定义了相同的Person模型,并将其传递给同一个解析器:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
load_dotenv()
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
class Person(BaseModel):
name: str = Field(description='Name of the person')
age: int = Field(gt=18, description='Age of the person')
city: str = Field(description='Name of the city the person belongs to')
parser = PydanticOutputParser(pydantic_object=Person)
template = PromptTemplate(
template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
input_variables=['place'],
partial_variables={
'format_instruction': parser.get_format_instructions()
}
)
chain = template | model | parser
final_result = chain.invoke({'place': 'Indian'})
print(final_result)
处理流程保持不变:
PromptTemplate
↓
ChatHuggingFace
↓
PydanticOutputParser
↓
Pydantic Object
只有提供方有所变化;Pydantic模型和解析器是共享的。较小的模型更容易违反约束条件或产生多余文本,而这正是验证功能发挥作用的情况:不良响应会在边界处被捕获,而不会渗入数据中。
选择合适的解析器
决策的关键在于响应的接收方真正需要多少结构化信息以及多强的验证功能。以下是各选项的简要总结。
StrOutputParser
当模型的输出仅为文本时使用此解析器。
- 最适合用于:报告、解释、摘要、聊天回复。
- 返回值:字符串。
- 不支持JSON解析。
- 模式:无。
- 验证:无。
JsonOutputParser
当需要 JSON 格式但能接受或处理结构不固定的数据时使用。
- 最适合用于:探索性结构化输出、灵活的负载数据。
- 返回值:字典或列表。
- JSON 解析:支持。
- 模式:无(此处使用的是无参数形式)。
- 验证:仅检查“是否为有效 JSON”。
StructuredOutputParser
当代码要求特定的键,如 fact_1 到 fact_3 时使用。
- 最适合用于:字段名称已知的简单记录。
- 返回值:包含已声明键的字典。
- JSON 解析:支持。
- 模式:支持,包含字段名称和描述。
- 验证:仅检查键是否存在,不进行类型检查。
PydanticOutputParser
当输出需直接用于应用程序逻辑且必须保证正确性时,请使用此方式。
- 最适合用于:存储的数据、需要计算的数据或传递给 API 的数据。
- 返回值:您的 Pydantic 模型的实例。
- JSON 解析:支持。
- 模式定义:支持,包含完整的类型与约束条件。
- 验证功能:支持。
简易概念模型
StrOutputParser:只需文本即可。JsonOutputParser:任何有效的 JSON 格式均可。StructuredOutputParser:JSON 必须包含指定键。PydanticOutputParser:不仅要求指定键和类型,还会检查所有约束条件。
请选择能满足您需求的最简单解析器。层级越高,指令所需的提示词越多,响应被拒绝的情况也会更多,因此提升严格程度应是经过深思熟虑后的选择。
还有一种选择值得考虑。这四种解析器都是通过在提示词中描述格式,然后再对文本进行解析来工作的。许多聊天模型还支持原生结构化输出或工具调用,LangChain通过模型上的with_structured_output()方法实现了这一点。如果服务提供商支持该功能,对于具有固定结构的数据而言,这种方式通常更为可靠;而对于不支持该功能的提供商和模型,基于提示词的解析器依然很有用。
关键要点
- 输出解析器可将模型的输出转换为代码可使用的值,并通过管道运算符集成到LCEL链中。
StrOutputParser会移除消息包装层,从而使一个模型的输出可直接作为下一个提示词的输入。JsonOutputParser可以解析JSON,但除非提供架构定义,否则不会自动调整其结构。
StructuredOutputParser通过ResponseSchema来修正键名,但不会检查值类型。PydanticOutputParser将解析与类型检查及约束结合在一起,最终返回真实的对象。一旦响应能以稳定的格式返回,接下来的自然步骤就是将多个提示词、模型和解析器整合到更大的工作流中,这些工作流包括使用 RunnableParallel 和 RunnableBranch 构建的顺序、并行和条件链。关于这方面的内容,请参阅 使用 LCEL 构建 LangChain 工作流。