LangChain笔记--Model I/O
LangChain基本架构如下
我们可以把对模型的使用过程拆解成三块,分别是输入提示、调用模型和输出解析。这三块形成了一个整体,因此在LangChain中这个过程被统称为 Model I/O(Input/Output)。
- 提示模板:使用模型的第一个环节是把提示信息输入到模型中,你可以创建LangChain模板,根据实际需求动态选择不同的输入,针对特定的任务和应用调整输入。
- 语言模型:LangChain允许你通过通用接口来调用语言模型。这意味着无论你要使用的是哪种语言模型,都可以通过同一种方式进行调用,这样就提高了灵活性和便利性。
- 输出解析:LangChain还提供了从模型输出中提取信息的功能。通过输出解析器,你可以精确地从模型的输出中获取需要的信息,而不需要处理冗余或不相关的数据,更重要的是还可以把大模型给回的非结构化文本,转换成程序可以处理的结构化数据。
提示工程
构建Prompt
说白了就是如何让模型能更好的了解你的问题,从而给出更满意的答案,这就需要在向大模型询问时需要给出更清晰的指令,遵从一些标准去构建问题,这样可以引导模型, 按照预设的思路去思考问题、输出内容。
例如在不使用提示工程,你的问题可能是“如何学习编程?”, 在使用提示词后,你的问题就变成了“如何学习编程?假设我是一名从未编程过的学生,请分阶段给出学习建议。”,后者比前者多给出了问题背景,以及解答流程,肯定结果要优于前者。这里介绍的就是使用提示模板,让我们可以更方便的写出好的Prompt
使用 PromptTemplate
下面通过示例简单说明一下PromptTemplate的使用。
# 导入LangChain中的提示模板
from langchain.prompts import PromptTemplate
# 创建原始模板
template = """您是一位专业的鲜花店文案撰写员。\n
对于售价为 {price} 元的 {flower_name} ,您能提供一个吸引人的简短描述吗?
"""
# 根据原始模板创建LangChain提示模板
prompt = PromptTemplate.from_template(template)
# 打印LangChain提示模板的内容
print(prompt)
这段代码设置了提示模板为,您是一位专业的鲜花店文案撰写员。\n对于售价为 {price} 元的 {flower_name} ,您能提供一个吸引人的简短描述吗?, {}类似python中的f-string一样的占位符,用于用户按需修改prompt, 使用方式如下
input = prompt.format(flower_name="玫瑰", price='50')
print(prompt)
>>> 对于售价为 50 元的 玫瑰 ,您能提供一个吸引人的简短描述吗?
使用 ChatPromptTemplate
对于OpenAI推出的ChatGPT这一类的聊天模型,LangChain也提供了一系列的模板,这些模板的不同之处是它们有对应的角色。
# 导入聊天消息类模板
from langchain.prompts import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
# 模板的构建
template = "你是一位专业顾问,负责为专注于{product}的公司起名。"
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
human_template = "公司主打产品是{product_detail}。"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
prompt_template = ChatPromptTemplate.from_messages(
[system_message_prompt, human_message_prompt]
)
# 格式化提示消息生成提示
prompt = prompt_template.format_prompt(
product="鲜花装饰", product_detail="创新的鲜花设计"
).to_messages()
print(prompt)
输出生成的prompt
>>>>>> [SystemMessage(content='你是一位专业顾问,负责为专注于鲜花装饰的公司起名。'), HumanMessage(content='公司主打产品是创新的鲜花设计。')]
可以看到prompt中有不同角色, 所以更适合chat模型
给模型提供示例--FewShot
接下来深入了解提示工程,它的构建可以参考以下标准
- 写清晰的指示
- 给模型提供参考(也就是示例)
- 将复杂任务拆分成子任务
- 给GPT时间思考
- 使用外部工具
- 反复迭代问题
这里首先主要介绍示例,对人来说,示例往往能更有利去理解一个问题,对机器也是一样,我们可以在提示词中加入示例,如下
# 1. 创建一些示例
samples = [
{
"flower_type": "玫瑰",
"occasion": "爱情",
"ad_copy": "玫瑰,浪漫的象征,是你向心爱的人表达爱意的最佳选择。",
},
{
"flower_type": "康乃馨",
"occasion": "母亲节",
"ad_copy": "康乃馨代表着母爱的纯洁与伟大,是母亲节赠送给母亲的完美礼物。",
},
{
"flower_type": "百合",
"occasion": "庆祝",
"ad_copy": "百合象征着纯洁与高雅,是你庆祝特殊时刻的理想选择。",
},
{
"flower_type": "向日葵",
"occasion": "鼓励",
"ad_copy": "向日葵象征着坚韧和乐观,是你鼓励亲朋好友的最好方式。",
},
]
__import__("pysqlite3")
import sys
sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
# 2. 创建一个提示模板
from langchain.prompts.prompt import PromptTemplate
prompt_sample = PromptTemplate(
input_variables=["flower_type", "occasion", "ad_copy"],
template="鲜花类型: {flower_type}\n场合: {occasion}\n文案: {ad_copy}",
)
print(prompt_sample.format(**samples[0]))
输出
鲜花类型: 玫瑰
场合: 爱情
文案: 玫瑰,浪漫的象征,是你向心爱的人表达爱意的最佳选择。
这也就是我们想让给模型的示例, 让模型在示例中找到规律
然后加入问题构建完整的prompt
# 3. 创建一个FewShotPromptTemplate对象
from langchain.prompts.few_shot import FewShotPromptTemplate
prompt = FewShotPromptTemplate(
examples=samples,
example_prompt=prompt_sample,
suffix="鲜花类型: {flower_type}\n场合: {occasion}",
input_variables=["flower_type", "occasion"],
)
print(prompt.format(flower_type="野玫瑰", occasion="爱情"))
prompt如下:
鲜花类型: 玫瑰
场合: 爱情
文案: 玫瑰,浪漫的象征,是你向心爱的人表达爱意的最佳选择。
鲜花类型: 康乃馨
场合: 母亲节
文案: 康乃馨代表着母爱的纯洁与伟大,是母亲节赠送给母亲的完美礼物。
鲜花类型: 百合
场合: 庆祝
文案: 百合象征着纯洁与高雅,是你庆祝特殊时刻的理想选择。
鲜花类型: 向日葵
场合: 鼓励
文案: 向日葵象征着坚韧和乐观,是你鼓励亲朋好友的最好方式。
鲜花类型: 野玫瑰
场合: 爱情
模型推理是按照Token计费的,如果上传很多与问题无关的示例, 这样不仅不会给模型太多指引,反而会浪费Token数, 所以这里可以使用示例选择器选择合适的示例, SemanticSimilarityExampleSelector对象可以根据语义相似性选择最相关的示例。然后,使用该选择器来创建了一个新的FewShotPromptTemplate对象, 这样可以选择最相关的示例生成提示。
Cot (Chain of Thought)
就是给模型提供链式思考步骤
比如, 对于如下问题
如果商店里有 24 支铅笔,卖出了一半,又进货了 10 支铅笔,现在商店有多少支铅笔?
加入Cot引导就是,指导模型该怎么回答这个问题, 如下
如果商店里有 24 支铅笔,卖出了一半,又进货了 10 支铅笔,现在商店有多少支铅笔?
要求一步一步地解释计算过程,并详细说明每一步的数量变化,有助于获得准确答案。
Prompt中给出Cot以及示例我们称为Few-Shot CoT, 只给出Cot不给出示例我们称为Zero-Shot CoT
ToT
ToT是一种解决复杂问题的框架,它在需要多步骤推理的任务中,引导语言模型搜索一棵由连贯的语言序列(解决问题的中间步骤)组成的思维树,而不是简单地生成一个答案。ToT框架的核心思想是:让模型生成和评估其思维的能力,并将其与搜索算法(如广度优先搜索和深度优先搜索)结合起来,进行系统性地探索和验证。
ToT 框架为每个任务定义具体的思维步骤和每个步骤的候选项数量。例如,要解决一个数学推理任务,先把它分解为3个思维步骤,并为每个步骤提出多个方案,并保留最优的5个候选方案。然后在多条思维路径中搜寻最优的解决方案。
语言模型
通过以下三种方法可以完成模型的调用
HuggingFace库
用HuggingFace的Transformers库来调用Llama
# 导入必要的库
from transformers import AutoTokenizer, AutoModelForCausalLM
# 加载预训练模型的分词器
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
# 加载预训练的模型
# 使用 device_map 参数将模型自动加载到可用的硬件设备上,例如GPU
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-chat-hf",
device_map = 'auto')
# 定义一个提示,希望模型基于此提示生成故事
prompt = "请给我讲个玫瑰的爱情故事?"
# 使用分词器将提示转化为模型可以理解的格式,并将其移动到GPU上
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
# 使用模型生成文本,设置最大生成令牌数为2000
outputs = model.generate(inputs["input_ids"], max_new_tokens=2000)
# 将生成的令牌解码成文本,并跳过任何特殊的令牌,例如[CLS], [SEP]等
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# 打印生成的响应
print(response)
HuggingFace Hub与LangChain集成
# 导入HuggingFace API Token
import os
os.environ['HUGGINGFACEHUB_API_TOKEN'] = '你的HuggingFace API Token'
# 导入必要的库, 这里需要LLMChain完成huggingFace和LangChain的连接
from langchain import PromptTemplate, HuggingFaceHub, LLMChain
# 初始化HF LLM
llm = HuggingFaceHub(
repo_id="google/flan-t5-small",
#repo_id="meta-llama/Llama-2-7b-chat-hf",
)
# 创建简单的question-answering提示模板
template = """Question: {question}
Answer: """
# 创建Prompt
prompt = PromptTemplate(template=template, input_variables=["question"])
# 调用LLM Chain
llm_chain = LLMChain(
prompt=prompt,
llm=llm
)
# 准备问题
question = "Rose is which type of flower?"
# 调用模型并返回结果
print(llm_chain.run(question))
LangChain调用本地模型
# 导入需要的库
from llama_cpp import Llama
from typing import Optional, List, Mapping, Any
from langchain.llms.base import LLM
# 模型的名称和路径常量
MODEL_NAME = 'llama-2-7b-chat.ggmlv3.q4_K_S.bin'
MODEL_PATH = '/home/huangj/03_Llama/'
# 自定义的LLM类,继承自基础LLM类
class CustomLLM(LLM):
model_name = MODEL_NAME
# 该方法使用Llama库调用模型生成回复
def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
prompt_length = len(prompt) + 5
# 初始化Llama模型,指定模型路径和线程数
llm = Llama(model_path=MODEL_PATH+MODEL_NAME, n_threads=4)
# 使用Llama模型生成回复
response = llm(f"Q: {prompt} A: ", max_tokens=256)
# 从返回的回复中提取文本部分
output = response['choices'][0]['text'].replace('A: ', '').strip()
# 返回生成的回复,同时剔除了问题部分和额外字符
return output[prompt_length:]
# 返回模型的标识参数,这里只是返回模型的名称
@property
def _identifying_params(self) -> Mapping[str, Any]:
return {"name_of_model": self.model_name}
# 返回模型的类型,这里是"custom"
@property
def _llm_type(self) -> str:
return "custom"
# 初始化自定义LLM类
llm = CustomLLM()
# 使用自定义LLM生成一个回复
result = llm("昨天有一个客户抱怨他买了花给女朋友之后,两天花就枯了,你说作为客服我应该怎么解释?")
# 打印生成的回复
print(result)
输出解析
就是希望模型输出的格式,这个同样要作为Prompt传递给大模型
Pydantic(JSON)解析器
自定义类作为输出格式
# 定义我们想要接收的数据格式
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库来创建带有类型注解的类FlowerDescription,它可以自动验证输入数据,确保输入数据符合你指定的类型和其他验证条件。
创建输出解析器实例
# ------Part 3
# 创建输出解析器
from langchain.output_parsers import PydanticOutputParser
output_parser = PydanticOutputParser(pydantic_object=FlowerDescription)
# 获取输出格式指示
format_instructions = output_parser.get_format_instructions()
# 打印提示
print("输出格式:",format_instructions)
PydanticOutputParser自动生成了Prompt, 输出的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"]}
接着我们结合之前的提示工程的代码,就得到了完整的Prompt实现
# 创建提示模板
from langchain import PromptTemplate
prompt_template = """您是一位专业的鲜花店文案撰写员。
对于售价为 {price} 元的 {flower} ,您能提供一个吸引人的简短中文描述吗?
{format_instructions}"""
# 根据模板创建提示,同时在提示中加入输出解析器的说明
prompt = PromptTemplate.from_template(prompt_template,
partial_variables={"format_instructions": format_instructions})
# 打印提示
print("提示:", prompt)
将Prompt送入模型预测,得到的结果可以很方便的转换为字典
# 根据提示准备模型的输入
input = prompt.format(flower=flower, price=price)
# 获取模型的输出
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'))
Pydantic的优点就是容易解析,而解析之后的字典格式的列表在进行数据分析、处理和存储时非常方便。每个字典代表一条记录,它的键( 即 "flower_type"、"price"、"description" 和 "reason")是字段名称,对应的值是这个字段的内容。这样一来,每个字段都对应一列,每个字典就是一行,适合以DataFrame的形式来表示和处理。
还有很多输出解释器,比如自动修复解析器(OutputFixingParser)(格式转换), 重试解析器(RetryWithErrorOutputParser)(缺省重试)等大家感兴趣可以去了解一下