目录
第五章:文本聚类和主题建模-优快云博客https://blog.youkuaiyun.com/m0_67804957/article/details/145844319前几章初步介绍了大型语言模型(LLM)及其在文本表示(如BERT)和文本生成(如GPT)中的应用,并探讨了通过提示工程提升生成文本质量的方法。在本章中,我们将深入研究生成型模型,重点探讨提示工程、推理、验证和输出评估等关键领域。
一、使用文本生成模型
在开始提示工程的基础知识之前,了解如何使用文本生成模型的基本知识至关重要。我们该如何选择要使用的模型?我们是使用专有模型还是开源模型?我们如何控制生成的输出?这些问题将成为我们使用文本生成模型的起点。
1.1 选择文本生成模型
选择文本生成模型的起点是选择专有模型还是开源模型。尽管专有模型通常性能更强,但本书更多地关注开源模型,因为它们提供了更大的灵活性,并且可以免费使用。

上图展示了一些具有影响力的基础模型,这些 LLM 在大量文本数据上进行了预训练,并且通常会针对特定应用进行微调。
从这些基础模型中,已经有数百甚至数千个模型被微调,其中一些更适合某些任务。选择要使用的模型可能是一项艰巨的任务!
我们建议从较小的基础模型开始。因此,我们继续使用 Phi3--mini,它有 38 亿个参数。这使得它适合在具有高达 8GB 视频内存的设备上运行。总体而言,向上扩展到更大的模型通常比向下扩展更愉快。较小的模型提供了很好的入门体验,并为转向更大的模型奠定了坚实的基础。
1.1.1 OpenAI Chat接口的重要参数
1.1.2 加载文本生成模型
加载模型的最直接方法是利用我们在前几章中使用过的 transformers 库:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
# 加载模型和分词器
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")
# 创建一个管道
pipe = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
return_full_text=False,
max_new_tokens=500,
do_sample=False,
)
与前几章相比,我们将更仔细地研究开发和使用提示模板。
为了说明,让我们回顾第 1 章中的例子,我们让 LLM 讲一个关于chicken的笑话:
# 提示
messages = [
{"role": "user", "content": "Create a funny joke about chickens."}
]
# 生成输出
output = pipe(messages)
print(output[0]["generated_text"])
为什么鸡不喜欢去健身房?因为它们无法破解“蛋-存在”的谜团!
在底层,transformers.pipeline 首先将我们的消息转换为特定的提示模板。我们可以通过访问底层的分词器来探索这个过程:
# 应用提示模板
prompt = pipe.tokenizer.apply_chat_template(messages, tokenize=False)
print(prompt)
<s><|user|>
Create a funny joke about chickens.<|end|>
<|assistant|>
你可能在第 2 章中认出了特殊的标记 <|user|>
和 <|assistant|>
。这个提示模板在模型训练过程中被使用(如下图 所示)。它不仅提供了谁说了什么的信息,还用于指示模型何时停止生成文本(参见 <|end|>
标记)。这个提示直接传递给 LLM 并一次性处理。
在下一章中,我们将自己定制这个模板的一部分。在本章中,我们可以使用 transformers.pipeline 来为我们处理聊天模板。接下来,让我们探索如何控制模型的输出。
1.2 控制模型输出
除了提示工程之外,我们还可以通过调整模型参数来控制我们想要的输出类型。在前面的例子中,你可能注意到我们在 pipe 函数中使用了几个参数,包括温度(temperature)和 top_p。
这些参数控制输出的随机性。LLM 令人兴奋的一部分是它可以为完全相同的提示生成不同的响应。每次 LLM 需要生成一个标记时,它都会为每个可能的标记分配一个可能性数字。
如下图所示,在句子“我正在驾驶……”中,该句子后面出现“汽车”或“卡车”等标记的可能性通常高于“大象”。然而,“大象”仍然有可能被生成,只是可能性要低得多。

当我们加载模型时,我们特意将 do_sample 设置为 False,以确保输出相对一致。这意味着不会进行采样,只会选择最有可能的下一个标记。然而,为了使用温度和 top_p 参数,我们将 do_sample 设置为 True,以便利用它们。
1.2.1 温度
温度控制生成文本的随机性或创造性。它定义了选择不太可能的标记的可能性。其基本原理是,温度为 0 时,每次生成相同的响应,因为它总是选择最有可能的词。如下图所示,更高的值允许生成不太可能的词。

