提示词工程和大型语言模型(LLM)指南
提示词工程是一门与生成式 AI 模型交流的艺术。本文主要介绍一些 GitHub 提示词工程方法,以及如何使用提示词构建 LLM 应用程序。

Marc Andreessen 在 2011 年发表过一篇文章,文中说道,“软件正在吞噬我们的世界”。十多年后,我们见证了生成式 AI 的出现,它对我们的世界产生着巨大的影响。在这种创新型 AI 中,有一种独特的大型语言模型(LLM),这种模型是十年来突破性研究的成果,在完成某些任务方面能超越人类。使用 LLM 构建软件不需要在机器学习方面有多高深的理论知识,已经有开发人员在使用基本的 HTTP 请求和自然语言提示构建 LLM 软件了。

本文将结合 GitHub 与 LLM,帮助开发人员学习如何更好地利用这项技术。本文包括两个主要部分:第一部分将描述 LLM 的高级功能以及如何构建 LLM 应用程序。第二部分将深入探讨 LLM 应用程序的一个重要示例:GitHub Copilot 代码补全。

其他人对我们的工作进行了令人印象深刻的编目。接下来,我们将与大家分享 GitHub Copilot 取得持续成功的一些经验和反思。

我们开始吧。


你需要了解的提示词工程信息

当你在手机上敲击文字信息时,屏幕中间的小键盘上方,会有一个接受建议词按钮?这就是 LLM 在做的工作之一。

iMessage 文本预测功能示例

与手机上的文本不同,LLM 可以预测下一组最佳字母,这些字母就是“token”。 你可以不断点击中间的按钮来完成短信,LLM 可以持续预测下一个单词直至完成文档。LLM 能持续预测单词,直到达到最大的 token 阈值或遇到提示“停!文档到此结束!”的特殊 token 时才会停止。

不过这两者有一个重要的区别。手机中的语言模型非常简单,指令类似于:“根据最后输入的两个词,下一个词最有可能是什么?”相比之下,LLM 生成的输出结果更类似于 “根据公共领域已知的所有文档的全部内容,文档中最有可能出现的下一个 token 是什么?LLM 在庞大的数据集上训练的一个架构良好的大型模型,能理解基本的常识,例如,LLM 能理解“放在桌子上的玻璃球可能会滚落并破碎”这个常识。

训练 LLM 而使其具有意识或“常识”的示例

但要注意:LLM 有时也会提供一些不真实的信息,这些信息通常被称为“幻觉”或“虚构”。LLM 还能学会一些没有被训练的工作。一般来说,自然语言模型都是为一次性任务创建的,比如对推文的情感进行分类、从电子邮件中提取业务实体或识别类似文档,但现在你可以要求 ChatGPT 等 AI 工具执行从未训练过的任务。

John 与 ChatGPT 讨论严肃的事情


使用 LLM 构建应用程序

文档补全引擎与持续涌现的 LLM 应用相比,简直是小巫见大巫,LLM 应用包括对话式搜索、写作助手、自动化 IT 支持和代码补全工具(例如:GitHub Copilot)。但是,为什么所有工具实际上都是文档补全工具呢?因为任何使用 LLM 的应用实际上都是两个域之间的映射:用户域和文档域。

与 LLM 通信时的用户流程图,本示例为用户 Dave 的流程图


左边是用户 Dave,他在今天举办了世界杯观赛派对,但是无线网络却断了,必须尽快修好。戴夫打电话给网络供应商,但接电话的是自动助理。假设我们是 LLM 应用程序的自动助理,我们怎么帮助他?

问题的关键在于如何将用户域转换为文档域。首先,我们需要将用户的语音转录为文本。自动支持代理说,“请说明您遇到的电缆问题。”时,戴夫脱口而出:

“我在看世界杯决赛,我的电视连着无线网络,但我撞到了柜台,无线网络盒掉下来摔坏了,现在我们没法看比赛了,我很着急啊!”

这种情况下,我们获得了文本,但用处不大。作为自动助理,你可能会说,“我想,我会打电话给我哥哥,看看我们能不能和他一起看比赛”。没有上下文的 LLM 可能也会输出这样的答案。所以,我们需要给 LLM 一些上下文,确定这是什么类型的文件:

