LangChain Output Parser 实战:从字符串到结构化数据的完整指南

前言

在前面的学习中,我们已经能够通过 LangChain 调用大模型,也学会了使用 Prompt Template 组织提示词。不过,大模型默认返回的依然是一段文本------即使它看起来像列表或者 JSON,对 Python 程序来说,它通常仍然只是一个字符串。

例如,让模型列出 5 个中国汽车品牌,模型可能返回:

text 复制代码
比亚迪, 吉利, 长城, 红旗, 奇瑞

人可以直接看懂,但程序如果要继续遍历、筛选或者保存,更希望得到真正的 Python 列表:

python 复制代码
["比亚迪", "吉利", "长城", "红旗", "奇瑞"]

同样,如果要求模型从电影简介中提取电影名、导演和题材,程序真正需要的也不是一段随意组织的说明,而是一份字段固定、类型明确的数据。

这正是 Output Parser(输出解析器)要解决的问题。

本文基于课堂中的两个 Notebook 进行讲解:

  • 04 Output Parser _List.ipynb:把模型输出解析成 Python 列表
  • 05 Output Parser _ JSON.ipynb:把电影信息解析成 Pydantic 结构化对象

通过本文,你将掌握:

  1. Output Parser 的核心工作方式
  2. 使用 CommaSeparatedListOutputParser 解析列表
  3. 使用 PydanticOutputParser 解析为结构化对象
  4. 两种解析器的适用场景与对比
  5. 常见问题与调试方法

项目效果展示

模型返回逗号分隔的字符串后,解析器将其转换为 Python 列表:

text 复制代码
模型原始输出:比亚迪, 吉利, 长城, 红旗, 奇瑞
解析后结果:['比亚迪', '吉利', '长城', '红旗', '奇瑞']
结果类型:<class 'list'>

模型返回 JSON 字符串后,解析器将其转换为 FilmInfo 对象:

text 复制代码
模型原始输出:{"film_name": "复仇者联盟4:终局之战", "author_name": "安东尼·罗素和乔·罗素", "genres": ["动作", "科幻"]}
解析后对象:FilmInfo(film_name='复仇者联盟4:终局之战', author_name='安东尼·罗素和乔·罗素', genres=['动作', '科幻'])
直接访问属性:复仇者联盟4:终局之战

完整工程代码

以下为两个案例的完整可运行代码,可直接复制执行。

案例一:列表解析完整代码

python 复制代码
import os

from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import CommaSeparatedListOutputParser
from langchain_core.prompts import ChatPromptTemplate

# 1. 创建列表解析器
output_parser = CommaSeparatedListOutputParser()
parser_instructions = output_parser.get_format_instructions()

# 2. 构建提示词模板
prompt_template = ChatPromptTemplate.from_messages([
    ("system", "{parser_instructions}"),
    ("human", "列出5个{subject}国家的汽车品牌。")
])

# 3. 构造最终提示词
final_prompt = prompt_template.invoke({
    "subject": "中国",
    "parser_instructions": parser_instructions
})

# 4. 初始化模型
model = ChatOpenAI(
    model="qwen-plus",
    openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
    openai_api_base=os.getenv(
        "DASHSCOPE_BASE_URL",
        "https://dashscope.aliyuncs.com/compatible-mode/v1"
    )
)

# 5. 调用模型并解析
response = model.invoke(final_prompt)
result = output_parser.invoke(response)

print("模型原始文本:", response.content)
print("解析后的列表:", result)
print("结果类型:", type(result))

案例二:JSON 解析完整代码

python 复制代码
import os
from typing import List

from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic.v1 import BaseModel, Field


# 1. 定义数据模型
class FilmInfo(BaseModel):
    film_name: str = Field(
        description="电影的名字",
        example="拯救大兵瑞恩"
    )
    author_name: str = Field(
        description="电影的导演",
        example="斯皮尔伯格"
    )
    genres: List[str] = Field(
        description="电影的题材",
        example=["历史", "战争"]
    )


