《Hands-On Large Language Models》第6章 提示工程

\(\text{6}\) 提示工程

Prompt Engineering

在本书的前几章中,我们迈出了大型语言模型\(\text{LLM}\))世界的第一步。我们深入研究了各种应用,例如监督式和无监督式分类,使用了专注于表征文本的模型,例如 \(\text{BERT}\) 及其衍生模型。

随着我们的进展,我们使用了主要为文本生成而训练的模型,这些模型通常被称为生成式预训练 \(\text{Transformer}\) (\(\text{GPT}\))。这些模型具有根据用户的提示生成文本的卓越能力。通过提示工程\(\text{prompt engineering}\)),我们可以以增强生成文本质量的方式来设计这些提示。

在本章中,我们将更详细地探索这些生成模型,并深入研究提示工程使用生成模型进行推理验证,甚至评估其输出的领域。

使用文本生成模型

Using Text Generation Models

在我们开始提示工程的基础知识之前,了解利用文本生成模型的基本原理至关重要。我们如何选择要使用的模型?我们是使用专有模型还是开源模型?我们如何控制生成的输出?这些问题将成为我们使用文本生成模型的垫脚石

选择文本生成模型

Choosing a Text Generation Model

选择文本生成模型始于专有模型开源模型之间的选择。虽然专有模型通常性能更高,但本书更侧重于开源模型,因为它们提供了更大的灵活性并且免费使用

\(\text{6}-1\) 展示了具有影响力的基础模型\(\text{foundation models}\))的一小部分,这些 \(\text{LLM}\) 已在海量文本数据上进行了预训练,并且通常针对特定应用进行微调

F6.1

从这些基础模型中,衍生出了数百甚至数千个经过微调的模型,每个模型都比另一个更适合某些任务。选择要使用的模型可能是一项艰巨的任务

我们建议从一个小的基础模型开始。因此,让我们继续使用参数量为 \(\text{3.8}\) 亿的 \(\text{Phi}-\text{3}-\text{mini}\)。这使得它适用于 \(\text{8 GB VRAM}\) 及以下的设备上运行。总的来说,扩展到更大的模型往往比缩小模型提供更好的体验。较小的模型提供了一个很好的介绍,并为过渡到更大的模型奠定了坚实的基础

加载文本生成模型

Loading a Text Generation Model

正如我们在前几章中所做的那样,加载模型最直接的方法是利用 \(\text{transformers}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
# Load model and tokenizer
model = AutoModelForCausalLM.from_pretrained(
"microsoft/Phi-3-mini-4k-instruct",
device_map="cuda",
torch_dtype="auto",
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")
# Create a pipeline
pipe = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
return_full_text=False,
max_new_tokens=500,
do_sample=False,
)

与前几章相比,我们将更仔细地研究开发和使用提示模板

为了说明,让我们重温第 \(\text{1}\) 章中的示例,我们在其中要求 \(\text{LLM}\) 编一个关于鸡的笑话:

1
2
3
4
5
6
7
# Prompt
messages = [
{"role": "user", "content": "Create a funny joke about chickens."}
]
# Generate the output
output = pipe(messages)
print(output[0]["generated_text"])
1
2
Why don't chickens like to go to the gym? Because they can't crack the egg-
sistence of it!

在底层,\(\text{transformers.pipeline}\) 首先将我们的 \(\text{messages}\) 转换为特定的提示模板。我们可以通过访问底层的分词器来探索这个过程:

1
2
3
# Apply prompt template
prompt = pipe.tokenizer.apply_chat_template(messages, tokenize=False)
print(prompt)
1
2
3
<s><|user|>
Create a funny joke about chickens.<|end|>
<|assistant|>

您可能从\(\text{2}\)中认出了 <|user|><|assistant|> 等特殊词元。如图 \(\text{6}-2\) 所示,这个提示模板是在模型训练期间使用的。它不仅提供了谁说了什么的信息,还用于指示模型何时应停止生成文本(参见 <|end|> 词元)。这个提示被直接传递给 \(\text{LLM}\) 并一次性处理

F6.1

在下一章中,我们将自定义这个模板的某些部分。在本章中,我们可以使用 \(\text{transformers.pipeline}\) 来为我们处理聊天模板处理。接下来,让我们探索如何控制模型的输出

控制模型输出

Controlling Model Output

除了提示工程,我们还可以通过调整模型参数来控制我们想要的输出类型。在前面的例子中,您可能已经注意到我们在 \(\text{pipe}\) 函数中使用了几个参数,包括 \(\text{temperature}\)(温度)和 \(\text{top\_p}\)

这些参数控制着输出的随机性。使 \(\text{LLM}\) 成为一项令人兴奋的技术的部分原因在于它能对完全相同的提示生成不同的响应。每次 \(\text{LLM}\) 需要生成一个词元时,它都会为每个可能的词元分配一个似然值\(\text{likelihood number}\))。

如图 \(\text{6}-3\) 所示,在句子“\(\text{I am driving a...}\)”中,紧随其后的词元如 “\(\text{car}\)”(汽车)或 “\(\text{truck}\)”(卡车)的似然值通常高于像 “\(\text{elephant}\)”(大象)这样的词元。然而,生成 “\(\text{elephant}\)”的可能性仍然存在,只是低得多

F6.1

当我们加载模型时,我们故意将 \(\text{do\_sample}\) 设置为 \(\text{False}\),以确保输出保持一定的一致性。这意味着不会进行采样,并且只选择最可能的下一个词元。然而,为了使用 \(\text{temperature}\)\(\text{top\_p}\) 参数,我们将设置 \(\text{do\_sample}=\text{True}\),以便利用它们。

温度 (\(\text{Temperature}\))

