3.Model I/O之Prompt Template
提示工程(Prompt Engineering)大家应该比较熟悉,这个概念是指在与大语言模型(LLMs),如GPT-3、Qwen等模型进行交互时,精心设计输入文本(即提示)的过程,以获得更精准、相关或有创造性的输出。在我们第一级学习计划中通过采用Few-Shot、Chain of Thought (CoT)等高级提示技巧,可以显著提高大模型在推理任务上的表现。
3.1 使用str.format语言构建模版
在LangChain的默认设置下, PromptTemplate 使用 Python 的 str.format() 方法进行模板化。
Python的str.format()方法是一种字符串格式化的手段,允许我们在字符串中插入变量。使用这种方法,可以创建包含占位符的字符串模板,
占位符由花括号{}标识。调用format()方法时,可以传入一个或多个参数,这些参数将被顺序替换进占位符中。str.format()提供了灵活的方式来构造字符串,
支持多种格式化选项,包括数字格式化、对齐、填充、宽度设置等。
在LangChain中,基本采用了Python的原生
str.format()方法对输入数据进行格式化,这样在模型接收输入前,可以根据需要对数据进行预处理和结构化,以此来引导大模型进行更准确的推理。
#PromptTemplate
from langchain.prompts import PromptTemplate
prompt_template = PromptTemplate.from_template(
"请给我一个关于{topic}的{type}解释。"
)
prompt= prompt_template.format(type="详细", topic="量子力学")
#ChatPromptTemplate
from langchain_core.prompts import ChatPromptTemplate
chat_template = ChatPromptTemplate.from_messages(
[
("system", "你是一个有帮助的AI机器人,你的名字是{name}。"),
("human", "你好,最近怎么样?"),
("ai", "我很好,谢谢!"),
("human", "{user_input}"),
]
)
messages = chat_template.format_messages(name="小明", user_input="你叫什么名字?")
result = chat.invoke(messages)
print(result.content)
从输出上看,其构造函数在实例化prompt_template时,主要由两个关键参数进行指定:
- input_variables:这是一个列表,包含模板中需要动态填充的变量名。这些变量名在模板字符串中以花括号(如{name})标记。通过指定这些变量,可以在后续过程中动态地替换这些占位符。
- template:这是定义具体提示文本的模板字符串。它可以包含静态文本和input_variables列表中指定的变量占位符。当调用format方法时,这些占位符会被实际的变量值替换,生成最终的提示文本。
3.2 构造Few-Shot模版
在LangChain中,很多的功能抽象、链路抽象本质上都是在对大模型的“涌现能力”能够应用落地的一种具体实现方法,而其推理的不稳定,在不修改模型本身参数(微调)的情况下,模型涌现能力极度依赖对模型的提示过程,即对同样一个模型,不同的提示方法将获得质量完全不同的结果。最为简单的提示工程的方法就是通过输入一些类似问题和问题答案,让模型参考学习,并在同一个prompt的末尾提出新的问题,依次提升模型的推理能力。
from langchain.prompts import (
ChatPromptTemplate,
FewShotChatMessagePromptTemplate,
)
examples = [
{"input": "罗杰有五个网球,他又买了两盒网球,每盒有3个网球,请问他现在总共有多少个网球?",
"output": "罗杰一开始有五个网球,又购买了两盒网球,每盒3个,共购买了6个网球,因此现在总共由5+6=11个网球。因此答案是11。"},
{"input": "食堂总共有23个苹果,如果他们用掉20个苹果,然后又买了6个苹果,请问现在食堂总共有多少个苹果?",
"output": "食堂最初有23个苹果,用掉20个,然后又买了6个,总共有23-20+6=9个苹果,答案是9。"},
{"input": "杂耍者可以杂耍16个球。其中一半的球是高尔夫球,其中一半的高尔夫球是蓝色的。请问总共有多少个蓝色高尔夫球?",
"output": "总共有16个球,其中一半是高尔夫球,也就是8个,其中一半是蓝色的,也就是4个,答案是4个。"},
]
# This is a prompt template used to format each individual example.
example_prompt = ChatPromptTemplate.from_messages(
[
("human", "{input}"),
("ai", "{output}"),
]
)
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples,
)
final_prompt = ChatPromptTemplate.from_messages(
[
few_shot_prompt,
("human", "{input}"),
]
)
chain = final_prompt | chat
response = chain.invoke({"input": "艾米需要4分钟才能爬到滑梯顶部,她花了1分钟才滑下来,水滑梯将在15分钟后关闭,请问在关闭之前她能滑多少次?"})
response.content
3.3 示例选择器
from langchain.prompts.few_shot import FewShotChatMessagePromptTemplate
from langchain.prompts import ChatPromptTemplate
examples = [
# 数学推理
{
"question": "小明的妈妈给了他10块钱去买文具,如果一支笔3块钱,小明最多能买几支笔?",
"answer": "小明有10块钱,每支笔3块钱,所以他最多能买3支笔,因为3*3=9,剩下1块钱不够再买一支笔。因此答案是3支。"
},
{
"question": "一个篮球队有12名球员,如果教练想分成两个小组进行训练,每组需要有多少人?",
"answer": "篮球队总共有12名球员,分成两个小组,每组有12/2=6名球员。因此每组需要有6人。"
},
# 逻辑推理
{
"question": "如果所有的猫都怕水,而Tom是一只猫,请问Tom怕水吗?",
"answer": "根据题意,所有的猫都怕水,因此作为一只猫的Tom也会怕水。所以答案是肯定的,Tom怕水。"
},
{
"question": "在夏天,如果白天温度高于30度,夜晚就会很凉爽。今天白天温度是32度,请问今晚会凉爽吗?",
"answer": "根据题意,只要白天温度高于30度,夜晚就会很凉爽。今天白天的温度是32度,超过了30度,因此今晚会凉爽。"
},
# 常识问题
{
"question": "地球绕太阳转一圈需要多久?",
"answer": "地球绕太阳转一圈大约需要365天,也就是一年的时间。"
},
{
"question": "水的沸点是多少摄氏度?",
"answer": "水的沸点是100摄氏度。"
},
# 文化常识
{
"question": "中国的首都是哪里?",
"answer": "中国的首都是北京。"
},
{
"question": "世界上最长的河流是哪一条?",
"answer": "世界上最长的河流是尼罗河。"
},
]
#如上所示的示例中涵盖了数学推理、逻辑推理、常识问题以及文化常识四种不同的语义场景,并为每个场景提供了两个问题和答案。接下来将上述示例构建成提示模版
example_prompt = ChatPromptTemplate.from_messages(
[
("human", "{question}"),
("ai", "{answer}"),
]
)
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples,
)
print(few_shot_prompt.format())
final_prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个无所不能的人,无论什么问题都可以回答。"),
few_shot_prompt,
("human", "{input}"),
]
)
from langchain_openai import ChatOpenAI
chain = final_prompt | chat
chain.invoke({"input": "世界上最高的山峰是哪一座"})
上述过程,其实就对应了我们之前提到的问题,针对“世界上最高的山峰是哪一座?”这类问题,实际上只需输入与文化常识相关的提示就足够了。而如果要实现这一功能,就需要借助LangChain中的example_selector模块。在该模块中,有如下两个参数需要关注:
- example_selector :负责为给定输入选择少数样本(以及它们返回的顺序)。它们实现了 BaseExampleSelector 接口。一个常见的例子是向量存储支持的 SemanticSimilarityExampleSelector - example_prompt :通过其 format_messages 方法将每个示例转换为 1 条或多条消息。一个常见的示例是将每个示例转换为一条人工消息和一条人工智能消息响应,或者一条人工消息后跟一条函数调用消息。
LangChain已经内置了多个预定义的示例选择器,每种选择器都有其特定的功能和适用场景。在这个案例中,我们先以SemanticSimilarityExampleSelector为例进行探索。这个选择器的目的是在给定的示例集合中选出与输入在语义上最接近的示例。主要的实现步骤如下:
- 向量化表示:首先,输入文本和示例集中的每个示例都会被转换成向量化的表示。通过Embedding模型将文本转换成高维空间中的点,其中语义上相似的文本会被映射到空间中相近的位置。
- 计算语义相似度:一旦得到了输入和示例的向量化表示,下一步是计算输入与每个示例之间的语义相似度。通过计算向量之间的距离来实现,常见的度量方式包括余弦相似度、欧氏距离等。
- 选择最相似的示例:基于计算出的相似度,选择一个或多个与输入最相似的示例。这个选择过程可以是简单地选取相似度最高的示例,或者根据相似度分布采取更复杂的策略,例如选择相似度高于某个阈值的所有示例。
get_ipython().system(' pip install chromadb')
from langchain.prompts import SemanticSimilarityExampleSelector
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# 使用OpenAI的Embedding模型构建向量,并存储至chromadb向量数据库中。
to_vectorize = [" ".join(example.values()) for example in examples]
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002",api_key=openai.api_key ,base_url=openai.api_base)
vectorstore = Chroma.from_texts(to_vectorize, embeddings, metadatas=examples)
# 创建完矢量存储后,接下来需要创建 example_selector 。可以通过`k`参数指定获取多少个与输入问题最相关的示例。这里我们选择2。
example_selector = SemanticSimilarityExampleSelector(
vectorstore=vectorstore,
k=2,
)
example_selector.select_examples({"input": "内蒙古的省会是哪里"})
example_selector = SemanticSimilarityExampleSelector(
vectorstore=vectorstore,
k=2,
)
example_selector.select_examples({"input": "罗杰有五个网球,他又买了两盒网球,每盒有3个网球,请问他现在总共有多少个网球?"})
从上面两个示例我们观察到,在处理基于文化常识的查询时(例如,询问“内蒙古的省会是哪里?”),选定的few-shot模板会来源自文化常识类别。相反,当遇到需要推理的问题时,则倾向于选择我们预先定义好的数学推理类提示示例。这种动态匹配策略展示了利用语义相似性选择器在大语言模型中进行精准模板选择的能力,从而有效地应对不同类别的查询,确保模型输出的相关性和准确性。
from langchain.prompts import (
ChatPromptTemplate,
FewShotChatMessagePromptTemplate,
)
# 创建一个 FewShotChatMessagePromptTemplate 对象
few_shot_prompt = FewShotChatMessagePromptTemplate(
input_variables=["input"], # 定义输入变量的列表
example_selector=example_selector, # 使用动态的示例选择器
# 定义每一轮对话的格式化文本
example_prompt=ChatPromptTemplate.from_messages(
[("human", "{question}"), ("ai", "{answer}")]
),
)
print(few_shot_prompt.format(input="罗杰有五个网球,他又买了两盒网球,每盒有3个网球,请问他现在总共有多少个网球?"))
print(few_shot_prompt.format(input="月亮每天什么时候出现"))
final_prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个无所不能的人,无论什么问题都可以回答。"),
few_shot_prompt,
("human", "{input}"),
]
)
from langchain_openai import ChatOpenAI
chain = final_prompt | chat
chain.invoke({"input": "月亮每天什么时候出现"})
from langchain_openai import ChatOpenAI
chain = final_prompt | chat
response = chain.invoke({"input": "内蒙的省会是哪座城市?"})
通过对SemanticSimilarityExampleSelector的应用,我们展示了如何动态地选择适合各种输入提示的示例模板。在LangChain框架中,示例选择器的功能和作用依赖于其具体的定义和实现。我们使用的SemanticSimilarityExampleSelector示例选择器,该过程涉及到示例的向量化表示、相似度计算和返回Tok这样的流程。到目前为止,LangChain已经定义了以下四种示例选择器,每种都有其独特的选择机制:
- Similarity:基于输入与示例之间的语义相似度来选择示例。这种方法通过比较语义内容的接近程度来确定最相关的示例。
- MMR (Maximum Margin Relevance):根据输入与示例之间的最大边际相关性来挑选示例。这种方法旨在平衡相关性和多样性,通过选择既相关又能提供新信息的示例。
- Length:依据指定长度内能够容纳的示例数量来进行选择。这个方法简单直接,特别适用于需要控制输出长度的场景。
- Ngram:通过计算输入与示例之间的n-gram重叠来选择示例。这种方法重视文本表面的匹配度,适用于需要精确文本匹配的情境。
3.4 自定义示例选择器
LangChain的ExampleSelector模块封装了一系列较为通用的示例选择器,例如我们上一小节使用的SemanticSimilarityExampleSelector,它能够基于语义相似度来选择最相关示例的,已经能够满足多数提示示例使用场景的需求。然而,现实中根据不同的业务需求,可能会遇到这些通用选择器无法完全满足特定需求的情况。
在LangChain中,Example Selector的基本接口定义如下:
class BaseExampleSelector(ABC):
"""用于选择要包含在提示中的示例的接口。"""
@abstractmethod
def select_examples(self, input_variables: Dict[str, str]) -> List[dict]:
"""根据输入选择使用哪些示例。"""
@abstractmethod
def add_example(self, example: Dict[str, str]) -> Any:
"""向存储中添加新的示例。"""
ABC,全称为“Abstract Base Class”(抽象基类),是Python中abc模块的一部分。在 Python 中,抽象基类用于定义其他类必须遵循的基本接口或蓝图,但不能直接实例化。其主要目的是为了提供一种形式化的方式来定义和检查子类的接口。抽象基类中,可以定义抽象方法,它没有实现(也就是说,它没有方法体)。任何继承该抽象基类的子类都必须提供这些抽象方法的实现。
从上述基本接口来看,它需要定义的唯一方法是 select_examples 方法,其接受输入变量,然后返回示例列表。如何选择这些示例取决于每个具体的实现,也就是我们自定义的逻辑。
为了演示示例选择器的自定义过程,我们设计这样一个简单的场景:聊天机器人的回答选择器。在这个场景中,聊天机器人需要根据用户的输入从一个预设的回答库中选择最合适的回答。这个预设库包含了多个输入-回答对,机器人的任务是找到与用户输入在长度上最接近的问题,然后返回相应的预设回答。通过这种方法来可以帮助机器人处理未知或罕见的用户输入,通过匹配相近长度的问题来给出一个看似合适的回答,增加用户满意度。
examples = [
{"input": "你好吗?", "output": "我很好,谢谢!你呢?"},
{"input": "你是谁?", "output": "我是一个聊天机器人。"},
{"input": "你能做什么?", "output": "我可以回答简单的问题,比如现在的时间或天气。"},
{"input": "现在几点了?", "output": "抱歉,我无法提供实时信息。"},
{"input": "你喜欢音乐吗?", "output": "我不能听音乐,但我可以帮你找到音乐信息。"},
{"input": "告诉我一些关于中国的事情。", "output": "中国是一个拥有悠久历史和丰富文化的国家。"},
{"input": "最近有什么好玩的电影吗?", "output": "我不太清楚当前的电影信息,但我推荐你查看电影推荐网站。"},
{"input": "你能帮我学习编程吗?", "output": "当然,我可以提供一些学习资源和编程练习。"}
]
这个examples列表包含了几个不同长度的问题及其对应的答案。根据聊天机器人的回答选择器的需求设定,我们设计的示例选择器功能就应该是:当用户输入一段文本时,自定义示例选择器的的select_examples方法会根据输入的长度选择一个最接近的问题,并返回那个问题的答案。这样,即使用户的问题没有直接出现在预设的问题列表中,聊天机器人也能提供一个相关的回答。
from langchain_core.example_selectors.base import BaseExampleSelector
class ChatbotExampleSelector(BaseExampleSelector):
def __init__(self, examples):
# examples是一个列表,包含多个字典,每个字典都有'input'和'output'键
self.examples = examples
def add_example(self, example):
# 向examples列表添加一个输入-输出对
self.examples.append(example)
def select_examples(self, input_variables):
# 此方法找到与用户输入长度最接近的示例,并返回相应的输出
new_word = input_variables["input"]
new_word_length = len(new_word)
best_match = None
## 声明一个无穷大的变量
smallest_diff = float("inf")
for example in self.examples:
current_diff = abs(len(example["input"]) - new_word_length)
if current_diff < smallest_diff:
smallest_diff = current_diff
best_match = example
# 如果找到了最佳匹配项,返回相应的输出;否则,返回None
return [best_match] if best_match else []
example_selector = ChatbotExampleSelector(examples)
example_selector.select_examples({"input": "你好呀。"})
example_selector.select_examples({"input": "我特别的喜欢打篮球"})
example_selector.select_examples({"input": "今天的天气很好,能推荐一个好玩的去处吗?"})
example_selector.select_examples({"input": "今天的天气很好,非常适合春游,能帮我推荐一个适合全家人出游的好去处吗?"})
example_selector.add_example({"input": "春天到了,大家都喜欢出去春游,但是很多地方并不是很好,请问有推荐码?", "output": "如果你喜欢春天春游的话,你可以去一些国家公园,景色非常好。"})
example_selector.select_examples({"input": "今天的天气很好,非常适合春游,能帮我推荐一个适合全家人出游的好去处吗?"})
这里就可以匹配到最新添加的提示模版了。而对接大模型推理过程就和常规的使用方式无异。首先在对话模版中接入ChatbotExampleSelector示例选择器,代码如下:
from langchain.prompts import (
ChatPromptTemplate,
FewShotChatMessagePromptTemplate,
)
# 创建一个 FewShotChatMessagePromptTemplate 对象
few_shot_prompt = FewShotChatMessagePromptTemplate(
input_variables=["input"], # 定义输入变量的列表
example_selector=example_selector, # 使用动态的示例选择器
# 定义每一轮对话的格式化文本
example_prompt=ChatPromptTemplate.from_messages(
[("human", "{input}"), ("ai", "{output}")]
),
)
print(few_shot_prompt.format(input="今天的天气很好,非常适合春游,能帮我推荐一个适合全家人出游的好去处吗?"))
print(few_shot_prompt.format(input="你好呀。"))
final_prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个无所不能的人,无论什么问题都可以回答。"),
few_shot_prompt,
("human", "{input}"),
]
)
from langchain_openai import ChatOpenAI
chain = final_prompt | chat
chain.invoke({"input": "你好呀"})
from langchain_openai import ChatOpenAI
chain = final_prompt | chat
chain.invoke({"input": "你是谁?"})
from langchain_openai import ChatOpenAI
chain = final_prompt | chat
response = chain.invoke({"input": "今天的天气很好,非常适合春游,能帮我推荐一个适合全家人出游的好去处吗?"})
response.content