AI实战课程笔记-07 输出解析

165 阅读17分钟

LangChain六大核心组件

  • Model
  • Prompts
  • Chains
  • Memory
  • Agents
  • Indexes

image.png

1 输出解析器

1.1 构建输出解析器

输出解析器是一种专用于处理和构建语言模型响应的类。一个基本的输出解析器类通常需要实现两个核心方法。

  • get_format_instructions:这个方法需要返回一个字符串,用于指导如何格式化语言模型的输出,告诉它应该如何组织并构建它的回答。
  • parse:这个方法接收一个字符串(也就是语言模型的输出)并将其解析为特定的数据结构或格式。这一步通常用于确保模型的输出符合我们的预期,并且能够以我们需要的形式进行后续处理。

还有一个可选的方法。

  • parse_with_prompt:这个方法接收一个字符串(也就是语言模型的输出)和一个提示(用于生成这个输出的提示),并将其解析为特定的数据结构。这样可以根据原始提示来修正或重新解析模型的输出,确保输出的信息更加准确和贴合要求。
class OutputParser:
    def __init__(self):
        pass

    def get_format_instructions(self):
        # 返回一个字符串,指导如何格式化模型的输出
        pass

    def parse(self, model_output):
        # 解析模型的输出,转换为某种数据结构或格式
        pass

    def parse_with_prompt(self, model_output, prompt):
        # 基于原始提示解析模型的输出,转换为某种数据结构或格式
        pass

1.2 LangChain中的解析器

  1. 列表解析器(List Parser):这个解析器用于处理模型生成的输出,当需要模型的输出是一个列表的时候使用。例如,如果你询问模型“列出所有鲜花的库存”,模型的回答应该是一个列表。
  2. 日期时间解析器(Datetime Parser):这个解析器用于处理日期和时间相关的输出,确保模型的输出是正确的日期或时间格式。
  3. 枚举解析器(Enum Parser):这个解析器用于处理预定义的一组值,当模型的输出应该是这组预定义值之一时使用。例如,如果你定义了一个问题的答案只能是“是”或“否”,那么枚举解析器可以确保模型的回答是这两个选项之一。
  4. 结构化输出解析器(Structured Output Parser):这个解析器用于处理复杂的、结构化的输出。如果你的应用需要模型生成具有特定结构的复杂回答(例如一份报告、一篇文章等),那么可以使用结构化输出解析器来实现。
  5. Pydantic(JSON)解析器:这个解析器用于处理模型的输出,当模型的输出应该是一个符合特定格式的JSON对象时使用。它使用Pydantic库,这是一个数据验证库,可以用于构建复杂的数据模型,并确保模型的输出符合预期的数据模型。
  6. 自动修复解析器(Auto-Fixing Parser):这个解析器可以自动修复某些常见的模型输出错误。例如,如果模型的输出应该是一段文本,但是模型返回了一段包含语法或拼写错误的文本,自动修复解析器可以自动纠正这些错误。
  7. 重试解析器(RetryWithErrorOutputParser):这个解析器用于在模型的初次输出不符合预期时,尝试修复或重新生成新的输出。例如,如果模型的输出应该是一个日期,但是模型返回了一个字符串,那么重试解析器可以重新提示模型生成正确的日期格式。

2 Pydantic(JSON)解析器实战

Pydantic 是一个 Python 数据验证和设置管理库,主要基于 Python 类型提示。尽管它不是专为 JSON 设计的,但由于 JSON 是现代 Web 应用和 API 交互中的常见数据格式,Pydantic 在处理和验证 JSON 数据时特别有用。

2.1 创建模型实例

方法一:选择 text-davinci-003 作为大语言模型

# 设置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')

方法二:选择课程内置的大模型

import os
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model=os.environ.get("LLM_MODELEND"),
)

2.2 定义输出数据的格式

创建一个空的DataFrame

import pandas as pd
df = pd.DataFrame(columns=["flower_type", "price", "description", "reason"])

首先创建了一个空的 DataFrame ,用于存储从模型生成的描述。

数据准备

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="为什么要这样写这个文案")

通过一个名为FlowerDescription的Pydantic BaseModel类,定义了我们想要接收的数据格式(也就是数据的结构)。负责数据格式验证的Pydantic库可以创建带有类型注解的类 FlowerDescription ,它可以自动验证输入数据,确保输入数据符合指定的类型和其他验证条件。

Fieldpydantic库中的一个类,用于定义数据模型中的字段。通过使用Field,可以为数据模型中的字段指定类型、默认值、描述等属性。

Pydantic的特点

  1. 数据验证:当向Pydantic类赋值时,它会自动进行数据验证。例如,如果创建了一个字段需要是整数,但试图向它赋予一个字符串,Pydantic会引发异常。
  2. 数据转换:Pydantic不仅进行数据验证,还可以进行数据转换。例如,如果有一个需要整数的字段,但你提供了一个可以转换为整数的字符串,如 "42",Pydantic会自动将这个字符串转换为整数42。
  3. 易于使用:创建一个Pydantic类就像定义一个普通的Python类一样简单。只需要使用Python的类型注解功能,即可在类定义中指定每个字段的类型。
  4. JSON支持:Pydantic类可以很容易地从JSON数据创建,并可以将类的数据转换为JSON格式。

