Skip to main content
Open In ColabOpen on GitHub

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

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

有两种方法可以实现自定义解析器:

  1. 使用 LCEL 中的 RunnableLambdaRunnableGenerator —— 我们强烈推荐在大多数用例中使用这种方法
  2. 通过继承输出解析的基类之一 —— 这是比较“硬核”的做法

这两种方法之间的区别主要在于表面上,并且主要体现在触发的回调(例如 on_chain_starton_parser_start)以及在 LangSmith 等跟踪平台中如何可视化 runnable lambda 与解析器。

可运行的 Lambda 和生成器

推荐的解析方式是使用可运行的 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!'
tip

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)
API Reference:RunnableGenerator
important

请将流式解析器包装在 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 来表明由于输入错误导致解析失败。使用此异常可以使使用该解析器的代码以一致的方式处理异常。

解析器是 Runnables!🏃

由于 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 Reference:ChatAnthropic
AIMessage(content='OKAY')

让我们测试一下我们的解析器是否工作!

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

解析器可以处理来自 LLM(字符串)或来自聊天模型(AIMessage)的输出!

解析原始模型输出

有时,除了原始文本之外,模型输出还包含重要的附加元数据。一个例子是工具调用,其中用于传递给已调用函数的参数在单独的属性中返回。如果需要这种更细粒度的控制,可以改用 BaseGenerationOutputParser 类。

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

同时支持 GenerationChatGeneration 允许解析器同时用于常规 LLM 和聊天模型。

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.'