温度控制着生成的文本的随机性或创造性。它定义了选择不太可能的词元的可能性。其基本思想是,温度为 \(\text{0}\)每次都会生成相同的响应,因为它总是选择最可能的词语。如图 \(\text{6}-4\) 所示,更高的值允许生成不太可能的词语

F6.1

因此,更高的温度(例如 \(\text{0.8}\))通常会导致更多样化的输出,而更低的温度(例如 \(\text{0.2}\))则会产生更具确定性的输出

您可以在管线中按如下方式使用温度:

1
2
3
# Using a high temperature
output = pipe(messages, do_sample=True, temperature=1)
print(output[0]["generated_text"])
1
2
Why don't chickens like to go on a rollercoaster? Because they're afraid they
might suddenly become chicken-soup!

请注意,每次您重新运行这段代码时,输出都会改变\(\text{temperature}\) 引入了随机行为,因为模型现在会随机选择词元

\(\text{top\_p}\)

\(\text{top\_p}\),也称为核心采样\(\text{nucleus sampling}\)),是一种采样技术,它控制 \(\text{LLM}\) 可以考虑的词元子集(即核心)。它将考虑词元直到达到它们的累积概率。如果我们将 \(\text{top\_p}\) 设置为 \(\text{0.1}\),它将考虑词元直到累积概率达到该值。如果我们将 \(\text{top\_p}\) 设置为 \(\text{1}\),它将考虑所有词元

如图 \(\text{6}-5\) 所示,通过降低该值,模型将考虑更少的词元,通常会给出较少“创造性”的输出;而增加该值则允许 \(\text{LLM}\) 从更多的词元中选择

F6.1

同样,\(\text{top\_k}\) 参数精确控制着 \(\text{LLM}\) 可以考虑多少个词元。如果将其值更改为 \(\text{100}\)\(\text{LLM}\)只考虑最可能的 \(\text{100}\) 个词元

您可以在管线中按如下方式使用 \(\text{top\_p}\)

1
2
3
# Using a high top_p
output = pipe(messages, do_sample=True, top_p=1)
print(output[0]["generated_text"])
1
2
Why don't chickens make good comedians? Because their 'jokes' always 'feather'
the truth!

如表 \(\text{6}-1\) 所示,这些参数允许用户在具有创造性(高 \(\text{temperature}\)\(\text{top\_p}\))和具有可预测性(低 \(\text{temperature}\)\(\text{top\_p}\))之间进行滑动调整

F6.1

提示工程入门

Intro to Prompt Engineering

提示工程\(\text{Prompt engineering}\))是使用文本生成式 \(\text{LLM}\)重要组成部分。通过精心设计我们的提示,我们可以引导 \(\text{LLM}\) 生成所需的响应。无论提示是问题、陈述还是指令,提示工程的主要目标从模型中引出有用的响应

提示工程不仅仅是设计有效的提示。它还可以用作评估模型输出以及设计保障措施和安全缓解方法的工具。这是一个提示优化的迭代过程,需要实验完美的提示设计不存在,而且未来也不太可能出现

在本节中,我们将介绍提示工程的常用方法,以及理解某些提示效果的小技巧和窍门。这些技能使我们能够理解 \(\text{LLM}\) 的能力,并构成了与这类模型进行交互的基础

我们首先回答一个问题:一个提示中应该包含什么?

提示的基本要素

The Basic Ingredients of a Prompt

大型语言模型 (\(\text{LLM}\)) 是一台预测机器。它根据特定的输入(即提示 \(\text{prompt}\)),尝试预测后面可能出现的词语。如图 \(\text{6}-6\) 所示,从核心来看,提示不需要超过几个词就能引出 \(\text{LLM}\) 的响应。

F6.1

然而,尽管该图示可作为基本示例,但它未能完成特定的任务。相反,我们通常进行提示工程时,会询问 \(\text{LLM}\) 应该完成的特定问题或任务。为了引出所需的响应,我们需要一个结构更清晰的提示

例如,如图 \(\text{6}-7\) 所示,我们可以要求 \(\text{LLM}\) 将一个句子分类为具有积极或消极情感。这将最基本的提示扩展为包含两个组成部分指令本身与指令相关的数据

F6.1

更复杂的用例可能需要在提示中包含更多组成部分。例如,为了确保模型只输出\(\text{negative}\)”(消极)或“\(\text{positive}\)”(积极),我们可以引入输出指示符来帮助引导模型。在图 \(\text{6}-8\) 中,我们在句子前加上“\(\text{Text:}\)”,并添加“\(\text{Sentiment:}\)”,以阻止模型生成完整的句子。相反,这种结构表明我们期望得到“\(\text{negative}\)”或“\(\text{positive}\)”的答案。尽管模型可能没有直接针对这些组成部分进行训练,但它被输入了足够的指令,使其能够泛化到这种结构。

F6.1

我们可以不断添加或更新提示的元素,直到引出我们想要的响应。我们可以添加额外的示例更详细地描述用例提供额外的上下文等。这些组成部分仅仅是示例,而不是一个有限的可能性集合。设计这些组成部分所带来的创造力是关键

尽管提示是单个文本片段,但将其视为更大拼图的碎片会非常有帮助。我是否描述了我的问题的上下文?提示中是否有输出的示例

基于指令的提示

Instruction-Based Prompting

尽管提示(\(\text{prompting}\))有许多不同的形式,从与 \(\text{LLM}\) 讨论哲学到与你最喜欢的超级英雄进行角色扮演,但提示通常用于让 \(\text{LLM}\) 回答一个具体问题解决一个特定任务。这被称为基于指令的提示\(\text{instruction-based prompting}\))。

\(\text{6}-9\) 展示了基于指令的提示发挥重要作用的一些用例。我们在前面的示例中已经做过其中之一,即监督式分类

F6.1

这些任务中的每一个都需要不同的提示格式,更具体地说,需要向 \(\text{LLM}\) 提出不同的问题。要求 \(\text{LLM}\) 总结一段文本不会突然导致分类结果。为了说明,图 \(\text{6}-10\) 中列出了一些用例的提示示例。

F6.1

尽管这些任务需要不同的指令,但用于提高输出质量的提示技术实际上存在很多重叠。这些技巧的非详尽列表包括:

特异性 (\(\text{Specificity}\))

准确描述您想要实现的目标。与其要求 \(\text{LLM}\)\(\text{Write a description for a product}\)”(为产品写一段描述),不如要求它“\(\text{Write a description for a product in less than two sentences and use a formal tone}\)”(用少于两句话使用正式语气为产品写一段描述)。

幻觉 (\(\text{Hallucination}\))

\(\text{LLM}\) 可能会自信地生成不正确的信息,这被称为幻觉\(\text{hallucination}\))。为了减少其影响,我们可以要求 \(\text{LLM}\) 只有在知道答案时才生成答案。如果它不知道答案,它可以回应“\(\text{I don’t know}\)”(我不知道)。

顺序 (\(\text{Order}\))

要么以指令开始提示,要么以指令结束提示。特别是在长提示中,中间的信息通常会被遗忘\(\text{LLM}\) 倾向于关注提示开头首因效应 \(\text{primacy effect}\))或提示结尾近因效应 \(\text{recency effect}\))的信息。

在这里,特异性可以说是最重要的方面。通过限制和明确模型应该生成的内容,它生成与您用例不相关内容的可能性就会较小。例如,如果我们跳过指令“\(\text{in two to three sentences}\)”(用两到三句话),它可能会生成完整的段落。就像人类对话一样,如果没有任何具体的指令或额外的上下文,很难推断出手头的任务到底是什么

高级提示工程

Advanced Prompt Engineering

从表面上看,创建一个好的提示(\(\text{prompt}\))似乎很简单。问一个具体的问题,保持准确,添加一些示例,就完成了!然而,提示很快就会变得复杂起来,因此它往往是利用 \(\text{LLM}\) 的一个被低估的组成部分

在这里,我们将介绍几种构建提示的高级技术,从构建复杂提示的迭代工作流程开始,一直到顺序使用 \(\text{LLM}\) 以获得改进结果。最终,我们甚至会深入到高级推理技术

提示的潜在复杂性

The Potential Complexity of a Prompt

正如我们在提示工程入门中探讨的那样,一个提示通常由多个组成部分构成。在我们最初的示例中,我们的提示由指令数据输出指示符组成。正如我们之前提到的,任何提示都不局限于这三个组成部分,您可以根据需要将其构建得尽可能复杂

这些高级组成部分可以快速使提示变得相当复杂。一些常见的组成部分包括:

  • 人设 (\(\text{Persona}\)):描述 \(\text{LLM}\) 应该扮演的角色。例如,如果您想问一个关于天体物理学的问题,可以使用“\(\text{You are an expert in astrophysics}\)”(你是一位天体物理学专家)。
  • 指令 (\(\text{Instruction}\))任务本身。确保这尽可能具体。我们不希望留有太多的解读空间。
  • 上下文 (\(\text{Context}\)):描述问题或任务背景的额外信息。它回答了诸如“指令的原因是什么?”之类的问题。
  • 格式 (\(\text{Format}\))\(\text{LLM}\) 应该用来输出生成文本的格式。如果没有它,\(\text{LLM}\) 将自己设计格式,这在自动化系统中很麻烦。
  • 受众 (\(\text{Audience}\))生成文本的目标对象。这也描述了生成输出的水平。出于教育目的,使用 \(\text{ELI5}\)(“\(\text{Explain it like I’m 5}\)”,像我 \(\text{5}\) 岁一样解释)通常很有帮助。
  • 语气 (\(\text{Tone}\))\(\text{LLM}\) 在生成文本中应该使用的语调。如果您正在给老板写一封正式的邮件,您可能不希望使用非正式的语调。
  • 数据 (\(\text{Data}\)):与任务本身相关的主要数据

为了说明,让我们扩展我们前面提到的分类提示,并使用所有上述组成部分。这在图 \(\text{6}-11\) 中有所展示。

F6.1

这个复杂的提示展示了提示的模块化特性。我们可以自由地添加和删除组成部分,并判断它们对输出的影响。如图 \(\text{6}-12\) 所示,我们可以循序渐进地构建我们的提示,并探索每次更改的效果

F6.1

这些更改不限于简单地引入或移除组成部分。它们的顺序,正如我们之前看到的近因效应\(\text{recency effect}\))和首因效应\(\text{primacy effect}\))一样,也会影响 \(\text{LLM}\) 输出的质量。换句话说,在为您的用例寻找最佳提示时,实验至关重要。通过提示,我们实际上让自己处于一个迭代的实验循环中。

亲自尝试一下!使用这个复杂提示,通过添加和/或移除部分来观察它对生成输出的影响。您会很快注意到哪些“拼图碎片”值得保留。您可以通过将其添加到 \(\text{data}\) 变量中来使用您自己的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Prompt components
persona = "You are an expert in Large Language models. You excel at breaking
down complex papers into digestible summaries.\n"
instruction = "Summarize the key findings of the paper provided.\n"
context = "Your summary should extract the most crucial points that can help
researchers quickly understand the most vital information of the paper.\n"
data_format = "Create a bullet-point summary that outlines the method. Follow
this up with a concise paragraph that encapsulates the main results.\n"
audience = "The summary is designed for busy researchers that quickly need to
grasp the newest trends in Large Language Models.\n"
tone = "The tone should be professional and clear.\n"
text = "MY TEXT TO SUMMARIZE"
data = f"Text to summarize: {text}"
# The full prompt - remove and add pieces to view its impact on the generated
output
query = persona + instruction + context + data_format + audience + tone + data

