File size: 15,236 Bytes
256a159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# Prompt 模板

## 背景

在语言模型的评测中,我们常会将原始数据集以一定的规则构造成 prompt,以便模型能够按照要求回答问题。

通常,我们会在 prompt 开头放入指令,几个 in-context example(上下文样例),再在最后放入题目。例如:

```text
Solve the following questions.
1+1=?
2
3+9=?
12
5+6=?
```

大量的实验表明,即便测试的原始题目相同,对于 prompt 的不同构造方式会对模型的表现产生影响。可能影响的因素包括:

- Prompt 本身的构成方式,包括指令、in-context example、题目的写法;
- in-context example 的选择,包括了选择的数量和方式;
- 对 prompt 的使用方式。是让模型基于 prompt 进行补全,还是从候选的 prompt 中选择一个最好的作为答案?

OpenCompass 将 prompt 的构建策略定义在了数据集配置中的 `infer_cfg` 部分。一个典型的 `infer_cfg` 如下所示:

```python
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),  # 使用何种方式推理得到 prediction
)
```

本文档中,我们将会主要介绍 `ice_template``prompt_template``inferencer` 的定义方法。对于 `retriever` 的介绍请参考其他章节。

我们首先介绍 prompt 的基本语法。

## 字符串式 prompt

字符串式的模板是比较经典的模板形式,考虑下面的模板:

```python
prompt_template=dict(
    type=PromptTemplate,
    template="{anything}\nQuestion: {question}\nAnswer: {answer}"
)
```

运行时,花括号`{}`内的字段会被替换成数据样本内的对应字段。如果数据样本中没有对应的字段,则会保持原样输出。

例如我们有一个数据 example 如下:

```python
example = {
    'question': '1+1=?',
    'answer': '2',  # 假设 answer 被写在了 reader_cfg.output_column 中
    'irrelavent_infos': 'blabla',
}
```

则填入模板后的结果为:

```text
{anything}
Question: 1+1=?
Answer:
```

可以看到,问题的实际答案 `answer` 并没有出现在生成的结果中。这是因为 OpenCompass 会遮盖被写在 `reader_cfg.output_column` 中的字段,避免答案泄露。关于 `reader_cfg` 的详细说明,请参考介绍数据集配置的相关文档。

## 对话式 prompt

在实际的测试中,简单的补全式测试并不能很好地测试出对话式的模型的性能,因此我们更希望 prompt 能以对话的格式输入到模型中。另外,不同的模型对对话的格式定义也不一样,因此我们也需要数据集侧产生的 prompt 更加通用,在测试时再结合具体模型生成符合需求的提示词。

因此,OpenCompass 在字符串式模板之上,增加了对对话式模板的支持。对话式模板更加灵活,它可以结合模型侧不同的 [meta_template](./meta_template.md) 生成不同对话形式的提示词,同时适用于基座和对话模型,但定义也相对复杂。

现在,让我们假设有一个数据样本如下:

```python
example = {
    'question': '1+1=?',
    'answer': '2',  # 假设 answer 被写在了 reader_cfg.output_column 中
    'irrelavent_infos': 'blabla',
}
```

接下来,我们来展示几个例子:

`````{tabs}

````{tab} 普通对话
```python
prompt_template=dict(
    type=PromptTemplate,
    template=dict(
        round=[
            dict(role="HUMAN", prompt="Question: {question}"),
            dict(role="BOT", prompt="Answer: {answer}"),
        ]
    )
)
```

OpenCompass 把数据填入模板后得到的中间结果为:

```python
PromptList([
    dict(role='HUMAN', prompt='Question: 1+1=?'),
    dict(role='BOT', prompt='Answer: '),
])
```

````

````{tab} 多轮对话
```python
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}"),
        ]
    )
)
```

OpenCompass 把数据填入模板后得到的中间结果为:

```python
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: '),
])
```
````


````{tab} 带 SYSTEM 的对话

```python
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}"),
        ]
    )
)
```

OpenCompass 把数据填入模板后得到的中间结果为:

```python
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 角色。

````

`````

可以见到,在对话式的模板中,prompt 是以不同角色 `role` 的对话为形式进行组织的。在当前 OpenCompass 的预定义数据集配置中,一个 prompt 中常有的角色有:

- `HUMAN`:人类,通常为提问的一方
- `BOT`:语言模型,通常为回答的一方
- `SYSTEM`:系统,通常用在提示词的开头,负责下达指令。

另外与字符串式的模板不同,经过对话式模板所生成的 prompt 从固定的字符串变成了一个中间结构 PromptList。这个结构会进一步与模型侧的 [meta template](./meta_template.md) 相结合,拼装完成得到最终的提示词。如果不指定 meta template,PromptList 中各项的 prompt 则会直接按行拼接成字符串。

```{note}
上面例子中 PromptList 中的内容并非模型最终的输入,而取决于 meta template 的处理。一个容易产生误解的地方是,在生成式的评测中,最后一个 `BOT` 角色的 prompt `Answer: ` **不会**实际输入到模型。这是由于 API 模型通常并无法自定义模型回复的开头,因此这一设定保持了语言模型与 API 模型在评测上行为的一致。更多信息可以参考 [meta template](./meta_template.md) 的文档。
```

<details>
<summary>点击查看完整参数介绍</summary>

- `begin``end` :(list,可选) prompt 的开头和结尾,通常是一些系统级别的指令。里面的每一项**允许是一个字典或字符串**- `round`:(list) 对话的模板格式。列表的每一项**只允许是一个字典**。

每一个字典的参数如下:

- `role`(str): 参与对话的角色名,用于与 `meta_template` 中的名称进行关联,不会影响实际生成的 prompt。

- `fallback_role` (str) : 缺省角色名,假设 `meta_template` 中找不到 `role`,则会尝试使用 `fallback_role` 进行关联。默认为 `None`

- `prompt` (str) : 角色的对话内容。

</details>

## Prompt 模板 与 `inferencer`

在明白了 prompt 模板的基础定义方式后,我们还要根据 `inferencer` 的类型组织 prompt 模板。

OpenCompass 中主要支持了两种 Infernecer:`GenInferencer``PPLInferencer`,它们对应着两种不同的推理方式。

`GenInferencer` 对应生成式的推理。在推理时,模型被要求以输入的提示词为基准,继续往下续写。此时,`template` 则单一地表示这一句话对应的模板,例如:

`````{tabs}

