Deep Read

LangChain - 输出解析器 (Output Parser)

一句话理解

大模型只会"说人话"——它返回的永远是一段文本字符串。但程序需要的是能直接用的数据(列表、字典、日期、对象)。输出解析器就是这两者之间的翻译官。

看个最直白的例子。你问模型"列出三个机器学习框架":

模型返回(字符串):  "TensorFlow, PyTorch, scikit-learn"

这段文本人一看就懂,但对程序来说它只是一坨字符:不能遍历、取不出第二个元素。你得自己写 split(",")、再逐个 strip() 去空格……

解析器帮你把这步自动做掉:

解析后(Python 列表):  ['TensorFlow', 'PyTorch', 'scikit-learn']

现在 result[0]len(result)for 循环都能直接用了。


它的两个职责:一前一后

解析器承担两个职责,放到调用模型的时间线上看就顺了:

①【调用模型之前】 get_format_instructions()
   在提示词里加一句:"请用逗号分隔来回答"
   目的:让模型按规定格式输出,否则它可能写成 "1. xx 2. xx"

          ↓  模型回答(仍是字符串)

②【拿到回答之后】 parse()
   把字符串翻译成 list / dict / datetime / 对象

关键在于:"约束格式(①)"是为了让"解析(②)"能成功——先告诉模型按什么格式说话,它说了,你再按那个格式翻译。这就是后文说的"职责闭环"。

在 LCEL 链式写法中,解析器放在最后,chain.invoke() 的返回值就直接是解析后的结果,无需手动调用 parse():

chain = prompt | llm | parser

并不是所有解析器都需要职责 ①:

  • 只解析、不约束格式:如 StrOutputParserJsonOutputParserCommaSeparatedListOutputParser,可以直接接在链尾。
  • 必须先约束格式:如 DatetimeOutputParserPydanticOutputParser,格式很严格,不先把 get_format_instructions() 注入 Prompt,模型就输出不出可被正确解析的内容。

StrOutputParser

最简单的解析器:从 AIMessage 中提取纯文本的 .content,返回字符串。用于"只要回答文本"的场景,也能让链的输出统一为 str 而不是 AIMessage 对象。

LangChain - 提示模板LangChain - 提示模板  为什么需要提示模板 直接给模型写死一段提示词有两个问题:① 提示词固定,无法根据不同输入灵活变化;② 复制粘贴拼字符串,难以复用和维护。 提示模板(Prompt Template) 就是带 {变量} 占位符的提示字符串。它接收原始用户输入,把变量填进去,生成一段「准备好发给模型」的提示词。好处是:清晰可读、可复用、易维护、参数化生成。 四种常见模板 | 模板 | 适用场景 | | --- | --- | | PromptTemplate | 单条字符串提示,最基础 | | ChatPromptTemplate | 多角色对话提示(system / human / ai),聊天模型首选 | | FewShotPromptTemplate | 给几个示例「教」模型怎么答 | | 部分填充 partial | 先固定一部分变量,其余后续再填(不是独立模板,是通用能力) | 一、PromptTemplate 最基础的字符串模板:一段含 {变量} 的文本,填充后得到完整提示词。 1. 两种用法对比 模板本身只负责「把变量填进字符串」。填好之后既可以手动调用模型,也 中的测试代码改为使用 StrOutputParser:

template = ChatPromptTemplate.from_template("请将{text}翻译成英文")

message = template.invoke({"text": "我是程序员"})
# 真正调用大模型,返回 AI 的回答(AIMessage)
aimessage = llm.invoke(message)

# 用解析器取出纯文本
parser = StrOutputParser()
result = parser.invoke(aimessage)
print(result)

等价的链式写法:

chain = template | llm | StrOutputParser()
print(chain.invoke({"text": "我是程序员"}))

CommaSeparatedListOutputParser

把模型返回的"逗号分隔文本"自动解析成 Python 的 list(列表),这正是开头那个例子。

基本用法

from langchain.output_parsers import CommaSeparatedListOutputParser

template = ChatPromptTemplate.from_messages(
    [("system", "你是一名专业的程序员"), ("user", "{input}")]
)

parser = CommaSeparatedListOutputParser()
chain = template | llm | parser

print(chain.invoke({"input": "列举三个常见的机器学习框架, 用逗号分隔"}))
# 输出: ['TensorFlow', 'PyTorch', 'scikit-learn']

更规范的做法:注入格式说明

上面靠在 prompt 里手写"用逗号分隔"并不稳妥。更可靠的方式是用 get_format_instructions() 把官方格式说明注入 Prompt(也就是职责 ①):

parser = CommaSeparatedListOutputParser()
print(parser.get_format_instructions())
# Your response should be a list of comma separated values, eg: `foo, bar, baz`