我们可以添加各种各样的组成部分,以及一些富有创意的元素,例如使用情感刺激(例如,“\(\text{This is very important for my career.}\)”)。提示工程的乐趣在于您可以尽可能地发挥创意,找出哪种提示组合最有助于您的用例。开发适合您的格式的限制很少

从某种意义上说,这是一种尝试逆向工程\(\text{reverse engineer}\)模型所学到的内容以及它如何响应某些提示的尝试。但是请注意,由于训练数据可能不同训练目的不同某些提示对于特定模型的效果会优于其他模型

上下文学习:提供示例

In-Context Learning: Providing Examples

在前面的部分中,我们试图准确地描述 \(\text{LLM}\) 应该做什么。尽管准确和具体的描述有助于 \(\text{LLM}\) 理解用例,但我们可以更进一步。与其描述任务,为什么不直接展示任务呢?

我们可以向 \(\text{LLM}\) 提供我们想要实现的目标的精确示例。这通常被称为上下文学习\(\text{in}-\text{context learning}\)),即我们向模型提供正确的示例

如图 \(\text{6}-13\) 所示,这根据您向 \(\text{LLM}\) 展示的示例数量而有多种形式:零样本提示\(\text{Zero}-\text{shot prompting}\)不使用示例单样本提示\(\text{one}-\text{shot prompts}\)使用一个示例;而少样本提示\(\text{few}-\text{shot prompts}\)使用两个或更多示例

F6.1

沿用原始的说法,我们相信“一个例子抵得上千言万语”。这些示例提供了关于 \(\text{LLM}\) 应该实现什么以及如何实现直接榜样

我们可以用摘自描述该方法的原始论文的一个简单示例来说明此方法。该提示的目标是生成一个包含虚构词语的句子。为了提高生成句子的质量,我们可以向生成模型展示一个包含虚构词语的恰当句子的示例

为此,我们需要区分我们的问题\(\text{user}\))和模型提供的答案\(\text{assistant}\))。我们还展示了如何使用模板来处理这种交互:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Use a single example of using the made-up word in a sentence
one_shot_prompt = [
{
"role": "user",
"content": "A 'Gigamuru' is a type of Japanese musical instrument. An example of a sentence that uses the word Gigamuru is:"
},
{
"role": "assistant",
"content": "I have a Gigamuru that my uncle gave me as a gift. I love to play it at home."
},
{
"role": "user",
"content": "To 'screeg' something is to swing a sword at it. An example of a sentence that uses the word screeg is:"
}
]
print(tokenizer.apply_chat_template(one_shot_prompt, tokenize=False))
1
2
3
4
5
6
7
8
9
10
<s><|user|>
A 'Gigamuru' is a type of Japanese musical instrument. An example of a sen-
tence that uses the word Gigamuru is:<|end|>
<|assistant|>
I have a Gigamuru that my uncle gave me as a gift. I love to play it at home.<|
end|>
<|user|>
To 'screeg' something is to swing a sword at it. An example of a sentence that
uses the word screeg is:<|end|>
<|assistant|>

该提示说明了区分用户和助手的必要性。如果我们不这样做,看起来就像我们在自言自语一样。使用这些交互,我们可以按如下方式生成输出:

1
2
3
# Generate the output
outputs = pipe(one_shot_prompt)
print(outputs[0]["generated_text"])
1
During the intense duel, the knight skillfully screeged his opponent's shield, forcing him to defend himself.

正确地生成了答案

与所有提示组成部分一样,单样本或少样本提示并非提示工程的万能药。我们可以将其用作拼图中的一块,以进一步增强我们给出的描述。模型仍然可能通过随机采样“选择”忽略指令

链式提示:分解问题

Chain Prompting: Breaking up the Problem

在前面的示例中,我们探讨了将提示拆分成模块化组件以提高 \(\text{LLM}\) 的性能。尽管这适用于许多用例,但对于高度复杂的提示或用例来说可能不可行

我们可以不在一个提示内分解问题,而是在提示之间进行分解。本质上,我们将一个提示的输出用作下一个提示的输入,从而创建一个解决问题的连续交互链

为了说明,假设我们想使用 \(\text{LLM}\) 根据一些产品特征来为我们创建产品名称、标语和销售宣传语。虽然我们可以要求 \(\text{LLM}\) 一次性完成,但我们可以将问题分解成几个部分

因此,如图 \(\text{6}-14\) 所示,我们得到了一个顺序管线:首先创建产品名称;然后使用该名称和产品特征作为输入来创建标语;最后,使用特征、产品名称和标语创建销售宣传语

F6.1

这种链式提示的技术允许 \(\text{LLM}\) 将更多时间花在每个单独的问题上,而不是同时处理整个问题。让我们用一个小例子来说明这一点。我们首先为一款聊天机器人创建一个名称和标语