2.3 创建输出解析器

创建输出解析器

from langchain.output_parsers import PydanticOutputParser
output_parser = PydanticOutputParser(pydantic_object=FlowerDescription)

使用LangChain库中的 PydanticOutputParser 创建了输出解析器,pydantic_object=FlowerDescription 指定了使用 FlowerDescription 这个 Pydantic 模型来解析输出,也就是说要求解析处的模型输出符合 FlowerDescription 的格式。

获取输出格式提示

format_instructions = output_parser.get_format_instructions()
print("输出格式:",format_instructions)

然后,使用解析器的 get_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"]
    }
  • The output should be formatted as a JSON instance that conforms to the JSON schema below. 这句话的意思是输出格式应该按照下面给出JSON实例格式化成JSON格式。
  • {"properties": {...}}:这部分定义了对象的属性。每个属性都有一个名称和一个描述该属性的模式。示例中 description 中的中文被转成了UTF-8编码。
    • "flower_type": 这是一个字符串类型的属性,用于描述鲜花的种类。它有一个标题 "Flower Type" 和一个描述 "鲜花的种类"。
    • "price": 这是一个整数类型的属性,用于描述鲜花的价格。它有一个标题 "Price" 和一个描述 "鲜花的价格"。
    • "description": 这是一个字符串类型的属性,用于描述鲜花的描述文案。它有一个标题 "Description" 和一个描述 "鲜花的描述文案"。
    • "reason": 这是一个字符串类型的属性,用于描述为什么要这样写这个文案。它有一个标题 "Reason" 和一个描述 "为什么要这样写这个文案"。
  • "required": ["flower_type", "price", "description", "reason"]:这部分指定了对象必须包含的属性。

2.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
  1. input_variables=['flower', 'price']:这是一个包含想要在模板中使用的输入变量的列表。我们在模板中使用了 'flower' 和 'price' 两个变量,后面我们会用具体的值(如玫瑰、20元)来替换这两个变量。
  2. output_parser=None:这里可以选择在模板中使用的一个输出解析器。在此例中,我们并没有选择在模板中使用输出解析器,而是在模型外部进行输出解析,所以这里是 None
  3. partial_variables:包含了你想要在模板中使用,但在生成模板时无法立即提供的变量。在这里,我们通过 'format_instructions' 传入输出格式的详细说明,就是我们之前用解析器生成的输出格式。
  4. template:这是模板字符串本身。它包含了想要模型生成的文本的结构。在此例中,模板字符串是你询问鲜花描述的问题,以及关于输出格式的说明。
  5. template_format='f-string':这是一个表示模板字符串格式的选项。此处是f-string格式。
  6. validate_template=True:表示是否在创建模板时检查模板的有效性。这里选择了在创建模板时进行检查,以确保模板是有效的。

2.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'))
  • for flower, price in zip(flowers, prices):zip(flowers, prices)flowersprices两个列表中的元素一一对应打包成元组,然后通过for循环逐个取出这些元组,以便进行进一步处理。

在Python中,zip()函数用于将多个可迭代对象(例如列表、元组等)中对应位置的元素打包成一个元组,然后返回由这些元组组成的新的可迭代对象。zip()函数会以最短的输入长度为准进行打包,如果输入的可迭代对象长度不一致,那么多余的元素会被忽略。

  • Pydantic的优点就是容易解析,而解析之后的字典格式的列表在进行数据分析、处理和存储时非常方便。因此要将解析输出的Pydantic格式转换为字典。每个字典代表一条记录,它的键( 即 "flower_type""price""description" 和 "reason")是字段名称,对应的值是这个字段的内容。
  • 这样一来,每个字段都对应一列,每个字典就是一行,适合以DataFrame的形式来表示和处理。就可以将每一条解析后的输出添加到DataFrame中。

2.6 报错解析