template = ChatPromptTemplate.from_messages([
    ("system", "你是一个助手。\n{format_instructions}"),
    ("user", "{input}"),
]).partial(format_instructions=parser.get_format_instructions())

chain = template | llm | parser

局限:它只是按逗号 split。若某个元素本身含逗号(如 "1,000"),会被错误拆分。这类结构化需求建议改用 JsonOutputParserPydanticOutputParser


JsonOutputParser

把模型返回的 JSON 文本解析为 Python 的 dict / list,是最常用的结构化输出方式。

from langchain_core.output_parsers import JsonOutputParser

template = ChatPromptTemplate.from_messages(
    [("system", "你是一名专业的程序员"), ("user", "{input}")]
)

parser = JsonOutputParser()
chain = template | llm | parser

result = chain.invoke(
    {"input": "langchain是什么? 问题用question 回答用ans 返回一个JSON格式"}
)

print(result)          # {'question': '...', 'ans': '...'}
print(type(result))    # <class 'dict'>

要点:

  • 容错性强:即使 JSON 被 ```json 代码块包裹也能正确提取。
  • 支持流式输出:streaming 时会逐步返回正在累积的、合法的部分 JSON。
  • 想约束字段结构时,可配合 Pydantic 模型:JsonOutputParser(pydantic_object=MyModel),再把 get_format_instructions() 注入 Prompt。

DatetimeOutputParser

把模型返回的时间字符串解析为 Python 的 datetime 对象。必须先注入格式说明,模型才知道按什么日期格式回答。

template = """
回答用户的问题:{question}

{format_instructions}
"""

output_parser = DatetimeOutputParser()

prompt = PromptTemplate.from_template(
    template,
    # partial 表示提前预置变量的值
    partial_variables={"format_instructions": output_parser.get_format_instructions()},
)

chain = prompt | client | output_parser
output = chain.invoke({"question": "新中国是什么时候成立的?"})

print(output)        # 1949-10-01 00:00:00
print(type(output))  # <class 'datetime.datetime'>

要点:get_format_instructions() 会要求模型严格按 %Y-%m-%dT%H:%M:%S.%fZ 这类格式输出;若不注入,模型可能返回"1949年10月1日"导致解析失败——这正是职责 ① 不可省略的原因。


PydanticOutputParser

把模型输出解析为一个强类型的 Pydantic 对象,带字段校验。是需要严格 schema、字段类型/约束时的首选。

from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class Book(BaseModel):
    title: str = Field(description="书名")
    author: str = Field(description="作者")
    year: int = Field(description="出版年份")

parser = PydanticOutputParser(pydantic_object=Book)

prompt = PromptTemplate.from_template(
    "提取书籍信息:{query}\n{format_instructions}",
    partial_variables={"format_instructions": parser.get_format_instructions()},
)

chain = prompt | llm | parser
book = chain.invoke({"query": "《活着》是余华1993年的作品"})
print(book.title, book.author, book.year)  # 活着 余华 1993
print(type(book))                          # <class '__main__.Book'>

要点:解析失败时(模型没按 schema 输出)会抛异常,可配合 OutputFixingParser 自动让模型修正后重新解析。


解析器对比

解析器返回类型是否需注入格式说明适用场景
StrOutputParserstr只要纯文本回答
CommaSeparatedListOutputParserlist[str]建议简单列表
JsonOutputParserdict / list可选结构化数据、流式输出
DatetimeOutputParserdatetime日期时间
PydanticOutputParserPydantic 对象强类型 + 字段校验
XMLOutputParserdict可选XML 格式输出

get_format_instructions() 统一机制

绝大多数解析器都实现了 get_format_instructions(),这是 LangChain 的统一约定,也就是前面说的职责 ①:

  • 它返回一段给模型看的格式说明文本;
  • 通过 partial_variables.partial() 注入 Prompt;
  • 模型据此输出符合格式的文本,再由同一个解析器 parse() 还原成数据。
prompt = PromptTemplate.from_template(
    template,
    partial_variables={"format_instructions": parser.get_format_instructions()},
)

这种"格式说明 + 解析"成对出现的设计,使解析器既约束输入又负责输出,职责闭环。


content_blocks:跨模型的标准化内容块

content_blocks 在 LangChain 层把各模型提供商异构的 API 响应统一转换成标准化的内容块,让上层应用代码与底层具体模型实现解耦,无需为每个模型编写定制化的解析代码——也适用于工具调用结果。

from langchain_community.chat_models import ChatTongyi
from models import get_ali_model_client

qwen_model = ChatTongyi(model_name="qwen-max")
deepseek_model = get_ali_model_client()

qwen_response = qwen_model.invoke("你是谁")
deepseek_response = deepseek_model.invoke("你是谁")

# 不同模型,统一的解析方式
print(qwen_response.content_blocks)
print(deepseek_response.content_blocks)