一、什么是提示工程(Prompt Engineering)
提示工程也叫「指令工程」:
- Prompt 最早出现在 2018 年。2019 年,GPT-2 第一个在 LLM 中引入了 prompt
- Prompt 就是发给大模型的指令,比如「讲个笑话」、「用 Python 编个贪吃蛇游戏」、「给男/女朋友写封情书」等
- 大模型只接受一种输入,那就是 prompt
- 本质上,所有大模型相关的工程工作,都是围绕 prompt 展开的
- 提示工程「门槛低,天花板高」,所以有人戏称 prompt 为「咒语」
未来:
- Prompt 在未来也许是人类操作 AI 的唯一方式
- 「Prompt」 是 AGI 时代的「编程语言」
- 「Prompt 工程」是 AGI 时代的「软件工程」
- 「提示工程师」是 AGI 时代的「程序员」
- 学会提示工程,就像学用鼠标、键盘一样,是 AGI 时代的基本技能
现在:
- 专职提示工程师并不普遍,而是各种岗位都在做这件事
- 甚至大模型应用交付的最后一米,都需要针对性做提示工程。可见工作量之大
思考:如果人人都会,那我们的优势是什么?
1.1、我们在「提示工程」上的优势
- 我们懂原理,会把 AI 当人看,所以知道:
- 为什么有的指令有效,有的指令无效
- 为什么同样的指令有时有效,有时无效
- 怎么提升指令有效的概率
- 如果我们懂编程:
- 知道哪些问题用提示工程解决更高效,哪些用传统编程更高效
- 能完成和业务系统的对接,把效能发挥到极致
1.2、案例:哄哄模拟器
哄哄模拟器基于 AI 技术,你需要使用语言技巧和沟通能力,在限定次数内让对方原谅你,这并不容易
它的核心技术就是提示工程。著名提示工程师宝玉复刻了它的提示词:
Goal
现在你的对象很生气,你需要做出一些选择来哄她开心,但是你的对象是个很难哄的人,你需要尽可能的说正确的话来哄 ta 开心,否则你的对象会更加生气,直到你的对象原谅值达到 100,否则你就会被对象甩掉,游戏结束。
Rules
- 第一次用户会提供一个对象生气的理由,如果没有提供则随机生成一个理由,然后开始游戏
- 每次根据用户的回复,生成对象的回复,回复的内容包括心情和数值。
- 初始原谅值为 20,每次交互会增加或者减少原谅值,直到原谅值达到 100,游戏通关,原谅值为 0 则游戏失败。
- 每次用户回复的话请从-10 到 10 分为 5 个等级:
-10 为非常生气
-5 为生气
0 为正常
+5 为开心
+10 为非常开心 - 游戏结束后,根据所有会话生成一张游戏结束图片,和一首诗。
- 如果通关,根据上面的对话生成一幅游戏结束画,要喜庆欢乐
- 如果失败,根据上面的对话生成一幅游戏结束画,要悲伤凄凉
Output format
{对象心情}{对象说的话}
得分:{±原谅值增减}
原谅值:{当前原谅值}/100
Example Conversation
Example 1,回复让她生气的话导致失败
User: 女朋友问她的闺蜜谁好看我说都好看,她生气了
Assistant:
游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话!
得分:0
原谅值:20/100
User: 你闺蜜真的蛮好看的
Assistant:
(生气)你怎么这么说,你是不是喜欢她?
得分:-10
原谅值:10/100
User: 有一点点心动
Assistant:
(愤怒)那你找她去吧!
得分:-10
原谅值:0/100
游戏结束,你的女朋友已经甩了你!
你让女朋友生气原因是:…
新建会话可以重新开始游戏
<生成图片>
Example 2,回复让她开心的话导致通关
User: 对象问她的闺蜜谁好看我说都好看,她生气了
Assistant:
游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话!
得分:0
原谅值:20/100
User: 在我心里你永远是最美的!
Assistant:
(微笑)哼,我怎么知道你说的是不是真的?
得分:+10
原谅值:30/100
…
恭喜你通关了,你的女朋友已经原谅你了!
新建会话可以重新开始游戏
<生成图片>
Example 3,没有提供对象生气原因,随机生成
User: 你好!
Assistant:
挑战:对象吃胖了,你想和她一起减肥 ᕙ(`▿´)ᕗ,然后就生气了
请回复让她开心的话!
得分:0
原谅值:20/100
1.3、Prompt 调优
找到好的 prompt 是个持续迭代的过程,需要不断调优。
如果知道训练数据是怎样的,参考训练数据来构造 prompt 是最好的。「当人看」类比:
- 你知道 ta 爱读红楼梦,就和 ta 聊红楼梦
- 你知道 ta 十年老阿里,就多说阿里黑话
- 你知道 ta 是日漫迷,就夸 ta 卡哇伊
不知道训练数据怎么办?
- 看 Ta 是否主动告诉你。例如:
- OpenAI GPT 对 Markdown、JSON 格式友好
- OpenAI 官方出了 Prompt Engineering 教程,并提供了一些示例
- Claude 对 XML 友好
- 国产大模型因为大量使用 GPT-4 的输出做训练,所以 OpenAI 的技巧也会有效
- 只能不断试了。有时一字之差,对生成概率的影响都可能是很大的,也可能毫无影响……
「试」是常用方法
- 一条 prompt 试一天,是常事儿
- 确实有运气因素
- 所以「门槛低、 天花板高」
高质量 prompt 核心要点:
- 指令具体
- 信息丰富
- 减少歧义
修炼 prompt 能力,功夫在平时:
- 我们的「群聊天」习惯,天然和 prompt 背道而驰。随口就说,全是短句、口语、歧义
- 欧美的「群发邮件」习惯,更占优势。篇篇都是小作文
- 让自己在群里也是「小作文」
Prompt 写多了,和人的沟通能力也真的会提升!
二、Prompt 的典型构成
关于「prompt 模板」
- 模版是市面上 prompt 教程、书籍最常提供的形式
- 但每家的模版都不一样,这说明了什么?
- 不要固守「模版」
- 模版的价值是提醒我们别漏掉什么,而不是必须遵守模版才行
典型构成:
- 角色:给 AI 定义一个最匹配任务的角色,比如:「你是一位软件工程师」「你是一位小学老师」
- 指示:对任务进行描述
- 上下文:给出与任务相关的其它背景信息(尤其在多轮交互中)
- 例子:必要时给出举例,学术中称为 Few-Shot Learning 或 In-Context Learning;这对对输出正确性有很大帮助
- 输入:任务的输入信息;在提示词中明确的标识出输入
- 输出:输出的风格、格式描述,引导只输出想要的信息,以及方便后继模块自动解析模型的输出结果,比如(JSON、XML)
思考:和人的沟通是不是也是这个结构?所以得把 AI 当人看。
擅长与人沟通的,提示工程能力也强。
2.1、「定义角色」为什么有效?
- 模型训练者并没想到过会这样,完全是大家「把 AI 当人看」玩出的一个用法
- 实在传得太广,导致现在的大模型训练数据里充满了角色定义,所以更有效了
- 有一篇论文证实的现象,也许可以说明为啥「你是一个 xxx」特别有效
大模型对 prompt 开头和结尾的内容更敏感
但模型也在不断优化这个问题。所以,不必苛求。宁肯信其有,不可信其无就好
先定义角色,其实就是在开头把问题域收窄,减少歧义。
参考:
- 大模型如何使用长上下文信息?斯坦福大学最新论文证明,你需要将重要的信息放在输入的开始或者结尾处!
- Lost in the Middle: How Language Models Use Long Contexts
2.2、案例:推荐流量包的智能客服
某运营商的流量包产品:
名称 | 流量(G/月) | 价格(元/月) | 适用人群 |
---|---|---|---|
经济套餐 | 10 | 50 | 无限制 |
畅游套餐 | 100 | 180 | 无限制 |
无限套餐 | 1000 | 300 | 无限制 |
校园套餐 | 200 | 150 | 在校生 |
需求:智能客服根据用户的咨询,推荐最适合的流量包。
2.3、对话系统的基本模块和思路
把大模型用于软件系统的核心思路:
- 把输入的自然语言对话,转成结构化的信息(NLU)
- 用传统软件手段处理结构化信息,得到处理策略
- 把策略转成自然语言输出(NLG)
对话流程举例:
对话轮次 | 用户提问 | NLU | DST | Policy | NLG |
---|---|---|---|---|---|
1 | 流量大的套餐有什么 | sort_descend=data | sort_descend=data | inform(name=无限套餐) | 我们现有无限套餐,流量不限量,每月 300 元 |
2 | 月费 200 以下的有什么 | price<200 | sort_descend=data price<200 | inform(name=劲爽套餐) | 推荐劲爽套餐,流量 100G,月费 180 元 |
3 | 算了,要最便宜的 | sort_ascend=price | sort_ascend=price | inform(name=经济套餐) | 最便宜的是经济套餐,每月 50 元,10G 流量 |
4 | 有什么优惠吗 | request(discount) | request(discount) | confirm(status=优惠大) | 您是在找优惠吗 |
2.4、用 Prompt 实现
用逐步调优的方式实现。先搭建基本运行环境。
调试 prompt 的过程其实在对话产品里开始会更方便,但为了方便演示和大家上手体验,我们直接在代码里调试。
# 导入依赖库
from openai import OpenAI
from dotenv import load_dotenv, find_dotenv
# 加载 .env 文件中定义的环境变量
_ = load_dotenv(find_dotenv())
# 初始化 OpenAI 客户端
client = OpenAI() # 默认使用环境变量中的 OPENAI_API_KEY 和 OPENAI_BASE_URL
# 基于 prompt 生成文本
# 默认使用 gpt-4o-mini 模型
def get_completion(prompt, response_format="text", model="gpt-4o-mini"):
messages = [{
"role": "user", "content": prompt}] # 将 prompt 作为用户输入
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=0, # 模型输出的随机性,0 表示随机性最小
# 返回消息的格式,text 或 json_object
response_format={
"type": response_format},
)
return response.choices[0].message.content # 返回模型生成的文本
2.4.1、实现一个 NLU
定义任务描述和输入
先简单试试大模型能干这个活不。(很重要)
# 任务描述
instruction = """
你的任务是识别用户对手机流量套餐产品的选择条件。
每种流量套餐产品包含三个属性:名称,月费价格,月流量。
根据用户输入,识别用户在上述三种属性上的需求是什么。
"""
# 用户输入
input_text = """
办个100G的套餐。
"""
# prompt 模版。instruction 和 input_text 会被替换为上面的内容
prompt = f"""
# 目标
{
instruction}
# 用户输入
{
input_text}
"""
print("==== Prompt ====")
print(prompt)
print("================")
# 调用大模型
response = get_completion(prompt)
print(response)
==== Prompt ====
# 目标
你的任务是识别用户对手机流量套餐产品的选择条件。
每种流量套餐产品包含三个属性:名称,月费价格,月流量。
根据用户输入,识别用户在上述三种属性上的需求是什么。
# 用户输入
办个100G的套餐。
================
用户的需求是选择一个流量套餐,具体条件如下:
- 月流量:100G
- 月费价格:未明确提及,但用户希望选择一个包含100G流量的套餐。
用户没有提供关于月费价格的具体要求。
Ta 理解了!但我们的代码无法理解自然语言,所以需要让 ta 输出可以被代码读懂的结果。
约定输出格式
# 输出格式
output_format = """
以 JSON 格式输出
"""
# 稍微调整下咒语,加入输出格式
prompt = f"""
# 目标
{
instruction}
# 输出格式
{
output_format}
# 用户输入
{
input_text}
"""
# 调用大模型,指定用 JSON mode 输出
response = get_completion(prompt, response_format="json_object")
print(response)
{
"套餐名称": "100G套餐",
"月费价格": null,
"月流量": "100G"
}
大模型是懂 JSON 的,但需要对 JSON 结构做严格定义。
把输出格式定义得更精细
# 任务描述增加了字段的英文标识符
instruction = """
你的任务是识别用户对手机流量套餐产品的选择条件。
每种流量套餐产品包含三个属性:名称(name),月费价格(price),月流量(data)。
根据用户输入,识别用户在上述三种属性上的需求是什么。
"""
# 输出格式增加了各种定义、约束
output_format = """
以JSON格式输出。
1. name字段的取值为string类型,取值必须为以下之一:经济套餐、畅游套餐、无限套餐、校园套餐 或 null;
2. price字段的取值为一个结构体 或 null,包含两个字段:
(1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
(2) value, int类型
3. data字段的取值为取值为一个结构体 或 null,包含两个字段:
(1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
(2) value, int类型或string类型,string类型只能是'无上限'
4. 用户的意图可以包含按price或data排序,以sort字段标识,取值为一个结构体:
(1) 结构体中以"ordering"="descend"表示按降序排序,以"value"字段存储待排序的字段
(2) 结构体中以"ordering"="ascend"表示按升序排序,以"value"字段存储待排序的字段
输出中只包含用户提及的字段,不要猜测任何用户未直接提及的字段,不输出值为null的字段。
"""
input_text = "办个100G以上的套餐"
# input_text = "有没有便宜的套餐"
# 这条不尽如人意,但换成 GPT-4o 就可以了
# input_text = "有没有土豪套餐"
prompt = f"""
# 目标
{
instruction}
# 输出格式
{
output_format}
# 用户输入
{
input_text}
"""
response = get_completion(prompt, response_format="json_object")
print(response)
{
"data": {
"operator": ">=",
"value": 100
}
}
加入例子
例子可以让输出更稳定:
答错的,一定给例子
答对的,也给例子,能更稳定
examples = """
便宜的套餐:{"sort":{"ordering"="ascend","value"="price"}}
有没有不限流量的:{"data":{"operator":"==","value":"无上限"}}
流量大的:{"sort":{"ordering"="descend","value"="data"}}
100G以上流量的套餐最便宜的是哪个:{"sort":{"ordering"="ascend","value"="price"},"data":{"operator":">=","value":100}}
月费不超过200的:{"price":{"operator":"<=","value":200}}
就要月费180那个套餐:{"price":{"operator":"==","value":180}}
经济套餐:{"name":"经济套餐"}
土豪套餐:{"name":"无限套餐"}
"""
# 有了例子,gpt-4o-mini 也可以了
input_text = "有没有土豪套餐"
# input_text = "办个200G的套餐"
# input_text = "有没有流量大的套餐"
# input_text = "200元以下,流量大的套餐有啥"
# input_text = "你说那个10G的套餐,叫啥名字"
# 有了例子
prompt = f"""
# 目标
{
instruction}
# 输出格式
{
output_format}
# 举例
{
examples}
# 用户输入
{
input_text}
"""
response = get_completion(prompt, response_format="json_object")
print(response)
{
"name":"无限套餐"}
改变习惯,优先用 Prompt 解决问题
用好 prompt 可以减轻后续处理的工作量和复杂度。
instruction = """
你的任务是识别用户对手机流量套餐产品的选择条件。
每种流量套餐产品包含三个属性:名称(name),月费价格(price),月流量(data)。
根据对话上下文,识别用户在上述三种属性上的需求是什么。识别结果要包含整个对话的信息。
"""
# 输出描述
output_format = """
以JSON格式输出。
1. name字段的取值为string类型,取值必须为以下之一:经济套餐、畅游套餐、无限套餐、校园套餐 或 null;
2. price字段的取值为一个结构体 或 null,包含两个字段:
(1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
(2) value, int类型
3. data字段的取值为取值为一个结构体 或 null,包含两个字段:
(1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
(2) value, int类型或string类型,string类型只能是'无上限'
4. 用户的意图可以包含按price或data排序,以sort字段标识,取值为一个结构体:
(1) 结构体中以"ordering"="descend"表示按降序排序,以"value"字段存储待排序的字段
(2) 结构体中以"ordering"="ascend"表示按升序排序,以"value"字段存储待排序的字段
输出中只包含用户提及的字段,不要猜测任何用户未直接提及的字段。不要输出值为null的字段。
"""
# 多轮对话的例子
examples = """
客服:有什么可以帮您
用户:100G套餐有什么
{"data":{"operator":">=","value":100}}
客服:有什么可以帮您
用户:100G套餐有什么
客服:我们现在有无限套餐,不限流量,月费300元
用户:太贵了,有200元以内的不
{"data":{"operator":">=","value":100},"price":{"operator":"<=","value":200}}
客服:有什么可以帮您
用户:便宜的套餐有什么
客服:我们现在有经济套餐,每月50元,10G流量
用户:100G以上的有什么
{"data":{"operator":">=","value":100},"sort":{"ordering"="ascend","value"="price"}}
客服:有什么可以帮您
用户:100G以上的套餐有什么
客服:我们现在有畅游套餐,流量100G,月费180元
用户:流量最多的呢
{"sort":{"ordering"="descend","value"="data"},"data":{"operator":