1. 本章目标
- 理解区别:明白自然语言输出和结构化输出的不同应用场景
- 掌握基础:使用
StrOutputParser获取纯文本结果 - 定义结构:使用
Pydantic定义模型应该返回的数据格式 - 解析输出:使用
PydanticOutputParser解析模型返回的结构化文本 - 直接获取:使用
with_structured_output更简洁地获取结构化对象 - 处理异常:处理结构化输出失败的情况
- 实战应用:完成简历信息抽取和商品评论分析等实际案例
学习建议:本章内容层层递进,建议按顺序学习。先从简单的StrOutputParser开始,再逐步学习更复杂的结构化输出。
:::
2. 为什么需要结构化输出?
想象一下,你正在开发一个简历筛选系统,需要从简历中提取以下信息:
- 姓名
- 工作年限
- 技能
- 目标岗位
场景对比
场景一:模型返回自然语言
候选人姓名是张三,工作 3 年,熟悉 Python、FastAPI 和 MySQL,希望应聘后端开发工程师。程序收到这段文字后,还需要:
- 用正则表达式或NLP技术提取信息
- 处理各种表达方式(“工作3年” vs “有3年工作经验”)
- 解析技能列表(逗号分隔、顿号分隔等)
场景二:模型返回结构化数据
{"name":"张三","years_of_experience":3,"skills":["Python","FastAPI","MySQL"],"target_position":"后端开发工程师"}程序可以直接使用:
# 直接访问属性,无需额外解析print(resume.name)# 输出:张三print(resume.skills)# 输出:['Python', 'FastAPI', 'MySQL']结构化输出的应用场景
- 信息抽取:从文档中提取特定信息(如简历、合同、报告)
- 文本分类:将文本归类到预定义的类别中
- 情感分析:分析文本的情感倾向(正面/负面/中性)
- 工单分类:自动将用户问题分类到相应部门
- 内容审核:识别违规内容并分类
- 数据入库:将非结构化数据转换为数据库可存储的格式
- 接口调用:将模型输出作为其他业务系统的输入
关键理解:结构化输出让程序能直接处理模型结果,而不是让人去阅读和理解。
:::
3. Output Parser 是什么?
Output Parser(输出解析器)是 LangChain 中处理模型输出的组件。你可以把它想象成一个"翻译官":
模型原始输出(AIMessage) → Output Parser → 程序可用的数据LangChain 中常见的输出方式
| 方式 | 作用 | 适用场景 |
|---|---|---|
StrOutputParser | 将模型回复转换成字符串 | 只需要文本内容,不关心结构 |
PydanticOutputParser | 将模型回复解析成Pydantic 对象 | 需要结构化数据,且模型返回JSON文本 |
with_structured_output | 让模型直接按照指定结构返回 | 模型服务支持结构化输出时使用 |
本章学习路线:
- 先学
StrOutputParser(最简单) - 再学
PydanticOutputParser(最常用) - 最后学
with_structured_output(最方便)
:::
4. StrOutputParser:获取纯文本输出
4.1 基本用法
当调用大模型时,返回的通常是AIMessage对象:
# 直接调用模型response=model.invoke("请介绍 LangChain")print(response.content)# 输出:LangChain是一个用于开发大模型应用的框架...StrOutputParser的作用就是把AIMessage对象转换成普通字符串:
fromlangchain_core.output_parsersimportStrOutputParser parser=StrOutputParser()text=parser.invoke(response)# 将 AIMessage 转换为字符串print(text)4.2 适用场景
StrOutputParser适合以下场景:
- 文案生成:广告文案、文章创作
- 内容总结:长文本摘要
- 普通问答:知识问答、咨询回复
- 翻译任务:文本翻译
重要提示:如果业务只需要文本内容,不关心数据结构,使用StrOutputParser就足够了,不需要复杂的结构化输出。
:::
4.3 深入理解:为什么需要 StrOutputParser?
你可能会有疑问:直接打印response.content和使用StrOutputParser有什么区别?
# 方式一:直接打印response=model.invoke("请介绍 LangChain")print(response.content)# 方式二:使用解析器parser=StrOutputParser()text=parser.invoke(response)print(text)表面上看:两种方式都打印字符串,效果几乎一样。
实际上:StrOutputParser的核心价值在于链式(Pipeline)支持。
4.4 链式编程的重要性
LangChain 推崇使用管道(|)连接各个组件,形成处理流水线:
# ❌ 错误写法:.content 是属性,不是 Runnable 对象chain=model|response.content# 报错!# ✅ 正确写法:使用 StrOutputParserfromlangchain_core.output_parsersimportStrOutputParser parser=StrOutputParser()chain=model|parser# 正确:parser 是 Runnable 对象result=chain.invoke("请介绍 LangChain")print(result)# result 直接就是字符串,无需再调用 .content关键区别:
- 返回类型:
model.invoke()返回的是AIMessage对象 - 解析器作用:
StrOutputParser是 LangChain 标准输出解析器 - 链式支持:
parser是Runnable对象,可以参与链式拼接
总结:
- 如果只是单独调用
model.invoke()然后打印,两种方式效果几乎无差别 - 一旦需要构建处理链路(如:模型 → 解析器 → 后续处理),必须使用
StrOutputParser StrOutputParser的最大价值:作为Runnable参与链式拼接
:::
5. 案例一:文本总结
让我们通过一个简单的文本总结案例,实践StrOutputParser的使用。
5.1 创建脚本文件
创建01_text_summary.py:
fromlangchain_core.output_parsersimportStrOutputParserfromlangchain_core.promptsimportChatPromptTemplatefromutils.model_factoryimportget_deepSeek_model# 1. 获取模型model=get_deepSeek_model()# 2. 创建提示词模板chat_prompt=ChatPromptTemplate.from_messages([("system","你是一个内容编辑工程师,擅长提炼文本重点"),("human","""请将下面内容总结成一句话,不超过 50 字。 内容:{question}""")])# 3. 准备输入prompt=chat_prompt.invoke({"question":"LangChain 是一个用于开发大模型应用的框架,""提供模型调用、Prompt 管理、文档处理、检索和工具调用等能力。"})# 4. 调用模型resp=model.invoke(prompt)# 5. 方式一:直接获取内容(不推荐在链式中使用)print("=== 方式一:直接获取 ===")print(resp.content)print()# 6. 方式二:使用 StrOutputParser(推荐)print("=== 方式二:使用解析器 ===")parser=StrOutputParser()text=parser.invoke(resp)print(text)5.2 运行脚本
python 01_text_summary.py5.3 运行结果
两种方式都会输出类似的内容:
LangChain是一个大模型应用开发框架,提供模型调用、Prompt管理、文档处理等能力。5.4 关键点
- 这个案例最终得到的是普通字符串
- 方式一和方式二在单独使用时效果相同
- 方式二(使用
StrOutputParser)更适合在链式编程中使用
练习建议:尝试修改提示词,让模型用不同风格总结(如:技术文档风格、产品介绍风格、朋友圈风格)。
:::
6. 使用 Pydantic 定义输出结构
Pydantic 可以用来定义模型应该返回哪些字段。
dantic 源自 pedantic /pɪˈdæntɪk/
✅ pedantic 释义:迂腐的、严谨教条的、拘泥规则的
:::
例如简历信息:
frompydanticimportBaseModel,FieldclassResumeInfo(BaseModel):name:str=Field(description="候选人姓名")years_of_experience:int=Field(description="工作年限")skills:list[str]=Field(description="掌握的技术技能")target_position:str=Field(description="目标岗位")这个模型既描述了字段类型,也描述了字段含义。
如果模型返回的数据不符合字段类型,解析时就会报错。
7. PydanticOutputParser [ 帕泽 ]
PydanticOutputParser可以根据 Pydantic 模型生成格式要求,并解析模型返回结果。
创建 Parser:
fromlangchain_core.output_parsersimportPydanticOutputParser parser=PydanticOutputParser(pydantic_object=ResumeInfo)获取格式说明:
instructions 指令format_instructions=parser.get_format_instructions()把格式说明传给 Prompt:
prompt_template=ChatPromptTemplate.from_messages([("system","你是一名招聘信息分析助手。\n{format_instructions}",),("human","请从下面简历中提取信息:\n{resume_text}"),])最后解析:
result = parser.parse(response.content)8. 完整案例:简历信息抽取
创建02_resume_extractor.py:
fromlangchain_core.output_parsersimportPydanticOutputParserfromlangchain_core.promptsimportChatPromptTemplatefrompydanticimportBaseModel,Fieldfromutils.model_factoryimportget_deepSeek_model model=get_deepSeek_model()classResumeInfo(BaseModel):name:str=Field(description="姓名")years_of_experience:int=Field(description="工作年限")skills:list[str]=Field(description="掌握的技术技能")target_position:str=Field(description="目标岗位")parser=PydanticOutputParser(pydantic_object=ResumeInfo)# 格式化指令format_instructions=parser.get_format_instructions()template=ChatPromptTemplate.from_messages([("system",""" 你是一名招聘信息分析助手。 请严格按照指定格式返回结果。 {format_instructions} """),("human","{resume_content}")])resume_content=""" 我叫张三,我干大模型开发10年了,我擅长的技术是 python,langchain,fastapi等,我比较喜欢养猫养狗,我想找一份智能体开发的工作。 """prompt=template.invoke({"format_instructions":format_instructions,"resume_content":resume_content})# 调用大模型,获取AIMessageresponse=model.invoke(prompt)# 将大模型的返回值,进行格式化输出result=parser.invoke(response)print(result)print(result.name)print(result.years_of_experience)print(result.skills)print(result.target_position)预期得到类似结果:
姓名:张三 工作年限:3 技能:['Python', 'FastAPI', 'MySQL', 'Redis'] 目标岗位:Python 后端开发工程师9. with_structured_output
较新的 LangChain 模型组件通常提供:
with_structured_output它可以让模型按照 Pydantic 模型返回结构化结果。
基本写法:
structured_model=model.with_structured_output(ResumeInfo)result=structured_model.invoke("从简历中提取信息")这种写法比手动获取格式说明、再调用 Parser 更简洁。
但是否支持、底层使用哪种结构化方式,和模型服务能力有关。
使用 DeepSeek 的 OpenAI 兼容接口时,可以使用 JSON 模式:
structured_model=model.with_structured_output(ResumeInfo,method="json_mode",)Prompt 中需要明确要求模型返回 JSON。
假如你使用了 deepseek 大模型,但是没有使用method=“json_mode”,会报如下错误:
openai.BadRequestError:Error code:400-{'error':{'message':'This response_format type is unavailable now','type':'invalid_request_error','param':None,'code':'invalid_request_error'}}10. 案例三:商品评论分析
本案例使用with_structured_output分析商品评论。
创建03_review_analyzer.py:
Literal[“正面”, “中性”, “负面”]
字面量类型约束
表示这个字段只能取三个值中的一个:
“正面” / “中性” / “负面”
不允许其他任何字符串
有点类似于枚举
:::
fromtypingimportLiteralfromlangchain_core.promptsimportChatPromptTemplatefrompydanticimportBaseModel,Fieldfromutils.model_factoryimportget_deepSeek_modelclassReviewAnalysis(BaseModel):sentiment:Literal["正面","负面","中性"]=Field(description="情感分析的值")keywords:list[str]=Field(description="评论中的关键词")summary:str=Field(description="对评论的简短总结")needs_reply:bool=Field(description="商家是否需要回复")model=get_deepSeek_model()template=ChatPromptTemplate.from_messages([("system","""你是专业商品评论分析助手,严格遵守以下规则,仅输出纯JSON,无任何多余文字、解释、markdown: 1. 输出JSON必须包含4个字段:sentiment、keywords、summary、needs_reply,缺一不可; 2. sentiment 仅允许三个中文值:「正面」「中性」「负面」,绝对不能使用 mixed / positive / negative 等英文; 3. keywords 是字符串数组,提取评论核心描述词; 4. summary 用一句话概括整条评论优缺点; 5. needs_reply:商品存在质量问题、故障、严重不满设为true,单纯好评设为false。 """),("human",""" 请分析下面的商品评论: {review} """)])prompt=template.invoke({"review":"鼠标手感不错,也很安静,但是用了两周滚轮就有异响。"})structurted_model=model.with_structured_output(ReviewAnalysis,method="json_mode")response=structurted_model.invoke(prompt)print(response)print(response.sentiment)print(response.keywords)print(response.summary)print(response.needs_reply)str Literal : 字符串字面量
number Literal : 数值字面量
:::
预期得到类似结果:
情感:负面 关键词:['手感', '静音', '滚轮异响'] 总结:用户认可鼠标手感和静音效果,但反馈滚轮出现质量问题。 是否需要回复:True问题解析:
这个代码中的 method=“json_mode” 我怎么没有看到任何的 json 格式呢?
method="json_mode"不是让你代码里手动处理 JSON 字符串。 底层流程是这样:
- LLM 收到指令,输出一段合法 JSON 文本
- LangChain 内部自动把 JSON 解析 → 实例化成你的
ReviewAnalysisPydantic 对象 - 你拿到手直接是模型对象,看不到原始 JSON
你打印result得到的是 Pydantic 实例,不是原始 JSON 字符串,所以直观上看不到 JSON。
:::
11. 两种结构化方式怎么选
| 方式 | 特点 | 适合场景 |
|---|---|---|
PydanticOutputParser | 通过 Prompt 要求格式,再解析文本 | 需要明确学习 Parser 工作方式 |
with_structured_output | 调用更简洁 | 模型服务支持结构化输出 |
- 先掌握
PydanticOutputParser - 实际项目优先考虑
with_structured_output - 使用前确认模型服务是否支持对应方式
PydanticOutputParser是“让模型按提示输出 JSON,然后我在本地解析”;with_structured_output是“直接把结构化输出能力绑定到模型调用上,让模型/API 尽量按 schema 生成”。
12. 案例四:工单分类
本案例将用户问题分类为程序可以使用的数据。
智能体客服,接到用户的问题反馈之后,自动将用户的问题,划分到不同的种类,并且显示优先级。
:::
创建04_ticket_classifier.py:
fromtypingimportLiteralfromlangchain_core.output_parsersimportPydanticOutputParserfromlangchain_core.promptsimportChatPromptTemplatefrompydanticimportBaseModel,Fieldfromutils.model_factoryimportget_deepSeek_modelclassTicketResult(BaseModel):category:Literal["订单","物流","退款","产品","其他"]=Field(description="工单分类")priority:Literal["低","中","高"]=Field(description="工单优先级")reason:str=Field(description="分类原因")model=get_deepSeek_model()# 得到解析器parser=PydanticOutputParser(pydantic_object=TicketResult)# 得到解析器的规则format_instructions=parser.get_format_instructions()template=ChatPromptTemplate.from_messages([("system","""你是一名客服工单分类助手。 请根据用户问题完成分类。 {format_instructions}"""),("human","用户问题:{question}")])prompt=template.invoke({"format_instructions":format_instructions,"question":"订单显示已签收,但我没有收到商品,请尽快处理。"})response=model.invoke(prompt)result=parser.invoke(response)print(result.category)print(result.priority)print(result.reason)ifresult.priority=="高":print("转人工客服优先处理")这个结果可以继续用于程序判断:
if result.priority == "高": print("转人工客服优先处理")这就是结构化输出的实际价值:模型结果可以继续进入业务流程。
13. 处理解析错误
模型输出不稳定时,可能出现解析失败。
可以捕获OutputParserException:
fromlangchain_core.exceptionsimportOutputParserExceptiontry:result=parser.parse(response.content)print(result)exceptOutputParserExceptionasexc:print("结构化输出解析失败")print("模型原始输出:",response.content)print("错误信息:",exc)处理建议:
- 保存模型原始输出
- 记录错误日志
- 优化 Prompt
- 必要时重新请求模型
- 重要业务不能完全依赖模型自行保证格式
fromtypingimportLiteralfromlangchain_core.output_parsersimportPydanticOutputParserfromlangchain_core.promptsimportChatPromptTemplatefrompydanticimportBaseModel,Fieldfromutils.model_factoryimportget_deepSeek_modelclassTicketResult(BaseModel):category:Literal["订单","物流","退款","产品","其他"]=Field(description="工单分类")priority:Literal["低","中","高"]=Field(description="工单优先级")reason:str=Field(description="分类原因")model=get_deepSeek_model()# 得到解析器parser=PydanticOutputParser(pydantic_object=TicketResult)# 得到解析器的规则format_instructions=parser.get_format_instructions()template=ChatPromptTemplate.from_messages([("system","""你是一名客服工单分类助手。 请根据用户问题完成分类。 {format_instructions}"""),("human","用户问题:{question}")])prompt=template.invoke({"format_instructions":format_instructions,"question":"订单显示已签收,但我没有收到商品,请尽快处理。"})response=model.invoke(prompt)try:result=parser.invoke(response)print(result.category)print(result.priority)print(result.reason)ifresult.priority=="高":print("转人工客服优先处理")exceptExceptionasexception:print("失败的原因是:",exception)print("大模型响应的内容是:",response.content)14. 本章重点
本章最重要的是掌握:
- 普通文本适合人阅读,结构化数据适合程序处理
StrOutputParser用于获取字符串Pydantic模型用于定义输出结构PydanticOutputParser用于解析模型文本with_structured_output可以更直接地获取结构化对象- 结构化输出失败时需要异常处理