LLM提示工程的12个教训

在过去的两个月里,我构建了一个由大型语言模型 (LLM) 支持的应用程序。 这是一次令人兴奋的、智力上的刺激,但有时又令人沮丧的经历。 我对提示工程的整个概念——以及LLM的可能性——在项目过程中发生了变化。

我很乐意与你分享我的一些最大收获,目的是阐明提示工程中一些经常不言而喻的方面。 我希望在阅读了我的考验和磨难之后,你将能够做出更明智、及时的工程决策。 如果你已经涉足提示工程,我希望这可以帮助你在自己的旅程中前进!

作为上下文,以下是我们将要学习的项目的 TL;DR:

  • 我和我的团队构建了 VoxelGPT,这是一个将LLM与 FiftyOne 计算机视觉查询语言相结合的应用程序,可以通过自然语言搜索图像和视频数据集。 VoxelGPT 还回答了有关 FiftyOne 本身的问题。
  • VoxelGPT 是开源的(FiftyOne 也是如此!)。 所有代码均可在 GitHub 上获取。
  • 你可以在 gpt.fiftyone.ai 免费试用 VoxelGPT。
  • 如果你对我们如何构建 VoxelGPT 感到好奇,可以在此处的 TDS 上阅读有关它的更多信息。

1、科学? 工程? 黑魔法?

提示工程既是工程,也是实验。 编写提示的方法有无数种,从问题的具体措辞,到您输入的上下文的内容和格式。这可能会让人不知所措。 我发现最简单的方法是从简单的开始并建立直觉,然后检验假设。

在计算机视觉中,每个数据集都有自己的模式、标签类型和类名称。 VoxelGPT 的目标是能够处理任何计算机视觉数据集,但我们从一个数据集开始:MS COCO。 保持所有额外的自由度固定,使我们能够首先确定LLM编写语法正确的查询的能力。

一旦你确定了一个在有限环境下成功的公式,那么就弄清楚如何概括并在此基础上进行构建。

2、使用哪种大模型?

人们说,大型语言模型最重要的特征之一是它们相对可互换。 理论上,你应该能够在不实质性改变组织的情况下将一个LLM大模型换成另一个。

虽然更改使用的 LLM 确实通常就像更换 API 调用一样简单,但在实践中肯定会出现一些困难。

  • 有些模型的上下文长度比其他模型短得多。 切换到上下文较短的模型可能需要进行重大重构。
  • 开源固然很棒,但开源 LLM 的性能(目前)还不如 GPT 模型。 另外,如果你使用开源 LLM 部署应用程序,则需要确保运行模型的容器有足够的内存和存储空间。 这最终可能比仅使用 API 端点更麻烦(而且更昂贵)。
  • 如果你开始使用的是 GPT-4,然后因为成本而切换到 GPT-3.5,你可能会对性能下降感到震惊。 对于复杂的代码生成和推理任务,GPT-4 更好。

3、哪里可以使用LLM?

大型语言模型非常强大。 但仅仅因为它们可能能够完成某些任务并不意味着你需要——甚至应该——使用它们来完成这些任务。 考虑LLM的最佳方式是将其视为推动者。 LLM并不是整个解决方案:它们只是其中的一部分。 不要指望大型语言模型能完成所有事情。

例如,你正在使用的 LLM 可能(在理想情况下)生成格式正确的 API 调用。 但是,如果你知道 API 调用的结构应该是什么样子,并且实际上有兴趣填写 API 调用的各个部分(变量名称、条件等),那么只需使用 LLM 来完成这些任务,然后使用 (经过适当后处理的)LLM 输出可自行生成结构化 API 调用。 这将更便宜、更高效、更可靠。

一个拥有LLM的完整系统肯定会有大量的组织和经典逻辑,再加上大量的传统软件工程和机器学习工程组件。 找到最适合你的应用程序的方法。

4、LLM 存在偏见

语言模型既是推理引擎又是知识存储。 通常,用户会对LLM的知识存储方面非常感兴趣——许多人使用LLM作为搜索引擎的替代品! 到目前为止,任何使用过LLM的人都知道他们很容易编造虚假的“事实”——这种现象被称为幻觉。

