随着大模型的飞速发展,Prompt Engineering 正逐渐成为一项关键技能。它不仅仅是一门技术,更是一种艺术。通过精心设计的提示(prompt),我们可以引导 AI 模型生成我们所需要的信息、数据或者解决方案而不需要对模型进行任何改变。仅通过改变交互形式即可激发出大模型的巨大潜力,这即是 Prompt Engineering 的魅力所在。
1. 什么是 Prompt Engineering ?
Prompt Engineering 是设计和优化与 AI 模型交互的提示的过程。这些提示是用户输入的文本,用于引导模型生成特定的输出。它是一个目标导向的过程,旨在创造能够激发模型最佳性能的提示。
关于如何设计出适合自己任务的 prompt,这是一门学问,或者说是一门技术。吴恩达老师在他的 ChatGPT Prompt Engineering for Developers 公开课中,给出了两个大的原则:第一条原则是写出清晰而具体的指示,第二条原则是给模型思考的时间。而在 OpenAI 的官方文档 GPT 最佳实践 中,也给出了和上面这两大原则一脉相承的6大策略。分别是:
- 写清晰的指示
- 给模型提供参考(也就是示例)
- 将复杂任务拆分成子任务
- 给 GPT 时间思考
- 使用外部工具
- 反复迭代问题
其实仔细观察这六条策略就会发现,这份指导模型的策略跟我们人类解决问题的过程十分相似。将这些策略的主体换成你自己,你会发现它们也能指导你的思维过程,让你解决问题的思路更加清晰、科学。这其实也就是人工智能的对人类思维的模仿过程。
2. Prompt 的结构
Prompt 的结构设计对于引导模型生成预期的输出至关重要,一般来说精心设计的 Prompt 往往能让你事半功倍。具体来说,一个 Prompt 大致包含以下几个部分:
-
指令(Instruction) :
- 这是Prompt中告诉模型需要做什么以及怎么做的部分,比如如何使用外部信息、如何处理查询、如何构造输出以及当模型无法直接生成答案时可以使用那些外部工具等等。
-
上下文(Context) :
- 提供背景信息,充当模型的额外知识来源,以帮助模型理解任务的背景和环境。上下文信息可以通过外部数据库检索获得(RAG技术)或者通过使用一些外部工具如搜索引擎等获取。
-
输入数据(Prompt Input) :
- 这是模型需要处理的具体信息。在不同的任务中,输入数据可以是文本、图片、声音等。这部分信息通常作为变量,在调用模型之前传递给提示模板,以形成具体的提示。
-
输出提示器(Output Indicator) :
- 标记要生成的文本的开始。就像我们数学考试时,不管题会不会总是要先写个解一样(doge),LangChain 中的代理在构建提示模板时,经常会用一个 “Thought:”(思考)作为引导词,指示模型开始输出自己的推理(Reasoning)
-
格式说明(Format Specification)[可选]:
- 指定输出的格式或结构可以引导模型尽可能输出我们期望的结果。例如告诉模型:“请以列表的形式列出主要观点” 或者 “你的返回格式应该遵循如下json格式.....” ,那么模型的回复大概率就是你想要的格式。这一部分一般加在指令的末尾。
-
示例(Examples)[可选] :
- 提供一些示例以帮助模型更好地理解我们预期的输出样式。例如,我们可以给出一个简短的文章和相应的总结示例,然后模型就会从这个示例中学到一些经验以辅助模型后续的输出。
-
目标(Objective)[可选] :
- 明确任务的目标,帮助模型集中于特定的输出结果。例如总是在 Prompt 最后或者开头强调我们的最终目的,从而让模型时刻明确任务的最终目标,防止模型的思考跑偏。
-
角色扮演(Role-Playing)[可选] :
- 在某些情况下,Prompt 可能包含角色扮演的元素,指导模型以特定角色的视角或风格生成输出。这部分常常跟指令部分融合,一个常见用例是在 Prompt 开头告诉模型:“你是一个有用的XX助手”,这会让它更认真地对待自己的角色。而除了这种用法外还有像 CAMEL 这种通过多 AI 角色扮演进行交互的模拟交互框架。
3. LangChain 提示模板的类型
LangChain 中提供 String(StringPromptTemplate)和 Chat(BaseChatPromptTemplate)两种基本类型的模板,并基于它们构建了不同类型的提示模板:
在 LangChain 中上述模版的导入方式如下:
from langchain.prompts.prompt import PromptTemplate
from langchain.prompts import FewShotPromptTemplate
from langchain.prompts.pipeline import PipelinePromptTemplate
from langchain.prompts import ChatPromptTemplate
from langchain.prompts import (
ChatMessagePromptTemplate,
SystemMessagePromptTemplate,
AIMessagePromptTemplate,
HumanMessagePromptTemplate,
)
4. LangChain 中提示模版的使用
4.1 PromptTemplate 的使用
方式一:
from langchain import PromptTemplate
template = """\
你是业务咨询顾问。
你给一个销售{product}的电商公司,起一个好的名字?
"""
prompt = PromptTemplate.from_template(template)
print(prompt.format(product="鲜花"))
LangChain 中使用 PromptTemplate 的一种方法是使用 PromptTemplate 类的 from_template 方法来创建一个提示模板(prompt)。这个方法会根据传入的模板字符串自动识别并提取其中的变量(也就是模版字符串中用大括号围起来的占位符),然后创建一个 PromptTemplate 对象。这个对象可以用 PromptTemplate 类的 format 方法来格式化提示,即将变量替换为具体的值,生成最终的提示文本。
方式二:
prompt = PromptTemplate(
input_variables=["product", "market"],
template="你是业务咨询顾问。对于一个面向{market}市场的,专注于销售{product}的公司,你会推荐哪个名字?",
)
print(prompt.format(product="鲜花", market="高端"))
使用 PromptTemplate 的另一种方法则是直接定义模板字符串并创建 PromptTemplate 对象,这种方法需要在 PromptTemplate 对象中手动指定模版字符串和输入变量等参数,后续的格式化方法跟第一种方法一致。
这两种创建方式的主要区别在于代码的简洁性和可读性。
-
使用
.from_template方法创建PromptTemplate对象:这种方式更加简洁,只需要传入模板字符串即可,PromptTemplate类会自动识别模板中的变量并创建相应的PromptTemplate对象。 -
直接定义模板字符串并创建
PromptTemplate对象:这种方式需要手动指定input_variables和template参数,代码相对冗长,但可以更清晰地看到模板字符串和输入变量的定义。
这两种方式都可以创建
PromptTemplate对象用于后续的提示生成。选择哪种方式取决于你的编码风格和项目需求。如果更注重代码的简洁性,可以选择.from_template方法;如果需要更清晰地观察模板字符串和输入变量的定义,可以选择直接定义的方式。
4.2 ChatPromptTemplate 的使用
对于 OpenAI 推出的 ChatGPT 这一类的聊天模型,LangChain 也提供了一系列的模板,这些模板跟普通模板的不同之处是它们有对应的角色(Role),这也是文本模型跟聊天模型的主要区别之一。通常来讲,聊天模型中的角色一般分为三类,分别是 system(系统),user(用户) 和 assistant(助手)。
对于 gpt-3.5-turbo 和 GPT-4 这类聊天模型,传递给模型的消息需要以消息对象数组的形式呈现,每个对象都需明确指定角色(可以是 “系统”、“用户” 或 “助理” )和所要传达的内容。对话的构成可以仅包含单一回合的交互,也可以包含多轮次的交流。
下面代码展示的是 OpenAI 的 Chat Model 中的各种消息角色:
import openai
openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
{"role": "user", "content": "Where was it played?"}
]
)
- 系统消息:这些消息用于为对话设定背景/基调,比如定义助手的响应风格或提供特定的行为指导。系统消息并不是必须的,但它们的使用可以帮助塑造对话的流程和助手的表现。若未设置系统消息,助手通常会采用默认设置来响应,如将自己视作一个提供帮助的助手。
- 用户消息:这类消息包含了用户向助手提出的请求或评论,它们是触发助手响应的直接原因。
- 助理消息:这些消息记录了助手的历史回复,有助于维持对话的连贯性。此外,开发者也可以预先编写助理消息来人为提供对话的背景信息以指导模型向着我们预期的方向进行思考。
在组织对话时,通常的做法是先设定一条系统消息来明确助手的角色和行为准则,随后是用户消息和助理消息的交替出现,形成自然的对话流程。这样的结构设计有助于创建一个清晰、有序且目标明确的交流环境。
下面是 ChatPromptTemplate 这一模版的使用示例:
# 导入聊天消息类模板
from langchain.prompts import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
AIMessagePromptTemplate,
)
# 模板的构建
template = "你是一位{product}专业顾问,负责为专注于{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]
)
print(prompt_template)
# 格式化提示消息生成提示
prompt = prompt_template.format_prompt(
product_detail="创新的鲜花设计。", product="鲜花装饰"
).to_messages()
这里我们使用了系统消息(SystemMessagePromptTemplate)和用户消息(HumanMessagePromptTemplate),因为对话比较简短所以没有使用助手消息(当你想要进行多轮对话的时候可以通过助手来存储以前的响应)。这些模版的使用跟 PromptTemplate 类差不多,都是通过 .from_template 和 .format 方法来格式化模版内容。不同的是聊天模型处理的是聊天提示模版,需要使用 ChatPromptTemplate.from_messages 组合多个角色的消息模版以形成一个完整的聊天提示,然后用于生成聊天模型的输入。
4.3 FewShotPromptTemplate
4.3.1 Few-Shot 的介绍
Few-Shot(少样本)、One-Shot(单样本) 和与之对应的 Zero-Shot(零样本) 的概念都起源于机器学习,这些技术的出现是为了解决某些现实场景下无法获取大量训练数据而导致模型极易过拟合的问题,这本质上其实是对人类智能的模拟。对人类而言,即使是几岁的孩童,也能很快地理解新的视觉和语言概念并识别以前只见过一两次的陌生物体或者说概念,而这是深度学习模型所无法做到的,因为深度学习模型的训练往往基于大量训练数据。
-
对于 Few-Shot Learning,一个重要的参考文献是 2016 年 Vinyals, O. 的论文 小样本学习的匹配网络。这篇论文提出了一种新的学习模型——匹配网络(Matching Networks),专门针对单样本学习(One-Shot Learning)问题设计。
-
对于 Zero-Shot Learning,一个代表性的参考文献是 Palatucci, M. 在2009年提出的Zero-shot Learning with Semantic Output Codes,这篇论文首次提出了零次学习(Zero-Shot Learning)的概念,其中提出的学习系统可以根据类的语义描述来识别之前从未见过的类。
而除了传统的机器学习,小样本学习技术在大模型中也有广泛应用。OpenAI 在 GPT-3 的论文 Language models are Few-Shot learners 中,直接指出:GPT-3 模型作为一个大型的自我监督学习模型,通过提升模型规模,实现了出色的 Few-Shot 学习性能。这篇论文为大语言模型可以进行 Few-Shot 学习提供了扎实的理论基础。
下图是 GPT-3 论文中给出的 GPT-3 在翻译任务中,通过 FewShot 提示完成翻译的例子:
4.3.2 FewShotPromptTemplate 的使用
# 1. 创建一些示例
samples = [
{
"flower_type": "玫瑰",
"occasion": "爱情",
"ad_copy": "玫瑰,浪漫的象征,是你向心爱的人表达爱意的最佳选择。",
},
{
"flower_type": "康乃馨",
"occasion": "母亲节",
"ad_copy": "康乃馨代表着母爱的纯洁与伟大,是母亲节赠送给母亲的完美礼物。",
},
{
"flower_type": "百合",
"occasion": "庆祝",
"ad_copy": "百合象征着纯洁与高雅,是你庆祝特殊时刻的理想选择。",
},
{
"flower_type": "向日葵",
"occasion": "鼓励",
"ad_copy": "向日葵象征着坚韧和乐观,是你鼓励亲朋好友的最好方式。",
},
]
# 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}",
)
# 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="爱情"))
上面的代码是 LangChain 中少样本提示模版的一个使用示例。这里我们先定义了一个原始提示模板,然后使用之前定义的示例数据 samples 和提示模板 prompt_sample 创建了一个 FewShotPromptTemplate 对象。其中 suffix 参数定义了在示例数据之后添加的文本。上面这个例子的输出结果为:
鲜花类型: 玫瑰
场合: 爱情
文案: 玫瑰,浪漫的象征,是你向心爱的人表达爱意的最佳选择。
鲜花类型: 康乃馨
场合: 母亲节
文案: 康乃馨代表着母爱的纯洁与伟大,是母亲节赠送给母亲的完美礼物。
鲜花类型: 百合
场合: 庆祝
文案: 百合象征着纯洁与高雅,是你庆祝特殊时刻的理想选择。
鲜花类型: 向日葵
场合: 鼓励
文案: 向日葵象征着坚韧和乐观,是你鼓励亲朋好友的最好方式。
鲜花类型: 野玫瑰
场合: 爱情
可以看到,suffix 添加的文本只作用于我们的输入而不是示例数据。然后我们就能把这个模版传递给大模型并获取输出了。
# 4. 把提示传递给大模型
import os
os.environ["OPENAI_API_KEY"] = '你的Open AI Key'
from langchain.llms import OpenAI
model = OpenAI(model_name='gpt-3.5-turbo-instruct')
result = model(prompt.format(flower_type="野玫瑰", occasion="爱情"))
print(result)
4.3.3 示例选择器
假设我们的示例数据比较多,那要是每次运行都要把所有示例跟我们想问的问题一起传入模型,这会消耗大量的 Token,代价比较昂贵。而 LangChain 中的示例选择器就是用来解决这个问题的。示例选择器每次会在示例数据中选出跟我们的输入最相关的几个样本来生成提示,这跟 RAG 的过程很相似,在尽可能保证输出效果的同时缓解了输入全部示例时 Token 消耗过大的问题。
下面是示例选择器的一个简单使用:
# 5. 使用示例选择器
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
# 初始化示例选择器
example_selector = SemanticSimilarityExampleSelector.from_examples(
samples,
OpenAIEmbeddings(),
Chroma,
k=1
)
# 创建一个使用示例选择器的FewShotPromptTemplate对象
prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=prompt_sample,
suffix="鲜花类型: {flower_type}\n场合: {occasion}",
input_variables=["flower_type", "occasion"]
)
print(prompt.format(flower_type="红玫瑰", occasion="爱情"))
这里我们使用 SemanticSimilarityExampleSelector 类的 from_examples 方法初始化了一个示例选择器,需要我们传入示例数据、存储数据的向量数据库、Embedding 模型以及检索返回的相关示例条数作为参数。需要向量数据库和 Embedding 模型是因为示例选择器是通过向量的语义相似度对比来进行筛选的,因此需要把预设的示例数据转化成语义向量存进数据库中,在运行示例选择器时再根据向量相似度进行检索筛选。
总结
提供示例对于解决某些任务至关重要,通常情况下,FewShot 的方式能够激发大模型的潜力,显著提高模型回答的质量。不过,如果少样本提示效果不佳,这可能是因为模型本身在任务上学习不足。这时可能就需要对模型本身进行调整(微调、对齐)或者使用更高级的提示工程比如思维链或者思维树来提升模型的思考质量。