### ISP IT Support Transcript:
以下是 ISP 客户 Dave Anderson 与 IT 支持专家 Julia Jones 之间的对话录音。该记录可作为 Comcrash 为客户提供出色支持的范例。
*Dave:太倒霉了!今天我要看比赛,我的电视连接的是无线网络,但我撞到了柜台, 无线网络盒掉下来摔坏了!现在我们没法看比赛了。
*Julia:

如果你发现了这份伪文档,你会如何完成它呢?根据上下文,你会发现 Julia 是一位 IT 支持专家,而且是一位非常优秀的专家。你会希望接下来的话是帮助 Dave 解决问题的建议。虽然 Julia 实际上并不存在,这也不是录音对话,但这都不重要,重要的是,这些额外的词语提供了更多的上下文,让你知道完成对话可能是什么样子的。LLM 做的也是同样的事情。在阅读完这部分文档后,它将尽最大努力完成 Julia 的对话。

但我们还可以为 LLM 制作更好的文档。LLM 并不了解如何排除有线电视故障(尽管它已经阅读了网上所有的手册和 IT 文档)。我们假设它缺乏这一特定领域的知识。我们可以做的就是搜索对 Dave 有帮助的内容,并将其放入文档中。假设我们有一个投诉搜索引擎,可以找到相关文档,现在要做的就是将这些信息自然地编入我们的伪文档中。

接上文:

*Julia:(在包里翻来翻去,根据 Dave 的要求拿出了最符合要求的文件)。
常见的互联网连接问题
<......在此插入 1 页文本,这些文本来自客户支持历史数据库的搜索结果......>。
(阅读文件后,Julia 提出以下建议)
*Julia:

根据给定的这些完整的文本,LLM 有条件使用植入的文档,在“一位有帮助的 IT 专家”的背景下,模型将生成一个回复。这个回复会考虑到文档以及 Dave 的具体请求。

最后一步是从文档领域进入用户的问题领域。本例是将文本转换为语音。因为这是一个聊天应用,所以我们会在用户和文档域之间来回多次,每次都会延长转录的时间。

这个例子的核心就是提示词工程。在这个例子中,我们为 AI 精心设计了一个具有足够上下文的提示,使其能够产生最好的输出,本例就是为 Dave 提供有用的信息,让他的 Wi-Fi 重新启动和运行。下一节将介绍如何为 GitHub Copilot 改进提示词工程技术。

想了解如何使用 GitHub 和 LLM 来构建软件?
进一步了解 GitHub Copilot 背后的 LLM,以及过去几年的发展历程。
了解更多


提示词工程的艺术与科学

在用户域和文档域之间转换属于提示词工程的范畴——开发 GitHub Copilot 两年多以来,我们已经发现了这个过程中一些模式。

这些模式帮助我们正式确定了一个管道,我们认为这是一个可以帮助其他人为应用进行提示工程的适用模板。现在,我们将以 GitHub Copilot(我们的 AI 配对程序员)为例,展示这一流程是如何运作的。


GitHub Copilot 的提示词工程管道

从一开始,GitHub Copilot 的 LLM 就建立在 OpenAI 的 AI 模型之上。OpenAI 的模型在不断改进,但始终未变的是提示工程核心问题的答案:模型要完成什么样的文档?

我们使用的 OpenAI 模型是根据 GitHub 上的完整代码文件进行训练的。忽略一些并不会真正改变提示词工程游戏的过滤和分层步骤,这种分布与数据收集时最新提交到 main 的单个文件内容基本一致。

LLM 解决的文档补全问题与代码有关,GitHub Copilot 的任务则与补全代码有关,二者截然不同。

下面是一些例子:

  • 大多数提交到主系统的文件都已完成。首先,它们通常都能编译。在用户键入代码的大部分时间里,代码都不会编译,这是因为有未完成的地方,而这些地方会在推送提交之前得到修复。
  • 用户甚至可以按层次顺序编写代码,先写方法签名,再写主体,而不是逐行编写或混合编写。
  • 编写代码意味着跳来跳去,尤其是人们在编辑代码时经常需要在文档中跳转,并在文档中进行修改,例如,为函数添加参数。严格来说,如果 Codex 建议使用一个尚未导入的函数,不管该函数有多合理,都是错误的。但作为 GitHub Copilot 的一项建议,它还是很有用的。

