LangChain 中的输出解析器
输出解析器是一种专用于处理和构建语言模型响应的类。
一个基本的输出解析器类通常需要实现两个核心方法:
- get_format_instructions:这个方法需要返回一个字符串,用于指导如何格式化语言模型的输出,告诉它应该如何组织并构建它的回答。
- parse:这个方法接收一个字符串(也就是语言模型的输出)并将其解析为特定的数据结构或格式。这一步通常用于确保模型的输出符合我们的预期,并且能够以我们需要的形式进行后续处理。
还有一个可选的方法:
- parse_with_prompt:这个方法接收一个字符串(也就是语言模型的输出)和一个提示(用于生成这个输出的提示),并将其解析为特定的数据结构。这样,你可以根据原始提示来修正或重新解析模型的输出,确保输出的信息更加准确和贴合要求。
各种输出解析器:
- 列表解析器(List Parser):这个解析器用于处理模型生成的输出,当需要模型的输出是一个列表的时候使用。例如,如果你询问模型“列出所有鲜花的库存”,模型的回答应该是一个列表。
- 日期时间解析器(Datetime Parser):这个解析器用于处理日期和时间相关的输出,确保模型的输出是正确的日期或时间格式。
- 枚举解析器(Enum Parser):这个解析器用于处理预定义的一组值,当模型的输出应该是这组预定义值之一时使用。例如,如果你定义了一个问题的答案只能是“是”或“否”,那么枚举解析器可以确保模型的回答是这两个选项之一。
- 结构化输出解析器(Structured Output Parser):这个解析器用于处理复杂的、结构化的输出。如果你的应用需要模型生成具有特定结构的复杂回答(例如一份报告、一篇文章等),那么可以使用结构化输出解析器来实现。
- Pydantic(JSON)解析器:这个解析器用于处理模型的输出,当模型的输出应该是一个符合特定格式的JSON对象时使用。它使用Pydantic库,这是一个数据验证库,可以用于构建复杂的数据模型,并确保模型的输出符合预期的数据模型。
- 自动修复解析器(Auto-Fixing Parser):这个解析器可以自动修复某些常见的模型输出错误。例如,如果模型的输出应该是一段文本,但是模型返回了一段包含语法或拼写错误的文本,自动修复解析器可以自动纠正这些错误。
- 重试解析器(RetryWithErrorOutputParser):这个解析器用于在模型的初次输出不符合预期时,尝试修复或重新生成新的输出。例如,如果模型的输出应该是一个日期,但是模型返回了一个字符串,那么重试解析器可以重新提示模型生成正确的日期格式。
Pydantic (JSON) 解析器
1. 创建模型实例
# ------Part 1
# 设置OpenAI API密钥
import os
os.environ["OPENAI_API_KEY"] = '你的OpenAI API Key'
# 创建模型实例
from langchain import OpenAI
model = OpenAI(model_name='gpt-3.5-turbo-instruct')
2. 定义输出数据的格式
# ------Part 2
# 创建一个空的DataFrame用于存储结果
import pandas as pd
df = pd.DataFrame(columns=["flower_type", "price", "description", "reason"])
# 数据准备
flowers = ["玫瑰", "百合", "康乃馨"]
prices = ["50", "30", "20"]
# 定义我们想要接收的数据格式
from pydantic import BaseModel, Field
class FlowerDescription(BaseModel):
flower_type: str = Field(description="鲜花的种类")
price: int = Field(description="鲜花的价格")
description: str = Field(description="鲜花的描述文案")
reason: str = Field(description="为什么要这样写这个文案")
Pydantic有这样几个特点。
- 数据验证:当你向Pydantic类赋值时,它会自动进行数据验证。例如,如果你创建了一个字段需要是整数,但试图向它赋予一个字符串,Pydantic会引发异常。
- 数据转换:Pydantic不仅进行数据验证,还可以进行数据转换。例如,如果你有一个需要整数的字段,但你提供了一个可以转换为整数的字符串,如
"42",Pydantic会自动将这个字符串转换为整数42。 - 易于使用:创建一个Pydantic类就像定义一个普通的Python类一样简单。只需要使用Python的类型注解功能,即可在类定义中指定每个字段的类型。
- JSON支持:Pydantic类可以很容易地从JSON数据创建,并可以将类的数据转换为JSON格式。
3. 创建输出解析器
# ------Part 3
# 创建输出解析器
from langchain.output_parsers import PydanticOutputParser
output_parser = PydanticOutputParser(pydantic_object=FlowerDescription)
# 获取输出格式指示
format_instructions = output_parser.get_format_instructions()
# 打印提示
print("输出格式:",format_instructions)
输出:
输出格式: The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
Here is the output schema:
{"properties": {"flower_type": {"title": "Flower Type", "description": "\u9c9c\u82b1\u7684\u79cd\u7c7b", "type": "string"}, "price": {"title": "Price", "description": "\u9c9c\u82b1\u7684\u4ef7\u683c", "type": "integer"}, "description": {"title": "Description", "description": "\u9c9c\u82b1\u7684\u63cf\u8ff0\u6587\u6848", "type": "string"}, "reason": {"title": "Reason", "description": "\u4e3a\u4ec0\u4e48\u8981\u8fd9\u6837\u5199\u8fd9\u4e2a\u6587\u6848", "type": "string"}}, "required": ["flower_type", "price", "description", "reason"]}
4. 创建提示模版
# ------Part 4
# 创建提示模板
from langchain import PromptTemplate
prompt_template = """您是一位专业的鲜花店文案撰写员。
对于售价为 {price} 元的 {flower} ,您能提供一个吸引人的简短中文描述吗?
{format_instructions}"""
# 根据模板创建提示,同时在提示中加入输出解析器的说明
prompt = PromptTemplate.from_template(prompt_template,
partial_variables={"format_instructions": format_instructions})
# 打印提示
print("提示:", prompt)
输出:
提示:
input_variables=['flower', 'price']
output_parser=None
partial_variables={'format_instructions': 'The output should be formatted as a JSON instance that conforms to the JSON schema below.\n\n
As an example, for the schema {
"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}},
"required": ["foo"]}}\n
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema.
The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.\n\n
Here is the output schema:\n```\n
{"properties": {
"flower_type": {"title": "Flower Type", "description": "\\u9c9c\\u82b1\\u7684\\u79cd\\u7c7b", "type": "string"},
"price": {"title": "Price", "description": "\\u9c9c\\u82b1\\u7684\\u4ef7\\u683c", "type": "integer"},
"description": {"title": "Description", "description": "\\u9c9c\\u82b1\\u7684\\u63cf\\u8ff0\\u6587\\u6848", "type": "string"},
"reason": {"title": "Reason", "description": "\\u4e3a\\u4ec0\\u4e48\\u8981\\u8fd9\\u6837\\u5199\\u8fd9\\u4e2a\\u6587\\u6848", "type": "string"}},
"required": ["flower_type", "price", "description", "reason"]}\n```'}
template='您是一位专业的鲜花店文案撰写员。
\n对于售价为 {price} 元的 {flower} ,您能提供一个吸引人的简短中文描述吗?\n
{format_instructions}'
template_format='f-string'
validate_template=True
input_variables=['flower', 'price']:这是一个包含你想要在模板中使用的输入变量的列表。我们在模板中使用了'flower'和'price'两个变量,后面我们会用具体的值(如玫瑰、20元)来替换这两个变量。output_parser=None:这是你可以选择在模板中使用的一个输出解析器。在此例中,我们并没有选择在模板中使用输出解析器,而是在模型外部进行输出解析,所以这里是None。partial_variables:包含了你想要在模板中使用,但在生成模板时无法立即提供的变量。在这里,我们通过'format_instructions'传入输出格式的详细说明。template:这是模板字符串本身。它包含了你想要模型生成的文本的结构。在此例中,模板字符串是你询问鲜花描述的问题,以及关于输出格式的说明。template_format='f-string':这是一个表示模板字符串格式的选项。此处是f-string格式。validate_template=True:表示是否在创建模板时检查模板的有效性。这里选择了在创建模板时进行检查,以确保模板是有效的。
5. 生成提示,传入模型并解析输出
# ------Part 5
for flower, price in zip(flowers, prices):
# 根据提示准备模型的输入
input = prompt.format(flower=flower, price=price)
# 打印提示
print("提示:", input)
# 获取模型的输出
output = model(input)
# 解析模型的输出
parsed_output = output_parser.parse(output)
parsed_output_dict = parsed_output.dict() # 将Pydantic格式转换为字典
# 将解析后的输出添加到DataFrame中
df.loc[len(df)] = parsed_output.dict()
# 打印字典
print("输出的数据:", df.to_dict(orient='records'))
输出:
输出的数据: [{'flower_type': '玫瑰', 'price': 50, 'description': '50 元的玫瑰,绽放着浪漫与热情,是爱情与美好的象征。', 'reason': '突出玫瑰的浪漫和美好,以及价格的亲民,吸引顾客购买。'}, {'flower_type': '百合', 'price': 30, 'description': '纯洁高雅的百合,30 元一束,为你的生活增添一份清新与美好。', 'reason': '突出百合的纯洁高雅特质,强调价格实惠,吸引顾客购买。'}, {'flower_type': '康乃馨', 'price': 20, 'description': '温馨康乃馨,20 元一束,传递爱与关怀。', 'reason': '突出康乃馨的温馨特质,强调价格实惠,吸引顾客购买。'}]
自动修复解析器(OutputFixingParser)
先设计一个解析时出现的错误:
# 导入所需要的库和模块
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List
# 使用Pydantic创建一个数据格式,表示花
class Flower(BaseModel):
name: str = Field(description="name of a flower")
colors: List[str] = Field(description="the colors of this flower")
# 定义一个用于获取某种花的颜色列表的查询
flower_query = "Generate the charaters for a random flower."
# 定义一个格式不正确的输出
misformatted = "{'name': '康乃馨', 'colors': ['粉红色','白色','红色','紫色','黄色']}"
# 创建一个用于解析输出的Pydantic解析器,此处希望解析为Flower格式
parser = PydanticOutputParser(pydantic_object=Flower)
# 使用Pydantic解析器解析不正确的输出
parser.parse(misformatted)
程序尝试用PydanticOutputParser来解析JSON字符串时,Python期望属性名称被双引号包围,但在给定的JSON字符串中是单引号。错误如下:
langchain.schema.output_parser.OutputParserException: Failed to parse Flower from completion {'name': '康乃馨', 'colors': ['粉红色','白色']}. Got: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
使用 OutputFixingParser 自动修正错误:
# 从langchain库导入所需的模块
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import OutputFixingParser
# 设置OpenAI API密钥
import os
os.environ["OPENAI_API_KEY"] = '你的OpenAI API Key'
# 使用OutputFixingParser创建一个新的解析器,该解析器能够纠正格式不正确的输出
new_parser = OutputFixingParser.from_llm(parser=parser, llm=ChatOpenAI())
# 使用新的解析器解析不正确的输出
result = new_parser.parse(misformatted) # 错误被自动修正
print(result) # 打印解析后的输出结果
用上面的新的new_parser来代替Parser进行解析,JSON格式的错误问题被解决了,程序不再出错。本质在于,在OutputFixingParser内部,调用了原有的PydanticOutputParser,如果成功,就返回;如果失败,它会将格式错误的输出以及格式化的指令传递给大模型,并要求LLM进行相关的修复。
重试解析器(RetryWithErrorOutputParser)
OutputFixingParser不错,但它只能做简单的格式修复。如果出错的不只是格式,比如,输出根本不完整,有缺失内容,那么仅仅根据输出和格式本身,是无法修复它的。通过实现解析器中的parse_with_prompt方法,能够利用大模型的推理能力根据原始提示找回相关信息。
先设计一个解析时出现的错误:
# 定义一个模板字符串,这个模板将用于生成提问
template = """Based on the user question, provide an Action and Action Input for what step should be taken.
{format_instructions}
Question: {query}
Response:"""
# 定义一个Pydantic数据格式,它描述了一个"行动"类及其属性
from pydantic import BaseModel, Field
class Action(BaseModel):
action: str = Field(description="action to take")
action_input: str = Field(description="input to the action")
# 使用Pydantic格式Action来初始化一个输出解析器
from langchain.output_parsers import PydanticOutputParser
parser = PydanticOutputParser(pydantic_object=Action)
# 定义一个提示模板,它将用于向模型提问
from langchain.prompts import PromptTemplate
prompt = PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
prompt_value = prompt.format_prompt(query="What are the colors of Orchid?")
# 定义一个错误格式的字符串
bad_response = '{"action": "search"}'
parser.parse(bad_response) # 如果直接解析,它会引发一个错误
尝试用OutputFixingParser来解决这个问题:
from langchain.output_parsers import OutputFixingParser
from langchain.chat_models import ChatOpenAI
fix_parser = OutputFixingParser.from_llm(parser=parser, llm=ChatOpenAI())
parse_result = fix_parser.parse(bad_response)
print('OutputFixingParser的parse结果:',parse_result)
OutputFixingParser的parse结果:action='search' action_input='query'
-
解决的问题有:
- 不完整的数据:原始的bad_response只提供了action字段而没有action_input字段。OutputFixingParser已经填补了这个缺失,为action_input字段提供了值
'query'。
- 不完整的数据:原始的bad_response只提供了action字段而没有action_input字段。OutputFixingParser已经填补了这个缺失,为action_input字段提供了值
-
没解决的问题有:
- 具体性:尽管OutputFixingParser为action_input字段提供了默认值
'query',但这并不具有描述性。真正的查询是 “Orchid(兰花)的颜色是什么?”。所以,这个修复只是提供了一个通用的值,并没有真正地回答用户的问题。 - 可能的误导:
'query'可能被误解为一个指示,要求进一步查询某些内容,而不是作为实际的查询输入。
- 具体性:尽管OutputFixingParser为action_input字段提供了默认值
尝试用RetryWithErrorOutputParser来解决这个问题:
# 初始化RetryWithErrorOutputParser,它会尝试再次提问来得到一个正确的输出
from langchain.output_parsers import RetryWithErrorOutputParser
from langchain.llms import OpenAI
retry_parser = RetryWithErrorOutputParser.from_llm(
parser=parser, llm=OpenAI(temperature=0)
)
parse_result = retry_parser.parse_with_prompt(bad_response, prompt_value)
print('RetryWithErrorOutputParser的parse结果:',parse_result)
RetryWithErrorOutputParser的parse结果:action='search' action_input='colors of Orchid'
思考题
-
到目前为止,我们已经使用了哪些LangChain输出解析器?请你说一说它们的用法和异同。同时也请你尝试使用其他类型的输出解析器,并把代码与大家分享。
-
PydanticOutputParser
- 用法:基于Pydantic数据模型定义一个output_parser,然后将format_information嵌入到输入中,随后可以直接用于解析输出,生成Pydantic格式数据,可以返回字典供程序处理。
- 特点:更侧重于直接匹配预定义的数据结构
-
OutputFixingParser
- 用法:需先定义一个解析器,然后将llm和解析器传入OutputFixingParser即可,当已有解析器报错时,会将错因和输出一同传给llm,让llm进行修改。
- 特点:修复轻微格式错误或缺失内容
-
RetryWithErrorOutputParser
- 用法:需先定义一个解析器,然后将llm和解析器传入RetryWithErrorOutputParser即可,当已有解析器报错时,会将错因和输出一同传给llm,让llm进行修改。
- 特点:适用于输出内容不完整时重新生成结果,增加了上下文的补全能力。
-
DatetimeOutputParser
from langchain.output_parsers import DatetimeOutputParser from langchain_core.prompts import PromptTemplate from langchain_openai import ChatOpenAI import os output_parser = DatetimeOutputParser() prompt_template = """请告诉我{flower_type}的最佳种植日期。 {format_instructions}""" prompt = PromptTemplate.from_template( prompt_template, partial_variables={"format_instructions": output_parser.get_format_instructions()}, validate_template=False ) model = ChatOpenAI( model=os.environ.get("LLM_MODELEND"), ) input = prompt.format(flower_type='百合花') output = model.predict(input) print("输出: ", output) parsed_output = output_parser.parse(output) print("解析后的日期时间输出:", parsed_output)输出:
提示: input_variables=['flower_type'] partial_variables={'format_instructions': "**Write a datetime string that matches the following pattern: '%Y-%m-%dT%H:%M:%S.%fZ'.\n\nExamples: 1532-10-30T09:35:16.515144Z, 1236-09-17T18:04:34.672546Z, 727-03-20T00:32:09.057041Z\n\nReturn ONLY this string, no other words!**"} template='请告诉我{flower_type}的最佳种植日期。\n{format_instructions}' 输出: 1532-10-30T09:35:16.515144Z 解析后的日期时间输出: 1532-10-30 09:35:16.515144
-
-
为什么大模型能够返回JSON格式的数据,输出解析器用了什么魔法让大模型做到了这一点?
format_instruction
-
自动修复解析器的“修复”功能具体来说是怎样实现的?请做debug,研究一下LangChain在调用大模型之前如何设计“提示”。
def parse(self, completion: str) -> T: retries = 0 while retries <= self.max_retries: try: return self.parser.parse(completion) except OutputParserException as e: if retries == self.max_retries: raise e else: retries += 1 if self.legacy and hasattr(self.retry_chain, "run"): completion = self.retry_chain.run( instructions=self.parser.get_format_instructions(), completion=completion, error=repr(e), ) else: try: completion = self.retry_chain.invoke( dict( instructions=self.parser.get_format_instructions(), completion=completion, error=repr(e), ) ) except (NotImplementedError, AttributeError): # Case: self.parser does not have get_format_instructions completion = self.retry_chain.invoke( dict( completion=completion, error=repr(e), ) ) raise OutputParserException("Failed to parse")这是OutputFixingParser的parse函数,本质就是当预先定义的解析器报错时,让大模型根据format_instruction和错误输出生成新的输出,然后再给预先定义的解析器进行解析,直到解析正确或尝试次数用尽为止。
可以看到这里只用到了format_instruction,而没有用到prompt中的其他内容。
-
重试解析器的原理是什么?它主要实现了解析器类的哪个可选方法?
主要实现了解析器类的parse_with_prompt方法。
原理:
def parse_with_prompt(self, completion: str, prompt_value: PromptValue) -> T: retries = 0 while retries <= self.max_retries: try: return self.parser.parse(completion) except OutputParserException as e: if retries == self.max_retries: raise e else: retries += 1 if self.legacy and hasattr(self.retry_chain, "run"): completion = self.retry_chain.run( prompt=prompt_value.to_string(), completion=completion, error=repr(e), ) else: completion = self.retry_chain.invoke( dict( completion=completion, prompt=prompt_value.to_string(), error=repr(e), ) ) raise OutputParserException("Failed to parse")跟OutputFixingParser一致,只是多了prompt中的其他内容,这样LLM就能够通过实际的内容对错误解析的结果进行修改,而不是仅仅根据format_instruction的格式要求来进行简单修改。