# 2. 创建解析器
output_parser = PydanticOutputParser(pydantic_object=FilmInfo)

# 3. 构建提示词模板
prompt_template = ChatPromptTemplate.from_messages([
    ("system", "{parser_instructions} 你输出的结果请使用中文。"),
    (
        "human",
        "请你帮我从电影概述中,提取电影名、导演,以及电影的体裁。"
        "电影概述会被三个#符号包围。\n###{film_introduction}###"
    )
])

# 4. 准备电影简介
film_introduction = """
《复仇者联盟4:终局之战》是由安东尼·罗素和乔·罗素
联合执导,小罗伯特·唐尼、克里斯·埃文斯等主演的动作科幻片。
该片改编自漫威漫画,讲述复仇者联盟剩余成员再次集结,
利用时间装置穿越时空、重新创造希望的故事。
"""

# 5. 构造最终提示词
final_prompt = prompt_template.invoke({
    "film_introduction": film_introduction,
    "parser_instructions": output_parser.get_format_instructions()
})

# 6. 初始化模型
model = ChatOpenAI(
    model="qwen-plus",
    openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
    openai_api_base=os.getenv(
        "DASHSCOPE_BASE_URL",
        "https://dashscope.aliyuncs.com/compatible-mode/v1"
    )
)

# 7. 调用模型并解析
response = model.invoke(final_prompt)
result = output_parser.invoke(response)

print("模型原始文本:")
print(response.content)
print("\n解析后的对象:")
print(result)
print("\n电影名称:", result.film_name)
print("电影题材:", result.genres)

一、为什么大模型输出还需要解析

1. 从文本到数据

大模型默认返回的是一段文本。即使它看起来像列表或者 JSON,对 Python 程序来说,它通常仍然只是一个字符串。

核心问题:人可以直接看懂,但程序如果要继续遍历、筛选或者保存,更希望得到真正的 Python 数据结构。

2. Output Parser 的工作方式

Output Parser 通常承担两项工作:

第一项工作 ,是告诉模型应该按照什么格式回答。解析器会通过 get_format_instructions() 生成格式要求,然后我们把这段要求放进提示词。

第二项工作,是在模型回答之后,把文本转换成 Python 数据,并检查它是否符合预期结构。

完整流程如下:

二、实战一:把模型回答解析成 Python 列表

1. 导入所需组件

功能说明

导入模型客户端、列表解析器和提示词模板。

完整代码
python 复制代码
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import CommaSeparatedListOutputParser
from langchain_core.prompts import ChatPromptTemplate
核心实现逻辑

三个组件的职责分别是:

  • ChatOpenAI:通过 OpenAI 兼容接口调用通义千问
  • CommaSeparatedListOutputParser:处理以逗号分隔的文本
  • ChatPromptTemplate:构建包含变量的聊天提示词

2. 创建列表解析器并获取格式说明

完整代码
python 复制代码
output_parser = CommaSeparatedListOutputParser()
parser_instructions = output_parser.get_format_instructions()
print(parser_instructions)
输出
text 复制代码
Your response should be a list of comma separated values,
eg: `foo, bar, baz` or `foo,bar,baz`

关键理解parser_instructions 并不是最终结果,而是一段要发送给模型的格式说明。解析器通过这种方式"约束"模型按预期格式回答。

3. 把格式说明加入提示词

完整代码
python 复制代码
prompt_template = ChatPromptTemplate.from_messages([
    ("system", "{parser_instructions}"),
    ("human", "列出5个{subject}国家的汽车品牌。")
])

其中包含两个变量:

  • parser_instructions:解析器生成的格式要求
  • subject:要查询的国家
构造最终提示词
python 复制代码
final_prompt = prompt_template.invoke({
    "subject": "中国",
    "parser_instructions": parser_instructions
})

最终指令可以理解为:请列出 5 个中国汽车品牌,并且只用逗号分隔这些品牌。

4. 调用模型

完整代码
python 复制代码
import os