因此,更高的温度(例如 0.8)通常会产生更多样化的输出,而较低的温度(例如 0.2)则会产生更确定性的输出。
你可以在管道中使用温度,如下所示:
# 使用高温度
output = pipe(messages, do_sample=True, temperature=1)
print(output[0]["generated_text"])
为什么鸡不喜欢去坐过山车?因为它们害怕突然变成鸡汤!
请注意,每次重新运行这段代码时,输出都会改变!温度引入了随机行为,因为模型现在会随机选择标记。
1.2.2 top_p
top_p,也称为核采样,是一种采样技术,用于控制 LLM 可以考虑的标记子集(核)。它会考虑一个tokens的子集,直到累积概率达到top_p。如果我们把 top_p 设置为 0.1,它会考虑标记,直到达到该值。如果我们把 top_p 设置为 1,它会考虑所有标记。
如下图所示,通过降低值,它会考虑较少的标记,并通常产生不太“创造性”的输出,而增加值则允许 LLM 从更多标记中选择。

同样,top_k 参数可以精确控制 LLM 可以考虑的标记数量。如果你将其值改为 100,LLM 将只考虑最有可能的前 100 个标记。
你可以在管道中使用 top_p,如下所示:
# 使用高 top_p
output = pipe(messages, do_sample=True, top_p=1)
print(output[0]["generated_text"])
为什么鸡不能成为好的喜剧演员?因为它们的“笑话”总是“羽毛”了真相!
如表 6-1 所示,这些参数允许用户在创造性(高温度和高 top_p)和可预测性(低温度和低 top_p)之间进行调整。
表 6-1:选择温度和 top_p 值的用例示例。
示例用例 | 温度 | top_p | 描述 |
---|---|---|---|
头脑风暴会议 | 高 | 高 | 高随机性,潜在标记池大。结果将高度多样化,通常会产生非常有创意且意想不到的结果。 |
邮件生成 | 低 | 低 | 确定性输出,使用高概率预测标记。这会产生可预测、专注且保守的输出。 |
创意写作 | 高 | 低 | 高随机性,潜在标记池小。这种组合产生有创意的输出,但仍然保持连贯性。 |
翻译 | 低 | 高 | 确定性输出,使用高概率预测标记。产生连贯的输出,并使用更广泛的词汇,从而产生具有语言多样性的输出。 |
二、提示工程简介
提示工程是与文本生成型大型语言模型(LLM)交互的核心技能。通过精心设计提示,我们可以引导模型生成期望的响应。提示可以是问题、陈述或指令,其主要目标是从模型中获取有用的输出。提示工程不仅用于优化提示的有效性,还可以作为评估模型输出和设计安全机制的工具。这是一个需要不断实验的迭代过程,不太可能有完美的提示设计。
在本节中,我们将探讨常见的提示工程方法和一些小技巧,以理解某些提示的效果。这些技能有助于我们了解LLM的能力,并为与这类模型的交互奠定基础。
2.1 提示的基本要素
LLM本质上是一个预测机器,它根据输入的提示尝试预测接下来可能出现的词汇。在最基本的形式中,提示可能只是一个简短的句子(如下图所示),但这种简单的提示往往无法完成特定任务。因此,我们通常通过提出具体问题或任务来引导LLM生成期望的响应,这就需要更结构化的提示。
例如,我们可以要求LLM对一个句子的情感倾向进行分类(如下图所示)
这将提示扩展为两个组成部分:指令本身和与指令相关的数据。更复杂的应用场景可能需要在提示中加入更多组件,例如输出指示符(如下图所示),以确保模型仅输出“正面”或“负面”。虽然模型可能没有直接针对这些组件进行训练,但它已经接受了足够的指令,能够泛化到这种结构。
我们可以根据需要不断添加或更新提示的元素,直到获得期望的响应。例如,可以添加更多示例、更详细地描述用例或提供更多上下文。这些组件只是示例,设计这些组件的创造力是关键。
虽然提示是一段文本,但将提示视为一个更大拼图的一部分会非常有帮助。例如,是否描述了问题的上下文?提示中是否包含输出示例?
2.2 基于指令的提示
尽管提示的形式多种多样,从与LLM讨论哲学到与最喜欢的超级英雄角色扮演,提示通常用于让LLM回答具体问题或完成特定任务,这被称为基于指令的提示(如下图所示)。我们已经在前面的例子中展示了其中一个用例,即监督分类。
尽管这些任务需要不同的指令,但用于提高输出质量的提示技巧有很多重叠之处。这些技巧包括:
具体性:准确描述你想要实现的目标。例如,不要只是要求LLM“为产品写描述”,而是要求它“用正式语气写一个不超过两句话的产品描述”。
幻觉问题:LLM可能会自信地生成错误信息,这被称为“幻觉”。为了减少其影响,我们可以要求LLM只有在确知答案时才生成回答,否则回答“我不知道”。
顺序:将指令放在提示的开头或结尾。尤其是对于较长的提示,中间的信息往往容易被忽略。LLM倾向于关注提示开头(首因效应)或结尾(近因效应)的信息。
其中,具体性可能是最重要的方面。通过限制和明确指定模型应该生成的内容,可以减少生成与用例无关内容的可能性。例如,如果省略了“两到三句话”的指令,模型可能会生成完整的段落。就像人类对话一样,如果没有具体的指令或额外的上下文,很难确定当前任务的实际内容。
三、高级提示工程
表面上看,创建一个良好的提示似乎很简单:提出一个具体问题,保持准确性,添加一些示例,就完成了!然而,提示工程可能会迅速变得复杂,因此常常被低估为利用大型语言模型(LLM)的一个关键组成部分。
在本节中,我们将探讨一些高级提示构建技巧,从逐步构建复杂提示的迭代工作流,到按顺序使用LLM以获得更好的结果,甚至还会涉及高级推理技术。
3.1 提示词工程六大策略
3.1.1 策略1:编写清晰的指令
语言模型无法猜测你的需求,指令越清晰,输出越准确。提供具体要求、示例或格式,有助于获得更符合预期的答案。
3.1.2 策略2:提供参考文本
语言模型可能会生成错误或虚构的答案,尤其是在涉及冷门知识、引用或网址时。提供参考文本能减少错误,提高回答的准确性。
3.1.3 策略3:将复杂任务拆分为简单子任务
复杂任务的错误率较高,可通过将其拆解为多个简单步骤,提高准确性和可执行性。子任务的输出可作为下一步的输入,减少错误累积。
3.1.4 策略4:让模型有时间“思考’
当模型被要求立即回答复杂问题时,容易出现推理错误。通过引导型逐步推理或进行自我检查,可以提高答案的准确性和可靠性。
3.1.5 策略5:使用外部工具
语言模型并非万能,某些任务(如检索知识、计算复杂公式、调用 API)可以借助外部工具来提高准确性和效率。结合外部工具能弥补模型的弱点,提升任务执行效果 。
3.1.6 策略6:系统性地测试修改
在优化提示词时,单个示例可能会产生误导。使用系统化评测方法(如 A/B 测试或自动评估)来衡量整体效果,以确保改动能带来全局性的优化。
提示词技巧基本原则
- 具体化 specificity:不要留下自由发挥的空间
- 避免幻觉 hallucination:如果不会,不要硬答
- 指令顺序 开始和结束关注度最高 primacy/recency effect
- 从简单模式开始,根据结果逐步迭代
3.2 结构化撰写提示词
正如我们在提示工程入门中所探讨的,提示通常由多个组件组成。在我们的第一个例子中,提示包括指令、数据和输出指示符。正如我们之前提到的,提示并不局限于这三个组件,你可以根据需要将其构建得尽可能复杂。
这些高级组件可以迅速使提示变得复杂。一些常见的组件包括:
角色(Persona)
描述LLM应该扮演的角色。例如,如果你想问关于天体物理学的问题,可以使用“你是一位天体物理学专家”。指令(Instruction)
任务本身。确保指令尽可能具体,避免留下太多解释空间。上下文(Context)
描述问题或任务的背景信息。它回答了“为什么需要这个指令?”之类的问题。格式(Format)
LLM应使用的输出格式。如果没有指定,LLM可能会自行选择格式,这在自动化系统中可能会带来麻烦。受众(Audience)
生成文本的目标受众。它还描述了生成输出的水平。对于教育目的,通常使用“像我五岁一样解释”(ELI5)很有帮助。语气(Tone)
LLM在生成文本中应使用的语气。例如,如果你正在给老板写一封正式邮件,可能不希望使用非正式的语气。数据(Data)
与任务本身直接相关的数据。
为了说明,让我们扩展之前的情感分类提示,并使用上述所有组件。这在下图中有所展示。

