《Prompting Is Programming: A Query Language for Large Language Models,LMQL》论文学习
一、前言
大型语言模型在诸如对话问答、代码生成等广泛任务上表现出了出色的性能。
在较高的层次上,给定一段输入,大语言模型可用于按照概率统计方式自动补全序列。在此基础上,用户用指令(instructions)或示例(examples)去提示(prompt)大语言模型,以实施各种下游任务。
本质上,提示(prompt)方法是语言模型、用户和外部工具(例如计算器)之间的交互规范。
然而,要获得最先进的性能,或针对特定任务适配的语言模型,需要进行大量复杂的针对特定任务/特定模型的临时交互调整,这非常消耗时间,也不利于持续迭代LLM程序的整体表现效果。
基于这个背景,论文提出了语言模型编程(Language Model Programming,LMP)的思想。 LMP主要包括以下几个方面,
- 纯文本提示(text prompting)
- 文本提示(text prompting)和编程脚本(scripting)组合
- 语言模型输出约束(output constraints),这个特性使得LMP能够轻松适应许多任务,同时抽象语言模型内部结构并提供高级语义。
为了启用 LMP,论文实现了 LMQL(Language Model Query Language),它利用来自 LMP prompt的约束和控制流,以生成有效的推理过程,最大限度地减少对底层语言模型的昂贵调用的数量。
论文通过实验证明 LMQL 可以以直观的方式追上各种最先进的提示方法,特别是促进使用现有高级 API 难以实现的交互流程。
论文实验的评估表明我们保持或提高了几个下游任务的准确性,同时也显着减少使用付费 API 所需的计算量或成本(节省 26-85% 的成本)。
参考资料:
https://arxiv.org/pdf/2212.06094.pdf
二、INTRODUCTION
大型语言模型(large LMM)已被证明在各种基于语言的任务上取得了成功,例如机器翻译、文本摘要、问答、推理、从文本生成代码等等。基于这些令人惊艳的效果,LMM 已经超过机器学习社区,变得越来越流行,并正在慢慢集成到许多应用程序中。
大语言模型的本质是一种概率预测模型,它在内部最小操作单元是token,这种token预测方式不同于人类感知语言的方式。本质上,LLM的训练过程是在极大似然拟合一个“pre-token -> next-token”的条件概率函数。
虽然大语言模型可以用直观概念的指令(instructions)或者示例(examples)进行提示(prompt),但使用大语言模型依然存在很多挑战,例如,
- 首先,因为LMM是在token级别进行操作,因此很难将解码过程限制在合法单词或短语中
- 此外,许多prompt提示技术可能需要用户和LLM之间的来回交互(例如像 ChatGPT 这样的聊天机器人)或特定于任务的接口(例如执行与外部控制逻辑的算术计算)。为了实现这些prompt提示,需要大量的手动操作(与模型的解码过程进行交互),这限制了completions生成的通用性
- 最后,由于 LLM 一次仅生成一个(sub-word)token,完成一个序列可能需要多次调用。 此外,随着前缀、提示和迄今为止生成的响应的增长,解码变得越来越昂贵
同时由于语言模型通常是非常大的神经网络,因此实际推理中对计算成本存在较高的要求,同时也存在明显地延迟。对于付费使用的 API(例如 OpenAI 的 GPT 模型),这会导致每个回答的查询的使用成本很高。
在这项工作中,我们提出了通过LMQL进行语言模型编程的想法,通过在自然语言提示的基础上扩展了两大核心特性
- lightweight scripting(轻量级脚本)
- constraining of outputs(输出约束)
这种设计有利于在进行LLM prompting中实现前端/后端分离,即允许用户指定复杂的交互(interactions)、控制流(control flow)和约束(constraints),而无需了解 LLM 的内部结构,例如向量化(tokenization)、实现(implementation)和模型架构(architecture)。
此外,LMQL构建的程序屏蔽了底层LLM的细节,这大大提高了LLM迁移性(底层LLM可插拔)。
总体而言,语言模型编程(LMP)保留了简单的自然语言驱动的 LM 接口,还可以实现精确的约束、脚本编写、以及高效的解码。
为了启用 LMP,我们提出了一种称为语言模型查询的新颖语言和运行时语言 (LMQL)。 LMQL 是一种高级语言,具有类似 SQL 的声明性元素和脚本的命令式语法。 底层运行时与现有的LM语言模型兼容。
LMQL 可被用于表达种类丰富地prompt方法(prompt交互范式),而仅仅使用简单、简洁且与供应商无关的代码。
此外,专门设计的评估语法(evaluation semantics),支持部分评估(partial evaluation )和前瞻(lookahead),使我们能够对查询进行端到端的优化:LMQL 利用输出约束和脚本化提示,修剪了 LM 的搜索空间,带来了最高80%的推理成本降低。
我们在图 1 中展示了两个简单 LMQL 程序的示例。
Fig1. Two LMQL programs that demonstrate core features like scripted prompting, eager output constraining and validation, and prompting with control flow.
总结来说,论文的核心贡献是:
- 引入了语言模型编程的新范式,制定并解决了最近的语言模型提示技术带来的几个挑战。
- 提出了 LMQL,一种高效、高级的 LM 查询语言,支持脚本化提示和输出约束。
- 展示了如何用简单、简洁的 LMQL 程序,表达广泛和先进的prompt提示技术。不仅如此,LMQL 将推理成本和延迟降低了 26-80%,同时保持或提升了任务准确性。
笔者认为,LMQL的主要价值在于,提供了一套可编程的prompt开发框架,使得开发者可以更有效、更精确地构建和LLM的信息交互通道,LMQL为广大开发者开启LLM编程时代提供了有力的工具,类似在software1.0时代IDE的作用。
参考链接:
https://lmql.ai/#distribution
三、OVERVIEW: LANGUAGE MODEL PROGRAMMING
0x1:Background: (Large) Language Models
1、Few-Shot Prompting
Few-shot prompt 指的是语言模型不需要针对下游任务(例如分类、问题回答等)进行定制化地训练。 相反,使用广泛的文本序列预测数据集进行预训练,并在调用它们时以示例的形式提供上下文即可达到不错的效果。
我们在下图中展示了一个例子,
Fig. 3. Example of few-shot promptin
其中,我们的目标是将“奶酪”从英语翻译成法语。为此,我们提供了几个示例,然后要求语言模型以相同的语法完成“cheese”的翻译,
这样,翻译和其他任务就可以被重新定义为简单的序列补全任务,这使得 LM 成为强大的多任务推理机,甚至是一个通用计算机。
2、Multi-Part Prompting
由于其强大的推理能力,LM 不再只是被用于简单的prompt-completions,也可作为“组合推理引擎”被集成到更大的程序中。
LM 编程方案有很多不同的编程范式,例如,
- 迭代分解(Iterated Decompositions),典型地项目如 langchain,它更关注按顺序使用的多个提示的组成。
- 元提示(meta prompting)
- 工具使用( tool use)
- 思维链(Chain-Of-Thought)
- 键值存储(Key-Value Memory)
- LM 级联框架(LM cascades frame):在概率编程环境中使用。
0x2:Key Challenges
在本节中,我们先支出 LM 使用中的三个关键挑战,之后会讨论如何用语言模型编程和 LMQL 来克服它们。
挑战一:复杂任务需要和LLM多次交互才能得到最终结果
解码过程中的 LM 交互仍然是一个挑战。
考虑下面图4的一个例子,该方法讨论了元提示(meta prompt)的想法,为了获得特定问题的答案,首先要求语言模型扩展prompt提示,然后再次将扩展后的prompt输入到同一模型以获得答案。 在图4的例子中可以看到,我们的目标是找到“什么是地球的周长?”。在元提示(meta prompt)中,我们首先向语言模型询问能否回答这个问题的专家名字,然后询问该专家如何回答这个问题。
如果使用传统的人工 LM 界面,需要人工输入prompt提示的第一部分,手动调用 LM 获得专家名称,得到第一个completions序列,然后从该completions序列中提取专家名称,并手动将其输入到模板的其余部分,然后再次将其提供给LM以获得最终的回答。 这种方法需要通过 API 进行大量的手动交互,甚至人必须参与到整个循环(HITL)中。
这里需要注意的是,一旦某个值固定(例如专家姓名),解码算法将假定它是提示的固定部分,并且不会与提示的其余部分一起,优化最终的答案。在 HITL 循环流程中,这使用户能够手动尝试多个专家名称,并选择他们最喜欢的名称,完成completions的生成。 然而,它排除了自动联合优化所有模板参数,以此最大化总体似然的可能性,也排除了可能会产生更好的结果的可能。
挑战二:Constraints & Token Representation
下图示例中展示的另一个挑战问题是输出约束。
有时,LM 会在生成过程中偏离主题并产生太长的连续文本序列。虽然有些答案可以很好地被用于下一阶段的prompt提示,但大多数产生的结果是错误且无用的。如果这些生成的结果被用于下游另一个信息处理系统,这个问题就尤其严重,因为下游系统可能只能处理固定格式的输入数据。
为了规避这个问题,开发者需要对生成的文本进行限制,因为 LM 本质上是一个概率预测模型,它并不天然地遵守这些限制。
理想情况下,这些约束应该可以用人类可理解的概念和逻辑来表达,因为用户只能基于单词、句子和实体进行推理,而不是像 LM 那样在token级别上进行推理。
挑战三:Efficiency and Cost
最后,效率和性能仍然是巨大的挑战。 虽然人们已经投入了很大精力致力于使 LM 中的推理步骤更加高效,但它们仍然需要昂贵的、高端 GPU 以合理的性能运行。
正因为如此,许多实际用户求助于到在云上运行的托管模型,但即使托管到云上,依然需要使用付费 API 进行调用,LM 查询在计算和财务方面都可能变得非常昂贵。
然而,当引入语言模型编程和约束时,新的优化机会得以出现,因为“预定义行为”和“搜索空间限制”可以减少 LM 被调用的次数。 在这种设置下,验证、解析的成本与单个 LM 调用的巨大成本相比可以大幅减少。
Fig.4 Example of a meta prompt for the circumference of the earth and its scripted prompting counterpart
0x3:Language Model Programming in LMQL
这章我们讨论 LMQL 如何帮助克服传统LLM面临的挑战。
如下图(c)所示,我们编写与之前的人工promot-completions交互相同的查询LMQL 语法。
在LMQL query中,一切变量被输入到 LM 之前,以及通过LM预测得到的答案,都使用 [VAR] 进行了占位。大括号中的变量名 [VAR] 只是调用先前定义的变量。这大大简化了prompt提示并消除手动交互的需要。
此外在本例中,它还允许联合考虑专家姓名(LLM生成的临时中间变量结果)和答案的解码过程(LLM生成的最终结果)。
此外在本例中,为了解决运行时间较长的句子的问题,LMQL 允许对专家姓名(LLM生成的临时中间变量结果)施加约束。在上图中可以看到,约束强制执行 EXPERT 的解码标记最多为三个单词,并且序列需要以“.”结尾。 虽然可以使用当前的查询 API 指定最大长度,但它们通常作用在token级别上,因此无法精确控制最终序列的词/句生成结果。相比之下,LMQL 支持声明性高级约束,这些约束需要在解码期间强制执行。
总体而言,语言模型编程概括并自动化了许多 multi-part prompting方法,与用户必须手动尝试 EXPERT 的多个值然后选择最好的一个传统手工方式相比,LMQL 允许用户提前施加一组经过考虑的专家限制到prompt-completions生成过程中,并使该选择过程完全自动化。 一旦完成开发和测试,LMQL 查询(和约束)可以应用于无监督的许多不同输入。
LMQL 约束强制输出符合提示模板,并避免生成结果过长等故障情况。这可以提高的下游任务的准确性。
最后,LMQL 也比手动交互更加高效,不需要多次 LM 调用。
四、THE LMQL LANGUAGE
在接下来讨论运行时和语法语义之前,我们在这里提供一份 LMQL 语法的宏观解释。对于具体示例,请考虑图 1 中给出的 LMQL 程序。
Fig. 5. Syntax of LMQL. Brackets denote optional elements. Syntax is generally python based.
- 解码器(decoder):⟨decoder⟩ 表示 LMQL 运行时在求解问题时采用的解码过程。LMQL 支持 argmax、sample 和 beam。
- 实际查询(actual query):它符合和model进行交互,可以通俗将<query>理解为python函数体,但区别是:i)不允许声明内部函数(但是可以导入);ii) 每个top-level字符串都被视为对 LM 的直接查询。 这些查询字符串允许两个特殊转义的子字段,类似于 python f-strings1: 1) "{varname}" 调用 a 的值当前范围内的变量。 2.),“[varname]”代表将由LM生成的短语,也成为占位词。
- 指定查询模型的 from 子句(from):⟨model⟩ 表示要使用哪个 LM,标识来自 Hugging Face Model 存储库的文本生成模型,或通过 OpenAI API 提供的模型(例如 GPT 系列)。然而,这也可以扩展到其他本地模型或 API 后端。
- 指定约束的 where 子句(where):当LM为占位词(hole)生成值时,它们将受查询的 where 子句中定义的约束条件加以约束。其中 ⟨condition⟩ 对 [varname] 占位词变量施加约束,从而约束语言模型的生成结果。约束可以是任意的合取或析取 ⟨cond_expr⟩ 或成员资格(in)检查表达式。 在这些限制条件下,将使用⟨decoder⟩指定的解码过程。解码完成后,相应的变量将在查询程序的范围内创建并赋值为解码结果。如果同名变量已经存在,它将被覆盖。
- 分配指令(distribution instruction):在 ⟨python_expression⟩ 中,distribute ⟨var⟩ 是一个可选指令,可以被用来增加返回结果。 这里,⟨var⟩必须引用查询和python表达式集合(概率分布集合)中的最后一个变量。
解码器(decoder)和模型(model)两者都由字符串指定,而查询(query)和约束(where)则以 python 语法给出。
- 交互过程(interaction trace):即 LMQL 查询的整个文本记录,以及被 LM 的答案替换的占位词。
- 占位词(hole)词典:所有占位词变量的集合都是可访问的,允许客户直接访问 LM 响应的特定部分。
对于sample和beam,参数 𝑛 指定了sample或beam的次数,在这种情况下,将返回与各个变量的 n 个交互轨迹。
为了更好的说明查询(query)和解码(decoder),请考虑上图 1a,它利用纯粹字符串,而上图 1b,则利用了字符串和控制流的组合。
注意在图 1b 的程序中,THING 在循环的每次迭代中都被重复赋值,这符合python的语义。
相应的交互跟踪如下图 6 所示。
Fig. 6. The interaction trace for the query from Fig. 1b for different decoding methods.
对于带有distribution子句的查询,交互跟踪只会评估到待解码的倒数第二个词,最后一个变量不会被解码,而是一个概率分布。LMQL会对概率分布中的每个值评估该输出的可能性。
下图 7 显示了图 1b 中的示例。
Fig. 7. Continuation of the example from Fig. 1b and Fig. 6a when appending distribute ITEM over things to the query
这对于编码分类任务(例如情感分析)特别有用,因为下游应用可能对概率分布感兴趣,例如 {正/负}。
0x1:Built-in Functions
在where子句中,除了标准python代码之外,还额外支持了一组内置函数。例如,
- word
- sentence:给定字符串或token表示形式的句子,将其转换为所需的表示形式。
- stop_at t(⟨var⟩, ⟨str⟩):用户能够明确定义停止标准,表示当变量 ⟨var⟩ 已解码到指定的 <str> 短语时,应停止变量的解码。
- len:它会重载其默认的 python 函数,它返回字符串(或可迭代)的长度。
为了实现这些指定的内置函数,我们实现了一套高效的附加语义,用于输出验证和解码掩码的生成。
五、THE LMQL RUNTIME: QUERY EXECUTION & DECODING
我们现在讨论 LMQL 运行时如何执行查询。
我们将 ⟨query⟩ 的执行视为一个Python程序,在执行过程中,我们作出如下假设,
- 函数是纯净的,且不会引起副作用
- 函数是确定性的
⟨query⟩ 是逐行执行的,就像常规 python 函数一样,唯一有一个区别:在执行开始时,交互跟踪 𝑢 ← 𝜖 被初始化为空字符串𝜖。
每当程序执行中遇到top-level字符串𝑠时,下图中的 algorithm 1 就会被触发执行:
我们在下图 9 中说明了这个执行模型,其中列出了评估图 1b 中前 7 行的步骤。
Fig. 9. Example execution of the first 7 lines in Fig. 1b. Text generated by the LM 𝒇 in blue.
当调用𝑑𝑒𝑐𝑜𝑑𝑒时,在顶部声明的解码过程,LMQL 程序用于生成占位符的值。
解码通常会在以下条件触发时停止
- 当产生序列结束token时
- 当因为给定的约束,而无法产生更多token时
LMQL和LM语言模型进行了深度集成,但正如下图展示的解码算法
除了能够访问token vocabulary的结果分布表之外,我们不对语言模型f施加任何限制。因为,从根本上来说,这是大多数语言模型的核心接口,我们可以轻松地将它们集成在一起,无需进一步更改。
事实上,我们基于 HuggingFace 转换器包中的generate() 函数实现了上图所示的算法。因此,LMQL 已经支持 HuggingFace 中可用的大量 LM模型。
从性能方面进行考虑,对于在大量样本上执行很大的 𝑛 query查询可能会很昂贵,特别是如果在之上调用计算密集型函数用于LM 输出。 然而,由于我们假设函数是纯粹的和确定性的,因此可以基于函数参数缓存结果,这样可以大大减少了所需函数的调用总数。
LMQL 还可以在 LM 预测其下一个 token 分布前,并行地评估约束、控制流和计算token mask。只有符合约束,token mask才会被用于继续文本生成。 这意味着 LMQL 运行时可以与 LM 同步进行,不会产生额外的延迟。
在上图的解码算法中,对于每个新令牌,我们计算一个掩码𝒎词汇表,它只允许产生符合where条件的序列token,直到我们获得了一个序列结束的 eos token 才停止继续生成,我们停止了。
笔者认为,LMQL探索了一种新的LLM编程范式,既认为:
基于英文或者纯中文自然语言prompt并不是最佳的和LLM进行”通信“的协议语言,而在LMQL的帮助下,开发者可以和LLM在token级别进行更细粒度和更复杂的逻辑编程和格式控制,LMQL封装了开发者和LLM操作系统交互通信的复杂接口,提供了一种简洁的编程开发范式。
六、VALIDATION AND CONSTRAINT DECODING
七、EVALUATION
在这章,我们评估 LMQL 作为一种编程语言,以及赋能prompt engineers的工具的有效性。
我们在三个不同的案例研究中评估 LMQL,涵盖广泛的prompt场景。
我们的评估重点关注三个核心问题:
- 表现力(Expressiveness):我们能否用简单、简洁的query逻辑,轻松实现常见和高级的提示技术(prompting techniques),特别是在interactive prompting的情况下?
- 性能(Performance):LMQL 能否用于有效降低所需的模型查询数量,从而降低使用 LM 的隐含计算或 API 相关成本?
- 准确性(Accuracy): LMQL 的受限解码(constrained decoding)是否会影响 LM 完成任务的准确性?
LMQL提供了比较高级的接口,接近自然语言提示。 因此,我们评估 LMQL 主要是依据其他现有高级、基于文本的、基于Python接口和LM进行交互的替代方案。
0x1:Case Study 1: Chain-of-Thought Prompting
Fig. 10. LMQL query implementing chain-of-thought prompting for the Odd One Out classification task.
Table 3. Average performance statistics (over queries) for constrained LMQL chain-of-thought decoding compared with standard chunk-wise decoding for the Odd One Out and Date Understanding datasets.
0x2:Case Study 2: Interactive Prompting
Fig. 11. LMQL code for interactive ReAct prompting scheme for question answering.
笔者思考
本质上,像chain-of-thought这种few-shot prompting技术,是在通过修改input,进而影响decoding过程中token mask,进而影响最终result的生成。而LMQL的思想也很简单,它通过重写了LLM decode函数,直接将约束条件施加到了token mask上,进而达到影响最终生成result的目的,这种方式比通过修改input来的更直接,也更有效。
但缺点也是相对的,就是LMQL框架库的需要和LLM的内部细节进行深度耦合和绑定,LMQL的性能和逻辑完善度直接影响到最终的生成结果。
再回到prompt programing对软件开发范式带来的改变这个话题上来,LLM驱动的software dev 2.0和人工对世界仿真建模的software dev 1.0有什么本质的区别呢?
笔者认为,其核心区别就在于一句话: