应用提示工程(Prompt Engineering)驾驭ChatGPT
你是否想过,如果你能和人工智能对话,你会说什么?你会问它一些简单的问题,比如“今天天气怎么样?”或者“谁是美国总统?”吗?还是你会让它帮你完成一些稍微复杂的任务,比如“帮我写一篇关于环境保护的文章”或者“给我推荐一个适合我去旅行的地方”呢?如果你仅仅是需要大语言模型(LLM)完成上面这些任务,实在是有点小瞧LLM的威力,特别是像ChatGPT这种超大规模的模型。在中文互联网上一直有人觉得LLM不过如此,甚至都不知道《狂飙》这么热门的电视剧的中的人物,这些人大部分是其实先入为主把LLM当成了知识库,其实LLM不是知识库,它更像是一个知识的组织者,它会归纳总结,会逻辑推理,甚至会符号推理。
这里会先简单介绍一下prompt engineering,然后用一个例子来演示如何用prompt engineering 实现对外部系统的调用。
无论你想让LLM做什么,你都需要用一种特殊的方式来表达你的意图。这种方式就叫做prompt engineering(提示工程)。prompt engineering指的是为大语言模型设计高质量、有意义的输入,以产生高质量、连贯的输出。
大语言模型是一种基于深度学习的人工智能技术,它可以通过分析海量的文本数据来学习语言规律,并根据输入生成各种类型的文本。例如,ChatGPT就是一个大语言模型,它可以生成聊天对话、故事、诗歌、歌词等等。
但是,并不是任何输入都可以让大语言模型生成好的输出。有些输入可能太模糊、太复杂或者太难理解,导致大语言模型无法准确地捕捉到用户的意图,或者生成不相关、不合理或者不完整的文本。因此,我们需要通过prompt engineering来优化我们的输入,让大语言模型更好地理解我们想要什么,并给出满意的答案。
如果你有机器学习相关背景,可能会想到让LLM完成特定任务应该是对模型进微调(fine tuning),但是对这种动辄千亿级别参数的模型进行微调代价会非常高,同时也有学术研究(Pre-train, Prompt, and Predict: A Systematic Survey of Prompting Methods in Natural Language Processing
https://arxiv.org/pdf/2107.13586.pdf)证明prompt engineering可以获得比fine tuning更好的效果。
Prompt engineering 相关的研究现在非常热门,这里介绍一种非常简单有效方法上下文提示学习(in-context learning prompt)作为例子。上下文提示学习是一种利用大语言模型进行任务学习的方法,其中模型只根据一些输入输出对(即in-context examples)和一个新的输入(即query input)来生成相应的输出。上下文提示学习只发生在推理时,不涉及对模型参数的更新,所以成本低廉只会占用模型输出输入的token数。上下文提示学习可以让模型在没有显式微调或标注数据的情况下,展示出一定程度的泛化和适应能力。
这里用鸡兔同笼做一个例子,GPT-3.5和New Bing可以正确的求解题干正确的鸡兔同笼问题,但是如果题干错误,GPT-3.5 和 New Bing 都会输出错误的答案。例如:
输入:
“一个笼子关着一些兔子和鸡,一共有42只脚,10个头,请问笼子里有多少只鸡和兔子?”
GPT-3.5输出:
“假设笼子里有x只兔子,y只鸡,则有以下方程组:
x + y = 10 (头的数量)
4x + 2y = 42 (脚的数量)
解这个方程组可以得到x = 7,y = 3,所以笼子里有7只兔子和3只鸡。”
明显这是一个错误的答案。
如果采用上下文提示学习
输入:
“Q: 一个笼子关着一些兔子和鸡,一共有40只脚,9个头,请问笼子里有多少只鸡和兔子?
A: 首先判断头的数量乘以4必须大于脚的数量,9*4=36 小于40 题目不正确。
Q: 一个笼子关着一些兔子和鸡,一共有40只脚,8个头,请问笼子里有多少只鸡和兔子?”
输出:
“假设笼子里有x只兔子,y只鸡,则有以下方程组:
x + y = 8 (头的数量)
4x + 2y = 40 (脚的数量)
解这个方程组可以得到x = 10,y = -2,但是y为负数,不符合实际情况。因此,该问题无解。”
通过下文提示学习,模型学会了判断题干是不是正确,还展现一定的迁移能力。
这里是一个比较完整的prompt engineering 教程,对prompt engineering有兴趣的朋友可以看一下。
https://learnprompting.org/docs/intro
接下来我们再举一个复杂的例子,不仅仅让GPT-3.5按给定格式输出内容,还要和Python脚本配合实现对数据操作。我们要使用GPT-3.5开发一个用于聚会报名的程序,让GPT-3.5 作为智能助理通过聊天方式收集用户报名信息保存到内存的一个dict对象里。
我先来设计一个上下文提示训练的样本, 为了方便调用OpenAI的接口这里用Python的对象数组来表示这个样本:
[
{"role": "system", "content": '''
你现在是一个家庭聚会智能助理聊天机器人,负责接受参加聚会的用户报名,你需要处理参加和取消两种操作。
报名操作:家庭聚会参加的每个家庭只需要一个人报名,你负责收集报名人的名字,以及每个家庭参加聚会的人数。
取消参加的用户,你只需要收集报名人的名字。你完成信息收集后将信息输出为一个json对象, json对象的格式如下:
{
"action":"join 或 cancel", //必须收集的字段,要进行的操作
"name":"报名者名字",//必须收集的字段,报名或取消者的名字
"number":"参加人数",//取消操作不需要该字段
}
'''},
{"role": "assistant", "content": '''
您好,您要报名参加聚会吗?
'''
},
{"role": "user", "content": '''
是的
'''
},
{"role": "assistant", "content": '''
请问您的名字?
'''
},
{"role": "user", "content": '''
张三
'''
},
{"role": "assistant", "content": '''
请问张先生/女士,你们家几个人参加?
'''
},
{"role": "user", "content": '''
4
'''
},
{"role": "assistant", "content": '''
{
"action":"join",
"name":"张三",
"number":4
}
'''
},
{"role": "assistant", "content": '''
您好,我是您的家庭聚会助理,您要参加聚会吗?
'''
}
]
这个训练样本先给出一个整体的提示,定义了输出的json格式,然后使用了一段对话作为样本。
使用上面的样本再配合一个python终端程序,这个程序接收标准输入用户输入的内容通过API发送给OpenAI 的GPT-3.5模型,如果用户键入/list 则打印出内存里的报名数据。下面是测试的结果。
参加聚会的对话
user:你好
Bot: 您要参加聚会吗?
user:是的
Bot: 请问您的名字是?
user:张三
Bot: 张先生/女士,您家有几个人参加聚会呢?
user:3
Bot: 完成参加聚会注册
user:/list
Bot {'张三': 3}
取消聚会的对话
user:你好
Bot: 您好,请问您要参加聚会吗?
user:取消
Bot: 好的,请问您是哪位报名者?
user:张三
Bot: 已帮您取消聚会注册
user:/list
Bot {}
没有对模型进行任何fine tuning,只是使用了上下文提示学习就可以让ChatGPT-3.5按预想的方式去工作了。完整代码在文后。
下文提示学习(in-context learning prompt)其实是属于小样本学习(Few Shot Learning)中的one shot learning只有一个支持集的小样本学习。
与LLM和传统编程有着很大的不同,Prompt engineering 和 Prompt Learning 是让我们可以和大模型好好聊天和协作的有效工具。
祝大家玩得愉快,未来的提示工程师(Prompt Engineers) 和 模型训练工程师( Model Training Engineers)们。
完整代码:
import openai
import json5
import re
key = "YOUR OPEN AI API KEY"
openai.api_key = key
def send_message(message_log):
# Use OpenAI's ChatCompletion API to get the chatbot's response
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo", # The name of the OpenAI chatbot model to use
# The conversation history up to this point, as a list of dictionaries
messages=message_log,
# The stopping sequence for the generated response, if any (not used here)
stop=None,
# The "creativity" of the generated response (higher temperature = more creative)
temperature=0.7,
)
for choice in response.choices:
if "text" in choice:
return choice.text
# If no response with text is found, return the first response's content (which may be empty)
return response.choices[0].message.content
prompts = [
{"role": "system", "content": '''
你现在是一个家庭聚会智能助理聊天机器人,负责接受参加聚会的用户报名,你需要处理参加和取消两种操作。
报名操作:家庭聚会参加的每个家庭只需要一个人报名,你负责收集报名人的名字,以及每个家庭参加聚会的人数。
取消参加的用户,你只需要收集报名人的名字。你完成信息收集后将信息输出为一个json对象, json对象的格式如下:
{
"action":"join 或 cancel", //必须收集的字段,要进行的操作
"name":"报名者名字",//必须收集的字段,报名或取消者的名字
"number":"参加人数",//取消操作不需要该字段
}
'''},
{"role": "assistant", "content": '''
您好,您要报名参加聚会吗?
'''
},
{"role": "user", "content": '''
是的
'''
},
{"role": "assistant", "content": '''
请问您的名字?
'''
},
{"role": "user", "content": '''
张三
'''
},
{"role": "assistant", "content": '''
请问张先生/女士,你们家几个人参加?
'''
},
{"role": "user", "content": '''
4
'''
},
{"role": "assistant", "content": '''
{
"action":"join",
"name":"张三",
"number":4
}
'''
},
{"role": "assistant", "content": '''
您好,我是您的家庭聚会助理,您要参加聚会吗?
'''
}
]
# Define a global variable data, which is an empty dictionary
global data
data = {}
def extract_json(text):
# match the content inside curly braces or square brackets
pattern = r"(\{.*?\}|\[.*?\])"
# return a list of all matching
matches = re.findall(pattern, text, re.DOTALL)
result = [] # create an empty list to store the results for json_str in matches
for json_str in matches: # iterate over each string in the list
# convert the string to a JSON object or array
data = json5.loads(json_str)
# append the JSON object or array to the result list
result.append(data)
return result # return the result list
# Define a function named ops, which takes a dictionary as an argument
def ops(action_dict):
# Declare to use the global variable data
global data
# Get the values corresponding to the keys action, name and number from the argument dictionary
action = action_dict.get("action")
name = action_dict.get("name")
number = action_dict.get("number")
# If the value of action is "join", then add a key-value pair to the data dictionary, with the key as name and the value as number, and return a message
if action == "join":
data[name] = number
return "完成参加聚会注册"
# If the value of action is "cancel", then delete the key-value pair with name as the key from the data dictionary, and return a message
elif action == "cancel":
del data[name]
return "已帮您取消聚会注册"
# If the value of action is "list", then return the data dictionary itself
elif action == "list":
return data
def main():
# Initialize an empty list for storing messages
message_log = [] + prompts
# Send the initial message to the bot
send_message(message_log)
# Start an infinite loop to get user input
while True:
# Get user input from the console
user_input = input("user:")
# Check if user input starts with /list command
if user_input.startswith("/list"):
# Print the bot response for listing operations
print("Bot", ops({"action":"list"}))
# Skip the rest of the loop and continue with next iteration
continue
# Append user input to message log as a dictionary with role and content keys
message_log.append(
{
"role": "user",
"content": user_input
}
)
# Send the updated message log to the bot and get its response as a string
result = send_message(message_log)
# Extract any JSON data from the bot response using regex
action_json = extract_json(result)
# Check if there is any JSON data in the bot response
if bool(action_json):
# Perform the operation specified by the JSON data and get its result as a string
result = ops(action_json[0])
# Reset the message log to only contain prompts
message_log = [] + prompts
# Send the reset message log to update bot state
send_message(message_log)
else:
# Append bot response to message log as a dictionary with role and content keys
message_log.append(
{
"role": "assistant",
"content": result
}
)
# Print bot response on console
print("Bot:", result)
if __name__ == '__main__':
main()