model = ChatOpenAI(
    model="qwen-plus",
    openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
    openai_api_base=os.getenv(
        "DASHSCOPE_BASE_URL",
        "https://dashscope.aliyuncs.com/compatible-mode/v1"
    )
)

response = model.invoke(final_prompt)
print(response.content)
输出
text 复制代码
比亚迪, 吉利, 长城, 红旗, 奇瑞

关键点 :此时结果虽然长得像列表,但 response.content 仍然是字符串:

python 复制代码
print(type(response.content))
text 复制代码
<class 'str'>

5. 将字符串解析成列表

完整代码
python 复制代码
result = output_parser.invoke(response)
print(result)
输出
python 复制代码
['比亚迪', '吉利', '长城', '红旗', '奇瑞']

现在的 result 才是真正的 Python 列表,可以继续遍历或按索引访问:

python 复制代码
for brand in result:
    print(brand)

6. 为什么不直接使用 split

对于这个简单案例,我们当然也可以写:

python 复制代码
brands = response.content.split(",")

但是,这种方式只负责字符串切割,不负责指导模型如何回答。只要模型改用中文逗号、换行或者增加解释性文字,结果就可能需要额外清理。

Output Parser 的优势在于,它把"要求模型按格式回答"和"程序解析回答"放在同一套规则中。项目越复杂,这种统一约束越重要。

三、实战二:提取电影信息并生成结构化对象

1. 为什么列表已经不够用了

汽车品牌案例中的每个元素都是同一类数据,用列表表示很合适。

电影信息则包含多个含义不同的字段:

  • 电影名称是字符串
  • 导演名称是字符串
  • 电影题材可能有多个,因此应当是字符串列表

如果仍然只返回一个普通列表,我们很难知道每个位置究竟代表什么。因此,我们需要使用 PydanticOutputParser 定义结构化输出。

2. 导入相关组件

完整代码
python 复制代码
from typing import List
import os

from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic.v1 import BaseModel, Field
from langchain_openai import ChatOpenAI

3. 使用 Pydantic 编写数据说明书

完整代码
python 复制代码
class FilmInfo(BaseModel):
    film_name: str = Field(
        description="电影的名字",
        example="拯救大兵瑞恩"
    )
    author_name: str = Field(
        description="电影的导演",
        example="斯皮尔伯格"
    )
    genres: List[str] = Field(
        description="电影的题材",
        example=["历史", "战争"]
    )
核心实现逻辑

可以把 BaseModel 理解成一份数据说明书 ,而 Field 用来补充每个字段的含义和示例。

其中的类型标注非常重要:

字段 类型 含义
film_name str 电影名必须是字符串
author_name str 导演必须是字符串
genres List[str] 题材必须是字符串列表

4. 创建 Pydantic 输出解析器

完整代码
python 复制代码
output_parser = PydanticOutputParser(pydantic_object=FilmInfo)

参数 pydantic_object=FilmInfo 表示:后续返回结果必须符合 FilmInfo 定义的结构。

查看解析器生成的格式说明
python 复制代码
print(output_parser.get_format_instructions())

核心输出为 JSON Schema:

json 复制代码
{
  "properties": {
    "film_name": {
      "description": "电影的名字",
      "type": "string"
    },
    "author_name": {
      "description": "电影的导演",
      "type": "string"
    },
    "genres": {
      "description": "电影的题材",
      "type": "array",
      "items": {"type": "string"}
    }
  },
  "required": ["film_name", "author_name", "genres"]
}

这份 Schema 同时说明了字段名称、字段类型和必填字段。我们不需要手写整段格式要求,解析器会根据 Pydantic 模型自动生成

5. 构建电影信息提取模板

完整代码
python 复制代码
prompt_template = ChatPromptTemplate.from_messages([
    (
        "system",
        "{parser_instructions} 你输出的结果请使用中文。"
    ),
    (
        "human",
        "请你帮我从电影概述中,提取电影名、导演,以及电影的体裁。"
        "电影概述会被三个#符号包围。\n###{film_introduction}###"
    )
])

这次系统消息中既包含结构要求,也要求使用中文输出。