1
2
3
4
5
6
7
8
# Create name and slogan for a product
product_prompt = [
leverages LLMs."}
{"role": "user", "content": "Create a name and slogan for a chatbot that
]
outputs = pipe(product_prompt)
product_description = outputs[0]["generated_text"]
print(product_description)
1
2
Name: 'MindMeld Messenger'
Slogan: 'Unleashing Intelligent Conversations, One Response at a Time'

然后,我们可以使用生成的输出作为 \(\text{LLM}\) 生成销售宣传语的输入

1
2
3
4
5
6
7
8
# Based on a name and slogan for a product, generate a sales pitch
sales_prompt = [
{"role": "user", "content": f"Generate a very short sales pitch for the
following product: '{product_description}'"}
]
outputs = pipe(sales_prompt)
sales_pitch = outputs[0]["generated_text"]
print(sales_pitch)
1
2
3
4
5
6
7
Introducing MindMeld Messenger - your ultimate communication partner! Unleash
intelligent conversations with our innovative AI-powered messaging platform.
With MindMeld Messenger, every response is thoughtful, personalized, and
timely. Say goodbye to generic replies and hello to meaningful interactions.
Elevate your communication game with MindMeld Messenger - where every message
is a step toward smarter conversations. Try it now and experience the future
of messaging!

尽管我们需要对模型进行两次调用,但一个主要优点是我们可以为每次调用设置不同的参数。例如,名称和标语所需的词元数量相对较少,而宣传语可以长得多。

这可用于各种用例,包括:

  • 响应验证 (\(\text{Response validation}\)):要求 \(\text{LLM}\) 重新检查先前生成的输出。
  • 并行提示 (\(\text{Parallel prompts}\))并行创建多个提示,并进行最后一步合并它们。例如,要求多个 \(\text{LLM}\) 并行生成多个食谱,然后使用合并的结果创建购物清单。
  • 撰写故事 (\(\text{Writing stories}\)):通过将问题分解成组件来利用 \(\text{LLM}\) 撰写书籍或故事。例如,首先撰写摘要、开发角色、构建故事节拍,然后再深入创建对话。

在下一章中,我们将自动化这一过程,并超越简单的链式 \(\text{LLM}\)。我们将把其他技术组件(如记忆、工具使用等)连接在一起!在此之前,我们将在接下来的部分中进一步探索提示链的这一想法,这些部分描述了更复杂的提示链方法,如自洽性\(\text{self}-\text{consistency}\))、思维链\(\text{chain}-\text{of}-\text{thought}\))和思想树\(\text{tree}-\text{of}-\text{thought}\))。

使用生成模型进行推理

Reasoning with Generative Models

在前面的部分中,我们主要关注提示的模块化组件,并通过迭代来构建它们。这些高级提示工程技术,例如链式提示\(\text{prompt chaining}\)),被证明是使用生成模型进行复杂推理的第一步

推理人类智能的核心组成部分,它经常被拿来与 \(\text{LLM}\) 的涌现行为进行比较,后者通常类似于推理。我们特意强调“类似于”,因为在撰写本书时,这些模型通常被认为是通过对训练数据的记忆和模式匹配来展示这种行为的。

然而,它们展示的输出可以体现复杂的行为,尽管这可能不是“真正的”推理,但它们仍然被称为推理能力。换句话说,我们通过提示工程\(\text{LLM}\) 协同工作,以便模仿推理过程,从而提高 \(\text{LLM}\) 的输出

为了促成这种推理行为,现在是回顾并探索推理在人类行为中意味着什么的好时机。简单来说,我们的推理方法可以分为系统 \(\text{1}\)系统 \(\text{2}\) 思考过程。

系统 \(\text{1}\) 思考代表着自动、直觉近乎即时的过程。它与生成模型有相似之处,后者自动生成词元而没有任何自我反思的行为。相比之下,系统 \(\text{2}\) 思考是一个有意识、缓慢逻辑的过程,类似于头脑风暴和自我反思

如果我们可以赋予生成模型模仿某种形式的自我反思的能力,我们本质上就是在模拟系统 \(\text{2}\) 的思考方式,这种方式倾向于产生比系统 \(\text{1}\) 思考更深思熟虑的响应。在本节中,我们将探索几种旨在模仿人类推理者这类思维过程、以提高模型输出的技术。

思维链:先思考再回答

Chain-of-Thought: Think Before Answering

朝着生成模型中复杂推理迈出的第一步也是重要一步是通过一种称为思维链\(\text{Chain-of-Thought, CoT}\))的方法。思维链旨在让生成模型先进行“思考”,而不是在没有任何推理的情况下直接回答问题

如图 \(\text{6}-15\) 所示,它在提示中提供了一些示例,这些示例展示了模型在生成响应之前应该进行的推理过程。这些推理过程被称为“思绪”(\(\text{thoughts}\))。这对涉及较高复杂度的任务(如数学问题)非常有帮助。添加这个推理步骤允许模型在推理过程中分配更多的计算资源。模型不再是基于几个词元来计算整个解决方案,而是推理过程中的每一个额外的词元都允许 \(\text{LLM}\) 稳定其输出

F6.1

