Prompt 模板
背景
在语言模型的评测中,我们通常会将原始数据集按照一定的规则构造成提示词(Prompt),以便模型能够按照要求回答问题。
通常,我们会在提示词开头放入指令,接着放入几个 in-context example(上下文样例,也称为 shot case),最后放入待测试的题目。例如:
Solve the following questions.
1+1=?
2
3+9=?
12
5+6=?
大量的实验表明,即便测试的原始题目相同,不同的提示词构造方式也会对模型的表现产生显著影响。可能影响的因素包括:
提示词的构成方式:包括指令、in-context example、题目的写法
in-context example 的选择:包括选择的数量和选择策略
提示词的使用方式:是让模型基于提示词进行补全,还是从候选的提示词中选择一个最好的作为答案
AISBench 将提示词的构建策略定义在数据集配置中的 infer_cfg 部分。一个典型的 infer_cfg 配置如下所示:
infer_cfg=dict(
ice_template=dict( # 用于构造 In Context Example (ice) 的模板
type=PromptTemplate,
template='{question}\n{answer}'
),
prompt_template=dict( # 用于构造主干 prompt 的模板
type=PromptTemplate,
template='Solve the following questions.\n</E>{question}\n{answer}',
ice_token="</E>"
),
retriever=dict(type=FixKRetriever, fix_id_list=[0, 1]), # 定义 in context example 的获取方式
inferencer=dict(type=GenInferencer), # 使用何种推理器进行推理
)
本文档主要介绍 ice_template 和 prompt_template 的定义方法。关于其他组件的说明:
retriever(检索器):用于指定 shot case 的提取策略,详细介绍请参考 retrieverinferencer(推理器):推理任务的具体实现,包含模型的调用和推理结果的保存逻辑,详细介绍请参考 inferencer
我们首先介绍 prompt 的基本语法。
字符串式 prompt
字符串式的模板是比较经典的模板形式,考虑下面的模板:
reader_cfg=dict(
input_columns=['anything', 'question'],
output_column='answer',
)
prompt_template=dict(
type=PromptTemplate,
template="{anything}\nQuestion: {question}\nAnswer: {answer}"
)
其中 reader_cfg 需要定义清楚从数据集中读取哪些字段,以及将哪个字段作为答案。通常 input_columns 可以包含多个字段(列表或字符串),但 output_column 只能包含一个字段(字符串)。
运行时,花括号{}内的字段会被替换成数据样本内的对应字段。如果数据样本中没有对应的字段,则会保持原样输出。
例如我们有一个数据 example 如下:
example = {
'anything': 'blabla', # 假设 anything 被写在了 reader_cfg.input_columns 中
'question': '1+1=?', # 假设 question 被写在了 reader_cfg.input_columns 中
'answer': '2', # 假设 answer 被写在了 reader_cfg.output_column 中
}
则填入模板后的结果为:
blabla
Question: 1+1=?
Answer:
可以看到,问题的实际答案 answer 并没有出现在生成的结果中。这是因为 AISBench 会自动遮盖 reader_cfg.output_column 中指定的字段,避免答案泄露影响评测结果。
固定对话式 prompt
在实际的评测中,简单的补全式测试并不能很好地评估对话式模型的性能,因此我们希望提示词能够以对话的格式输入到模型中。另外,不同的模型对对话格式的定义也不一样,因此我们需要数据集侧产生的提示词更加通用,在测试时再结合具体模型的配置生成符合模型需求的提示词。
因此,AISBench 在字符串式模板之上,增加了对对话式模板的支持。对话式模板更加灵活,它可以结合模型侧不同的 meta_template 生成不同对话形式的提示词,同时适用于基座和对话模型,但定义也相对复杂。
现在,让我们假设有一个数据样本如下:
example = {
'anything': 'blabla', # 假设 anything 被写在了 reader_cfg.input_columns 中
'question': '1+1=?', # 假设 question 被写在了 reader_cfg.input_columns 中
'answer': '2', # 假设 answer 被写在了 reader_cfg.output_column 中
}
接下来,我们来展示几个例子:
普通对话
prompt_template=dict(
type=PromptTemplate,
template=dict(
round=[
dict(role="HUMAN", prompt="Question: {question}"),
dict(role="BOT", prompt="Answer: {answer}"),
]
)
)
infer_cfg = dict(
prompt_template=prompt_template,
retriever=...,
inferencer=dict(type=GenInferencer, **args))
AISBench 把数据填入模板后得到的中间结果为:
PromptList([
dict(role='HUMAN', prompt='Question: 1+1=?'),
dict(role='BOT', prompt='Answer: '),
])
多轮对话
prompt_template=dict(
type=PromptTemplate,
template=dict(
round=[
dict(role="HUMAN", prompt="Question: 2+2=?"),
dict(role="BOT", prompt="Answer: 4"),
dict(role="HUMAN", prompt="Question: 3+3=?"),
dict(role="BOT", prompt="Answer: 6"),
dict(role="HUMAN", prompt="Question: {question}"),
dict(role="BOT", prompt="Answer: {answer}"),
]
)
)
infer_cfg = dict(
prompt_template=prompt_template,
retriever=...,
inferencer=dict(type=GenInferencer, **args))
AISBench 把数据填入模板后得到的中间结果为:
PromptList([
dict(role='HUMAN', prompt='Question: 2+2=?'),
dict(role='BOT', prompt='Answer: 4'),
dict(role='HUMAN', prompt='Question: 3+3=?'),
dict(role='BOT', prompt='Answer: 6'),
dict(role='HUMAN', prompt='Question: 1+1=?'),
dict(role='BOT', prompt='Answer: '),
])
带 SYSTEM 的对话
prompt_template=dict(
type=PromptTemplate,
template=dict(
begin=[
dict(role='SYSTEM', fallback_role='HUMAN', prompt='Solve the following questions.'),
],
round=[
dict(role="HUMAN", prompt="Question: {question}"),
dict(role="BOT", prompt="Answer: {answer}"),
]
)
)
AISBench 把数据填入模板后得到的中间结果为:
PromptList([
dict(role='SYSTEM', fallback_role='HUMAN', prompt='Solve the following questions.'),
dict(role='HUMAN', prompt='Question: 1+1=?'),
dict(role='BOT', prompt='Answer: '),
])
在具体的 meta template 处理时,如果模型定义中存在 SYSTEM 角色,则会调用 SYSTEM 的模板进行处理。否则,会调用 fallback_role 指定的角色模板进行处理,也就是这个例子中的 HUMAN 角色。
可以看到,在对话式模板中,提示词是以不同角色(role)的对话形式进行组织的。在 AISBench 的预定义数据集配置中,常用的角色包括:
HUMAN:人类角色,通常为提问的一方BOT:语言模型角色,通常为回答的一方SYSTEM:系统角色,通常用在提示词的开头,负责下达系统级别的指令
另外,与字符串式模板不同,经过对话式模板生成的提示词从固定的字符串变成了一个中间结构 PromptList。这个结构会进一步与模型侧的 meta template 相结合,最终拼装成模型所需的提示词格式(PromptType)。当前 meta template 已在模型实现中预设,同时也支持用户通过模型参数 meta_template 进行自定义。
备注
上面例子中 PromptList 的内容并非模型最终的输入,最终输入格式取决于 meta template 的处理。一个容易产生误解的地方是,在生成式评测中,最后一个 BOT 角色的 prompt(如 Answer: )不会实际输入到模型。这是因为 API 模型通常无法自定义模型回复的开头,因此这一设定保持了本地模型与 API 模型在评测行为上的一致性。更多信息可以参考 meta template 的文档。
点击查看完整参数介绍
begin、end(list,可选):提示词的开头和结尾,通常是一些系统级别的指令。列表中的每一项可以是字典或字符串。round(list):对话的模板格式。列表中的每一项必须是字典。
每个字典的参数如下:
role(str):参与对话的角色名,用于与meta_template中的名称进行关联,不会影响实际生成的提示词内容。fallback_role(str,可选):缺省角色名。如果meta_template中找不到role对应的角色,则会尝试使用fallback_role进行关联。默认为None。prompt(str):角色的对话内容。
多轮对话 prompt
多轮对话的数据样本中通常包含多个问题,需要按照顺序构造多轮对话的 prompt。AISBench 提供了多轮对话式模板(MultiTurnPromptTemplate)来支持多轮对话的构造。其构造方式与固定对话式prompt一致,只是将round固定为:
reader_cfg=dict(
input_columns=['question'],
output_column='answer',
)
prompt_template=dict(
type=MultiTurnPromptTemplate,
template=dict(round=[
dict(role="HUMAN", prompt="{question}"),
dict(role="BOT", prompt="{answer}"),
]))
infer_cfg = dict(
prompt_template=prompt_template,
retriever=...,
inferencer=dict(type=MultiTurnGenInferencer, infer_mode="every", **args))
其中 infer_mode 表示多轮对话的请求拼接模式,当前支持三种配置:
every:每次BOT都采用上一次模型的推理结果进行拼接every_with_gt:每次BOT都采用答案的 ground truth 进行拼接last:只采用最后一次模型的推理结果进行拼接
question 需要在数据样本中被定义为列表,每个元素为一个问题。
假设有如下样本:
example = {
'question': ['1+1=?', '2+2=?', '3+3=?'],
'answer': ['2', '4', '6'],
}
则填入模板后的中间结果为:
infer_mode=every 时,总共包含三次推理过程:
PromptList([
dict(role='HUMAN', prompt='1+1=?')
])
# 模型返回:answer1
PromptList([
dict(role='HUMAN', prompt='1+1=?'),
dict(role='BOT', prompt='answer1'),
dict(role='HUMAN', prompt='2+2=?')
])
# 模型返回:answer2
PromptList([
dict(role='HUMAN', prompt='1+1=?'),
dict(role='BOT', prompt='answer1')
dict(role='HUMAN', prompt='2+2=?')
dict(role='BOT', prompt='answer2'),
dict(role='HUMAN', prompt='3+3=?')
])
infer_mode=every_with_gt 时,不关注模型上一轮推理结果,只会将答案的 ground truth 作为拼接,总共包含三次推理过程:
PromptList([
dict(role='HUMAN', prompt='1+1=?'),
])
PromptList([
dict(role='HUMAN', prompt='1+1=?'),
dict(role='BOT', prompt='2'),
dict(role='HUMAN', prompt='2+2=?'),
])
PromptList([
dict(role='HUMAN', prompt='1+1=?'),
dict(role='BOT', prompt='2'),
dict(role='HUMAN', prompt='2+2=?'),
dict(role='BOT', prompt='4'),
dict(role='HUMAN', prompt='3+3=?')
])
infer_mode=last 时,将除最后一轮答案外其他每一轮的文本和答案进行拼接,总共包含一次推理过程:
PromptList([
dict(role='HUMAN', prompt='1+1=?'),
dict(role='BOT', prompt='2'),
dict(role='HUMAN', prompt='2+2=?'),
dict(role='BOT', prompt='4'),
dict(role='HUMAN', prompt='3+3=?')
])
多模态 prompt
多模态模型不仅包含文本信息(text),还可能包含图像(image)、音频(audio)、视频(video)等多模态信息。这些信息无法采用字符串式模板进行构造,因此 AISBench 提供了多模态式模板(MMPromptTemplate)来支持多模态信息的构造。
多模态式模板的语法与对话式模板一致,只是将 prompt 替换为 prompt_mm。prompt_mm 是一个字典,键为模态类型,值为模态信息。
reader_cfg=dict(
input_columns=['anything', 'question'],
output_column='answer',
)
# 多模态数据类型为url:
prompt_template=dict(
type=MMPromptTemplate,
template=dict(round=[dict(role="HUMAN", prompt_mm={
"text": {"type": "text", "text": "{anything}\nQuestion: {question}"},
"image": {"type": "image_url", "image_url": {"url": "file://{image}"}},
"video": {"type": "video_url", "video_url": {"url": "file://{video}"}},
"audio": {"type": "audio_url", "audio_url": {"url": "file://{audio}"}}})])
)
# 多模态数据类型为base64:
prompt_template=dict(
type=MMPromptTemplate,
template=dict(round=[dict(role="HUMAN", prompt_mm={
"text": {"type": "text", "text": "{anything}\nQuestion: {question}"},
"image": {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,{image}"}},
"video": {"type": "video_url", "video_url": {"url": "data:video/jpeg;base64,{video}"}},
"audio": {"type": "audio_url", "audio_url": {"url": "data:audio/wav;base64,{audio}"}}})])
)
infer_cfg = dict(
prompt_template=prompt_template,
retriever=...,
inferencer=dict(type=GenInferencer, **args))
其中,prompt_mm 中的键为模态类型,值为模态信息。当前支持的模态类型为 text、image、audio、video。每个键值内部是一个包含 type 和对应类型值的字典。数据集中加载的数据类型需要与 prompt_mm 中的键值类型一致。此外,样本数据需要采用 prompt.py 中定义的标识符进行拼接,标识不同数据内容的类别,如下所示:
text:
<AIS_TEXT_START>{text_content}<AIS_CONTENT_TAG>image:
<AIS_IMAGE_START>{image_content}<AIS_CONTENT_TAG>audio:
<AIS_AUDIO_START>{audio_content}<AIS_CONTENT_TAG>video:
<AIS_VIDEO_START>{video_content}<AIS_CONTENT_TAG>
数据填充的内容根据模型实际输入的要求进行调整,例如对于图片输入,可以采用如下格式: <AIS_IMAGE_START>image.jpg<AIS_CONTENT_TAG> <AIS_IMAGE_START>file://image.jpg<AIS_CONTENT_TAG> <AIS_IMAGE_START>data:image/jpeg;base64,base64_data<AIS_CONTENT_TAG> <AIS_IMAGE_START>https://xxx.com/image.jpg<AIS_CONTENT_TAG>
让我们假设有一个数据样本如下:
example = {
'anything': 'blabla', # 假设 anything 被写在了 reader_cfg.input_columns 中
'question': '<AIS_TEXT_START>What is this?<AIS_CONTENT_TAG><AIS_IMAGE_START>{image_data}<AIS_CONTENT_TAG><AIS_AUDIO_START>{audio_data}<AIS_CONTENT_TAG><AIS_VIDEO_START>{video_data}<AIS_CONTENT_TAG>', # 假设 question 被写在了 reader_cfg.input_columns 中,image_data为图片的base64数据或URL,audio_data为音频的base64数据或URL,video_data为视频的base64数据或URL
'answer': 'a cat', # 假设 answer 被写在了 reader_cfg.output_column 中
}
则填入模板后的结果会根据 template 格式进行填充。
多模态数据类型为 URL 时,中间结果为:
PromptList([
{"role": "HUMAN",
"prompt": [
{"type": "text", "text": "blabla\nQuestion: What is this?"},
{"type": "image_url", "image_url": {"url": "{image_data}"}},
{"type": "audio_url", "audio_url": {"url": "{audio_data}"}},
{"type": "video_url", "video_url": {"url": "{video_data}"}}]
}
])
多模态数据类型为 base64 时,中间结果如下:
PromptList([
{"role": "HUMAN",
"prompt": [
{"type": "text", "text": "blabla\nQuestion: What is this?"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,{image_data}"}},
{"type": "audio_url", "audio_url": {"url": "data:audio/wav;base64,{audio_data}"}},
{"type": "video_url", "video_url": {"url": "data:video/mp4;base64,{video_data}"}}]
}
])
ice_template 与 prompt_template
在 AISBench 中,对于 0-shot 的评测,我们通常只需要定义 prompt_template 字段,即可完成提示词的构造。但对于 few-shot(有 shot case 的评测),我们还需要定义 ice_template 字段,用于管理上下文学习中样例所对应的提示词模板。
ice_template 和 prompt_template 两者遵循的语法和规则一致,完整提示词的构造流程可以使用如下的伪代码表示:
def build_prompt():
ice = ice_template.format(*ice_example) # 根据ice_template格式化shot case
prompt = prompt_template.replace(prompt_template.ice_token, ice).format(*prompt_example) # 将shot case的内容填充到prompt_template中
return prompt
现在,让我们假设有两个训练数据作为shot case (ex1, ex2) 和一个测试数据 (ex3):
ex1 = {
'question': '2+2=?',
'answer': '4',
'irrelavent_infos': 'blabla',
}
ex2 = {
'question': '3+3=?',
'answer': '6',
'irrelavent_infos': 'blabla',
}
ex3 = {
'question': '1+1=?',
'answer': '2', # 假设 answer 被写在了 reader_cfg.output_column 中
'irrelavent_infos': 'blabla',
}
接下来,我们看一下不同的 prompt 构造方法对应的实际效果:
字符串式模板
模板配置如下:
infer_cfg=dict(
ice_template=dict(
type=PromptTemplate,
template='{question}\n{answer}'
),
prompt_template=dict(
type=PromptTemplate,
template='Solve the following questions.\n</E>{question}\n{answer}',
ice_token='</E>',
)
)
会得到以下字符串:
Solve the following questions.
2+2=?
4
3+3=?
6
1+1=?
对话式模板
模板配置如下:
infer_cfg=dict(
ice_template=dict(
type=PromptTemplate,
template=dict(
round=[
dict(role="HUMAN", prompt="{question}"),
dict(role="BOT", prompt="{answer}"),
]
)
),
prompt_template=dict(
type=PromptTemplate,
template=dict(
begin=[
dict(role='SYSTEM', fallback_role='HUMAN', prompt='Solve the following questions.'),
'</E>',
],
round=[
dict(role="HUMAN", prompt="{question}"),
dict(role="BOT", prompt="{answer}"),
],
),
ice_token='</E>',
)
)
AISBench 把数据填入模板后得到的中间结果为:
PromptList([
dict(role='SYSTEM', fallback_role='HUMAN', prompt='Solve the following questions.'),
dict(role='HUMAN', prompt='2+2=?'),
dict(role='BOT', prompt='4'),
dict(role='HUMAN', prompt='3+3=?'),
dict(role='BOT', prompt='6'),
dict(role='HUMAN', prompt='1+1=?'),
dict(role='BOT', prompt=''),
])
省略式使用方法
值得一提的是,为了简化配置文件,prompt_template 这一字段是可被省略的。当 prompt_template 字段被省略时,ice_template 会同时被作为 prompt_template,用于拼装得到完整的提示词。以下两份 infer_cfg 配置是等价的:
| 完整写法 | 省略写法 |
|---|---|
infer_cfg=dict(
ice_template=dict(
type=PromptTemplate,
template="Q: {question}\nA: {answer}",
),
prompt_template=dict(
type=PromptTemplate,
template="</E>Q: {question}\nA: {answer}",
ice_token="</E>",
),
# ...
)
|
infer_cfg=dict(
ice_template=dict(
type=PromptTemplate,
template="</E>Q: {question}\nA: {answer}",
ice_token="</E>",
),
# ...
)
|
更一般地,即便在 0-shot learning 的情况下(即 retriever 为 ZeroRetriever)时,这一机制依然生效。因此以下配置也是合法的:
datasets = [
dict(
infer_cfg=dict(
ice_template=dict(
type=PromptTemplate,
template="Q: {question}\nA: {answer}",
),
retriever=dict(type=ZeroRetriever),
inferencer=dict(type=GenInferencer),
)
),
]