Skip to main content
Open In ColabOpen on GitHub

如何创建自定义输出解析器

在某些情况下,您可能需要实现一个自定义 解析器,将模型输出结构化为自定义格式。

有两种方式实现自定义解析器:

  1. LCEL 中使用 RunnableLambdaRunnableGenerator —— 我们强烈建议大多数用例采用此方式
  2. 通过继承我们解析的基类之一——这是做事情的困难方式

这两种方法之间的差异主要是表面上的,主要在于触发的回调(例如,on_chain_starton_parser_start),以及可运行的 lambda 函数与解析器在类似 LangSmith 的追踪平台中如何被可视化。

可运行 Lambdas 和生成器

推荐的解析方式是使用 可运行 lambda可运行生成器

在这里,我们将创建一个简单的解析器,用于反转模型输出的大小写。

例如,如果模型输出:"Meow",解析器将生成 "mEOW"。

from typing import Iterable

from langchain_anthropic.chat_models import ChatAnthropic
from langchain_core.messages import AIMessage, AIMessageChunk

model = ChatAnthropic(model_name="claude-2.1")


def parse(ai_message: AIMessage) -> str:
"""Parse the AI message."""
return ai_message.content.swapcase()


chain = model | parse
chain.invoke("hello")
'hELLO!'
提示

LCEL 在使用 | 语法进行组合时,会自动将函数 parse 升级为 RunnableLambda(parse)

如果您不喜欢这样,可以手动导入 RunnableLambda 然后运行parse = RunnableLambda(parse)

流式处理有效吗?

for chunk in chain.stream("tell me about yourself in one sentence"):
print(chunk, end="|", flush=True)
i'M cLAUDE, AN ai ASSISTANT CREATED BY aNTHROPIC TO BE HELPFUL, HARMLESS, AND HONEST.|

不,它不会这样,因为解析器在解析输出之前会先聚合输入。

如果我们想实现一个流式解析器,可以让解析器接受输入的可迭代对象,并在结果可用时立即返回它们。

from langchain_core.runnables import RunnableGenerator


def streaming_parse(chunks: Iterable[AIMessageChunk]) -> Iterable[str]:
for chunk in chunks:
yield chunk.content.swapcase()


streaming_parse = RunnableGenerator(streaming_parse)
重要

请将流式解析器包装在 RunnableGenerator 中,因为我们可能会自动停止使用 | 语法对其进行升级。

chain = model | streaming_parse
chain.invoke("hello")
'hELLO!'

让我们确认流式传输是否有效!

for chunk in chain.stream("tell me about yourself in one sentence"):
print(chunk, end="|", flush=True)
i|'M| cLAUDE|,| AN| ai| ASSISTANT| CREATED| BY| aN|THROP|IC| TO| BE| HELPFUL|,| HARMLESS|,| AND| HONEST|.|

继承解析基类

实现解析器的另一种方法是继承 BaseOutputParserBaseGenerationOutputParser 或另一个基础解析器,具体取决于您的需求。

通常,我们推荐大多数用例采用这种方法,因为它会导致需要编写更多代码,而带来的收益却并不显著。

最简单的输出解析器扩展了 BaseOutputParser 类,并必须实现以下方法:

  • parse: 接收模型的字符串输出并对其进行解析
  • (可选) _type: 标识解析器的名称。

当聊天模型或LLM的输出格式不正确时,OutputParserException可能会被抛出以表明解析因输入错误而失败。使用此异常允许利用解析器的代码以一致的方式处理异常。

解析器也是可运行对象!🏃

因为 BaseOutputParser 实现了 Runnable 接口,所以您以此方式创建的任意自定义解析器都将成为有效的 LangChain Runnables,并受益于自动异步支持、批处理接口、日志支持等功能。

简单解析器

这是一个简单的解析器,它可以解析布尔值的字符串表示形式(例如YESNO),并将其转换为对应的boolean类型。

from langchain_core.exceptions import OutputParserException
from langchain_core.output_parsers import BaseOutputParser


# The [bool] desribes a parameterization of a generic.
# It's basically indicating what the return type of parse is
# in this case the return type is either True or False
class BooleanOutputParser(BaseOutputParser[bool]):
"""Custom boolean parser."""

true_val: str = "YES"
false_val: str = "NO"

def parse(self, text: str) -> bool:
cleaned_text = text.strip().upper()
if cleaned_text not in (self.true_val.upper(), self.false_val.upper()):
raise OutputParserException(
f"BooleanOutputParser expected output value to either be "
f"{self.true_val} or {self.false_val} (case-insensitive). "
f"Received {cleaned_text}."
)
return cleaned_text == self.true_val.upper()

@property
def _type(self) -> str:
return "boolean_output_parser"
parser = BooleanOutputParser()
parser.invoke("YES")
True
try:
parser.invoke("MEOW")
except Exception as e:
print(f"Triggered an exception of type: {type(e)}")
Triggered an exception of type: <class 'langchain_core.exceptions.OutputParserException'>

让我们测试参数化的更改

parser = BooleanOutputParser(true_val="OKAY")
parser.invoke("OKAY")
True

让我们确认其他 LCEL 方法是否存在

parser.batch(["OKAY", "NO"])
[True, False]
await parser.abatch(["OKAY", "NO"])
[True, False]
from langchain_anthropic.chat_models import ChatAnthropic

anthropic = ChatAnthropic(model_name="claude-2.1")
anthropic.invoke("say OKAY or NO")
API 参考:ChatAnthropic
AIMessage(content='OKAY')

让我们测试一下我们的解析器是否有效!

chain = anthropic | parser
chain.invoke("say OKAY or NO")
True
注意

解析器将适用于大语言模型(LLM)的输出(一个字符串)或聊天模型的输出(一个AIMessage)!

解析原始模型输出

有时除了原始文本外,模型输出中还包含其他重要的元数据。一个例子是工具调用(tool calling),其中传递给被调用函数的参数会返回到一个单独的属性中。如果你需要这种更细粒度的控制,你可以转而继承 BaseGenerationOutputParser 类。

此类需要一个方法 parse_result。该方法接收原始模型输出(例如,GenerationChatGeneration 的列表)并返回解析后的输出。

同时支持 GenerationChatGeneration 使得解析器能够与常规大语言模型以及聊天模型配合工作。

from typing import List

from langchain_core.exceptions import OutputParserException
from langchain_core.messages import AIMessage
from langchain_core.output_parsers import BaseGenerationOutputParser
from langchain_core.outputs import ChatGeneration, Generation


class StrInvertCase(BaseGenerationOutputParser[str]):
"""An example parser that inverts the case of the characters in the message.

This is an example parse shown just for demonstration purposes and to keep
the example as simple as possible.
"""

def parse_result(self, result: List[Generation], *, partial: bool = False) -> str:
"""Parse a list of model Generations into a specific format.

Args:
result: A list of Generations to be parsed. The Generations are assumed
to be different candidate outputs for a single model input.
Many parsers assume that only a single generation is passed it in.
We will assert for that
partial: Whether to allow partial results. This is used for parsers
that support streaming
"""
if len(result) != 1:
raise NotImplementedError(
"This output parser can only be used with a single generation."
)
generation = result[0]
if not isinstance(generation, ChatGeneration):
# Say that this one only works with chat generations
raise OutputParserException(
"This output parser can only be used with a chat generation."
)
return generation.message.content.swapcase()


chain = anthropic | StrInvertCase()

让我们使用新的解析器!它应该反转模型的输出。

chain.invoke("Tell me a short sentence about yourself")
'hELLO! mY NAME IS cLAUDE.'