我们使用作者在他们的论文中使用的例子来演示这种现象:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Answering with chain-of-thought
cot_prompt = [
{"role": "user", "content": "Roger has 5 tennis balls. He buys 2 more cans
of tennis balls. Each can has 3 tennis balls. How many tennis balls does he
have now?"},
{"role": "assistant", "content": "Roger started with 5 balls. 2 cans of 3
tennis balls each is 6 tennis balls. 5 + 6 = 11. The answer is 11."},
{"role": "user", "content": "The cafeteria had 23 apples. If they used 20
to make lunch and bought 6 more, how many apples do they have?"}
]
# Generate the output
outputs = pipe(cot_prompt)
print(outputs[0]["generated_text"])
1
2
3
The cafeteria started with 23 apples. They used 20 apples, so they had 23 - 20
= 3 apples left. Then they bought 6 more apples, so they now have 3 + 6 = 9
apples. The answer is 9.

请注意模型不是只生成答案,而是在此之前提供了解释。通过这样做,它可以利用它迄今为止生成的知识来计算最终答案

虽然思维链是增强生成模型输出的一个好方法,但它要求在提示中提供一个或多个推理示例,而用户可能无法获取这些示例。我们可以不提供示例,而是简单地要求生成模型提供推理零样本思维链 \(\text{zero}-\text{shot chain}-\text{of}-\text{thought}\))。有许多不同的表达方式都有效,但一个常见且有效的方法是使用短语“\(\text{Let’s think step-by-step}\)”(让我们一步一步地思考),如图 \(\text{6}-16\) 所示。

F6.1

使用我们之前用过的例子,我们只需将该短语附加到提示的末尾,即可启用类似思维链的推理

1
2
3
4
5
6
7
8
9
# Zero-shot chain-of-thought
zeroshot_cot_prompt = [
{"role": "user", "content": "The cafeteria had 23 apples. If they used 20
to make lunch and bought 6 more, how many apples do they have? Let's think
step-by-step."}
]
# Generate the output
outputs = pipe(zeroshot_cot_prompt)
print(outputs[0]["generated_text"])
1
2
3
4
5
Step 1: Start with the initial number of apples, which is 23.
Step 2: Subtract the number of apples used to make lunch, which is 20. So, 23
- 20 = 3 apples remaining.
Step 3: Add the number of apples bought, which is 6. So, 3 + 6 = 9 apples.
The cafeteria now has 9 apples.

不需要提供示例的情况下,我们再次获得了相同的推理行为。这就是为什么在进行计算时展示您的工作如此重要。通过处理推理过程\(\text{LLM}\) 可以使用先前生成的信息作为生成最终答案的指导

💡 尽管提示“\(\text{Let’s think step by step}\)”可以改善输出,但您不受限于这种确切的措辞。替代方案包括“\(\text{Take a deep breath and think step-by-step}\)”(深吸一口气,一步一步地思考)和“\(\text{Let’s work through this problem step-by-step}\)”(让我们一步一步地解决这个问题)。

自洽性:对输出进行采样

Self-Consistency: Sampling Outputs

如果我们通过像 \(\text{temperature}\)\(\text{top\_p}\) 这样的参数允许一定程度的创造性,那么多次使用相同的提示可能会导致不同的结果。因此,输出的质量可能会根据词元的随机选择提高或降低。换句话说,这取决于运气

为了抵消这种随机性并提高生成模型的性能,人们引入了自洽性\(\text{self-consistency}\))。该方法要求生成模型多次使用相同的提示,并将多数结果作为最终答案。在这个过程中,每个答案都可以受到不同的 \(\text{temperature}\)\(\text{top\_p}\)的影响,以增加采样的多样性

如图 \(\text{6}-17\) 所示,该方法可以通过添加思维链提示来进一步改进其推理能力,同时仅使用答案进行投票程序

F6.1

然而,这确实需要多次询问同一个问题。因此,尽管该方法可以提高性能,但它的速度会慢 \(n\),其中 \(n\)输出样本的数量

思想树:探索中间步骤

Tree-of-Thought: Exploring Intermediate Steps

思维链自洽性的理念旨在实现更复杂的推理。通过从多个“思绪”中进行采样并使它们更深思熟虑,我们的目标是改善生成模型的输出

这些技术仅触及了当前为模仿复杂推理所做工作的皮毛。这些方法的改进可以在思想树\(\text{Tree-of-Thought, ToT}\))中找到,它允许对多个想法进行深入探索

该方法的工作原理如下:当面对一个需要多个推理步骤的问题时,将其分解成多个部分通常会有所帮助。如图 \(\text{6}-18\) 所示,在每一步,都会提示生成模型探索针对当前问题的不同解决方案。然后,它会投票选出最佳解决方案,并继续下一步

F6.1

在需要考虑多种路径时,例如在撰写故事提出创意想法时,这种方法非常有帮助。该方法的一个缺点是它需要多次调用生成模型,这会显著减慢应用程序的速度。幸运的是,已经有人成功地将思想树框架转换成一种简单的提示技术

我们不多次调用生成模型,而是要求模型通过模仿多位专家之间的对话模拟这种行为。这些专家将相互提问,直到他们达成共识。一个零样本思想树提示的示例如下:

1
2
3
4
5
6
7
# Zero-shot tree-of-thought
zeroshot_tot_prompt = [
{"role": "user", "content": "Imagine three different experts are answering this question. All experts will write down 1 step of their thinking, then share it with the group. Then all experts will go on to the next step, etc. If any expert realizes they're wrong at any point then they leave. The question is
'The cafeteria had 23 apples. If they used 20 to make lunch and bought 6 more,how many apples do they have?'
Make sure to discuss the results."
}
]

我们可以使用这个提示来探索 \(\text{LLM}\) 如何响应复杂问题

1
2
3
# Generate the output
outputs = pipe(zeroshot_tot_prompt)
print(outputs[0]["generated_text"])
1
2
3
4
5
6
7
8
9
Expert 1: Step 1 - Start with the initial number of apples: 23 apples.
Expert 2: Step 1 - Subtract the apples used for lunch: 23 - 20 = 3 apples
remaining.
Expert 3: Step 1 - Add the newly bought apples: 3 + 6 = 9 apples.
Expert 1: Step 2 - Confirm the final count: The cafeteria has 9 apples.
Expert 2: Step 2 - Review the calculations: 23 - 20 = 3, then 3 + 6 = 9. The
calculations are correct.
Expert 3: Step 2 - Agree with the result: The cafeteria indeed has 9 apples.
All experts agree on the final count: The cafeteria has 9 apples.

我们再次得到了正确的答案,但这次是通过“专家之间的讨论”。看到这种“专家”之间的交流是很有趣的,它展示了提示工程所带来的创造力

输出验证

Output Verification

使用生成模型构建的系统和应用程序最终可能会投入生产环境。在这种情况下,验证和控制模型输出防止应用程序崩溃并创建鲁棒的生成式 \(\text{AI}\) 应用程序至关重要。

验证输出的原因可能包括:

  • 结构化输出 (\(\text{Structured output}\)):默认情况下,大多数生成模型创建自由格式的文本,除了自然语言定义的结构外,不遵循特定的结构。某些用例要求它们的输出以特定格式(如 \(\text{JSON}\))进行结构化

  • 有效输出 (\(\text{Valid output}\)):即使我们允许模型生成结构化输出,它仍然有能力自由生成其内容。例如,当模型被要求输出两种选择之一时,它不应该凭空想出第三种

  • 伦理 (\(\text{Ethics}\)):一些开源生成模型没有护栏,会生成不考虑安全或伦理考量的输出。例如,用例可能要求输出不含亵渎、个人身份信息 (\(\text{PII}\))、偏见、文化刻板印象等。

  • 准确性 (\(\text{Accuracy}\)):许多用例要求输出符合某些标准或性能。目的是再次检查生成的信息是否事实准确、连贯没有幻觉

控制生成模型的输出,正如我们通过像 \(\text{top\_p}\)\(\text{temperature}\) 这样的参数所探讨的那样,并非易事。这些模型需要帮助才能生成符合特定指南的、一致的输出

通常,有三种控制生成模型输出的方法:

  1. 示例 (\(\text{Examples}\)):提供预期输出的多个示例。
  2. 语法 (\(\text{Grammar}\))控制词元选择过程
  3. 微调 (\(\text{Fine-tuning}\)):用包含预期输出的数据调整模型

在本节中,我们将介绍前两种方法。第三种方法,即微调模型,将留到\(\text{12}\),届时我们将深入探讨微调方法。

提供示例

Providing Examples

修复输出的一个简单直接的方法是为生成模型提供输出应该是什么样子的示例。正如我们之前探讨的,少样本学习\(\text{few}-\text{shot learning}\))是一种有助于引导生成模型输出的有用技术。该方法也可以推广引导输出的结构

