4. 输出解析器(Output Parsers):让LLM的输出变得可控

说实话,刚接触LangChain那会儿,我最头疼的就是LLM的输出。你想想看,大模型吐出来的东西,有时候是纯文本,有时候带点Markdown,有时候又给你来段JSON——格式五花八门,根本没法直接给下游用。

输出解析器就是来解决这个问题的。它能把LLM的原始输出,转成我们程序能直接处理的结构化数据。说白了,就是给大模型的输出套上缰绳。

核心思路: 定义好你想要的输出格式 → 让LLM按格式生成 → 解析器帮你转成Python对象

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
我的经验: 给Field加description特别重要。LLM会根据描述来生成内容,描述越清晰,输出越准确。我曾经因为description写得太模糊,结果LLM把评分格式搞成了"8.5/10",解析直接报错。

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': ['打篮球', '看电影']}
踩坑提醒: 我曾经遇到过一个坑——LLM生成的JSON里,age字段是字符串"28"而不是整数28。虽然Python能隐式转换,但如果你下游有类型校验,就会报错。解决办法是在ResponseSchema里指定type为"integer",或者在提示词里强调「不要加引号」。

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', '城市': '北京'}, ...]
设计思路: 自定义解析器要继承BaseOutputParser,实现parse和get_format_instructions两个方法。前者做解析逻辑,后者告诉LLM应该按什么格式输出。这两者必须匹配,否则解析会失败。

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 知识体系总览

下面这张图总结了本章的核心内容,你可以看到不同解析器的定位和关系:

LangChain 输出解析器体系 输出解析器 Pydantic解析器 类型安全,复杂结构 列表解析器 逗号/换行分隔 JSON解析器 灵活字典输出 自定义解析器 继承BaseOutputParser 错误处理 OutputFixingParser 选择建议 结构固定 → Pydantic | 简单列表 → 列表解析器 | 灵活字典 → JSON | 特殊格式 → 自定义 所有场景都要搭配错误处理,LLM输出不可100%信任

4.7 实战对比:选哪个?

场景 推荐解析器 理由
提取结构化信息(姓名、年龄、地址) Pydantic解析器 类型安全,支持嵌套,可复用
生成标签/关键词列表 列表解析器 轻量,一行代码搞定
API返回数据,需要直接转字典 JSON解析器 灵活,不需要定义模型
解析Markdown表格/特殊格式 自定义解析器 完全掌控解析逻辑
生产环境,需要高可靠性 任意解析器 + OutputFixingParser 自动修复格式错误
重要提醒: 不管用哪种解析器,永远不要假设LLM的输出100%符合预期。我在一个金融项目中就吃过亏——LLM偶尔会把金额字段输出成"1,234.56"带逗号的格式,导致下游系统解析失败。从那以后,我所有解析器都加了异常处理和重试逻辑。

好了,这一章的内容就这些。输出解析器看似简单,但用好了能大幅提升应用的稳定性。记住一个原则:对LLM的输出永远保持怀疑,用解析器来建立信任。