简介

梅子黄时日日晴,小溪泛尽却山行。绿阴不减来时路,添得黄鹂四五声。小伙伴们好,我是微信公众号《小窗幽记机器学习》的小编:卖西瓜的小男孩。更多、更新文章欢迎关注微信公众号:小窗幽记机器学习。后续会持续整理模型加速、模型部署、模型压缩、LLM、AI艺术等系列专题,敬请关注。本文微信公众号链接:LLM 系列 | 04:ChatGPT Prompt编写指南

 

5月初吴恩达老师与OpenAI合作推出的《ChatGPT Prompt Engineering for Developers》课程,以指导开发者如何构建 Prompt 并基于 OpenAI API 构建各种基于 LLM 的应用。整个课程很简短,只有 9 个章节(掐头去尾,核心的课程其实只有7个章节),整体时长约一个半小时。

  1. Prompt的构建原则(Guidelines for Prompting)
  2. 如何迭代优化(Prompt Itrative)
  3. 文本总结(Summarizing)
  4. 文本推断(Inferring)
  5. 文本转换(Transforming)
  6. 文本扩展(Expanding)
  7. 聊天机器人(Chatbot)

今天这篇小作文从Prompt的构建原则开始,简要介绍构建Prompt的2大原则,并以具体的纯中文使用示例进一步演示说明。

原始的视频链接: https://learn.deeplearning.ai/chatgpt-prompt-eng/lesson/1/introduction

环境准备

库安装

首先需要安装所需第三方库:

openai

pip install openai

设置api key

系统环境变量中配置变量:

# 将自己的 API-KEY 导入系统环境变量
export OPENAI_API_KEY='sk-xxxx'

在代码中设置 openai API key:

import openai
import os

openai.api_key  = 'sk-xxxx'

此外,在网络访问上如果需要代理的话,可以如下设置:

# 如果网络原因, 需要设置代理的话
import os
os.environ['HTTP_PROXY'] = "代理的地址"
os.environ['HTTPS_PROXY'] = "代理的地址"

OpenAI接口封装

OpenAI 通过提供 ChatCompletion API 来进行处理。在这里先将它封装成一个函数,调用该函数输入 Prompt 其将会给出对应的 Completion。

# 一个封装 OpenAI 接口的函数,参数为 Prompt,返回对应结果
def get_completion(prompt, model="gpt-3.5-turbo"):
    '''
    prompt: 对应的 prompt
    model: 调用的模型,如果使用ChatGPT,该值取gpt-3.5-turbo,有内测资格的用户可以选择 gpt-4
    '
''
    messages = [{"role""user""content": prompt}]
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=0, # 模型输出的温度系数,控制输出的随机程度。如果为0,就是确切的结果,每次结果是相同的
    )
    # 调用 OpenAI 的 ChatCompletion 接口
    return response.choices[0].message["content"]

编写prompt的原则

  • 原则 1: 编写清晰、具体的指令
  • 原则 2: 给模型些时间思考

原则1:编写清晰、具体的指令

在与ChatGPT交互过程应该提供尽可能清晰、具体的指令(Prompt) 来表达希望模型所执行的操作。这将引导模型给出正确的输出,并减少得到无关或不正确回复的可能。编写清晰的指令不意味着简短的指令,因为在许多情况下,更长的提示指令(Prompt)实际上更清晰且提供了更多上下文。这实际上可能带来更详细、更相关的输出。具体可以通过以下一些策略来实现。

策略 1: 利用分隔符清晰地表示输入的不同部分

使用分隔符能够有助于清晰地表示输入的不同部分,分隔符可以是:```,"",<>,<tag>,<\tag>等。

可以使用任何明显的标点符号将特定的文本部分与提示的其余部分分开。通过这个标记符号可以使模型明确知道这是一个单独部分。此外使用分隔符可以避免Prompt注入。Prompt注入是指如果用户将某些输入添加到Prompt中,则可能会向模型提供与原本想要执行的操作相冲突的指令,从而使其遵循冲突的指令而不是执行真正想要的操作。比如输入里面可能包含其他指令,会覆盖掉真正要执行的指令。

以下是一个例子,给出一段话并要求 ChatGPT 进行总结,在该示例中使用 ``` 来作为分隔符

text = f"""
在图像生成领域,以 Stable Diffusion 为代表的扩散模型已然成为当前占据主导地位的范式。但扩散模型依赖于迭代推理,这是一把双刃剑,因为迭代方法可以实现具有简单目标的稳定训练,但推理过程需要高昂的计算成本。

在 Stable Diffusion 之前,生成对抗网络(GAN)是图像生成模型中常用的基础架构。相比于扩散模型,GAN 通过单个前向传递生成图像,因此本质上是更高效的。但由于训练过程的不稳定性,扩展 GAN 需要仔细调整网络架构和训练因素。因此,GAN 方法很难扩展到非常复杂的数据集上,在实际应用方面,扩散模型比 GAN 方法更易于控制,这是 GAN 式微的原因之一。

当前,GAN 主要是通过手动注释训练数据或先验 3D 模型来保证其可控性,这通常缺乏灵活性、精确性和通用性。然而,一些研究者看重 GAN 在图像生成上的高效性,做出了许多改进 GAN 的尝试。

最近,来自马克斯・普朗克计算机科学研究所、MIT CSAIL 和谷歌的研究者们研究了一种控制 GAN 的新方法 DragGAN,能够让用户以交互的方式「拖动」图像的任何点精确到达目标点。
"
""

Prompt及其执行代码如下:

# 需要总结的文本内容
prompt = f"""
把用三个反引号括起来的文本总结成一句话。
```{text}```
"""
# 指令内容,使用 ``` 来分隔指令和待总结的内容
response = get_completion(prompt)
print(response)

输出结果如下:

DragGAN是一种控制GAN的新方法,能够让用户以交互的方式「拖动」图像的任何点精确到达目标点,解决了GAN方法在可控性方面的不足。

策略 2: 结构化输出

结构化输出可以是 Json、HTML 等格式。这可以使模型的输出更容易被解析,例如可以在 Python 中将其读入字典或列表中。

以下示例中,要求 ChatGPT 生成三本书的标题、作者和类别, 并要求以 Json 的格式返回给,为便于解析,还指定了 Json格式的键。

prompt = f"""
生成三个虚构的书名,包含标题、作者和类别。\
以JSON格式输出,key如下:book_id, title, author, genre。value部分用中文表示。
"
""
response = get_completion(prompt)
print(response)

输出结果如下:

{
  "book1": {
    "title""迷失的时光",
    "author""张三",
    "genre""科幻"
  },
  "book2": {
    "title""爱情的迷宫",
    "author""李四",
    "genre""言情"
  },
  "book3": {
    "title""黑暗的秘密",
    "author""王五",
    "genre""悬疑"
  }
}

策略 3: 要求模型检查是否满足条件

任务执行的前提假设可能不满足,可以要求模型先检查这些假设,如果不满足,停止执行指令。还可以考虑潜在的边缘情况以及模型应该如何处理这些边缘情况,以避免意外的错误或结果。

在如下示例中,将分别给模型两段文本,分别是泡茶的步骤以及一段没有明确步骤的文本。我们将要求模型判断其是否包含一系列的指令步骤,如果包含则按照给定格式重新编写指令,否则回答未提供步骤。

text_1 = f"""
泡一杯茶非常简单!首先,你需要把一些水烧开。\
在水烧开的过程中,拿起一个杯子,把茶袋放进去。\
一旦水温足够热,就把水倒在茶袋上。让茶袋浸泡片刻。\
几分钟后,取出茶袋。如果你喜欢,\
可以加一些糖或牛奶调味。就这样!\
你就有了一杯美味的茶可以享用了。
"
""
prompt = f"""
你将获得由三个引号包围的文本。如果它包含一系列指令,\
则请使用以下格式重新编写这些指令:

步骤 1 - ...
步骤 2 - …

步骤 N - …

如果文本不包含一系列指令,则请简单地写下 \“未提供步骤\”。

\"\"\"{text_1}\"\"\"
"
""
response = get_completion(prompt)
print("Completion for Text 1:")
print(response)

输出结果如下:

Completion for Text 1:
步骤 1 - 把一些水烧开。
步骤 2 - 在水烧开的过程中,拿起一个杯子,把茶袋放进去。
步骤 3 - 一旦水温足够热,就把水倒在茶袋上。
步骤 4 - 让茶袋浸泡片刻。
步骤 5 - 几分钟后,取出茶袋。
步骤 6 - 如果你喜欢,可以加一些糖或牛奶调味。
步骤 7 - 就这样!你就有了一杯美味的茶可以享用了。

再举一个例子:

text_2 = f"""
今天阳光灿烂,鸟儿在欢唱。\
这是一个去公园散步的美好日子。\
花朵盛开,树木在微风中轻轻摇曳。\
人们四处走动,享受着这美好的天气。\
有些人正在野餐,而其他人则在玩游戏或者简单地躺在草地上放松。\
这是一个完美的日子,可以在户外度过时间,欣赏大自然的美丽。
"
""
prompt = f"""
你将获得由三个引号包围的文本。如果它包含一系列指令,\
则请使用以下格式重新编写这些指令:

步骤 1 - ...
步骤 2 - …

步骤 N - …

如果文本不包含一系列指令,则请简单地写下 \“未提供步骤\”。

\"\"\"{text_2}\"\"\"
"
""
response = get_completion(prompt)
print("Completion for Text 2:")
print(response)

输出结果如下:

Completion for Text 2:
“未提供步骤”

策略 4: "Few-shot" prompting

Few-shot prompting是在模型执行实际任务之前,提供给它少量待执行任务的示例。

例如,在以下的示例中,告诉模型其任务是以一致的风格回答问题,并先给它一个孩子和一个祖父之间的对话的例子。孩子说,“教我耐心”,祖父用这些隐喻回答。因此,由于已经告诉模型要以一致的语气回答,现在我们说“教我韧性”,由于模型已经有了这个少样本示例,它将以类似的语气回答下一个任务。

prompt = f"""
你的任务是以一致的风格回答。

<孩子>:教我耐心。\
<祖父>:刻出最深峡谷的河流,源于一泓不起眼的泉水;最宏伟的交响乐,起源于单一的音符;最复杂的挂毯,始于孤独的一根线。\
<孩子>:教我韧性。
"
""
response = get_completion(prompt)
print(response)

输出结果如下:

<祖父>:韧性就像是一棵树,需要经历风吹雨打才能成长茁壮。每一次挫折都是一次锻炼,只有坚持不懈,才能在生命的道路上走得更远更稳健。

原则 2: 让模型去思考

如果模型匆忙得出错误的结论,那么我们应该尝试重新构思Prompt,要求模型在提供最终答案之前进行一系列相关的推理。换句话说,如果我们给模型一个在短时间或用少量文字无法完成的任务,那么它可能会错误回答。这种情况对人来说也一样。对于复杂的数学问题,如果让某人在没有足够时间去计算而得到的答案,也可能是错的。因此,在这些情况下,我们可以指示模型花更多时间思考问题,这意味着它在任务上花费了更多的计算资源。

策略 1: 指定完成任务所需的步骤

接下来通过给定一个复杂任务,并指定完成该任务的一系列步骤,来展示这一策略的效果。

text = f"""
在一个迷人的小村庄里,\
兄弟姐妹杰克和吉尔踏上了一段从山顶井取水的任务。\
当他们欢快地唱着歌爬上山时,\
不幸降临了——杰克绊倒在一块石头上,\
沿着山坡滚下去,吉尔也随之跟着滚了下来。\
尽管稍微受了点伤,这对兄妹还是回到了家中,\
得到了温暖的拥抱。尽管发生了这个不幸的意外,\
他们的冒险精神依然没有减退,他们继续愉快地探索着。
"
""

prompt_1 和代码如下:

# example 1
prompt_1 = f"""
把下面用三个反引号包起来的文本做以下操作:
1- 用一句话概括这段文本
2- 把概括翻译成法语
3- 在法语概括中列出所有的名字
4- 输出一个JSON对象,包含以下键:french_summary, num_names

用换行符分隔你的答案。

Text:
```{text}```
"""

response = get_completion(prompt_1)
print("Completion for prompt 1:")
print(response)

输出结果如下:

prompt 1:
1- 兄弟姐妹在山上取水时发生意外,但最终平安回家并继续探索。
2- Les frères et sœurs ont eu un accident en allant chercher de l'eau sur la montagne, mais sont finalement rentrés chez eux en sécurité et ont continué à explorer joyeusement.
3- 杰克,吉尔
4- {"french_summary": "Les frères et sœurs ont eu un accident en allant chercher de l'
eau sur la montagne, mais sont finalement rentrés chez eux en sécurité et ont continué à explorer joyeusement.", "num_names": 2}

进一步指定输出格式:

prompt_2 = f"""
你的任务是执行以下操作:
1- 用一句话概括用<>括起来的文本。
2- 将摘要翻译成英语。
3- 列出英语摘要中的每个人名。
4- 输出一个JSON对象,其中包含以下键: eng_summary, num_names.

使用以下格式:
文本:<要概括的文本>
摘要:<概述>
翻译:<概述的翻译>
名称:<英语摘要中的人名列表>
输出JSON:<带有 eng_summary和num_names的JSON对象>

Text: <{text}>
"
""
response = get_completion(prompt_2)
print("\nCompletion for prompt 2:")
print(response)

输出结果如下:

Completion for prompt 2:
摘要:杰克和吉尔在山上取水时发生意外,但最终平安回家,继续探索冒险精神。
翻译:Jack and Jill had an accident while fetching water from a well on a hill, but they made it back home safely and continued their adventurous exploration.
名称:Jack, Jill
输出JSON:{"eng_summary""Jack and Jill had an accident while fetching water from a well on a hill, but they made it back home safely and continued their adventurous exploration.""num_names": 2}

策略 2: 要求模型在下结论之前看看自己的解法

有时候,在明确指导模型在做决策之前先思考解决方案时,会得到更好的结果。

接下来我们会给出一个问题和一个学生的解答,要求模型判断解答是否正确。

prompt = f"""
判断学生的解决方案是否正确。

问题:我正在建设一个太阳能发电设施,需要帮助计算财务情况。
- 土地费用为每平方英尺100美元
- 我可以以每平方英尺250美元的价格购买太阳能电池板
- 我谈判达成的维护合同将每年花费固定的10万美元,以及每平方英尺10美元的额外费用

以每平方英尺的面积作为函数,第一年运营的总成本是多少?

学生的解决方案:
设x为安装的面积(单位:平方英尺)。
费用:
1. 土地费用:100x
2. 太阳能电池板费用:250x
3. 维护费用: 100,000 + 100x

总成本:100x + 250x + 100,000+ 100x = 450x+100,000
"
""
response = get_completion(prompt)
print(response)

输出结果如下:

该学生的解决方案是正确的。

但是学生的解决方案实际上是错误的。可以通过指导模型先自行找出一个解法来解决这个问题。

在接下来这个 Prompt 中, 我们要求模型先自行解决这个问题,再根据自己的解法与学生的解法进行对比,从而判断学生的解法是否正确。同时,我们给定了输出的格式要求。通过明确步骤,让模型有更多时间思考,有时可以获得更准确的结果。在这个例子中,学生的答案是错误的,但如果我们没有先让模型自己计算,那么可能会被误导以为学生是正确的。

具体如下:

prompt = f"""
请判断学生的解决方案是否正确,请通过如下步骤解决这个问题:

步骤:

    首先,自己解决问题。
    然后将你的解决方案与学生的解决方案进行比较,并评估学生的解决方案是否正确。在自己完成问题之前,请勿决定学生的解决方案是否正确。

使用以下格式:

    问题:问题文本
    学生的解决方案:学生的解决方案文本
    实际解决方案和步骤:实际解决方案和步骤文本
    学生的解决方案和实际解决方案是否相同:是或否
    学生的成绩:正确或不正确

问题:

    我正在建造一个太阳能发电站,需要帮助计算财务。 
    - 土地费用为每平方英尺100美元
    - 我可以以每平方英尺250美元的价格购买太阳能电池板
    - 我已经谈判好了维护合同,每年需要支付固定的10万美元,并额外支付每平方英尺10美元
    作为平方英尺数的函数,首年运营的总费用是多少。

学生的解决方案:

    设x为发电站的大小,单位为平方英尺。
    费用:
    1. 土地费用:100x
    2. 太阳能电池板费用:250x
    3. 维护费用:100,000+100x
    总费用:100x+250x+100,000+100x=450x+100,000

实际解决方案和步骤:
"""
response = get_completion(prompt)
print(response)

输出结果如下:

正确的解决方案和步骤:
    1. 计算土地费用:100美元/平方英尺 * x平方英尺 = 100x美元
    2. 计算太阳能电池板费用:250美元/平方英尺 * x平方英尺 = 250x美元
    3. 计算维护费用:10万美元 + 10美元/平方英尺 * x平方英尺 = 10万美元 + 10x美元
    4. 计算总费用:100x美元 + 250x美元 + 10万美元 + 10x美元 = 360x + 10万美元

学生的解决方案和实际解决方案是否相同:否

学生的成绩:不正确

可以看出,这时候模型可能从自己的解答步骤和结果中得到学生给出的答案是错误的这一结论。