第14章 第 14 章:LMQL 语言参考
第 14 章:语言参考
LMQL 的核心语法与语义。
本文档旨在为 LMQL 提供更正式的描述,主要讨论其语法形式及对应语义。目前仍在修订中。如有任何问题或改进建议,请随时联系核心团队。
起源与动机
LMQL 是一种用于 LLM 编程的语言。LMQL 的主要设计目标是提供一个简洁而强大的接口,用于实现与 LLM 进行复杂、算法化控制交互的(多部分)推理流程。
为实现这一目标,LMQL 的核心包含以下关键组件:
- 通过受限解码引擎实现健壮且类型安全的生成。
- 轻量级的 Prompt 编程模型,包含抽象和模块化查询代码的方法。
- 对优化并行的 LLM 推理的供应商无关抽象层。
总体目标是提供一个快速且可靠的工具链,促进跨领域的 LLM 驱动推理和应用。这不仅包括现有的用例,如简单的聊天应用和数据解析,还扩展到更复杂的应用,如自主智能体、大规模数据处理以及程序化推理和规划。
使用 LMQL
LMQL 当前的参考实现是用 Python 编写的,可以通过多种方式使用:
- LMQL Playground 提供了一个交互式界面,用于运行、调试和实验 LMQL 程序。这是推荐的上手方式。在线版本位于 lmql.ai/playground。
- LMQL 可作为 Python 库使用,
lmql.query函数提供了一种直接从 Python 中简单使用 LMQL 的方法。更多信息请参阅 Python API 章节。 - lmql run CLI 工具可用于直接从命令行运行 LMQL 程序。
关于如何使用权 LMQL 的更多信息和基于示例的讨论,请参阅入门指南。
语法
LMQL 语言包含两种语法变体:
- 一种更现代、更极简的标准语法,它在一个标准的 Python 环境中依赖于非常小的语言结构集合。这是 LMQL 持续开发的主要关注点。
- 一种静态性质更强的传统独立语法,适用于独立的 LMQL 用例。
两种语法变体均兼容,可以互换使用。
文法表示法
我们使用一种略微修改的 EBNF 变体来描述 LMQL 语法。
我们用 <非终结符>(如 <SENTENCE>)和带引号的终结符(如 "dog")表示。操作符 [...]? 表示可选元素,* 表示零次或多次重复。| 操作符表示在两个或多个元素之间做选择,未加引号的终结符是用于匹配的 Regular Expression。
为避免表示上的开销,我们假设 Python 片段(如 <python.Expr>)指的是标准 Python,适应方式是子推导过程中也可以引用回 LMQL 表达式(具体为 <QUERY_STRING> 和 <CONSTRAINT_EXPR>),在适当情况下。
grammar
<SENTENCE> := "The quick" ["brown"]? <SUBJECT> <VERB> "over the lazy dog."
<SUBJECT> := "fox" | "dog"
<VERB> := "jumps" | "hops" | [.*]{4}
有效的 <SENTENCE> 推导示例:
lmql
"The quick fox jumps over the lazy dog."
"The quick brown fox jumps over the lazy dog."
"The quick dog jumps over the lazy dog."
"The quick brown dog hops over the lazy dog."
"The quick dog A9_D over the lazy dog."
标准语法
LMQL 的现代语法可以读作标准 Python 代码,并添加了一个新的结构:Query Strings(查询字符串)。Query Strings 只是顶层字符串表达式,被解释为发送给底层 LLM 的 Prompts。以下文法详细描述了这一语法。
grammar
<LMQL_PROGRAM> := # (optional) decoder clause
[ <DECODER> [ '(' <python.KeywordArguments> ')' ]? ]?
# program body
<STMT>*
<STMT> := # regular python statements
<python.Stmt> |
# query strings
<QUERY_STRING> |
# query strings with inline constraints
<QUERY_STRING> 'where' <CONSTRAINT_EXPR> |
# query strings with distribution clause
<QUERY_STRING> 'distribution' <DISTRIBUTION_EXPR>) |
<QUERY_STRING> := <...>
<CONSTRAINT_EXPR> := <...>
<DISTRIBUTION_EXPR> := <VARIABLE> 'in' <python.Expr>
有效程序示例
包含两个查询字符串的简单程序
lmql
"Hello [WHO]"
"Goodbye [WHO]"
不包含 LMQL 结构的 Python 程序也是有效的 LMQL 程序
python
a = 12
print("Hello, world!")
print("Goodbye, world!", a)
包含控制流的 LMQL 程序
lmql
"Q: What is 2x2? [ANSWER]"
while ANSWER != 4:
"Incorrect, try again: [ANSWER]"
"Good job!"
包含查询字符串、约束和字符串插值的程序
lmql
a = "Alice"
"Hello {a} [WHO]" where len(TOKENS(WHO)) < 10
print(WHO)
包含查询字符串和分布子句的程序
lmql
"Greet Alice\n:"
"Hello [WHO]" distribution WHO in ["Alice", "Bob"]
独立语法
独立查询语法不如现代语法变体灵活,通常被视为传统(Legacy)。它仍然支持独立用例,但建议用户今后依赖现代语法。尽管如此,它与现代语法共享许多语法结构,因此也在本文档中进行了描述。
grammar
<STANDALONE_QUERY> := [ <DECODER> [ '(' <python.KeywordArguments> ')' ]? ]?
<PROMPT>
[ 'from' <MODEL_EXPR> ]?
[ 'where' <CONSTRAINT_EXPR> ]?
[ 'distribution' <DISTRIBUTION_EXPR> ]?
<DECODER> := 'argmax' | 'sample' | 'beam' | 'beam_var' |
'var' | 'best_k' | <python.Identifier>
<PROMPT> := <QUERY_STRING> | <python.Stmt>
<QUERY_STRING> := <...>
<MODEL_EXPR> := "lmql.model" "(" <python.Arguments> ")" |
<python.StringLiteral>
<CONSTRAINT_EXPR> := <...>
<DISTRIBUTION_EXPR> := <VARIABLE> 'in' <python.Expr>
VARIABLE := <python.Identifier>
示例
Decoder 子句 + from
lmql
argmax "What is the capital of France? [ANSWER]" from "gpt2"
Decoder 子句 + where
lmql
argmax "What is the capital of France? [ANSWER]" \
where len(TOKENS(ANSWER)) < 10
Decoder 子句 + distribution
lmql
argmax "What is the capital of France? [ANSWER]" \
distribution ANSWER in ["A", "B"]
Decoder 子句 + from + where + distribution
lmql
argmax
"What is the capital of France? [ANSWER]"
from
"gpt2"
distribution
ANSWER in ["A", "B"]
Decoder 子句
grammar
DECODER_CLAUSE := <DECODER> [ '(' <python.KeywordArguments> ')' ]?
Decoder 子句定义了用于生成的解码算法。它是可选的,默认为 argmax。
算法
<DECODER> 是运行时支持的解码算法之一,例如 argmax, sample, beam, beam_var, var, best_k,或自定义 decoder 函数。详细描述请参阅解码文档章节。
程序级解码
每个查询程序只能指定一个 decoder 子句,即使用单个解码算法来执行所有查询字符串和占位符变量。这是因为 decoder 作用于程序级别,并根据当前程序状态分支地探索程序的多种可能延续。
查询字符串
查询字符串是 Prompt 构建和模型交互的核心结构。它们读起来像 Python 中的顶层字符串表达式,但被解释为发送给底层 LLM 的 Prompts,包括占位符变量、约束和字符串插值:
grammar
QUERY_STRING := <CONSTRAINED_QSTRING> | <DISTRIBUTION_QSTRING> | <PURE_QSTRING>
# qstring with constraints, e.g. "Hello, [NAME]!" where len(TOKENS(NAME)) < 10
<CONSTRAINED_QSTRING> := <PURE_QSTRING> 'where' <CONSTRAINT_EXPR>
# qstring with distribution clause, e.g. "Hello, [NAME]!" distribution NAME in ["Alice", "Bob"]
<DISTRIBUTION_QSTRING> := <PURE_QSTRING> 'distribution' <VARIABLE> 'in' <python.Expr>
# qstring without constraints, e.g. "Hello, [NAME]!"
<PURE_QSTRING> := <python.StringDelimiter>
(
'.*' | # arbitrary string prompt
<PLACEHOLDER_VARIABLE> |
<STRING_INTERPOLATION>
)* '"'
<python.StringDelimiter>
# placeholder variable, e.g. "[NAME]"
<PLACEHOLDER_VARIABLE> := "[" <python.Identifier> "]" |
# with type/tactic annotation
"[" <python.Identifier> ":" <python.Expr> "]" |
# with decorator
"[" <DECORATOR>? <python.Identifier> "]" |
# with decorator and type/tactic
"[" <DECORATOR>? <python.Identifier> ":" <python.Expr> "]"
# decorator, e.g. "@fct(a=12)"
<DECORATOR> := "@" <python.Call> | "@" [<python.Identifier>]
# standard Python f-string interpolation
<STRING_INTERPOLATION> := "{" <python.Expr> "}"
示例
无变量:"Hello, world!"
包含两个变量:"Hello, [NAME] and [NAME2]!"
带约束:"Hello, [NAME]!" where len(TOKENS(NAME)) < 10
带装饰器:"Hello, [@fct(a=12) NAME]!"
带类型注解:"Your Age: [AGE:int]!"
带嵌套调用(tactic 注解):"Q: What is 2x2? A: [ANSWER: chain_of_thought(shots=2)]"
带字符串插值:
lmql
NAME = "Alice"
"Hello, {NAME}, how old are you: [AGE:int]?"
转义
为避免歧义,查询字符串中需要转义以下字符:
[和]需转义为[[和]]。{和}需转义为{{和}}。
Python 字符串转义规则也适用,例如,需要转义字符串分隔符以消除字符串边界的歧义(例如 "\" ")。
Prompt 构建
执行过程中用于 LLM 调用的当前 Prompt 由迄今为止执行的所有查询字符串拼接而成,其中占位符变量被替换为对应的生成值。查询字符串沿着程序的控制流(例如在循环中多次调用时)以及单个包含多个占位符的查询字符串内的从左到右进行评估。在执行过程中的任何时间点,当调用 LLM 时,传递给模型的就是当前活动的 Prompt。
为此,如果需要换行,必须显式包含换行符 \n。
示例:Prompt 构建
作为 Prompt 构建的示例,考虑以下程序及其执行期间对应的 Prompt 状态:
lmql
"Hello, [NAME] and [NAME2]\n"
⬆ 1 ⬆ 2
"How are you doing?"
⬆ 3
" [FEELINGS]"
⬆ 4 ⬆ 5
| 点 | Prompt | 活动变量 | 动作 |
|---|---|---|---|
| 1 | "Hello, " | NAME | 生成 NAME 的值 |
| 2 | "Hello, <generated NAME> and " | NAME2 | 生成 NAME2 的值 |
| 3 | "Hello, <generated NAME> and <generated NAME2>\n" | - | 继续执行程序 |
| 4 | "Hello, <generated NAME> and <generated NAME2>\nHow are you doing? " | FEELINGS | 生成 FEELINGS 的值 |
| 5 | "Hello, <generated NAME> and <generated NAME2>\nHow are you doing? <generated FEELINGS>" | - | 结束程序执行 |
占位符变量
占位符变量定义了 LLM 生成文本的模板占位符,用方括号 [...] 表示。相对于程序状态,占位符变量将生成值分配给同名程序变量。
示例:变量赋值
就变量赋值而言,以下代码:
lmql
NAME = "Alice"
"Hello [NAME] and [NAME2]"
可以理解为以下伪代码:
python
NAME = "Alice"
NAME, NAME1 = model.fill_placeholders("Hello [NAME] and [NAME2]")
因此,执行查询字符串后,NAME 之前的值会被覆盖。
查询字符串约束与分布
查询字符串也可以使用 "..." where ... 语法进行约束。这定义了仅在生成相应查询字符串期间局部应用的解码约束。更多信息请参阅约束章节。
类似地,可以使用 "..." distribution VAR in [...] 语法构建分布。这为相应变量的给定替代值生成分数,并返回由此产生的似然值。更多信息请参阅分布子句章节。
示例:带约束的查询字符串
对变量 NAME 带有约束的查询字符串:
lmql
"Hello, [NAME]!" where len(TOKENS(NAME)) < 10
"Another [NAME]"
NAME 的 Token 长度约束仅应用于为第一个查询字符串(即 "Hello, [NAME]!")调用的生成。下一个查询字符串 "Another [NAME]" 不受该约束的影响。
类型与 Tactic
除了变量名称外,还可以使用 "[VAR: <tactic>]" 语法指定 tactic 或类型。语法上,tactic 表达式可以是任意 Python 表达式,但在运行时,解释器期望以下之一:
- 类似
int的类型引用,作为 LMQL 类型表达式被运行时支持。 - 类似
r"[a-z]+"的 Regular Expression,强制作为 Regex 约束。 - 引用另一个 LMQL 查询函数(如
chain_of_thought(shots=2)),作为嵌套查询执行。
示例:Tactic 表达式
对变量 AGE 带有整数类型注解的查询字符串:
lmql
"Your Age: [AGE:int]!"
对变量 NAME 带有 Regex tactic 的查询字符串:
lmql
"Hello, [NAME:r"[a-z]+"]!"
使用嵌套查询对变量 ANSWER 进行解码的查询字符串:
lmql
"Q: What is 2x2? A: [ANSWER: chain_of_thought(shots=2)]"
字符串插值
查询字符串被编译为 Python f-string,因此使用 "Hello {...}" 语法实现标准的 Python 字符串插值语义。例如,假设当前程序状态将 NAME 赋值为 "Alice",则 "Hello {NAME}" 求值结果为 "Hello Alice"。
约束
grammar
<CONSTRAINT_EXPR> := <CONSTRAINT> 'and' CONSTRAINT_EXPR |
<CONSTRAINT> 'or' CONSTRAINT_EXPR |
'not' <CONSTRAINT> |
<CONSTRAINT>
危险 TODO
模型表达式
grammar
<MODEL_EXPR> := "lmql.model" "(" <python.Arguments> ")" |
<python.StringLiteral>
危险 TODO
分布子句
grammar
<DISTRIBUTION_EXPR> := <VARIABLE> 'in' <python.Expr>
危险 TODO
Python 片段
LMQL 依赖以下 Python 语法片段来表达 LMQL 语言的部分内容。
参考:Python 语法在此处可用。
| 片段 | 描述 |
|---|---|
<python.Identifier> |
Python 标识符,定义为 Python 语法中的 NAME。示例:a, b, my_var1, MY_VAR2 |
<python.StringLiteral> |
Python 字符串字面量,定义为 Python 语法中的 STRING。包括支持字符串分隔符,如单引号 ('), 双引号 ("), 和三引号 (''' 或 """)。示例:'hello', "world", '''hello''', """world""" |
<python.StringDelimiter> |
Python 字符串分隔符,例如 ', ", ''', 或 """。 |
<python.Expr> |
常规 Python 表达式,定义为 Python 语法中的 expression。示例:a, a+b, a(b=12), a if b else c, a > 2, a == b |
<python.Stmt> |
常规 Python 语句,定义为 Python 语法中的 simple_stmt 或 compound_stmt。包括控制流语句,如 if, for, while, try, with 等。 |
<python.Call> |
Python 函数调用,定义为 Python 语法中的 call。包括表达式如 a(), a(b=12), a(b=12, c=13), a(b=12, c=13, some_dict)。 |
<python.KeywordArguments> |
函数调用的关键字参数,定义为 Python 语法中的 kwargs。包括表达式如 (), (a=1, b=2), (a=1, b=2, some_dict)。 |
<python.Arguments> |
函数调用的参数,定义为 Python 语法中的 args。包括表达式如 (), (a, b), (a, 1, 2, c=2), (a, b, *some_iterable, some_dict)。 |
类型
开发中
LMQL 类型可用于在生成过程中注解变量,以强制对生成值施加类型约束。结果值有两种表示形式:
- Prompt 表示:用于 Prompt 中的值,例如 1234 表示为字符串 "1234"。
- 程序表示:当从程序访问相应变量时返回的值,例如 1234 表示为 Python 整数 1234。
这种区分有助于实现富有表现力的 Prompt,即以便于 LLM 的方式表示值,同时允许对生成值进行类型安全且方便的程序化访问。
| 类型 | 描述 | Prompt 表示 | 程序表示 |
|---|---|---|---|
| str (默认) | 字符串类型,例如 "hello", "world", ... | str_value | class str |
| int | 整数类型,例如 1, 2, 3, ... | str(int_value) | class int |
所有占位符变量的默认类型是 str。要改变变量类型,可以将类型指定为占位符变量声明的一部分,例如 [NAME:int] 或 [NAME:float],如查询字符串部分所述。
查询函数
查询函数是 LMQL 程序的功能构建块。
LMQL 查询函数的定义类似于常规 Python 函数语法,但使用 @lmql.query 装饰器,并将 LMQL 代码作为 docstring 的一部分提供,而不是函数体。
lmql
@lmql.query
def my_query_function(person):
'''lmql
"Greet {person}. Hello [NAME]!"
'''
在 Python 内部,可以使用相同的语法来构建可从 Python 调用的查询函数。请参阅 Python 集成文档章节以获取更多详细信息。
LMQL 查询函数也可以声明为异步函数,从而支持异步执行。
函数调用与参数
查询函数可以在 LMQL 或 Python 代码中作为标准函数调用。它也可以在查询字符串内部作为嵌套查询调用。异步查询函数需要使用 await 关键字。
参数
除了函数签名中指定的函数参数外,查询函数还提供以下额外参数,用于控制生成过程:
model:用于生成的lmql.LLM模型引用(或字符串标识符)。decoder:用于生成的解码算法。另请参阅 decoder 子句章节。output_writer:生成过程中使用的输出写入回调。另请参阅输出流式传输文档章节。**kwargs:传递给 decoder 和解释器的额外关键字参数,如temperature,chunksize等。
参考实现
LMQL 当前的参考实现是用 Python 编写的,也可用作 Python 库。本文档中描述的语法和语义的参考实现可通过 GitHub 上的 github.com/eth-sri/lmql 获取。
编译器与运行时
LMQL Python 编译器将 LMQL 程序转换为异步、可分支的 Python 代码,遵循本文档描述的语义。生成的程序随后使用 LMQL 运行时执行,该运行时实现了(受限)解码算法、优化以及通过多个后端支持模型。
混合解析器
对于解析,实现利用了一种混合方法,主要依赖现有的 Python 解析器(ast.parse)和语法,仅为 LMQL 特定的结构添加额外的解析逻辑。这种方法使我们能够遵守 Python 语法,同时允许我们用原始 Python 语法中不存在的额外结构扩展语言。为了解析独立语法,我们在 Token 级别分段输入,然后多次调用解析器以获得例如 prompt 子句、where 子句或 distribution 子句的最终 AST。