人类消息中的 ### 是一种提示词分隔符,用来帮助模型区分"任务说明"和"待处理的电影简介"。

6. 填充电影简介并构造提示词

完整代码
python 复制代码
film_introduction = """
《复仇者联盟4:终局之战》是由安东尼·罗素和乔·罗素
联合执导,小罗伯特·唐尼、克里斯·埃文斯等主演的动作科幻片。
该片改编自漫威漫画,讲述复仇者联盟剩余成员再次集结,
利用时间装置穿越时空、重新创造希望的故事。
"""

final_prompt = prompt_template.invoke({
    "film_introduction": film_introduction,
    "parser_instructions": output_parser.get_format_instructions()
})

7. 调用模型获得 JSON 文本

完整代码
python 复制代码
model = ChatOpenAI(
    model="qwen-plus",
    openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
    openai_api_base=os.getenv(
        "DASHSCOPE_BASE_URL",
        "https://dashscope.aliyuncs.com/compatible-mode/v1"
    )
)

response = model.invoke(final_prompt)
print(response.content)
输出
json 复制代码
{
  "film_name": "复仇者联盟4:终局之战",
  "author_name": "安东尼·罗素和乔·罗素",
  "genres": ["动作", "科幻"]
}

关键认知 :这段输出已经符合 JSON 外观,但 response.content 依然只是字符串。

一个常见误区是:看到大括号就认为结果已经是 Python 字典。实际上,"长得像 JSON 的字符串"和"程序中的结构化对象"并不是一回事。

8. 解析并校验为 FilmInfo 对象

完整代码
python 复制代码
result = output_parser.invoke(response)
print(result)
输出
text 复制代码
FilmInfo(
    film_name='复仇者联盟4:终局之战',
    author_name='安东尼·罗素和乔·罗素',
    genres=['动作', '科幻']
)

此时 result 已经是 FilmInfo 对象,可以直接通过属性访问数据:

python 复制代码
print(result.film_name)
print(result.genres)

输出:

text 复制代码
复仇者联盟4:终局之战
['动作', '科幻']

与手动处理 JSON 字符串相比,这种方式不但完成了解析,还检查了字段是否存在、数据类型是否符合定义

四、两种解析器对比

对比总览

对比项 列表解析器 Pydantic 解析器
核心类 CommaSeparatedListOutputParser PydanticOutputParser
结果结构 一组同类元素 多个命名字段
类型约束 较弱 较强(由 Pydantic 模型定义)
使用难度 简单 稍高
典型结果类型 list[str] BaseModel 子类对象
适合场景 列举、推荐、标签 信息抽取、接口数据、入库

适用场景说明

CommaSeparatedListOutputParser 适合结构简单、每个元素含义相同的结果,例如:

  • 城市名称列表
  • 商品关键词列表
  • 推荐书单
  • 标签列表

PydanticOutputParser 适合字段含义不同、类型明确的业务数据,例如:

  • 电影信息(名称、导演、题材)
  • 商品数据(名称、价格、库存)
  • 简历字段(姓名、经验、技能列表)
  • 文章元数据(标题、作者、标签、发布时间)

五、常见问题与解决方法

问题1:为什么模型已经输出 JSON,解析器仍然报错