Traceback (most recent call last):
File "/home/cloudide/.local/lib/python3.12/site-packages/langchain_core/output_parsers/pydantic.py", line 28, in _parse_obj
return self.pydantic_object.parse_obj(obj)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/cloudide/.local/lib/python3.12/site-packages/pydantic/v1/main.py", line 526, in parse_obj
return cls(**obj)
^^^^^^^^^^
File "/home/cloudide/.local/lib/python3.12/site-packages/pydantic/v1/main.py", line 341, in __init__
raise validation_error
pydantic.v1.error_wrappers.ValidationError: 4 validation errors for FlowerDescription
flower_type
field required (type=value_error.missing)
price
field required (type=value_error.missing)
description
field required (type=value_error.missing)
reason
field required (type=value_error.missing)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/cloudide/workspace/LangChain-shizhanke/07_解析输出/01_Pydantic_Parser.py", line 75, in <module>
parsed_output = output_parser.parse(output)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/cloudide/.local/lib/python3.12/site-packages/langchain_core/output_parsers/pydantic.py", line 82, in parse
return super().parse(text)
^^^^^^^^^^^^^^^^^^^
File "/home/cloudide/.local/lib/python3.12/site-packages/langchain_core/output_parsers/json.py", line 98, in parse
return self.parse_result([Generation(text=text)])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/cloudide/.local/lib/python3.12/site-packages/langchain_core/output_parsers/pydantic.py", line 71, in parse_result
raise e
File "/home/cloudide/.local/lib/python3.12/site-packages/langchain_core/output_parsers/pydantic.py", line 67, in parse_result
return self._parse_obj(json_object)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/cloudide/.local/lib/python3.12/site-packages/langchain_core/output_parsers/pydantic.py", line 35, in _parse_obj
raise self._parser_exception(e, obj) from e
langchain_core.exceptions.OutputParserException: Failed to parse FlowerDescription from completion {"properties": {"flower_type": "\u5eb7\u4e43\u99a8", "price": 20, "description": "\u6e29\u99a8\u5eb7\u4e43\u99a8\uff0c\u4f20\u9012\u7231\u4e0e\u611f\u6069\uff0c\u4ec5\u9700 20 \u5143\uff0c\u8ba9\u7f8e\u597d\u7efd\u653e\u3002", "reason": "\u7a81\u51fa\u5eb7\u4e43\u99a8\u7684\u5bd3\u610f\uff0c\u540c\u65f6\u5f3a\u8c03\u4ef7\u683c\u5b9e\u60e0\uff0c\u5438\u5f15\u987e\u5ba2\u8d2d\u4e70\u3002"}}. Got: 4 validation errors for FlowerDescription
flower_type
field required (type=value_error.missing)
price
field required (type=value_error.missing)
description
field required (type=value_error.missing)
reason
field required (type=value_error.missing)

这个示例在运行过程中有时会出现上述报错,这是因为Pydantic 在尝试将输出解析为 FlowerDescription 对象时,发现这些字段没有被提供,因此抛出了 ValidationError。 这时可能需要再次运行程序,向模型重新发出请求,以获得符合我们格式要求的输出。

3 自动修复解析器(OutputFixingParser)实战

3.1 定义不正确的输出

# 导入所需要的库和模块
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': ['粉红色','白色','红色','紫色','黄色']}"

3.2 使用Pydantic(JSON)解析器输出

from langchain.output_parsers import PydanticOutputParser

# 创建一个用于解析输出的Pydantic解析器,此处希望解析为Flower格式
parser = PydanticOutputParser(pydantic_object=Flower)
# 使用Pydantic解析器解析不正确的输出
parser.parse(misformatted) #这行代码出错

报错

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)
  • Expecting property name enclosed in double quotes 表示程序尝试用 PydanticOutputParser 来解析JSON字符串时,Python期望属性名称被双引号包围,但在给定的JSON字符串中是单引号。
  • OutputParserException: Failed to parse Flower from completion {'name': '康乃馨', 'colors': ['粉红色','白色']}. 表示程序进一步引发了一个自定义异常:OutputParserException,它提供了更多关于错误的上下文。这个自定义异常的消息表示在尝试解析 flower 对象时遇到了问题。

3.3 使用OutputFixingParser自动解决格式错误

使用自己的OpenAI Key

# 从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) # 打印解析后的输出结果

使用课程内置的LLM模型

# 从langchain库导入所需的模块
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import OutputFixingParser

import os

# 使用OutputFixingParser创建一个新的解析器,该解析器能够纠正格式不正确的输出
new_parser = OutputFixingParser.from_llm(
    parser=parser,
    llm=ChatOpenAI(
        model=os.environ.get("LLM_MODELEND"),
    ),
)

# 使用新的解析器解析不正确的输出
result = new_parser.parse(misformatted) # 错误被自动修正
print(result) # 打印解析后的输出结果

OutputFixingParser 内部,调用了原有的 PydanticOutputParser ,如果成功,就返回;如果失败,它会将格式错误的输出以及格式化的指令传递给大模型,并要求LLM进行相关的修复。

4 重试解析器(RetryWithErrorOutputParser)实战

4.1 定义错误格式的字符串,并用Pydantic(JSON)解析器输出

# 定义一个模板字符串,这个模板将用于生成提问
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) # 如果直接解析,它会引发一个错误

这里 bad_response = '{"action": "search"}' 只提供了 action 字段,而没有提供 action_input 字段,这与 Action 数据格式的预期不符,所以解析会失败。

4.2 用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=''

课程运行的结果 OutputFixingParser的parse结果:action='search' action_input='query'

原始的bad_response只提供了action字段而没有action_input字段。在课程的运行结果中,OutputFixingParser已经填补了这个缺失,为action_input字段提供了值 'query'。而我的运行结果中action_input字段填充的值为空。

不管是默认值 'query'还是空值不具有描述性,这个修复只是提供了一个通用的值,并没有真正地回答用户的问题。同时,'query' 可能被误解为一个指示,被用户误认为是要求进一步查询某些内容,而不是作实际的查询输入。

4.3RetryWithErrorOutputParser解决问题

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通过尝试再次提问来得到一个正确的输出。

RetryWithErrorOutputParser的parse结果: action='search' action_input='What are the colors of Orchid?'