问题是:仅仅根据光标前的文本预测最有可能的后续文本,从而提出 GitHub Copilot 建议,会浪费很多机会。因为它忽略了大量的上下文。我们可以利用这些上下文来确定建议,比如元数据、光标下方的代码、导入的内容、版本库的其他内容或问题,并为 AI 助手创建一个强有力的提示。

软件开发是一项深度关联的多模式挑战,我们能训练并呈现给模型的复杂性越多,完成度就越高。


第 1 步:收集上下文

GitHub Copilot 是在 Visual Studio Code(VS Code)等集成开发环境(IDE)的背景下运行的,它可以使用集成开发环境中的任何信息,前提是集成开发环境处理的时候必须快速。在 GitHub Copilot 这样的交互式环境中,每一毫秒都很重要。GitHub Copilot 能处理常见的编码任务,要实现这一点,必须在开发者在集成开发环境中编写更多代码之前显示解决方案。我们粗略分析发现,我们提出一个建议每多花 10 毫秒,它及时到达的几率就会降低 1%。

那么,我们能快速说什么呢?这里有一个例子。以下是对一个简单的 Python 代码的建议:

一位开发人员提示 GitHub Copilot 用 Python 编写一个简单函数来计算斐波那契数字

错误!用户实际上想写 Ruby,如下:

一位开发者使用 GitHub Copilot 编写了一个用 Ruby 计算斐波那契数字的简单函数

这两种语言的语法非常相似,只有几行可能会产生歧义,尤其是在文件的开头,我们遇到的大部分都是模板注释。但现代集成开发环境(如 VS Code)通常知道用户使用哪种编写语言,所以混淆语言尤其令用户恼火,因为它打破了用户的“计算机应该知道”的隐含期望(大多数集成开发环境都会高亮显示语言语法)。

因此,我们把语言元数据放到想要包含的一堆上下文中,再加上整个文件名。如果可以,它通常会通过扩展名来暗示语言,此外,它还会为该文件的预期内容定下基调,这些小而简单的信息虽然不会扭转局势, 但还是很有帮助的。

另一端是资源库的其他部分。假设有一个文件定义了一个抽象类 DataReader。 另一个文件定义了一个子类 CsvReader。现在你又要编写一个新文件,定义另一个子类 SqlReader。在编写新文件时,你很可能也想查看这两个现有文件,因为它们提供了有用的上下文信息,让你了解需要实现什么以及如何实现。通常情况下,开发人员会在不同的标签页中打开这些文件,并通过切换来提醒自己注意定义、示例、类似模式或测试。

如果这两个文件的内容对你有用,那么很可能对 AI 也有用。因此,我们将其添加为上下文,因为集成开发环境知道资源库中还有哪些文件作为标签页在同一窗口中打开。资源库中可能有成百上千个文件,但只有部分文件是打开的,而这正是一个暗示,说明这些文件可能对当前的工作有用。当然,“一些”可能意味着很多,但注意,我们只考虑最近的 20 个标签页。


第 2 步:摘录

LLM 上下文中的无关信息会降低其准确性。此外,源代码往往很长,因此即使是单个文件也不能保证完全适合 LLM 的上下文窗口(大约五分之一的时间会出现此问题)。因此,除非用户使用较少的标签页,否则我们根本无法包含所有标签页。

因此,我们将文件剪切成(理想状态下)不超过 60 行的自然重叠片段,这就是为什么要对其进行打分,只选取最好的片段。在这种情况下,“分数”旨在反映相关性。为确定每个片段的得分,我们使用了 Jaccard 相似度,这是一种可用于衡量样本集相似性或多样性的统计方法(计算速度也超快,非常适合减少延迟)。


第 3 步:美化

现在,我们有了一些想要传递给模型的上下文,但怎么传呢?Codex 和其他模型不提供添加其他文件的 API,也不提供指定文档语言和文件名的 API。它们只完成一个文档。如上所述,你需要以自然的方式将上下文注入该文档。

路径和名称可能是最简单的。许多文件开头都会有一个前言,提供一些元数据,如作者、项目名称或文件名。因此,我们假设这里也是这样,在最顶端添加一行,内容类似 # filepath: foo/bar.py// filepath: foo.bar.js,具体取决于文件语言的注释语法。