这个复杂的提示展示了提示的模块化特性。我们可以自由地添加和移除组件,并判断它们对输出的影响。正如下图所示,我们可以逐步构建提示,探索每次更改的效果。

更改不仅限于引入或移除组件。正如我们之前提到的首因效应和近因效应,它们的顺序也会影响LLM输出的质量。换句话说,实验对于找到适合你用例的最佳提示至关重要。在提示工程中,我们本质上处于一个实验的迭代循环中。
尝试一下!使用复杂的提示添加和/或移除部分,观察其对生成输出的影响。你会很快注意到哪些部分是值得保留的。你可以通过将数据添加到data
变量中来使用自己的数据:
# 提示组件
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}"
# 完整的提示——移除和添加部分以查看其对生成输出的影响
query = persona + instruction + context + data_format + audience + tone + data
例子:
提示:
我们可以添加各种组件,甚至可以使用情感刺激(例如,“这对我的职业生涯非常重要”)等创意组件。提示工程的乐趣之一在于你可以尽可能发挥创造力,以确定哪些提示组件的组合适合你的用例。开发适合你的格式几乎没有限制。在某种程度上,这是一种尝试反向工程模型所学到的内容以及它对某些提示的反应。然而,需要注意的是,某些提示对某些模型更有效,因为它们的训练数据可能不同,或者它们是为了不同目的而训练的。
3.3 基于上下文的学习:提供示例
在前面的部分中,我们尝试准确描述LLM应该做什么。尽管准确和具体的描述有助于LLM理解用例,但我们还可以更进一步。与其描述任务,为什么不直接展示任务呢?
我们可以向LLM提供我们想要实现的示例。这通常被称为基于上下文的学习,即向模型提供正确的示例。
如下图所示,根据向LLM展示的示例数量,这可以有多种形式:
- 零样本提示(zero-shot prompting)不使用示例;
- 单样本提示(one-shot prompting)使用一个示例;
- 少样本提示(few-shot prompting)使用两个或更多示例。