常见原因

  • 模型在 JSON 前后增加了解释文字
  • 使用了 Markdown 代码块包围 JSON(如 json ...
  • 缺少必填字段
  • 字段名与 Pydantic 模型不一致
  • 字段类型错误,例如把列表写成普通字符串

解决方法

先查看模型原始回答:

python 复制代码
print(response.content)

不要一上来只检查解析器。很多解析失败,本质上是模型没有严格遵守输出格式。

问题2:如何提高解析成功率

改进方向

  • get_format_instructions() 放入系统消息
  • 明确要求不要添加解释、前言和 Markdown 代码块
  • 减少互相矛盾的提示词
  • 为字段提供清楚的描述和示例
  • 对失败请求增加重试或修复机制

例如,可以在系统提示中增加:

text 复制代码
请严格按照指定结构输出,不要添加任何额外解释。

问题3:为什么不能把 API Key 直接写进代码

课堂实践中我们已经把密钥保存到环境变量,因此代码只负责读取:

python 复制代码
api_key = os.getenv("DASHSCOPE_API_KEY")

这样做可以避免把密钥上传到 GitHub、CSDN 或共享项目中。

问题4:公共地址和专属地址应该使用哪个

如果使用普通的阿里云百炼 OpenAI 兼容接口,可以使用公共地址:

text 复制代码
https://dashscope.aliyuncs.com/compatible-mode/v1

如果账号页面提供了业务空间专属域名,则应使用自己账号对应的完整地址,不要复制他人的专属地址。

六、从解析器走向 Chain

当前代码的调用方式

本文为了讲清每一步,把提示词、模型调用和输出解析分开执行:

text 复制代码
prompt_template.invoke()
        ↓
model.invoke()
        ↓
output_parser.invoke()

Chain 方式的简化写法

LangChain 还可以使用管道符把它们连接起来:

python 复制代码
chain = prompt_template | model | output_parser

之后只需要调用一次:

python 复制代码
result = chain.invoke({
    "subject": "中国",
    "parser_instructions": parser_instructions
})

这种写法会把多个可运行组件组合成一条 Chain。它正是后续要讲解的重点。

新版 LangChain 的结构化输出

本文采用课堂锁定环境中的 PydanticOutputParser,便于理解结构化输出的底层流程。

在新版 LangChain 中,部分模型还支持 with_structured_output(),Agent 也可以根据模型能力采用原生结构化输出或工具调用策略。但不管接口如何变化,核心思想仍然相同:先定义数据结构,再让模型按照结构返回,最后对结果进行验证。

七、总结

本文完成了两个由浅入深的课堂案例:

第一个案例 使用 CommaSeparatedListOutputParser,将模型返回的逗号分隔字符串转换成 Python 列表。

第二个案例 使用 Pydantic 定义 FilmInfo 数据结构,再通过 PydanticOutputParser 提取并验证电影名称、导演和题材。

最值得记住的是下面这条完整思路:

text 复制代码
定义目标结构
   ↓
生成格式说明(get_format_instructions)
   ↓
将格式说明加入 Prompt
   ↓
调用大模型
   ↓
解析并验证输出(output_parser.invoke)
   ↓
交给后续 Python 程序使用

学会 Output Parser 后,大模型不再只是返回一段供人阅读的文本,而是可以稳定地向程序提供列表、对象和字段明确的数据。这也是构建 RAG、Agent、信息抽取和自动化工作流的重要基础。

参考资料

版权说明

本文为原创技术文章,转载请注明出处。

文中代码均经过实际测试,可根据项目需求进行修改与扩展。

如文章存在疏漏或错误,欢迎在评论区交流讨论。

相关推荐
TunerT_TQ1 小时前
Valhalla 静态工程审阅 #009|Continue 源码证据驱动评测【大厂开源基础设施特辑】
vscode·测试工具·开源·llm·github·jetbrains·ai编程助手
TunerT_TQ1 小时前
Valhalla 静态工程审阅 #008|RisingWave 源码证据驱动评测【大厂开源基础设施特辑】
rust·开源·github·实时数据处理·流式数据库·apacheflink·streamingsql
netho02 小时前
影刀rpa证书题库使用教学
运维·服务器·rpa
无足鸟ICT2 小时前
【RHCA+】$[]
linux·运维·服务器
运维技术小记3 小时前
国产化环境配置 VNC 远程桌面:麒麟 V10 实战
linux·运维·服务器
爱码少年3 小时前
一条Shell命令实现文件目录服务器间快速转移
服务器·shell
爱笑鱼3 小时前
Binder(八):远端进程死了,BinderProxy 为什么还能收到通知?
android
Android-Flutter3 小时前
Kotlin 冷流与热流详解
android·kotlin
laboratory agent开发3 小时前
智能体多工具串联执行中途失败,部分写入的数据如何回滚
运维·服务器·数据库