纲要
- 容错机制
- 自动重试解析器
RetryOutputParser - 修复解析器
OutputFixingParser
- 自动重试解析器
- 自定义解析器
- 案例一:纳美星语转换解析器(基于正则)
- 案例二:布尔值解析器(继承
BaseOutputParser)
- 完整可运行代码:模拟错误场景,展示容错修复与自定义解析全流程
引言
在实际的 LLM 应用开发中,输出解析器(Output Parser)承担着将大模型自然语言输出转换为结构化数据的关键职责。然而,由于模型生成过程的随机性(受 temperature 等采样参数影响),模型输出往往存在字段缺失、格式错乱等问题,导致解析失败。
LangChain 原生提供了两类容错机制来应对这一挑战:自动重试(Retry) 与 修复(Fixing)。当内置解析器无法满足特定业务需求时,开发者还可以通过继承基类实现完全自定义的解析逻辑。本文将结合可运行的代码示例,系统性地介绍这些能力的实现原理与最佳实践。
输出解析器的容错机制
自动重试:RetryOutputParser
当解析失败时,RetryOutputParser 会将原始提示词(Prompt)、模型错误输出以及捕获的异常信息一并重新提交给大模型,让模型在理解错误原因的基础上重新生成符合格式要求的回答。
其核心设计思路为:将解析异常作为反馈信号(Feedback Signal),引导模型进行自我修正,而非简单重复调用。
修复解析器:OutputFixingParser
与重试策略不同,OutputFixingParser 不依赖原始提示词,而是直接使用另一个大模型实例对错误输出进行“修补”。它适用于输出结果已接近目标格式的场景,例如 JSON 结构中仅缺失一个闭合花括号或引号。
两者的关键差异如下:
| 特性 | RetryOutputParser | OutputFixingParser |
|---|---|---|
| 输入依赖 | 原始提示词 + 错误输出 + 异常信息 | 错误输出 + 原解析器实例 |
| 模型消耗 | 较高(需重新理解完整上下文) | 较低(仅做格式修补) |
| 适用场景 | 输出内容与目标格式严重偏离 | 输出结构基本正确,仅有细微语法瑕疵 |
自定义解析器实战
纳美星语解析器
受电影《阿凡达》中纳美语(Na’vi)的启发,本节构建一个基于正则表达式的趣味解析器。该解析器无需调用任何模型,完全由规则驱动,可作为转换链(Chain)中的一个轻量级节点。
布尔值解析器
通过继承 BaseOutputParser 并实现泛型参数 bool,构建一个健壮的布尔值解析器。该解析器能够将 “yes/no”、“true/false”、“1/0” 等常见自然语言表达统一转换为 Python 布尔类型,并支持通过构造函数注入自定义真值映射表。
完整可运行代码
以下示例整合了容错机制与两类自定义解析器。代码采用 FakeListChatModel 模拟大模型的标准响应或错误响应,因此无需配置任何 API Key 即可直接运行。
环境依赖
pip install langchain langchain-core langchain-community pydantic
代码实现
from typing import Any, Dict, List
from pydantic import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import (
BaseOutputParser,
PydanticOutputParser,
OutputFixingParser,
)
from langchain_core.language_models import BaseChatModel
from langchain_community.chat_models.fake import FakeListChatModel
from langchain.output_parsers.retry import RetryOutputParser
from langchain_core.messages import AIMessage
# ===================== 1. 定义数据模型 =====================
class Action(BaseModel):
action: str = Field(description="下一步行动名称")
action_input: str = Field(description="行动所需的输入")
# ===================== 2. 模拟错误的模型响应 =====================
# 正确的 JSON 格式
correct_json = '{"action": "search", "action_input": "北京天气"}'
# 错误响应:缺少 action_input 字段
bad_json = '{"action": "search"}'
# 用于修复解析器的假模型(返回正确结果)
fix_model = FakeListChatModel(responses=[correct_json])
# 用于重试解析器的假模型(返回正确结果)
retry_model = FakeListChatModel(responses=[correct_json])
# 用于原始解析的模型(返回错误响应)
base_model = FakeListChatModel(responses=[bad_json])
# ===================== 3. 构建解析器与提示词 =====================
parser = PydanticOutputParser(pydantic_object=Action)
format_instructions = parser.get_format_instructions()
prompt = ChatPromptTemplate.from_template(
"根据用户的问题制定下一步计划。\n{format_instructions}\n问题:{question}"
)
prompt = prompt.partial(format_instructions=format_instructions)
# ===================== 4. 演示解析失败 =====================
print("=== 1. 无容错的原始解析(预期失败) ===")
try:
bad_chain = prompt | base_model | parser
result = bad_chain.invoke({"question": "今天北京天气怎么样?"})
print("解析成功:", result)
except Exception as e:
print("解析失败,错误信息:", str(e)[:100], "...")
# ===================== 5. 使用 OutputFixingParser 修复 =====================
print("\n=== 2. OutputFixingParser 修复示例 ===")
fixing_parser = OutputFixingParser.from_llm(
parser=parser,
llm=fix_model,
max_retries=1
)
# 手动构造一个 AIMessage 模拟错误输出
bad_message = AIMessage(content=bad_json)
try:
fixed_result = fixing_parser.parse(bad_message.content)
print("修复后解析成功:", fixed_result)
except Exception as e:
print("修复失败:", e)
# ===================== 6. 使用 RetryOutputParser 重试 =====================
print("\n=== 3. RetryOutputParser 重试示例 ===")
retry_parser = RetryOutputParser.from_llm(
parser=parser,
llm=retry_model,
max_retries=2
)
# 需要提供原始提示词值和错误的输出
prompt_value = prompt.invoke({"question": "今天北京天气怎么样?"})
try:
retry_result = retry_parser.parse_with_prompt(bad_message.content, prompt_value)
print("重试后解析成功:", retry_result)
except Exception as e:
print("重试失败:", e)
# ===================== 7. 自定义解析器:纳美星语转换 =====================
import re
def navi_transformer(text: str) -> str:
"""将英文转换为带有纳美语特征的文本"""
# 常见问候替换
text = re.sub(r'\bhello\b', 'Kaltxì', text, flags=re.IGNORECASE)
text = re.sub(r'\bthank you\b', 'Irayo', text, flags=re.IGNORECASE)
# 形容词加前缀 le-
text = re.sub(r'\b(beautiful|pretty|great)\b', r'le-\1', text, flags=re.IGNORECASE)
# 动词加语气后缀 -ti
text = re.sub(r'\b(go|come|see|help)\b', r'\1-ti', text, flags=re.IGNORECASE)
# 句首添加特征前缀 Tsa'u
if not text.startswith('Tsa\'u'):
text = 'Tsa\'u ' + text
return text
class NaviOutputParser(BaseOutputParser[str]):
"""自定义纳美星语解析器"""
def parse(self, text: str) -> str:
# 假设模型输出为纯文本,直接进行规则转换
return navi_transformer(text)
def get_format_instructions(self) -> str:
return "请输出一段简短的英文问候或陈述。"
@property
def _type(self) -> str:
return "navi_parser"
print("\n=== 4. 纳美星语解析器 ===")
navi_parser = NaviOutputParser()
# 模拟模型的英文输出
english_response = "Hello, how can I help you? You are beautiful."
navi_response = navi_parser.parse(english_response)
print("英文输入:", english_response)
print("纳美语输出:", navi_response)
# ===================== 8. 自定义布尔值解析器 =====================
class BooleanOutputParser(BaseOutputParser[bool]):
"""将自然语言 yes/no 转换为布尔值"""
true_values: List[str] = ["yes", "true", "ok", "1"]
false_values: List[str] = ["no", "false", "nok", "0"]
def parse(self, text: str) -> bool:
clean = text.strip().lower()
if clean in self.true_values:
return True
elif clean in self.false_values:
return False
else:
raise ValueError(f"无法识别的布尔值: {text},仅支持 {self.true_values + self.false_values}")
def get_format_instructions(self) -> str:
return f"请仅返回以下单词之一:{', '.join(self.true_values + self.false_values)}"
@property
def _type(self) -> str:
return "boolean_parser"
print("\n=== 5. 布尔值解析器 ===")
bool_parser = BooleanOutputParser()
print("输入 'yes' ->", bool_parser.parse("yes"))
print("输入 'No' ->", bool_parser.parse("No"))
# 自定义真值映射
custom_bool_parser = BooleanOutputParser(true_values=["go"], false_values=["stop"])
print("输入 'go' ->", custom_bool_parser.parse("go"))
try:
custom_bool_parser.parse("maybe")
except ValueError as e:
print("输入 'maybe' 触发错误:", e)
运行结果解读
- 无容错解析:直接抛出
ValidationError,原因是模型输出缺少必需的action_input字段。 - 修复解析:
OutputFixingParser调用辅助模型将{"action": "search"}补全为{"action": "search", "action_input": "北京天气"},解析成功。 - 重试解析:
RetryOutputParser将错误输出与原始提示词一并提交给模型,模型重新生成了符合Pydantic模式的结果。 - 纳美星语解析器:通过正则规则完成趣味转换,不依赖任何外部服务或模型。
- 布尔值解析器:完成了自然语言到布尔值的映射,支持自定义真/假值列表,异常输入会明确抛出
ValueError。
API 速览
| API 组件 | 所属模块 | 方法签名 | 说明 |
|---|---|---|---|
RetryOutputParser | langchain.output_parsers.retry | from_llm(parser, llm, max_retries) | 基于重试策略的容错解析器,需要原始提示词上下文 |
OutputFixingParser | langchain_core.output_parsers | from_llm(parser, llm, max_retries) | 基于修复策略的容错解析器,仅需错误输出 |
BaseOutputParser | langchain_core.output_parsers | parse(text) | 自定义解析器的抽象基类,需实现 parse 与 get_format_instructions |
PydanticOutputParser | langchain_core.output_parsers | pydantic_object | 将模型输出解析为 Pydantic 模型实例 |
FakeListChatModel | langchain_community.chat_models.fake | responses | 用于测试的模拟模型,按顺序返回预设响应 |
Demo 示例
本博客的完整代码已内嵌于上文“完整可运行代码”章节。如需独立运行:
- 将代码块内容保存为
parser_demo.py。 - 在虚拟环境中执行
pip install langchain langchain-core langchain-community pydantic。 - 运行
python parser_demo.py。
该 Demo 演示了以下核心技术点:
- 基于
FakeListChatModel的零成本模型模拟。 - 两种容错解析器的构造与调用差异。
- 自定义解析器的两种实现路径(函数式正则与面向对象继承)。
- 解析异常的统一捕获与错误反馈机制。
最佳实践与注意事项
- 重试次数:容错机制会额外消耗模型调用额度,生产环境中应合理设置
max_retries(通常为 1~2 次),避免因单次失败导致请求堆积。 - 策略选择:若输出与目标格式偏差较大(如内容完全不符),优先使用
RetryOutputParser;若输出主体正确但存在格式瑕疵,OutputFixingParser更为高效。 - 自定义解析器:建议在实现
parse方法的同时,重写get_format_instructions以向模型明确输出格式要求,减少解析失败概率。 - 国际化支持:布尔值解析器的真/假值列表可通过配置文件动态注入,便于适配多语言场景。
参考文档
官方文档
- LangChain Output Parsers 官方指南:https://python.langchain.com/docs/modules/model_io/output_parsers/
- LangChain Core API 参考:https://api.python.langchain.com/en/latest/core_api.html
- Pydantic 模型文档:https://docs.pydantic.dev/
参考链接
- LangChain 社区文档中的
RetryOutputParser与OutputFixingParser源码:https://github.com/langchain-ai/langchain/tree/master/libs/core/langchain_core/output_parsers - 纳美语(Na’vi)语言参考:https://learnnavi.org/
总结
本文系统性地介绍了 LangChain 输出解析器的两种容错机制——RetryOutputParser 与 OutputFixingParser,并深入剖析了它们的适用场景与性能差异。
同时,通过纳美星语解析器和布尔值解析器两个实战案例,展示了从正则规则驱动到面向对象继承的自定义解析器构建路径。
这些技术组件共同构成了 LLM 应用中结构化输出处理的健壮基石,为开发者应对模型输出的不确定性提供了灵活而可靠的解决方案。
403



被折叠的 条评论
为什么被折叠?