我们相信“一个示例胜过千言万语”。这些示例为LLM提供了直接的示例,展示了它应该实现的内容和方式。
我们可以用一个简单的例子来说明这种方法,该例子来自描述这种方法的原始论文。假设提示的目标是生成一个包含虚构单词的句子。为了提高生成句子的质量,我们可以向生成模型展示一个包含虚构单词的正确句子示例。
为此,我们需要区分用户的问题和模型提供的答案。我们还展示了如何使用模板处理这种交互:
# 使用一个虚构单词在句子中的示例
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))
<s><|user|>
A 'Gigamuru' is a type of Japanese musical instrument. An example of a sentence 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|>
如果不区分用户和助手,就好像我们在自言自语一样。通过这些交互,我们可以生成如下输出:
# 生成输出
outputs = pipe(one_shot_prompt)
print(outputs[0]["generated_text"])
>> 输出
During the intense duel, the knight skillfully screeged his opponent's shield, forcing him to defend himself.
它正确地生成了答案!
正如所有提示组件一样,单样本或少样本提示并不是提示工程的万能解决方案。我们可以将其视为拼图的一部分,以进一步增强我们提供的描述。模型仍然可以通过随机采样选择忽略指令。
3.4 链式提示:分解问题
在前面的例子中,我们探讨了将提示分解为模块化组件以提高LLM性能的方法。尽管这种方法适用于许多用例,但对于高度复杂的提示或用例,这种方法可能并不总是可行的。
与其在提示内部分解问题,我们可以在提示之间进行分解。本质上,我们将一个提示的输出用作下一个提示的输入,从而创建一个连续的交互链,以解决我们的问题。
为了说明这一点,假设我们希望LLM根据产品的一些功能特性,为我们生成一个产品名称、标语和销售文案。虽然我们可以要求LLM一次性完成所有任务,但我们可以选择将问题分解为多个部分。
如下图所示,我们得到一个顺序化的管道:首先生成产品名称,然后将名称和产品特性作为输入生成标语,最后利用特性、名称和标语生成销售文案。