````{group-tab} 字符串式模板
```python
prompt_template=dict(
    type=PromptTemplate,
    template='Solve the following questions.\n{question}\n{answer}'
)
```
````

````{group-tab} 对话式模板
```python
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}"),
            dict(role="BOT", prompt="{answer}"),
        ]
    )
)
```
````

`````

则模型的推理结果将会是往下续写的字符串。

而 `PPLInferencer` 对应判别式推理。在推理时,模型被要求计算多个输入字符串各自的混淆度 (PerPLexity / ppl),并将其中 ppl 最小的项作为模型的推理结果。此时 `template` 是一个 `dict`,表示每一句话所对应的模板,例如:

`````{tabs}

````{group-tab} 字符串式模板
```python
prompt_template=dict(
    type=PromptTemplate,
    template=dict(
        "A": "Question: Which is true?\nA. {A}\nB. {B}\nC. {C}\nAnswer: A",
        "B": "Question: Which is true?\nA. {A}\nB. {B}\nC. {C}\nAnswer: B",
        "C": "Question: Which is true?\nA. {A}\nB. {B}\nC. {C}\nAnswer: C",
        "UNK": "Question: Which is true?\nA. {A}\nB. {B}\nC. {C}\nAnswer: None of them is true.",
    )
)
```
````

````{group-tab} 对话式模板
```python
prompt_template=dict(
    type=PromptTemplate,
    template=dict(
        "A": dict(
            round=[
                dict(role="HUMAN", prompt="Question: Which is true?\nA. {A}\nB. {B}\nC. {C}"),
                dict(role="BOT", prompt="Answer: A"),
            ]
        ),
        "B": dict(
            round=[
                dict(role="HUMAN", prompt="Question: Which is true?\nA. {A}\nB. {B}\nC. {C}"),
                dict(role="BOT", prompt="Answer: B"),
            ]
        ),
        "C": dict(
            round=[
                dict(role="HUMAN", prompt="Question: Which is true?\nA. {A}\nB. {B}\nC. {C}"),
                dict(role="BOT", prompt="Answer: C"),
            ]
        ),
        "UNK": dict(
            round=[
                dict(role="HUMAN", prompt="Question: Which is true?\nA. {A}\nB. {B}\nC. {C}"),
                dict(role="BOT", prompt="Answer: None of them is true."),
            ]
        ),
    )
)
```
````

`````

此时模型的推理结果将会是 `template` 的四个 key 之一 ("A" / "B" / "C" / "UNK")

## `ice_template` 与 `prompt_template`

在 OpenCompass 中,对于 0-shot 的评测,我们通常只需要定义 `prompt_template` 字段,即可完成 prompt 的构造。但对于 few shot 的评测,我们还需要定义 `ice_template` 字段,管理上下文学习中样例所对应的 prompt 模板。

`ice_template``prompt_template` 两者遵循的语法和规则一致,完整 prompt 的构造流程可以使用如下的伪代码进行表示:

```python
def build_prompt():
    ice = ice_template.format(*ice_example)
    prompt = prompt_template.replace(prompt_template.ice_token, ice).format(*prompt_example)
    return prompt
```

现在,让我们假设有两个训练数据 (ex1, ex2) 和一个测试数据 (ex3):

```python
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 构造方法对应的实际效果:

`````{tabs}

````{group-tab} 字符串式模板

模板配置如下:

```python
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>',
    )
)
```

会得到以下字符串:

```text
Solve the following questions.
2+2=?
4
3+3=?
6
1+1=?

```

````

````{group-tab} 对话式模板

模板配置如下:

```python
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>',
    )
)
```

OpenCompass 把数据填入模板后得到的中间结果为:

```python
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`,用于拼装得到完整的 prompt。以下两份 `infer_cfg` 是等价的:

<table class="docutils">
  <thead>
  <tr>
      <th>完整写法</th>
      <th>省略写法</th>
  <tbody>
  <tr>
  <td>

```python
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>",
    ),
    # ...
)
```

</td>
  <td>

```python
infer_cfg=dict(
    ice_template=dict(
        type=PromptTemplate,
        template="</E>Q: {question}\nA: {answer}",
        ice_token="</E>",
    ),
    # ...
)
```

</td>
  </tr>
  </thead>
  </table>

更一般地,即便在 0-shot learning 的情况下(即 `retriever``ZeroRetriver`)时,这一机制依然生效。因此以下配置也是合法的:

```python
datasets = [
    dict(
        infer_cfg=dict(
            ice_template=dict(
                type=PromptTemplate,
                template="Q: {question}\nA: {answer}",
            ),
            retriever=dict(type=ZeroRetriever),
            inferencer=dict(type=GenInferencer),
        )
    ),
]
```

## 使用建议

建议使用 [Prompt Viewer](../tools.md) 工具对完成拼装后的 prompt 进行可视化,确认模板是否正确,结果是否符合预期。