然而,有时LLM会遇到相反的问题:他们过于执着于训练数据中的事实。

在我们的例子中,我们试图提示 GPT-3.5 确定将用户的自然语言查询转换为有效的 FiftyOne Python 查询所需的适当 ViewStages(逻辑操作管道)。 问题是 GPT-3.5 知道“Match”和“FilterLabels”ViewStage,它们在 FiftyOne 中已经存在了一段时间,但其训练数据不包括最近添加的功能,其中“SortBySimilarity”ViewStage 可用于查找图像 类似于文本提示。

我们尝试传递“SortBySimilarity”的定义、有关其用法的详细信息以及示例。 我们甚至尝试指示 GPT-3.5 不得使用“Match”或“FilterLabels”ViewStages,否则将受到惩罚。 无论我们尝试什么,LLM仍然以它所知道的为导向,无论这是正确的选择还是错误的。 我们正在与LLM的本能作斗争!

我们最终不得不在后期处理中处理这个问题。

5、痛苦的后期处理是不可避免的

无论你的例子有多好; 无论你的提示有多严格——大型语言模型总是会产生幻觉,给你格式不正确的响应,并在不理解输入信息时发脾气。 LLM最可预测的特性是其产出的不可预测性。

我花费了大量时间编写例程来进行模式匹配并纠正幻觉语法。 后处理文件最终包含近 1600 行 Python 代码!

其中一些子例程就像添加括号或将“and”和“or”更改为“&”和“|”一样简单。 在逻辑表达式中。 一些子例程的参与程度要高得多,例如验证 LLM 响应中实体的名称、在满足某些条件时将一个 ViewStage 转换为另一个 ViewStage、确保方法参数的数量和类型有效。

如果你在某种程度上受限的代码生成上下文中使用提示工程,我建议使用以下方法:

  • 使用抽象语法树(Python 的 ast 模块)编写自己的自定义错误解析器。
  • 如果结果在语法上无效,请将生成的错误消息输入到你的 LLM 中并重试。

这种方法无法解决更阴险的情况,即语法有效但结果不正确。 如果有人对此有好的建议(除了 AutoGPT 和“展示你的作品”风格的方法),请告诉我!

6、多多益善

为了构建 VoxelGPT,我使用了看似所有的提示技术:

  • “你是专家”
  • “你的任务是”
  • “你必须”
  • “你将受到惩罚”
  • “这是规则”

这些短语的组合无法确保某种类型的行为。 仅仅巧妙的提示是不够的。

话虽这么说,你在提示中使用的这些技巧越多,你就越能推动LLM朝着正确的方向前进!

7、示例 > 文档

现在众所周知(也是常识!),示例和其他上下文信息(例如文档)都可以帮助从大型语言模型中获得更好的响应。 我发现 VoxelGPT 就是这种情况。

一旦添加了所有直接相关的示例和文档,如果上下文窗口中有额外的空间,你应该怎么做? 根据我的经验,我发现相关的示例比相关的文档更重要。

8、模块化 >> 整体式

将总体问题分解为更小的子问题越多越好。 识别单独的选择和推理步骤(选择推理提示),并在每个步骤中仅输入相关信息,比提供数据集模式和端到端示例列表要有效得多。

出于以下三个原因,这是更可取的:

  • LLM更擅长一次完成一项任务,而不是同时完成多项任务。
  • 步骤越小,清理输入和输出就越容易。
  • 作为工程师,理解应用程序的逻辑是一项重要的练习。 LLM的目的不是让世界变成一个黑匣子。 它是为了启用新的工作流程。

9、我需要多少示例?

提示工程的一个重要部分是计算出给定任务需要多少个示例。 这是非常具体的问题。

对于某些任务(有效的查询生成和基于 FiftyOne 文档回答问题),我们无需任何示例即可完成。 对于其他内容(标签选择、聊天历史记录是否相关以及标签类的命名实体识别),我们只需要几个示例即可完成工作。 然而,我们的主要推理任务有近 400 个示例(这仍然是整体性能的限制因素),因此我们只在推理时传递最相关的示例。