这种链式提示技术允许LLM在每个单独的问题上花费更多时间,而不是一次性解决整个问题。让我们通过一个简单的例子来说明这一点。我们首先为一个利用LLM的聊天机器人生成名称和标语:
# 为产品创建名称和标语
product_prompt = [
{"role": "user", "content": "Create a name and slogan for a chatbot that leverages LLMs."}
]
outputs = pipe(product_prompt)
product_description = outputs[0]["generated_text"]
print(product_description)
输出示例:
Name: 'MindMeld Messenger'
Slogan: 'Unleashing Intelligent Conversations, One Response at a Time'
然后,我们可以将生成的输出作为输入,让LLM生成销售文案:
# 基于产品名称和标语,生成销售文案
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)
输出示例:
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!
尽管我们需要调用模型两次,但这种方法的一个主要好处是我们可以为每次调用设置不同的参数。例如,生成名称和标语时,生成的标记数量相对较少,而生成销售文案时可以更长。
这种方法可以应用于多种用例,包括:
响应验证
要求LLM验证之前生成的输出。并行提示
创建多个并行提示,并在最后合并结果。例如,让多个LLM并行生成多个食谱,然后将结果合并生成购物清单。创作故事
通过将问题分解为多个部分,利用LLM创作书籍或故事。例如,先写出概要,开发角色,构建情节节点,然后再创作对话。
在下一章中,我们将自动化这一过程,并超越链式LLM。我们将把其他技术组件(如记忆、工具使用等)串联起来!
在此之前,接下来将进一步探索链式提示的概念,描述更复杂的链式提示方法,如自我一致性(self-consistency)、思维链(chain-of-thought)和思维树(tree-of-thought)。
3.5 用生成型模型进行推理
推理是人类智能的核心组成部分,通常与LLM表现出的类似推理的新兴行为相比较。我们强调“类似”,因为这些模型在撰写本文时,通常被认为通过记忆训练数据和模式匹配来展示这种行为。尽管它们的输出可能表现出复杂的行为,但即使这不是“真正的”推理,它们仍被称为推理能力。换句话说,我们通过提示工程与LLM合作,以模仿推理过程,从而提高LLM的输出质量。
为了实现这种推理行为,我们需要退一步,探索推理在人类行为中的含义。
简化来说,人类的推理方法可以分为系统1和系统2思维过程。
- 系统1思维是一种自动的、直觉的、近乎瞬间的过程。它与生成型模型自动生成标记而不进行自我反思的行为相似;
- 系统2思维是一种有意识的、缓慢的、逻辑的过程,类似于头脑风暴和自我反思。
在生成式模型基础上增加系统2的能力:推理
- CoT/思维链
- Self-Consistency/自洽
- Tree-of-Thought/思维树
3.5.1 思维链:先思考再回答
生成型模型实现复杂推理的第一步和主要步骤是通过一种称为思维链的方法。思维链的目标是让生成型模型在回答问题之前先“思考”,而不是直接回答而不进行任何推理。
如下图所示,它在提示中提供推理示例,展示模型在生成回应之前应进行的推理过程。这些推理过程被称为“思考”。这在涉及更高复杂度的任务(如数学问题)中非常有帮助。增加这个推理步骤允许模型将更多计算资源分配给推理过程。与其基于少数标记计算整个解决方案,推理过程中的每个额外标记都能让LLM稳定其输出。