例如,让我们考虑一个我们希望生成模型\(\text{RPG}\) 游戏创建角色档案的例子。我们从不使用示例开始:

1
2
3
4
5
6
7
# Zero-shot learning: Providing no examples
zeroshot_prompt = [
{"role": "user", "content": "Create a character profile for an RPG game in JSON format."}
]
# Generate the output
outputs = pipe(zeroshot_prompt)
print(outputs[0]["generated_text"])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"characterProfile": {
"name": "Eldrin Stormbringer",
"class": "Warlock",
"race": "Half-Elf",
"age": 27,
"gender": "Male",
"alignment": "Chaotic Good",
"background": "Rogue",

},
"attributes": {
"strength": 10,
"dexterity": 17,
"constitution": 12,
"intelligence": 12,
"wisdom": 10,
"charisma

上面这个被截断的输出不是有效的 \(\text{JSON}\),因为模型在开始“\(\text{charisma}\)”属性后停止了生成词元。此外,我们可能不想要某些属性。相反,我们可以为模型提供一些示例指示预期的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# One-shot learning: Providing an example of the output structure
one_shot_template = """Create a short character profile for an RPG game. Make
sure to only use this format:
{
"description": "A SHORT DESCRIPTION",
"name": "THE CHARACTER'S NAME",
"armor": "ONE PIECE OF ARMOR",
"weapon": "ONE OR MORE WEAPONS"
}
"""
one_shot_prompt = [
{"role": "user", "content": one_shot_template}
]
# Generate the output
outputs = pipe(one_shot_prompt)
print(outputs[0]["generated_text"])
1
2
3
4
5
6
7
{
"description": "A cunning rogue with a mysterious past, skilled in stealth
and deception.",
"name": "Lysandra Shadowstep",
"armor": "Leather Cloak of the Night",
"weapon": "Dagger of Whispers, Throwing Knives"
}

模型完美地遵循了我们给出的示例,这使得行为更加一致。这也证明了利用少样本学习来改进输出结构不仅仅是内容的重要性。

这里一个重要的注意事项是,模型是否会遵循您建议的格式仍然取决于模型本身有些模型在遵循指令方面比其他模型做得更好

语法:约束采样

Grammar: Constrained Sampling

少样本学习有一个很大的缺点:我们不能明确地阻止生成某些输出。虽然我们引导模型给出指令,但它可能仍然不会完全遵循

相反,为了约束和验证生成模型的输出,一些软件包(如 \(\text{Guidance}\)\(\text{Guardrails}\)\(\text{LMQL}\))被迅速开发出来。如图 \(\text{6}-19\) 所示,它们部分地利用生成模型来验证其自身的输出。生成模型将检索输出作为新的提示,并尝试根据预定义的一系列护栏对其进行验证

F6.1

同样,如图 \(\text{6}-20\) 所示,这种验证过程也可以用于控制输出的格式,方法是由我们自己生成其格式的一部分,因为我们已经知道它应该如何结构化。

F6.1

这个过程可以更进一步,我们可以不在输出后进行验证,而是在词元采样过程中就进行验证。在采样词元时,我们可以定义一系列语法或规则\(\text{LLM}\) 在选择其下一个词元时应遵守这些规则。例如,如果我们要求模型在执行情感分类时只返回\(\text{positive}\)”(积极)、“\(\text{negative}\)”(消极)或“\(\text{neutral}\)”(中性),它仍然有可能返回其他内容。如图 \(\text{6}-21\) 所示,通过约束采样过程,我们可以让 \(\text{LLM}\) 只输出我们感兴趣的内容。请注意,这仍然受到诸如 \(\text{top\_p}\)\(\text{temperature}\)参数的影响

F6.1

让我们用 \(\text{llama}-\text{cpp}-\text{python}\) 来演示这个现象,这是一个类似于 \(\text{transformers}\) 的库,我们可以用它来加载我们的语言模型。它通常用于高效地加载和使用压缩模型(通过量化;见第 \(\text{12}\) 章),但我们也可以用它来应用 \(\text{JSON}\) 语法

我们将加载本章中一直使用的相同模型,但使用不同的格式,即 \(\text{GGUF}\)\(\text{llama}-\text{cpp}-\text{python}\) 预期使用这种格式,它通常用于压缩(量化)模型

由于我们正在加载一个新模型,建议重新启动 \(\text{notebook}\)。这将清除任何以前的模型并清空显存\(\text{VRAM}\))。您也可以运行以下代码来清空显存:

1
2
3
4
5
6
import gc
import torch
del model, tokenizer, pipe
# Flush memory
gc.collect()
torch.cuda.empty_cache()

现在我们已经清除了内存,我们可以加载 \(\text{Phi}-\text{3}\)。我们将 \(\text{n\_gpu\_layers}\) 设置为 \(\text{-1}\),表示我们希望模型的所有层都从 \(\text{GPU}\) 运行\(\text{n\_ctx}\) 指的是模型的上下文大小\(\text{repo\_id}\)\(\text{filename}\) 指的是模型所在的 \(\text{Hugging Face}\) 仓库

1
2
3
4
5
6
7
8
9
from llama_cpp.llama import Llama
# Load Phi-3
llm = Llama.from_pretrained(
repo_id="microsoft/Phi-3-mini-4k-instruct-gguf",
filename="*fp16.gguf",
n_gpu_layers=-1,
n_ctx=2048,
verbose=False
)

为了使用内部 \(\text{JSON}\) 语法生成输出,我们只需要将 \(\text{response\_format}\) 指定为一个 \(\text{JSON}\) 对象。在底层,它将应用 \(\text{JSON}\) 语法以确保输出遵循该格式

为了说明,让我们要求模型以 \(\text{JSON}\) 格式创建一个用于龙与地下城\(\text{Dungeons \& Dragons}\))游戏的角色档案:

1
2
3
4
5
6
7
8
# Generate output
output = llm.create_chat_completion(
messages=[
{"role": "user", "content": "Create a warrior for an RPG in JSON format."},
],
response_format={"type": "json_object"},
temperature=0,
)['choices'][0]['message']["content"]

为了检查输出是否确实是 \(\text{JSON}\),我们可以尝试像处理 \(\text{JSON}\) 一样处理它

1
2
3
4
import json
# Format as json
json_output = json.dumps(json.loads(output), indent=4)
print(json_output)
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
{
"name": "Eldrin Stormbringer",
"class": "Warrior",
"level": 10,
"attributes": {
"strength": 18,
"dexterity": 12,
"constitution": 16,
"intelligence": 9,
"wisdom": 14,
"charisma": 10
},
"skills": {
"melee_combat": {
"weapon_mastery": 20,
"armor_class": 18,
"hit_points": 35
},
"defense": {
"shield_skill": 17,
"block_chance": 90
},
"endurance": {
"health_regeneration": 2,
"stamina": 30
}
},
"equipment": [
{
"name": "Ironclad Armor",
"type": "Armor",
"defense_bonus": 15
},
{
"name": "Steel Greatsword",
"type": "Weapon",
"damage": 8,
"critical_chance": 20
}
],
"background": "Eldrin grew up in a small village on the outskirts of a
war-torn land. Witnessing the brutality and suffering caused by conflict, he
dedicated his life to becoming a formidable warrior who could protect those
unable to defend themselves."
}

该输出已正确地格式化为 \(\text{JSON}\)。这使我们能够更自信地在那些要求输出必须遵循特定格式的应用中使用生成模型

总结

在本章中,我们通过提示工程\(\text{prompt engineering}\))和输出验证探索了使用生成模型的基础知识。我们重点关注了提示工程所带来的创造性和潜在复杂性。提示的这些组成部分对于生成和优化适用于不同用例的输出至关重要。

我们进一步探索了高级提示工程技术,例如上下文学习\(\text{in}-\text{context learning}\))和思维链\(\text{chain}-\text{of}-\text{thought}\))。这些方法通过提供示例鼓励逐步思考的短语,来引导生成模型对复杂问题进行推理,从而模仿人类的推理过程

总的来说,本章表明提示工程是与 \(\text{LLM}\) 协作的关键方面,因为它使我们能够有效地将我们的需求和偏好传达给模型。通过掌握提示工程技术,我们可以释放 \(\text{LLM}\) 的部分潜力,并生成符合我们要求的高质量响应

下一章将以这些概念为基础,探索利用生成模型的更高级技术。我们将超越提示工程,探索 \(\text{LLM}\) 如何使用外部记忆和工具

书籍各章的机翻md文件:
《Hands-On Large Language Models》目录及前言
《Hands-On Large Language Models》第1章 大型语言模型简介
《Hands-On Large Language Models》第2章 词元与嵌入
《Hands-On Large Language Models》第3章 深入了解大型语言模型
《Hands-On Large Language Models》第4章 文本分类
《Hands-On Large Language Models》第5章 文本聚类和主题建模
《Hands-On Large Language Models》第6章 提示工程
《Hands-On Large Language Models》第7章 高级文本生成技术与工具
《Hands-On Large Language Models》第8章 语义搜索与检索增强生成
《Hands-On Large Language Models》第9章 多模态大型语言模型
《Hands-On Large Language Models》第10章 创建文本嵌入模型
《Hands-On Large Language Models》第11章 微调用于分类的表征模型
《Hands-On Large Language Models》第12章 生成模型的微调