LangChain笔记--Model I/O

113 阅读12分钟

LangChain笔记--Model I/O

LangChain基本架构如下

我们可以把对模型的使用过程拆解成三块,分别是输入提示调用模型输出解析。这三块形成了一个整体,因此在LangChain中这个过程被统称为 Model I/O(Input/Output)。

  1. 提示模板:使用模型的第一个环节是把提示信息输入到模型中,你可以创建LangChain模板,根据实际需求动态选择不同的输入,针对特定的任务和应用调整输入。
  2. 语言模型:LangChain允许你通过通用接口来调用语言模型。这意味着无论你要使用的是哪种语言模型,都可以通过同一种方式进行调用,这样就提高了灵活性和便利性。
  3. 输出解析: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

接下来深入了解提示工程,它的构建可以参考以下标准

  1. 写清晰的指示
  2. 给模型提供参考(也就是示例)
  3. 将复杂任务拆分成子任务
  4. 给GPT时间思考
  5. 使用外部工具
  6. 反复迭代问题

这里首先主要介绍示例,对人来说,示例往往能更有利去理解一个问题,对机器也是一样,我们可以在提示词中加入示例,如下

# 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)(缺省重试)等大家感兴趣可以去了解一下