我们使用作者在论文中使用的例子来展示这种现象:
# 使用思维链回答问题
cot_prompt = [
{"role": "user", "content": "Roger有5个网球。他又买了2罐网球。每罐有3个网球。他现在有多少个网球?"},
{"role": "assistant", "content": "Roger开始时有5个球。2罐网球,每罐3个,总共是6个网球。5 + 6 = 11。答案是11。"},
{"role": "user", "content": "食堂有23个苹果。如果他们用了20个来做午餐,又买了6个,他们现在有多少个苹果?"}
]
# 生成输出
outputs = pipe(cot_prompt)
print(outputs[0]["generated_text"])
输出示例:
食堂开始时有23个苹果。他们用了20个苹果,所以剩下23 - 20 = 3个苹果。然后他们又买了6个苹果,所以现在有3 + 6 = 9个苹果。答案是9。
请注意,模型不仅生成了答案,还在生成答案之前提供了推理过程。通过这种方式,它能够利用已生成的知识来计算最终答案。
尽管思维链是增强生成型模型输出的绝佳方法,但它需要在提示中提供一个或多个推理示例,而用户可能无法获得这些示例。与其提供示例,我们只需简单地要求生成型模型提供推理(零样本思维链)。有许多不同的形式可以实现,但一种常见且有效的方法是使用“让我们逐步思考”这一短语,如下图所示。

使用我们之前使用的例子,我们只需将这个短语附加到提示中,以启用类似思维链的推理:
# 零样本思维链
zeroshot_cot_prompt = [
{"role": "user", "content": "食堂有23个苹果。如果他们用了20个来做午餐,又买了6个,他们现在有多少个苹果?让我们逐步思考。"}
]
# 生成输出
outputs = pipe(zeroshot_cot_prompt)
print(outputs[0]["generated_text"])
输出示例:
步骤1:从初始苹果数量开始,即23个。
步骤2:减去用于午餐的苹果数量,即20个。所以,23 - 20 = 3个苹果剩余。
步骤3:加上购买的苹果数量,即6个。所以,3 + 6 = 9个苹果。
食堂现在有9个苹果。
无需提供示例,我们再次得到了相同的推理行为。这就是为什么在进行计算时“展示你的计算过程”如此重要。通过解决推理过程,LLM可以利用已生成的信息作为生成最终答案的指南。
提示:
尽管“让我们逐步思考”这一提示可以改善输出,但你并不局限于这种确切的表述。还有其他替代方案,如“深呼吸,然后逐步思考”和“让我们逐步解决这个问题”。
3.5.2 自洽:采样输出
使用相同的提示多次可能会得到不同的结果,如果我们通过温度和top_p等参数允许一定程度的创造性,那么输出的质量可能会根据标记的随机选择而改善或降低。换句话说,这取决于运气!
为了抵消这种随机性并提高生成型模型的性能,引入了自洽。这种方法要求生成型模型多次回答相同的提示,并将多数结果作为最终答案。在这个过程中,每个答案都可以受到不同的温度和top_p值的影响,以增加采样的多样性。
如下图所示,这种方法可以通过添加思维链提示来进一步改进,以改善其推理能力,同时仅使用答案进行投票程序。

然而,这确实需要多次询问同一个问题。因此,尽管这种方法可以提高性能,但它会变慢n倍,其中n是输出样本的数量。
3.5.3 思维树:探索中间步骤
思维链和自洽的想法旨在实现更复杂的推理。通过从多个“思考”中采样并使它们更具思考性,我们旨在改善生成型模型的输出。
这些技术只是目前模仿复杂推理的冰山一角。对这些方法的改进可以在思维树中找到,它允许深入探索多个想法。
该方法的工作原理如下:面对需要多个推理步骤的问题时,通常有助于将其分解为多个部分。在每个步骤中,如下图所示,生成型模型被提示探索当前问题的不同解决方案。然后,它对最佳解决方案进行投票,并继续下一步。

