4. 输出解析器(Output Parsers):让LLM的输出变得可控
说实话,刚接触LangChain那会儿,我最头疼的就是LLM的输出。你想想看,大模型吐出来的东西,有时候是纯文本,有时候带点Markdown,有时候又给你来段JSON——格式五花八门,根本没法直接给下游用。
输出解析器就是来解决这个问题的。它能把LLM的原始输出,转成我们程序能直接处理的结构化数据。说白了,就是给大模型的输出套上缰绳。
4.1 Pydantic输出解析器:最常用的结构化方案
我个人习惯用Pydantic来定义输出结构。为什么?因为它自带类型校验,还能嵌套复杂对象。你在项目中定义好数据模型,解析器就能自动把LLM的输出映射过来。
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from langchain.prompts import PromptTemplate
# 定义输出结构
class MovieReview(BaseModel):
title: str = Field(description="电影名称")
rating: float = Field(description="评分,0-10分")
summary: str = Field(description="一句话总结")
pros: list[str] = Field(description="优点列表")
cons: list[str] = Field(description="缺点列表")
# 创建解析器
parser = PydanticOutputParser(pydantic_object=MovieReview)
# 构造提示词
prompt = PromptTemplate(
template="请分析这部电影:{movie}\n{format_instructions}",
input_variables=["movie"],
partial_variables={"format_instructions": parser.get_format_instructions()}
)
# 使用
chain = prompt | llm | parser
result = chain.invoke({"movie": "《肖申克的救赎》"})
print(result.title) # 直接访问属性
print(result.rating) # 类型已经是float
4.2 列表输出解析器:处理批量数据
有时候你只需要LLM生成一个列表,比如「列出5个Python Web框架」。这时候用Pydantic就有点杀鸡用牛刀了。列表输出解析器更轻量。
from langchain.output_parsers import CommaSeparatedListOutputParser
parser = CommaSeparatedListOutputParser()
prompt = PromptTemplate(
template="列出{count}个{category}:\n{format_instructions}",
input_variables=["count", "category"],
partial_variables={"format_instructions": parser.get_format_instructions()}
)
chain = prompt | llm | parser
result = chain.invoke({"count": 5, "category": "Python Web框架"})
# 输出:['Django', 'Flask', 'FastAPI', 'Tornado', 'Pyramid']
嗯,这里要注意:默认是按逗号分隔的。如果你的列表项本身包含逗号,比如「苹果, 香蕉, 橙子, 葡萄」,那解析就会出问题。我建议在提示词里明确告诉LLM用换行符分隔。
4.3 JSON输出解析器:灵活的数据交换
JSON输出解析器适合那些结构不太固定,或者你不想定义Pydantic模型的场景。它直接把LLM的输出转成Python字典。
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
# 定义响应模式
response_schemas = [
ResponseSchema(name="name", description="用户姓名"),
ResponseSchema(name="age", description="用户年龄,整数"),
ResponseSchema(name="hobbies", description="兴趣爱好列表"),
]
parser = StructuredOutputParser.from_response_schemas(response_schemas)
prompt = PromptTemplate(
template="根据描述提取用户信息:{description}\n{format_instructions}",
input_variables=["description"],
partial_variables={"format_instructions": parser.get_format_instructions()}
)
chain = prompt | llm | parser
result = chain.invoke({"description": "张三,28岁,喜欢打篮球和看电影"})
# 输出:{'name': '张三', 'age': 28, 'hobbies': ['打篮球', '看电影']}
4.4 自定义输出解析器:掌控一切
当内置解析器满足不了你的需求时,就该自己动手了。比如你需要解析LLM输出的Markdown表格,或者处理一些特殊的格式。
from langchain.output_parsers import BaseOutputParser
import re
class MarkdownTableParser(BaseOutputParser[list[dict]]):
"""解析Markdown表格为字典列表"""
def parse(self, text: str) -> list[dict]:
# 提取表格行
lines = text.strip().split('\n')
if len(lines) < 3:
raise ValueError("表格至少需要表头、分隔线和一行数据")
# 解析表头
headers = [h.strip() for h in lines[0].split('|')[1:-1]]
# 跳过分隔线(第2行)
# 解析数据行
result = []
for line in lines[2:]:
if '|' not in line:
continue
values = [v.strip() for v in line.split('|')[1:-1]]
if len(values) == len(headers):
result.append(dict(zip(headers, values)))
return result
def get_format_instructions(self) -> str:
return "请用Markdown表格格式输出,第一行为表头,第二行为分隔线"
# 使用
parser = MarkdownTableParser()
result = parser.parse("""
| 姓名 | 年龄 | 城市 |
|------|------|------|
| 张三 | 28 | 北京 |
| 李四 | 32 | 上海 |
""")
# 输出:[{'姓名': '张三', '年龄': '28', '城市': '北京'}, ...]
4.5 结构化输出与错误处理
LLM不是完美的,它偶尔会输出格式错误的内容。比如该输出JSON时,它给你来了一段散文。这时候就需要错误处理机制。
from langchain.output_parsers import OutputFixingParser
from langchain.llms import OpenAI
# 用另一个LLM来修复解析错误
fixing_parser = OutputFixingParser.from_llm(
parser=parser, # 原来的解析器
llm=OpenAI(model="gpt-3.5-turbo")
)
# 或者手动处理
try:
result = parser.parse(bad_output)
except Exception as e:
print(f"解析失败:{e}")
# 可以尝试重新生成,或者用默认值
result = {"name": "未知", "age": 0}
我个人建议的做法是:
- 第一次解析失败 → 用OutputFixingParser自动修复
- 修复后还是失败 → 记录日志,用默认值兜底
- 连续失败超过3次 → 通知人工介入
为什么会这样?因为LLM的输出质量受很多因素影响:模型版本、提示词质量、甚至当前服务器的负载。你不能指望它每次都完美输出。
4.6 知识体系总览
下面这张图总结了本章的核心内容,你可以看到不同解析器的定位和关系:
4.7 实战对比:选哪个?
| 场景 | 推荐解析器 | 理由 |
|---|---|---|
| 提取结构化信息(姓名、年龄、地址) | Pydantic解析器 | 类型安全,支持嵌套,可复用 |
| 生成标签/关键词列表 | 列表解析器 | 轻量,一行代码搞定 |
| API返回数据,需要直接转字典 | JSON解析器 | 灵活,不需要定义模型 |
| 解析Markdown表格/特殊格式 | 自定义解析器 | 完全掌控解析逻辑 |
| 生产环境,需要高可靠性 | 任意解析器 + OutputFixingParser | 自动修复格式错误 |
好了,这一章的内容就这些。输出解析器看似简单,但用好了能大幅提升应用的稳定性。记住一个原则:对LLM的输出永远保持怀疑,用解析器来建立信任。