摘要
大语言模型(LLMs)已在全球引发广泛关注,使得许多以前难以实现的AI应用成为可能。LLM通过高度表达性的文本提示进行控制并返回文本答案。然而,这种输入和输出的不结构化文本使基于LLM的应用程序变得脆弱。这推动了提示框架的崛起,旨在调节LLM与外部世界的交互。然而,现有的提示框架要么具有较高的学习曲线,要么剥夺了开发人员对精确提示的控制。为了解决这一困境,本文引入了提示声明语言(PDL)。PDL是一种简单的声明式数据导向语言,以YAML为基础,将提示置于核心地位。PDL能够很好地与多个LLM平台和LLM协同工作,支持编写调用LLM和工具的交互式应用程序,并易于实现诸如聊天机器人、RAG或代理等常见用例。我们希望PDL能够使提示编程更加简单、稳健和愉快。
1. 引言
大语言模型(LLMs)取得了巨大进展,展示了执行各种有用任务的能力。由于LLM通过自然语言提示进行控制,提示工程已成为一种提升准确性的临时方法(White等,2023)。通过提示模式,例如上下文学习(Brown等,2020)、多次LLM调用链(Chase等,2022)、增强生成(RAG)(Lewis等,2020)、工具使用(Schick等,2023)、程序辅助语言模型(PAL)(Gao等,2023)和代理(Yao等,2023)可以解锁更多功能。然而,尽管强大,LLM仍然很脆弱:有时会出现幻觉,甚至无法遵守预期的语法和类型。
提示框架(Liu等,2023)使开发人员能够更轻松地使用LLM和相关的提示模式,同时减少其脆弱性。一些框架,如LangChain(Chase等,2022)和AutoGen(Wu等,2023),通过为RAG或代理等流行模式提供特定功能来实现。然而,这种特性会剥夺用户对基本提示的控制权,并迫使他们学习许多复杂的框架功能。相比之下,低级提示框架,如Guidance(Microsoft,2023)和LMQL(Beurer-Kellner等,2023),通过语法和类型提供更多控制。然而,它们要求用户使用Python或TypeScript等命令式语言编程。另一端的框架,如DSPy(Khattab等,2023)和Vieira(Li等,2024),通过自动生成提示完全避免手写提示。不幸的是,这进一步剥夺了开发者的控制权。问题因此变成了如何在保持简单的前提下,使LLM编程更加稳健并让开发者保持主导地位。
为了解决这个问题,我们借鉴了久经考验的编程语言设计思想。正交性的原则倡导使用一组小而简单的功能,通过组合实现强大的功能(van Wijngaarden等,1977)。在这里,正交性意味着尽量避免特例。对于提示框架,正交性是一种避免特定功能的方式。接下来,如果语言能够通过类型和角色检查(Hugging Face,2023)在结构上强制执行,开发者在脆弱性方面的挣扎将会减少。一个难以解决的紧张关系仍然存在:一方面,我们希望开发者能够控制精确的提示,另一方面,我们需要一个简单的声明式语言。为此,我们选择了数据导向的语言,故意模糊了程序(如用于链和工具)和数据(用于提示)之间的界限。这一灵感来自代码即数据的旧概念(McCarthy,1960),以及关于无层编程的开创性工作(Cooper等,2006)。
本文介绍了 Prompt Declaration Language(PDL),这是一种正交且类型化的数据导向语言。不同于嵌入在命令式语言中的其他提示语言,PDL 基于 YAML(Ben-Kiki 等,2004)。YAML 是一种数据序列化格式,既人类可读(通过促进对非结构化字符串的简单语法),又具备结构性(与 JSON 兼容)。PDL 中的变量也持有 JSON 值,并且可以选择使用 JSON Schema(Pezoa 等,2016)进行类型化。PDL 当前由解释器实现,该解释器执行动态类型检查。将程序表示为数据的一个优点是便于程序转换(Mernik 等,2005),例如用于优化。以数据表示格式渲染程序甚至可以方便 PDL 程序通过大语言模型生成 PDL 程序,类似于 PAL(Gao 等,2023)。
PDL 程序由块(YAML 对象)组成,每个块将数据添加到提示上下文中。此思维模型非常适合用于聊天机器人或代理等提示技术:程序执行会隐式地构建对话或轨迹,而不需要显式地进行管道处理。此上下文随后成为下一个 LLM 调用的输入。PDL 支持本地 LLM,以及由多家供应商托管的 LLM,包括但不限于 IBM Watsonx1 和 Replicate2 上的开源 Granite 模型(Abdelaziz 等,2024;Granite Team,IBM,2024)。PDL 提供循环和条件控制结构,以及函数和文件引入以实现模块化。PDL 采用 Jinja2(Ronacher,2008)表达式来模板化不仅仅是提示,还有整个程序。
本文通过一个介绍性示例概述 PDL(第 2 节),接着是对该语言的详细介绍(第 3 节)。它描述了运行和编辑 PDL 程序的工具(第 4 节),并提供案例研究展示 PDL 的更多应用(第 5 节)。最后,本文讨论了相关工作(第 6 节),并在第 7 节中总结。PDL 是开源的,可在 https://github.com/IBM/prompt-declaration-language 获取。总体而言,PDL 是一种简单而强大的新型 LLM 提示编程语言。
2. 概述
本节通过一个聊天机器人示例概述了 PDL 的功能。一个 PDL 程序执行一系列 块,每个块生成的数据会贡献到背景上下文中。块的类型多种多样,能够通过不同方式生成数据:模型调用、从 stdin 或文件读取数据、直接创建各种 JSON 数据以及执行代码。此外,还有多种控制块(if-then-else、for 和 repeat),这些块让 PDL 用户可以表达丰富的数据管道和 AI 应用。
图 1(a) 显示了一个简单聊天机器人的 PDL 代码。第 1-4 行的 read: 块打印一条消息,询问用户输入查询内容,并从 stdin 读取。图 1(b) 显示了相同程序的执行跟踪。例如,用户可能会问“什么是语言沙拉?”。为了避免重复,“contribute: [context]”子句将用户响应放入背景上下文,但不将结果(stdout 上的打印内容)放入。
repeat:until: 块在第 5–16 行,其中包含一个嵌套的 text: 块,而 text: 块中又包含了两个嵌套块的序列。text: 块将其嵌套块的结果转换为字符串并连接起来。第 7–9 行的 model: 块调用了一个大语言模型 (LLM),使用当前累积的上下文作为提示。在第一次循环迭代中,上下文仅包含两行:“What is your query?” 和 “What’s a language salad?”。‘stop: [\n\n]’ 模型参数会导致 LLM 在生成两个连续的换行符后停止生成 tokens。LLM 解释器以绿色打印 LLM 输出;图 1(b) 显示在此示例中,LLM 生成了 “A language salad is […]”。第 10–15 行的 read: 块使用 YAML 的多行字符串语法(从竖线 (|) 开始)打印消息。此示例展示了 PDL 如何将提示放在首位,同时使其易于阅读并赋予开发者精确的控制权。右侧的解释器跟踪显示用户输入了 “Say it as a poem!”,第 10 行在左侧将其定义为变量 question,并在第 12 行追加到上下文中。第 16 行的 until: 子句指定 Jinja2 表达式 ‘${question == "quit"}’
作为循环终止条件。PDL 使用 ‘${...}’ 语法嵌入 Jinja2 模板,而不是 ‘{{...}}’,因为后者与 YAML 的特殊字符(花括号)不兼容。
在第二次循环迭代中,上下文包含了第一次循环迭代的效果。因此,model: 块的第二次执行可以看到第一次执行的输出,并能将其作为一首诗进行改述,“In a world where many tongues […]” 如图 1(b) 所示。最终,在此示例的第二次 read: 块执行中,用户输入了 “quit”,导致循环终止。现在我们已经看到了一些常用的 PDL 块(read:, repeat:, text:, 和 model:)的实际应用,可以继续进入第 3 节,描述剩余的块和语言特性。
(a) 代码
- read:
contribute: [context]
message: |
您的查询是什么?
- repeat:
text:
- model: watsonx/ibm/granite-13b-chat-v2
parameters:
stop: ["\n\n"]
- def: question
read:
contribute: [context]
message: |
输入查询或说“quit”退出。
until: ${question == "quit"}
(b) 解释器追踪
您的查询是什么?
什么是语言沙拉?
语言沙拉是一个术语,用于描述在单一对话或文本中混合不同语言和方言。它可以被看作是[…]
输入查询或说“quit”退出。
用诗的形式表达!
在语言众多的世界中,
语言沙拉诞生,喜悦中成长。
词语交织,和谐中流动,
五彩缤纷的语言,活力绽放。
输入查询或说“quit”退出。
quit
图 1. 在PDL中的简单聊天机器人
3. 语言
图 2.PDL 快速参考
PDL 是一种嵌入到 YAML 中的语言,使得每个 PDL 程序都是一个符合 PDL 架构的有效 YAML 文档。图 2 是 PDL 的快速参考,本节将使用语法规则进行解释。程序是一个块或块的列表,其中块可以是表达式或结构化块,具体语法规则如下所示:
pdl ::= block | [block, . . . ,block]
block ::= expression | structured_block
本节中的所有语法规则都使用 YAML 的 flow-style 语法(例如,[block,…,block])。相同的 PDL 代码也可以呈现为 YAML 的 block-style 语法,例如:
- block
… - block
每个块包含一个块主体,其中的关键字指示块的类型(例如,model 或 read)。共有 15 种块主体(可选字段用问号注释):
block_body ::=
model:expression,input:?pdl,parameters:?
expression
| read:file,message:?
string,message:?
bool
| text:pdl
| lastOf:pdl
| array:pdl
| object:pdl
| data:json
| include:file
| function:args,return:pdl
| call:𝑓,args:args
| if:expression,then:pdl,else:?pdl
| for:args,repeat:𝑝𝑑𝑙,join:?
join
| repeat:pdl,num_iterations:n,join:?
join
| repeat:pdl,until:expression,join:?
join
| code:pdl,lang:string
我们在上一节中已经看到 model: 和 read: 块。model: 块调用大语言模型。除非指定了可选的 input: 字段,否则提示词来自当前上下文。可选的 parameters: 字段用于配置模型的推理行为。read: 块从文件读取输入,若未指定文件名则从标准输入读取。可选的 message: 字段用于向用户显示信息,可选的 multiline: 字段决定是否在换行符处停止。
创建数据的五种块类型包括: text:、lastOf:、array:、object: 和 data:。图 2 通过简单示例展示了它们。不带关键字的块列表表现为 lastOf:。object: 块和 data: 块的区别在于,PDL 解释器忽略 data: 块中的 PDL 关键字,将其视为普通的 JSON 字段。
为实现模块化,PDL 支持 include: 块和函数。include: 块打开指定相对路径下的 PDL 程序,并将其输出添加到出现的位置。函数参数的语法如下:
args::={x:expression,…,x:expression}
每个 x:expression 将参数名映射到类型规范(在 function: 定义中)或值(在 function call: 中)。return: 关键字提供函数体,可包含嵌套块;图 2 展示了一个简单的 Jinja2 表达式示例。可选的 pdl_context: 关键字可在调用期间重置上下文,例如重置为空上下文 []。
控制构造块有三种:if:、for: 和多种 repeat: 形式。它们可以包含嵌套块或简单表达式;若包含块列表,则列表默认表现为 lastOf:。若不想要 lastOf: 行为,常用方法是将循环体封装在 text: 块中,或使用 join: 关键字合并循环迭代结果:
join::=as:?(text∣array∣lastOf),with:?string
上述 15 种块体可与零个或多个适用于任意块的可选关键字组合使用:
structured_block::=
{ block_body,
description:?
string,
def:?
x,
defs:?
defs,
role:?
string,
contribute:?
contribute,
parser:?parser,
spec:?
type }
description: 是特殊注释。def: 将块的结果分配给变量;图 1 的第 10 行已有示例。相比之下,defs: 创建多个变量定义,每个定义有自己的名称 x,并通过嵌套的 PDL 程序赋值:
defs::={x:pdl,…,x:pdl}
role: 为块生成的数据赋予特定角色,例如 'user'、'assistant' 或 'system'。PDL 调用聊天模型时,遵循现代聊天 API 的通用做法,传递 {content:str, role:str} 对序列,而不是纯文本作为提示。然后,模型 API 应用模型特定的聊天模板,通过插入适当的控制标记展平该序列,从而使 PDL 程序具有一定的模型独立性。若块未显式指定 role:,则模型块默认为 'assistant',其他块默认为 'user'。内嵌块的角色与外层块一致。未来研究中,我们还计划利用角色实现基于权限的安全机制。
contribute: 关键字可用于指定一个(可能为空的)子集至两个目的地 ‘result’ 或 ‘context’。默认情况下,每个模块都对其自己的结果和用于后续大语言模型(LLM)调用的背景上下文做出贡献。图 1 的第 2 行显示了一个将模块贡献限制为仅背景上下文的示例,以简化输出。
parser: 关键字使得通常只生成平面字符串的模块(例如,一个LLM调用)能够生成结构化数据。支持的解析器包括 json, yaml, regex 和 jsonl。spec: 关键字可以指定类型。PDL 的类型是 JSON Schema 的子集(Pezoa 等, 2016),图 2 中简要展示了常用的简写语法。例如,类型 ‘{questions: [str], answers: [str]}’ 是一个包含两个字段的问题和答案的对象,这两个字段都包含字符串数组。第 5 节将说明 parser: 和 spec: 如何协同工作。未来的工作还将利用这些关键字进行约束解码(Scholak 等, 2021)。
一个原子块是一个表达式:
expression ::= bool | number | string | ${𝑗𝑖𝑛𝑗𝑎_𝑒𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛} | string_expression
表达式可以是基本值、Jinja2 表达式(Ronacher, 2008)或包含 Jinja 表达式的字符串。Jinja2 是一种方便的提示模板指定方法,提示的部分内容为硬编码,其他部分则通过表达式填充。但 PDL 进一步拓展了 Jinja2 的使用,允许开发者不仅模板化单个提示,还可以模板化整个模型调用链及其他模块。尽管我们建议读者参考 Jinja2 文档以获取可能表达式的完整列表,图 2 简要列出了一些最常用的表达式。PDL 仅采用 Jinja2 表达式,不包括 Jinja2 的语句如 {% if .. %}
或 {% for .. %}
,因为这些已与 PDL 的 if: 和 for: 功能重叠。
最后但同样重要的是,PDL 拥有一个 code: 模块,该模块允许在指定编程语言中执行代码(截至本文撰写时,仅支持 Python)。下一节将描述 PDL 工具,包括解释器,它提供了一个沙箱功能以降低执行任意代码带来的风险。要了解更多信息,请参阅 PDL 的 GitHub 仓库中的教程链接。
4. 工具
PDL 提供了一些工具,使 PDL 程序易于编写、运行和理解。
首先,PDL 解释器 是一个具有命令行界面的执行引擎,正如人们所期望的那样,这是一种脚本语言。解释器支持流模式,LLM 输出会随着生成过程逐步可见,从而提供更互动的聊天体验。解释器还支持沙盒,这使其在容器中启动,建议在执行 LLM 生成的操作或代码时使用。
PDL IDE 支持 增强了 VSCode,使得通过语法高亮、自动补全、PDL 关键字的工具提示和错误检查更容易编写 PDL 代码。这些功能部分是由 PDL 元模式驱动的,即定义有效 PDL 的 JSON 模式。
%%pdl
单元魔法 增强了 Jupyter Notebooks,开发人员可以直接用 PDL 编写代码单元。这样,托管的笔记本平台可以作为一个简单的游乐场,互动探索提示。在同一个笔记本中给定多个 PDL 代码单元,后面的单元可以使用前面单元中定义的变量。此外,后面单元的背景上下文会延续自前面的单元;当不希望这样时,开发人员可以通过 %%pdl --reset-context
来覆盖此行为。
PDL 实时文档可视化器 以彩色嵌套框的形式显示 PDL 程序的具体执行踪迹,类似于论文或关于 LLM 提示的博客文章中的典型图形。然后,用户可以选择其中一个框来显示相应的 PDL 代码,类似于电子表格单元格显示数据,但用户可以选择它们来检查生成该数据的公式。这种实时视图让用户快速理解具体数据,然后再转向理解生成这些数据的代码。
最后,PDL 具有一个 SDK(软件开发工具包),这是一个小型 Python 库,用于从 Python 调用 PDL。这对于扩展更大的 Python 应用程序以使用基于提示的程序(例如代理)非常有用。如第 3 节所讨论,PDL 文件可以在代码块中包含 Python。在使用 PDL 开发更大的应用程序时,我们发现将这些代码保持在几行之内很有用,通过在单独的 Python 文件中定义函数,然后从 PDL 调用它。一个好的实践是以 JSON 对象的形式在 PDL 和 Python 之间传递数据。可选地,可以使用 PDL 中的 spec:
关键字,以及 Python 端的 TypedDict 或 Pydantic 进行类型检查,如下一节图 3 所示。
(a) PDL 代码
1text:
2- lang: python
3 code: |
4 import rag_mbpp
5 PDL_SESSION.mbpp = rag_mbpp.initialize()
6 result = ""
7- defs:
8 test_query: >-
9 编写一个 Python 函数,从字符串中删除给定字符的第一个和最后一个出现。
12 retrieved:
13 lang: python
14 spec: [{query: str, answer: str}]
15 code: |
16 import rag_mbpp
17 result = rag_mbpp.retrieve(
18 PDL_SESSION.mbpp, "${test_query}", 5
19 )
20 text: >
21 给定文本在 "Q:" 之后,生成一个 Python 函数在 "A:" 之后。
24 这里有一些示例,请完成最后一个:
25- for:
26 few_shot_sample: ${retrieved}
27 repeat: |
28 Q: ${few_shot_sample.query}
29 A: ‘‘‘${few_shot_sample.answer}‘‘‘
30- |-
31 Q: ${test_query}
32 A:
33- model: watsonx/ibm/granite-3-8b-instruct
34 parameters:
35 stop: ["Q:", "A:"]
(b) Python 代码
1from typing import TypedDict
2import datasets
3from sklearn.feature_extraction.text \
4 import TfidfVectorizer
5
6def initialize():
7 train_in = datasets.load_dataset(
8 "mbpp", "sanitized", split="train"
9 )
10 corpus = [row["prompt"] for row in train_in]
11 tfidf = TfidfVectorizer().fit(corpus)
12 def embed(text):
13 sparse_result = tfidf.transform(
14 raw_documents=[text]
15 )
16 return sparse_result.toarray().flatten()
17 train_em = train_in.map(
18 lambda row: {"em": embed(row["prompt"])}
19 )
20 vec_db = train_em.add_faiss_index("em")
21 return vec_db, embed
22
23QA = TypedDict("QA", {"query":str,"answer":str})
24def retrieve(mbpp, query, n: int) -> list[QA]:
25 vec_db, embed = mbpp
26 key = embed(query)
27 nearest = vec_db.get_nearest_examples(
28 "em", key, n
29 )
30 queries = nearest.examples["prompt"]
31 answers = nearest.examples["code"]
32 return [
33 {"query": q, "answer": a}
34 for q, a in zip(queries, answers)
35 ]
图 3. RAG 示例在 PDL
5. 案例研究
我们在第 2 节中已经看到一个简单的 PDL 聊天机器人示例。本节将展示 PDL 的一些稍微复杂的用例:RAG、代理和从 PDL 生成 PDL。
5.1. 检索增强生成
检索增强生成,或称 RAG,的工作原理是首先检索相关上下文,然后将其添加到模型生成答案的提示中 (Lewis et al., 2020)。图 3(a) 显示了一个使用 RAG 检索少量示例以进行代码生成任务的 PDL 程序。代码:第 2–6 行使用 Python 初始化“主要是基本 Python 程序”的 MBPP 数据集的训练分割的向量数据库 (Austin et al., 2021)。它使用图 3(b) 中定义的 Python 函数,以及一个 PDL_SESSION 特殊变量,使其能够将状态传递到后面的代码块。图 3(a) 的第 8–11 行将变量 test_query 初始化为生成 Python 代码的自然语言请求。第 12-19 行将变量 retrieved 初始化为训练数据中五个最相似的示例。
第 20–24 行向上下文添加指令,第 25–29 行将少量示例添加到上下文,第 30–32 行将测试查询添加到上下文。第 25 行的 for: 循环是使用 PDL 生成数据的一种惯用方式,在这种情况下用于上下文学习。最后,第 33–35 行调用一个 Granite 3 模型 (Granite Team, IBM, 2024),使用累积的上下文,导致其生成测试查询请求的 Python 函数。虽然这是一个简单的示例,但我们还使用 PDL 与 Codellm-Devkit (Krishna et al., 2024) 结合使用,该工具对来自各种编程语言的源代码进行静态分析,以在提示 LLM 进行编码任务时检索其他相关上下文。
(a) 代码
1text:
2- read: react_few_shot_samples.txt
3- |
4
5 Hudson River 的发现者是什么时候出生的?
6- repeat:
7 text:
8 - def: thought
9 model: watsonx/ibm/granite-34b-code-instruct
10 parameters:
11 stop: ["Act:"]
12 include_stop_sequence: true
13 - def: action
14 model: watsonx/ibm/granite-34b-code-instruct
15 parameters:
16 stop: ["\n"]
17 parser: json
18 spec: {name: str, arguments: {topic: str}}
19 - def: observation
20 if: ${ action.name == "Search" }
21 then:
22 text:
23 - "Obs: "
24 - lang: python
25 code: |
26 import wikipedia
27 query = "${ action.arguments.topic }"
28 result = wikipedia.summary(query)
29 until: ${ action.name != "Search" }
(b) 解释器跟踪
科罗拉多造山运动东部区域的海拔范围是多少?
Tho: 我需要搜索科罗拉多造山运动,找出东部区域的范围。
Act: {”name”: ”Search”, ”arguments”: {”topic”: ”科罗拉多造山运动”}}
Obs: 科罗拉多造山运动是一个事件 […]
[…]
Hudson River 的发现者是什么时候出生的?
Tho: 我需要搜索 Hudson River 的发现者,找出他是什么时候出生的。
Act: {”name”: ”Search”, ”arguments”: {”topic”: ”Hudson River 的发现者”}}
Obs: Hudson River 是一条 315 英里长的 […]
Tho: Hudson River 的发现者是 Henry Hudson。我需要搜索 Henry Hudson,找出他是什么时候出生的。
Act: {”name”: ”Search”, ”arguments”: {”topic”: ”Henry Hudson”}}
Obs: Henry Hudson (约 1565 年 – 消失 […]
Tho: Henry Hudson 于 1565 年出生。Act: {”name”: ”Finish”, ”arguments”: {”topic”: ”1565”}}
图 4. PDL 中的 ReAct 代理
5.2.ReAct 代理
基于大语言模型的 代理 允许大语言模型选择和配置 动作,在 环境 中执行这些动作,并将动作的输出反馈给大语言模型作为 观察。此类代理有不同的模式,例如 ReAct (Yao et al., 2023) 和 ReWOO (Xu et al., 2023)。动作是基于大语言模型的 工具调用 (Schick et al., 2023),而代理在动态的大语言模型引导序列中链式组合多个工具调用。其目标是使基于 AI 的应用程序少一些规范性而多一些目标导向。此外,当动作出现问题时,代理可以利用观察作为反馈进行恢复。
图 4 显示了一个简单 ReAct 代理的 PDL 示例。ReAct 的核心是一个思考-行动-观察循环,在代码中表现为对思考(第 8 行)、行动(第 13 行)和观察(第 19 行)的变量定义。思考是模型生成的自然语言,例如,在图 4(b) 的解释器跟踪中,‘我需要搜索哈德逊河的发现者,找出他出生的时间’。行动是模型生成的 JSON,以匹配 Granite 模型的工具使用训练数据 (Abdelaziz et al., 2024)。左侧的第 17 行和第 18 行确保大语言模型输出被解析为 JSON,并符合 {name, arguments} 模式,而右侧的解释器跟踪显示模型确实生成了这样的对象。这使得可以使用 Jinja2 访问对象的字段,例如第 27 行的 ${ action.arguments.topic }。观察是由环境生成的,在本例中为调用维基百科的 Python 代码。如第 4 节所讨论,对于涉及运行(部分)由大语言模型生成的代码的案例,我们建议使用 PDL 的沙箱功能。
图 4(b) 中的解释器跟踪显示,此执行有两次代理循环的迭代。尽管这是一个简单的示例,但我们也使用 PDL 实现了一个代码编辑代理,该代理作为提交4 的一部分用于 SWE-bench Lite 排行榜 (Jimenez et al., 2024)。该提交首次在仅使用开源模型的情况下解决了 23.7% 的实例,这比之前任何使用开源模型的结果都要高,并且与前沿模型的结果相当。
5.3.利用大语言模型从 PDL 生成 PDL
前面的章节展示了人类开发者如何使用 PDL 编码不同的提示模式。本节转向大语言模型,并展示它们如何也可以用于生成 PDL。这种元 PDL 生成在大语言模型需要创建解决问题的计划时非常有用,例如作为代理工作流的一部分。传统上,这些计划仅是文本、JSON 或 Python 代码。利用 PDL,这些计划可以是完全可执行的模型和代码调用的组合。本节探索在 GSMHard 数据集5 上使用 PDL 元生成。
GSMHard 是 GSM8k 的一个更难的版本,其中包含需要简单算术或符号推理的学年数学问题。GSMHard 包含一个输入,即数学问题陈述,以及一个输出,即解决该问题的 Python 代码。我们实施了 PAL (Gao et al., 2023) 方法,但不是生成 Python 代码,而是要求大语言模型生成 PDL。文本链式思考表示为 PDL 文本块,而算术则使用 PDL 代码块进行。
图 6 显示了一个生成 PDL 代码并在同一程序中执行的 PDL 程序。变量 demos 保存了旨在教模型如何生成 PDL 代码的少量示例。在第 32 行,一个模型调用块使用这些示例以及一个自由变量的问题作为输入。结果是一个用于解决该问题的 PDL 程序。第 38 行提取 PDL 程序并在 Python 中执行。该程序应用于 GSMHard 数据集,其中问题填充了输入问题。
这一实验发现,GSMHard 数据集的 10% 实际上是错误的,因为基础真相与所提问的问题不一致。图 6 显示了此类不一致的示例。使用 PDL 帮助发现了这一点,因为生成的 PDL 代码是人类可读的,因此我们能够轻松检查与基础真相不匹配的数据点,发现基础真相在某些情况下是错误的。我们使用大语言模型覆盖整个数据集,并系统性地挑选出看似不一致的示例。然后,我们手动筛选结果以去除假阳性,并确定了 10% 的数据点存在此问题。
1 定义:
2 示例:
3 数据:
4 文本:
5 |
6 ...
7
8 问题: Roger 有 5 个网球。
9 他又购买了 2 罐网球。
10 每罐有 3 个网球。
11 现在他总共有多少个网球?
12
13 答案:
14 ```
15 文本:
16 - "Roger 起初有\n"
17 - 定义: tennis_balls
18 数据: 5
19 - "\n个网球。\n"
20 - "2 罐,每罐有 3 个网球,总共是\n"
21 - 语言: python
22 定义: bought_balls
23 代码: result = 2 * 3
24 - "\n个网球。\n"
25 - "结果是:\n"
26 - 语言: python
27 定义: RESULT
28 代码: result = ${ tennis_balls } + ${ bought_balls }
29 ```
30 原始: true
31 文本:
32 - 模型: watsonx/meta-llama/llama-3-70b-instruct
33 定义: PDL
34 输入:
35 文本:
36 - ${ demos.text }
37 - "问题: ${ question }"
38 - 语言: python
39 代码: |
40 from pdl.pdl import exec_str
41 s = """${ PDL }"""
42 pdl = s.split("```")[1]
43 result = exec_str(pdl)
44 定义: RESULT
图 5. PDL 元生成
James 决定每周跑 1793815 次冲刺
每次冲刺 60 米。
他每周总共跑多少米?
def solution():
sprints_per_day = 1793815
days_per_week = 3
meters_per_sprint = 60
total_sprints = sprints_per_day * days_per_week
total_meters = total_sprints * meters_per_sprint
result = total_meters
return result
图 6. GSMHard 样本问题数据点
6. 相关工作
一项最新的研究将 提示框架 定义为一种管理、简化和促进 LLM 与用户、工具或其他模型之间交互的层 (Liu 等人,2023)。该研究强调,提示框架的一个主要缺陷是学习曲线陡峭。
当今最受欢迎的提示框架可能是 LangChain (Chase 等人,2022),其丰富的功能使其既强大又复杂。MiniChain 的主要动机正是为了避免这种复杂性 (Rush,2023),它提供了更少、更简单的功能,可以组合用于高级应用。然而,LangChain 和 MiniChain 都是基于 Python 的框架,使其较为非声明性,因为开发者需要编写命令式代码。PDL 的动机与 MiniChain 类似,但更进一步,采用 YAML 而非 Python 作为基础。
和其他提示框架一样,PDL 的目标是使 LLM 更加稳健。Guidance (Microsoft,2023) 是一个基于 Python 的框架,提供了更具结构性的设计,但比 LangChain 更底层。同样,LMQL (Beurer-Kellner 等人,2023) 是嵌入在 Python 中的领域特定语言,利用类型和受限解码。PDL 在提示和编程的交织方面受到 LMQL 的一些启发,但与 LMQL 不同,它对命令式 Python 代码的依赖更少。Crouse 等人使用有限状态机正式指定各种智能循环的内部流程 (Crouse 等人,2024);尽管这是一个引人入胜的工作,但并未引入一个成熟的提示语言。
领域特定语言的一个优势是它们可以实现程序转换,例如用于优化 (Mernik 等人,2005)。DSPy 提示框架 (Khattab 等人,2023) 的口号是“编程,而非提示”:它自动生成提示,因此开发者无需手动编写。同样,Vieira (Li 等人,2024) 扩展了 Prolog,将 LLM 作为概率关系,并自动生成提示。DSPy 和 Vieira 都是非常高级的框架,但不同于 PDL,它们都削弱了开发者对具体提示的控制。Lale (Baudart 等人,2021) 是一种让用户逐步调整 AI 管道中自动化和控制之间权衡的语言,但它并不专注于 LLM 提示。DSPy、Vieira 和 Lale 优化的是预测性能,而另一种优化目标是计算性能。SGLang (Zheng 等人,2023) 通过更好地利用前缀缓存来实现这一点,从而在 KV 缓存中获得更多的缓存命中 (Kwon 等人,2023)。未来的工作将探索 PDL 的声明性特性是否能实现类似的计算性能优化。
最近,出现了一批以大语言模型 (LLM) 为基础的提示框架,专注于 LLM 代理。AutoGen(Wu 等,2023)是一个多代理框架,其中所有内容都由代理和对话组成。其他多代理框架包括 CrewAI(Moura,2023)和 GPTSwarm(Zhuge 等,2024)。这些框架优先支持代理,而非其他基于 LLM 的用例。PDL 虽然也支持代理,但它追求更加平衡的立场,将代理视为众多提示技术之一。
7. 结论
PDL 是一种声明式数据导向语言:程序由 YAML 块组成,每个块要么是字面数据,要么生成数据。其思维模型是执行一个块时将其数据追加到背景上下文中,随后的大语言模型调用使用该上下文作为提示。本论文通过示例程序以及语法和工具的导览来介绍该语言。该语言的声明式特性也使其易于实现速度、准确性和安全性方面的自动优化,这些将在未来的工作中逐步实现。PDL 现已准备好使用,并在以下网址开源:https://github.com/IBM/prompt-declaration-language。