【LangChain】Chapter1 - Models, Prompts and Output Parsers
说在前面
本节将结合课程的示例代码介绍LangChain 框架中的 Models 模型 , Prompts 提示, Parsers 解析器,三部分的简单使用方式,和相较于一般的方法, Langchain 在这里的优势。(视频时长18:22)
注: 所有的示例代码文件课程网站上都有(完全免费),并且是配置好的 Juptyernotebook 环境和配置好的 OPENAI_API_KEY
,不需要自行去花钱租用,建议代码直接上课程网站上运行。 课程网站
Main Content
本节的三个重要概念为:Models,Prompts,Parsers,简单介绍如下。
- Models - 语言模型
- Prompts - 翻译为 ‘提示’ 指创建输入,是用来给模型传递信息的一种方式。
- Parsers - 翻译为 ‘解析器’,接受模型的输出,并将输出结果解析成更结构化的格式,以便进行后续操作。
可以看出,当我使用LLM构建应用程序时,通常会有可重用的模块,我们会反复的提示模型,解析输出,LangChain提供了一套简单的抽象来执行此类操作。
实践说明
下面我们将通过实践来以及对比分析在开发LLM时,针对上面的三个重要组件,LangChain相较于一般方法的优势,以及使用方法。
前置工作
在开始实验之前,需要先完成实验环境配置和大模型的API调用。(建议直接上课程网站进行实验,课程网站上已完成环境设置和API的调用)
- 安装依赖。
# 使用Colab运行下面的命令进行环境的配置
#!pip install python-dotenv
#!pip install openai
- 配置 OpenAI API Key。课程网站上是已经配置好的了,在自己的环境中运行需要去OpenAI平台上获取API Key,参考教程
import os
import openai
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file
openai.api_key = os.environ['OPENAI_API_KEY']
- 设定使用的 LLM 版本名。使用的是
gpt-3.5-turbo
# account for deprecation of LLM model
import datetime
# Get the current date
current_date = datetime.datetime.now().date()
# Define the date after which the model should be set to "gpt-3.5-turbo"
target_date = datetime.date(2024, 6, 12)
# Set the model variable based on the current date
if current_date > target_date:
llm_model = "gpt-3.5-turbo"
else:
llm_model = "gpt-3.5-turbo-0301"
Chat API : OpenAI (对照组)
不使用 LangChain,直接通过API调用 OpenAI 的模型。(作为对照)
- 编写函数
get_completion()
对用于与OpenAI的ChatCompletion API交互进行简单的封装。它接收一个提示(prompt)和一个可选的模型名称(model),然后构建消息并调用API来获取回复。
# 定义一个函数,用于根据给定的提示和模型获取完成结果
def get_completion(prompt, model=llm_model):
# 构建消息列表,包含用户的角色和内容
messages = [{"role": "user", "content": prompt}]
# 调用OpenAI的ChatCompletion API创建聊天完成
# 参数model指定使用哪个模型,messages是对话历史,temperature设置生成文本的随机性(0表示完全确定)
response = openai.ChatCompletion.create(
model=model,
messages=messages,
temperature=0,
)
# 从API响应中提取第一个选择的消息内容并返回
return response.choices[0].message["content"]
- 调用编写的
get_completion()
完成一次对话,结果如下图所示。
# 内容自己设定即可
get_completion("What is 1+1?")
- 下面是一个调用API进行文本风格转换的例子。要求将顾客的邮件内容从海盗风格(pirate)的语言转换为标准的美式英语,并且要求语气保持冷静和尊重。我们通过 f 字符串进行
prompt
的构造,然后打印出得到的完整prompt
# 顾客email内容
customer_email = """
Arrr, I be fuming that me blender lid \
flew off and splattered me kitchen walls \
with smoothie! And to make matters worse,\
the warranty don't cover the cost of \
cleaning up me kitchen. I need yer help \
right now, matey!
"""
# 设定转换后的文本风格
style = """American English \
in a calm and respectful tone
"""
组装得到完整的 prompt
,并打印查看,结果如下图所示。
prompt = f"""Translate the text \
that is delimited by triple backticks
into a style that is {style}.
text: ```{customer_email}```
"""
print(prompt)
- 将
prompt
通过API 输入给 LLM获得response
,并打印,结果如下图所示。
response = get_completion(prompt)
response
Chat API : LangChain
这里我们使用 LangChain 来实现相同的功能,与直接使用 API 进行比较。
Model
- 不自行编写交互函数,直接使用 LangChain 库的封装函数
ChatOpenAI()
创建模型交互对象。可以看到创建的 chat 对象的所有信息。
from langchain.chat_models import ChatOpenAI
# 为了控制生成文本的随机性和创造性,我们将温度(temperature)设置为 0.0。
# 当温度为 0.0 时,模型将选择最有可能的下一个词,使得输出更加确定和一致。
# 创建一个 ChatOpenAI 实例,使用指定的模型和温度参数。
# 参数 temperature=0.0 表示我们希望生成的回复尽可能地确定和不随机。
# 参数 model 指定了要使用的语言模型,这里使用的是 llm_model 变量中定义的模型。
chat = ChatOpenAI(temperature=0.0, model=llm_model)
# 注意:ChatOpenAI 不是 OpenAI 官方库的一部分, 来自 LangChain 库,用于简化与 OpenAI API 的交互。
# 显示创建的 chat
chat
总结: 这样的模型交互函数的创建方式相比于直接使用会更简洁,模块化,当我们构建的LLM应用足够复杂时,优势就会体现出来。
Prompt template
使用 LangChain 的 ChatPromptTemplate
类来构造 prompt
模板对象。
- 定义一个模板字符串
template_string
# 定义一个模板字符串,用于构建提示给语言模型。
# 该模板包含两个占位符:{style} 和 {text}。
# {style} 将被替换为期望的目标风格描述,
# {text} 将被替换为需要翻译或转换风格的实际文本内容。
template_string = """Translate the text \
that is delimited by triple backticks \
into a style that is {style}. \
text: ```{text}```
"""
# 解释模板中的各个部分:
# "Translate the text" - 指示模型执行翻译或风格转换的任务。
# "that is delimited by triple backticks" - 指定实际文本将用三个反引号(```)包围,以便清晰地区分。
# "into a style that is {style}" - 指定目标风格,{style} 是一个占位符,稍后会被具体的风格描述替代。
# "text: ```{text}```" - 提供了实际需要转换的文本内容,{text} 是一个占位符,稍后会被具体的文本替代。
# 这个模板可以用来创建更复杂的提示,指导模型如何处理输入文本。
# 例如,你可以使用这个模板来要求模型将一段文本从一种风格转换为另一种风格,
# 或者将文本翻译成不同的语言,具体取决于你传递给 {style} 的值。
- 导入
ChatPromptTemplate
类,创建ChatPromptTemplate.from_template
实例。
# 导入 LangChain 库中的 ChatPromptTemplate 类。
from langchain.prompts import ChatPromptTemplate
# 使用 ChatPromptTemplate.from_template 方法创建一个 ChatPromptTemplate 实例。
# 这个实例可以根据提供的模板字符串生成具体的提示,
# 并允许我们通过传递参数来动态地填充模板中的占位符。
# 在这里,我们将 template_string 作为参数传递给 from_template 方法,
# 以便创建一个可以用来生成提示的模板对象。
prompt_template = ChatPromptTemplate.from_template(template_string)
# prompt_template 现在是一个可调用的对象,它可以根据传入的具体参数生成完整的提示。
# 例如,你可以使用如下方式来生成一个具体的提示:
# formatted_prompt = prompt_template.format(style="American English in a calm and respectful tone", text="你的文本内容")
# 这将会把 {style} 和 {text} 占位符替换为实际值,并返回一个格式化的提示字符串。
- 查看 prompt 模板中的第一个消息的具体内容。
# 访问提示模板中第一个消息的具体提示内容。
# prompt_template.messages 是一个列表,包含了多个消息对象。
# 每个消息对象都有一个 .prompt 属性,它是一个可格式化的 PromptValue 对象。
# 在这里,我们访问了列表中的第一个消息,并获取其 .prompt 属性。
# 这个属性包含了原始的模板字符串以及如何使用提供的参数进行格式化的信息。
prompt_template.messages[0].prompt
- 查看 prompt 模板中定义的输入变量。如下图所示,变量为
style
和text
。
# 查看提示模板中定义的输入变量。
# input_variables 是一个属性,它返回一个列表,列出了模板中所有需要填充的变量名称。
# 在这个例子中,它应该返回 ['style', 'text'],因为模板中有两个占位符 {style} 和 {text}。
prompt_template.messages[0].prompt.input_variables
- 定义目标风格
customer_style
。
# 定义目标风格,即希望将文本转换成的风格。
# 在这里,我们将风格设置为美式英语,语气平静且尊重。
customer_style = """American English \
in a calm and respectful tone
"""
- 定义客户邮件内容
customer_email
。
# 定义客户邮件内容,这是需要转换风格的原始文本。
# 客户邮件使用了一种海盗风格的语言,表达了对产品问题的不满。
customer_email = """
Arrr, I be fuming that me blender lid \
flew off and splattered me kitchen walls \
with smoothie! And to make matters worse, \
the warranty don't cover the cost of \
cleaning up me kitchen. I need yer help \
right now, matey!
"""
- 使用
prompt_template.format_messages
方法格式化消息,得到格式化后的结果customer_messages
。
# 使用 prompt_template.format_messages 方法格式化消息。
# 该方法接收两个参数:style 和 text,
# 分别对应模板中的 {style} 和 {text} 占位符。
# 这里我们将 customer_style 和 customer_email 作为参数传递进去,
# 以生成最终的格式化消息列表。
customer_messages = prompt_template.format_messages(
style=customer_style,
text=customer_email
)
- 打印
customer_messages
和customer_messages
列表中第一个元素的数据类型,以查看它是哪种类型的对象。可以看到customer_messages
为list
,customer_messages
为langchain.schema.HumanMessage
。如下图所示
# 打印 customer_messages 的数据类型,以查看它是哪种类型的对象。
print(type(customer_messages))
# 打印 customer_messages 列表中第一个元素的数据类型,
# 以查看单个消息对象的类型。
print(type(customer_messages[0]))
- 打印格式化后的第一个消息对象的内容。如下图所示。
# 打印格式化后的第一个消息对象的内容,
# 以查看具体的消息内容。
print(customer_messages[0])
- 调用 LLM 来根据格式化后的消息生成回复。并打印语言模型生成的回复内容,以查看最终结果。结果如下图所示。
# 调用 LLM 来根据格式化后的消息生成回复。
# chat 函数接收格式化后的消息列表,并返回模型生成的回复。
# 注意:这里的 chat 函数假设是 LangChain 库或其他类似库的一部分,
# 如果使用的是其他库,请根据实际情况调整调用方式。
customer_response = chat(customer_messages)
# 打印语言模型生成的回复内容,以查看最终结果。
print(customer_response.content)
- 我们重新创建一段文本内容,套用 prompt 模板来调用 LLM 进行回复。流程跟上面一样不重复讲解,效果如下图所示。
service_reply = """Hey there customer, \
the warranty does not cover \
cleaning expenses for your kitchen \
because it's your fault that \
you misused your blender \
by forgetting to put the lid on before \
starting the blender. \
Tough luck! See ya!
"""
service_style_pirate = """\
a polite tone \
that speaks in English Pirate\
"""
service_messages = prompt_template.format_messages(
style=service_style_pirate,
text=service_reply)
print(service_messages[0].content)
service_response = chat(service_messages)
print(service_response.content)
总结: 与对照组使用 f 字符串的方式构造 prompt
对比,使用 LangChain 构造 prompt 模板的方法,在创建单个 prompt 的时候区别不大。但是在实际应用中,我们可能需要构建复杂的应用程序,我们的提示可能会很长很详细,且可能有很多个不同的提示,此时 LangChain 构建的 prompt 模板就能发挥其作用,便于我进行重用。下面是对为什么使用 prompt 模板的整理。
Why use prompt templates?
- 可重用性(Reusability): 定义一次,多次使用。
- 关注点分离 (Separation of Concerns): 提示模板将提示格式化与模型调用分离开来,使得代码更加模块化。这意味着你可以独立地更改模板或模型,而不影响另一方。
- 动态提示 (Dynamic Prompts): 模板允许你通过填充模板变量来动态生成提示。例如,可以根据用户的偏好、历史记录或当前上下文调整提示内容,从而提供更个性化的服务。
- 可读性 (Readability): 模板可以封装复杂的提示逻辑,使其通过简单的接口呈现,提高代码的可读性和维护性。
- 易于维护 (Maintenance): 如果共享的提示逻辑发生变化,只需要在一个地方进行修改即可。
- LangChain 对一些常见操作有预置的 prompt,如摘要、回答问题、链接不同API。
参考资料:Introduction to Prompt Templates in LangChain
Output Parsers
我们从定义我们希望 LLM(大型语言模型)输出的格式和风格来讲解。
- 定义大模型输出的格式。
{
"gift": false, // 是否为礼物:false 表示这不是一个礼物。
"delivery_days": 5, // 配送天数:表示配送需要 5 天时间。
"price_value": "pretty affordable!" // 价格评价:表示这个商品的价格相当实惠!
}
- 设定顾客的评价
customer_review
和评价的模板review_template
customer_review = """\
This leaf blower is pretty amazing. It has four settings:\
candle blower, gentle breeze, windy city, and tornado. \
It arrived in two days, just in time for my wife's \
anniversary present. \
I think my wife liked it so much she was speechless. \
So far I've been the only one using it, and I've been \
using it every other morning to clear the leaves on our lawn. \
It's slightly more expensive than the other leaf blowers \
out there, but I think it's worth it for the extra features.
"""
review_template = """\
For the following text, extract the following information:
gift: Was the item purchased as a gift for someone else? \
Answer True if yes, False if not or unknown.
delivery_days: How many days did it take for the product \
to arrive? If this information is not found, output -1.
price_value: Extract any sentences about the value or price,\
and output them as a comma separated Python list.
Format the output as JSON with the following keys:
gift
delivery_days
price_value
text: {text}
"""
- 使用 LangChain 创建 prompt 模板。
from langchain.prompts import ChatPromptTemplate
prompt_template = ChatPromptTemplate.from_template(review_template)
print(prompt_template)
- 创建完整的
prompt
,并进行交互,得到 LLM 的response
,打印response
。结果如图所示。
messages = prompt_template.format_messages(text=customer_review)
chat = ChatOpenAI(temperature=0.0, model=llm_model)
response = chat(messages)
print(response.content)
- 打印
response
内容的类型,得到是str
字符串类型。response.content.get('gift')
的作用为从response.content
对象中获取键为'gift'
的值。我们会看到报错,因为'response.content'
并不是字典类型,且'gift'
并不是其中的键。
type(response.content)
# 这一行代码会引发错误,因为 'gift'并不是字典中的 key。
# 实际上,'gift' 是一个字符串。
response.content.get('gift')
Parse the LLM output string into a Python dictionary
介于上面的问题,我们将 LLM 输出的字符串解析为 Python 的字典类型。
- 导入两个 LangChain 的类,
ResponseSchema
和StructuredOutputParser
用于设置的我的解析器。
# 导入 ResponseSchema 类,用于定义我们期望从 LLM 输出中提取的字段结构。
# 每个 ResponseSchema 对象代表一个预期的输出字段,包括字段名称和描述。
from langchain.output_parsers import ResponseSchema
# 导入 StructuredOutputParser 类,用于解析 LLM 的自由文本输出,并将其转换为结构化数据。
# StructuredOutputParser 可以根据预定义的响应模式(ResponseSchema)从 LLM 的输出中提取特定信息。
from langchain.output_parsers import StructuredOutputParser
# 定义 gift_schema,用于描述是否该物品是作为礼物购买的
gift_schema = ResponseSchema(
name="gift", # 字段名称为 "gift"
description="Was the item purchased as a gift for someone else? Answer True if yes, False if not or unknown." # 描述是否该物品是作为礼物为其他人购买的。如果是,请回答 True;如果不是或未知,请回答 False。
)
# 定义 delivery_days_schema,用于描述产品送达所需的时间
delivery_days_schema = ResponseSchema(
name="delivery_days", # 字段名称为 "delivery_days"
description="How many days did it take for the product to arrive? If this information is not found, output -1." # 描述产品送达需要多少天。如果未找到相关信息,请输出 -1。
)
# 定义 price_value_schema,用于描述关于价值或价格的句子
price_value_schema = ResponseSchema(
name="price_value", # 字段名称为 "price_value"
description="Extract any sentences about the value or price, and output them as a comma separated Python list." # 描述提取有关价值或价格的所有句子,并将它们作为逗号分隔的 Python 列表输出。
)
# 将所有定义的响应模式(ResponseSchema)组合成一个列表 response_schemas
response_schemas = [
gift_schema, # 是否作为礼物购买的信息
delivery_days_schema, # 产品送达时间信息
price_value_schema # 有关价值或价格的描述信息
]
# 使用 StructuredOutputParser 类创建一个输出解析器实例。
# from_response_schemas 方法接收一个包含 ResponseSchema 对象的列表,
# 并根据这些模式定义创建一个解析器。
# 该解析器将用于从 LLM 的自由文本输出中提取结构化信息。
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
# 调用 output_parser 的 get_format_instructions 方法,
# 获取格式指导文本。该文本将作为提示的一部分提供给 LLM,
# 以确保其输出符合预定义的结构化模式。
format_instructions = output_parser.get_format_instructions()
print(format_instructions)
- 撰写用户的评论
prompt
模板,并于LLM交互得到回复进行格式化的输出。结果如下图。
review_template_2 = """\
For the following text, extract the following information:
gift: Was the item purchased as a gift for someone else? \
Answer True if yes, False if not or unknown.
delivery_days: How many days did it take for the product\
to arrive? If this information is not found, output -1.
price_value: Extract any sentences about the value or price,\
and output them as a comma separated Python list.
text: {text}
{format_instructions}
"""
prompt = ChatPromptTemplate.from_template(template=review_template_2)
messages = prompt.format_messages(text=customer_review,
format_instructions=format_instructions)
print(messages[0].content)
response = chat(messages)
可以看到的回复解析器解析成了一个 字典
print(response.content)
output_dict = output_parser.parse(response.content)
output_dict
type(output_dict)
成功的提取到字典里的元素。
output_dict.get('delivery_days')
总结: 上面这个例子是 让 LLM 输出 Json,并使用 LangChain解析这个输出。从产品评论中提取信息,并将输出格式化为 Json 格式。这会便于我们之后使用该回复结果进行更多的操作,从而构建一个完整的 LLM 应用。
总结
本部分对使用 LangChain 构建 LLM 应用时的三个重要组件进行了说明,并说明了其优点。通过 LangChain 来实现这三个部分,会更加的简洁,更易用,在面对现实场景下的复杂LLM应用的搭建会更能体现出其优势。另外,这三个部分是可以在最上面的简易流程图的基础上进行拓展,从而实现更好的推理效果。例如,在深度推理链中 就需要 LangChain 的 output paring 和 prompt templates 进行配合工作。