当需要考虑多条路径时,如写故事或产生创意时,这种方法非常有帮助。这种方法的缺点是它需要多次调用生成型模型,这会显著减慢应用程序的速度。幸运的是,有人成功地将思维树框架转化为一种简单的提示技术。
与其多次调用生成型模型,我们要求模型模仿这种行为,通过模拟多个专家之间的对话。这些专家会互相提问,直到达成共识。一个零样本思维树提示的例子是:
# 零样本思维树
zeroshot_tot_prompt = [
{"role": "user", "content": "想象有三位不同的专家在回答这个问题。每位专家都会写下他们思考的1个步骤,然后与小组分享。然后所有专家继续下一步,等等。如果任何专家在任何时候意识到他们错了,他们就退出。问题是:‘食堂有23个苹果。如果他们用了20个来做午餐,又买了6个,他们现在有多少个苹果?’确保讨论结果。"}
]
# 生成输出
outputs = pipe(zeroshot_tot_prompt)
print(outputs[0]["generated_text"])
输出示例:
专家1:步骤1 - 从初始苹果数量开始:23个苹果。
专家2:步骤1 - 减去用于午餐的苹果:23 - 20 = 3个苹果剩余。
专家3:步骤1 - 加上新买的苹果:3 + 6 = 9个苹果。
专家1:步骤2 - 确认最终数量:食堂有9个苹果。
专家2:步骤2 - 检查计算:23 - 20 = 3,然后3 + 6 = 9。计算正确。
专家3:步骤2 - 同意结果:食堂确实有9个苹果。
所有专家都同意最终数量:食堂有9个苹果。
我们再次得到了正确答案,但这次是通过“专家之间的讨论”。看到这种“专家”之间的对话,展示了提示工程所具有的创造力,这是非常有趣的。
四、输出内容检查与控制
使用生成型模型构建的系统和应用程序最终可能会投入生产。当这种情况发生时,验证和控制模型的输出变得至关重要,以防止应用程序出现故障并创建一个稳健的生成型AI应用。
验证输出的原因可能包括:
结构化输出
默认情况下,大多数生成型模型会生成自由形式的文本,而不遵循除自然语言之外的特定结构。某些用例可能要求输出以特定格式呈现,例如JSON。有效输出
即使我们允许模型生成结构化的输出,它仍然可以自由生成内容。例如,当模型被要求在两个选项中选择时,它不应该生成第三个选项。伦理问题
一些开源生成型模型没有任何限制,可能会生成不考虑安全或伦理因素的输出。例如,某些用例可能要求输出中不含亵渎语言、个人身份信息(PII)、偏见、文化刻板印象等。准确性
许多用例要求输出符合某些标准或性能要求。目标是双重检查生成的信息是否在事实上准确、连贯,或者是否没有“幻觉”。
控制生成型模型的输出并非易事,正如我们在使用top_p
和temperature
等参数时所探讨的那样。这些模型需要帮助以生成符合某些指导方针的一致输出。
通常,有三种方法可以控制生成型模型的输出:
示例
提供预期输出的示例。
如我们之前所探讨的,少样本学习(few-shot learning)是一种有助于指导生成型模型输出的有用技术。这种方法也可以推广到指导输出的结构。语法
控制标记选择过程。
近年来,一些工具包迅速发展,用于限制和验证生成型模型的输出,例如Guidance、Guardrails和LMQL。它们部分利用生成型模型来验证自己的输出(如图4-1所示)。生成型模型将输出作为新提示检索,并尝试根据预定义的限制条件进行验证。微调
在包含预期输出的数据上调整模型。
微调模型的方法将在第12章中深入探讨。
4.1 提供示例
一种简单直接的方法是为生成型模型提供预期输出的示例。例如,假设我们希望生成型模型为一款角色扮演游戏(RPG)创建一个角色档案。我们首先不提供任何示例:
# 零样本学习:不提供示例
zeroshot_prompt = [
{"role": "user", "content": "为RPG游戏创建一个JSON格式的角色档案。"}
]
# 生成输出
outputs = pipe(zeroshot_prompt)
print(outputs[0]["generated_text"])
输出示例:
{
"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":
上述截断的输出不是有效的JSON,因为模型在开始生成“charisma”属性后停止了标记生成。此外,我们可能不希望包含某些属性。相反,我们可以为模型提供一些示例,以指示预期的格式:
# 单样本学习:提供输出结构的示例
one_shot_template = """为RPG游戏创建一个简短的角色档案。确保仅使用以下格式:
{
"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}
]
# 生成输出
outputs = pipe(one_shot_prompt)
print(outputs[0]["generated_text"])
输出示例:
{
"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"
}
模型完美地遵循了我们提供的示例,从而实现了更一致的行为。这也展示了利用少样本学习来改善输出结构(而不仅仅是内容)的重要性。
需要注意的是,模型是否遵循你建议的格式仍然取决于模型本身。有些模型比其他模型更擅长遵循指令。
4.2 语法:受限采样
少样本学习的一个主要缺点是我们无法明确防止某些输出被生成。尽管我们指导模型并给出指令,但它可能仍然不会完全遵循。
相反,一些工具包迅速发展,用于限制和验证生成型模型的输出,例如Guidance、Guardrails和LMQL。部分工具利用生成型模型来验证自己的输出(如图4-1所示)。生成型模型将输出作为新提示检索,并尝试根据预定义的限制条件进行验证。

同样,如下图所示,这一验证过程也可以用于控制输出的格式,通过生成我们已经知道其结构的部分格式。

这一过程可以进一步扩展,我们不仅在生成输出后进行验证,还可以在标记采样过程中进行验证。在采样标记时,我们可以定义一些语法或规则,要求LLM在选择下一个标记时遵循。例如,如果我们要求模型在进行情感分类时返回“positive”、“negative”或“neutral”,它可能仍然会返回其他内容。如下图所示,通过限制采样过程,我们可以让LLM只输出我们感兴趣的内容。需要注意的是,这仍然会受到top_p
和temperature
等参数的影响。

示例:使用受限采样生成JSON格式的输出
为了说明这一现象,我们可以使用llama-cpp-python
库,这是一个类似于transformers
的库,用于加载语言模型。它通常用于高效加载和使用压缩模型(通过量化;见第12章),但也可以用于应用JSON语法。
加载模型之前,建议重启笔记本以清除任何先前的模型并释放显存。你也可以运行以下代码来清空显存:
import gc
import torch
del model, tokenizer, pipe
# 清空内存
gc.collect()
torch.cuda.empty_cache()
现在我们已经清空了内存,可以加载Phi-3模型。我们将n_gpu_layers
设置为-1
,表示希望模型的所有层都在GPU上运行。n_ctx
指的是模型的上下文大小。repo_id
和filename
指的是模型所在的Hugging Face仓库:
from llama_cpp.llama import Llama
# 加载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
)
为了使用内部的JSON语法生成输出,我们只需要将response_format
指定为JSON对象。在底层,它会应用JSON语法以确保输出符合该格式。
例如,让我们要求模型以JSON格式创建一个用于《龙与地下城》的角色:
# 生成输出
output = llm.create_chat_completion(
messages=[
{"role": "user", "content": "为RPG创建一个战士角色的JSON格式。"},
],
response_format={"type": "json_object"},
temperature=0,
)['choices'][0]['message']["content"]
为了验证输出是否为JSON格式,我们可以尝试将其作为JSON处理:
import json
# 格式化为JSON
json_output = json.dumps(json.loads(output), indent=4)
print(json_output)
输出示例:
{
"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."
}
输出被正确地格式化为JSON。这使我们能够更有信心地在应用程序中使用生成型模型,这些应用程序期望输出符合特定格式。
五、总结
在本章中,我们探讨了通过提示工程和输出验证使用生成型模型的基础知识。我们专注于提示工程所带来的创造力和潜在复杂性。这些提示的组成部分是生成和优化适用于不同用例的输出的关键。
我们进一步探讨了高级提示工程技术,例如基于上下文的学习和思维链。这些方法通过提供示例或鼓励逐步思考的短语,引导生成型模型通过复杂问题进行推理,从而模仿人类的推理过程。
总体而言,本章表明提示工程是与LLM交互的一个关键方面,因为它使我们能够有效地向模型传达我们的需求和偏好。通过掌握提示工程技术,我们可以释放LLM的一些潜力,并生成满足我们要求的高质量响应。
下一章将在这些概念的基础上,探索利用生成型模型的更高级技术。我们将超越提示工程,探讨LLM如何使用外部记忆和工具。