当你生成示例时,请尝试遵循两个准则:

  • 尽可能全面。 如果你的可能性空间有限,那么尝试为每种情况至少为LLM提供一个例子。 对于 VoxelGPT,我们试图为使用每个 ViewStage 的每一种语法上正确的方法至少提供一个示例 - 通常每个示例都有几个示例,以便LLM可以进行模式匹配。
  • 尽可能保持一致。 如果你将任务分解为多个子任务,请确保一个任务与下一个任务的示例保持一致。 你可以重复使用示例!

10、合成示例

生成示例是一个费力的过程,手工制作的示例只能带你到目前为止。 只是不可能提前考虑到所有可能的情况。 部署应用程序时,你可以记录用户查询并使用它们来改进示例集。

然而,在部署之前,最好的选择可能是生成合成示例。

以下是生成合成示例的两种方法,你可能会发现它们很有帮助:

  • 使用LLM生成示例。 你可以要求LLM改变其语言,甚至模仿潜在用户的风格! 这对我们不起作用,但我相信它可以适用于许多应用程序。
  • 根据输入查询本身的元素以编程方式生成示例(可能具有随机性)。 对于 VoxelGPT,这意味着根据用户数据集中的字段生成示例。 我们正在将其纳入我们的管道中,到目前为止我们看到的结果是有希望的。

11、LangChain

LangChain 之所以受欢迎是有原因的:该库可以轻松地以复杂的方式连接 LLM 输入和输出,从而抽象出繁琐的细节。 模型和提示模块尤其是一流的。

话虽这么说,LangChain 绝对是一项正在进行中的工作:它们的 Memories、Indexes 和 Chains 模块都有很大的局限性。 以下是我在尝试使用LangChain时遇到的一些问题:

  • 文档加载器和文本拆分器:在 LangChain 中,文档加载器应该将不同文件格式的数据转换为文本,而文本拆分器应该将文本拆分为语义上有意义的块。VoxelGPT 通过检索文档中最相关的块并将其传递到提示中来回答有关 FiftyOne 文档的问题。 为了对有关 FiftyOne 文档的问题生成有意义的答案,我必须有效地构建自定义加载器和拆分器,因为 LangChain 没有提供适当的灵活性。
  • Vectorstores:LangChain 提供 Vectorstore 集成和基于 Vectorstore 的检索器,以帮助查找相关信息并纳入 LLM 提示中。 理论上这很好,但实现起来缺乏灵活性。 我必须使用 ChromaDB 编写自定义实现,以便提前传递嵌入向量,而不是每次运行应用程序时都重新计算它们。 我还必须编写一个自定义检索器来实现我需要的自定义预过滤。
  • 来源问答:在根据 FiftyOne 文档构建问答时,我利用 LangChain 的“RetrievalQA”链得出了一个合理的解决方案。 当我想添加源时,我认为这就像将该链替换为 LangChain 的“RetrievalQAWithSourcesChain”一样简单。 然而,糟糕的提示技术意味着这条链表现出了一些不幸的行为,例如对迈克尔·杰克逊产生幻觉。 我再次必须把事情掌握在自己手中。

这一切意味着什么? 自己构建组件可能会更容易!

12、矢量数据库

矢量搜索可能已上线,但这并不意味着你的项目需要它。 我最初使用 ChromaDB 实现了类似的示例检索例程,但因为我们只有数百个示例,所以我最终切换到精确的最近邻搜索。 我确实需要自己处理所有元数据过滤,但结果是更快的例程和更少的依赖项。

13、TiToken

将 TikToken 添加到方程式中非常容易。 TikToken 总共向项目添加了不到 10 行代码,但让我们在计算标记并尝试将尽可能多的信息融入上下文长度时更加精确。 在工具方面,这是唯一真正无需思考的问题。


原文链接:What I Learned Pushing Prompt Engineering to the Limit

BimAnt翻译整理,转载请标明出处