有时路径是未知的,比如尚未保存的新文件。即使在这种情况下,只要集成开发环境知道,我们至少可以尝试指定语言。对于许多语言来说,我们有机会在代码中加入 #!/usr/bin/python#!/usr/bin/node 这样的“shebang”行。这是一个不错的技巧,可以有效地防止错误的语言识别。但这也有一定的危险性,因为带有 Shebang 行的文件是所有代码中带有偏见的子群。因此,对于语言识别错误风险较高的短文件,我们可以采用这种方法,而对于较大的或已命名的文件,则应避免采用这种方法。

如果说注释可以作为路径或语言等微小信息的传递系统,那么我们也可以将其作为 60 行相关代码的大块深度挖掘的传递系统。

注释用途广泛,GitHub 上到处都有注释代码。我们先看一些最常见的例子:

  • 不再适用的旧代码
  • 删除的功能
  • 代码的早期版本
  • 为便于记录,特留下示例代码
  • 从代码库其他部分摘录的代码

我们从最后一组示例中汲取灵感。熟悉(1)-(3)组的情况会让模型更易接受,但我们的片段旨在模仿(4)和(5)组:

# compare this snippet from utils/concatenate.py:
# def crazy_concat(a, b):
# return str(a) + str(b)[::-1]

注意,包含片段源的文件名和路径可能很有用。结合当前文件的路径,这可能会为引用导入的补全提供指导。


第 4 步:确定优先次序

现在,我们已经从多个来源获取了许多上下文片段:光标正上方的文本、光标下方的文本、其他文件中的文本以及语言和文件路径等元数据。

在绝大多数情况下(约 95%),我们必须做出艰难的选择,决定哪些内容可以包含,哪些不可以。

我们将可能包含的项目视为“愿望”,从而做出这种选择。每当我们发现一段上下文,比如打开的标签页中被注释的片段,我们就会许下一个愿望。愿望有一定的优先级,例如,“shebang”行的优先级就比较低。相似度较低的片段的优先级几乎没有提高。相比之下,光标正上方的行具有最高优先级。愿望在文档中的位置也有要求。Shebang 行需要排在最前面,而光标正上方的文本则排在最后——应该直接排在 LLM 完成之前。

按照优先级对愿望清单进行排序,是选择哪些愿望要填充,哪些愿望要放弃的最快方法。然后,我们可以不断删除优先级最低的愿望,直到剩下的都能在上下文窗口中找到。然后,我们再按照文档中的预期顺序进行排序,并将所有内容粘贴在一起。


第 5 步:AI 开始工作

既然我们已经准备好了内容丰富的提示,那么现在就是提出有用的完成语的时候了。在这里,我们一直面临着一个非常微妙的权衡——GitHub Copilot 需要一个高效快速的模型,因为延迟可能会导致根本无法提供建议。

所以,我们应该选择哪种 AI 来“完成任务”:最快的还是最准确的?这很难预测,因此 OpenAI 与 GitHub 合作开发了一组模型。我们向开发人员展示了两种不同的模型,但发现用户从速度更快的模型中获益最大(就接受和保留的完成度而言)。此后,经过进一步优化,模型速度显著提高,因此当前版本的 GitHub Copilot 有了更强大的模型支持。


第 6 步:任务完成!

生成式 AI 会生成一个字符串,如果不停止,它就会继续生成,直到预测到文件结束为止。这会浪费时间和计算资源,因此需要设定“停止”的标准。

最常见的停止标准实际上是寻找第一个换行符。多数情况下,软件开发者希望当前行结束。但 GitHub Copilot 最神奇的是它能同时建议多行代码。

多行补全在涉及单一语义单元(如函数体、if 分支或类)时很自然。GitHub Copilot 会查找正在启动此类代码块的情况,这可能是因为开发者刚刚编写了开头(如 header、if 或类声明),也可能是正在编写开头。如果代码块主体是空的,它就会试着提出建议,直到代码块看起来已经完成才会停止。

这时,编码员就会收到建议!

如果你想了解更多有关提示词工程、如何改进技术的信息,请查看 GitHub Copilot 入门指南



原文作者:Albert Ziegler,John Berryman
原文链接:https://github.blog/2023-07-17-prompt-engineering-guide-generative-ai-llms/
推荐阅读
相关专栏
开发者实践
182 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。