大模型在生成结构化数据时的表现并不稳定,尤其是当你的下游程序依赖严格的JSON格式时,格式错误会直接导致解析失败。你可能会尝试在提示词中反复强调“只输出JSON,不要包含任何其他文本”,但模型依然会偶尔输出一个多余的解释、一个缺失的右花括号,或者把年龄字段写成了字符串。传统方法无法从机制上禁止这些错误,因为模型在生成每个token时都有一定概率选择不符合格式的token。LMQL(Language Model Query Language)改变了这个局面,它允许你在解码过程中嵌入逻辑约束,把“必须符合格式”变成一个硬性条件,而不是概率性建议。

LMQL是一种与语言模型交互的查询语言,它扩展了传统提示词工程,让你可以在提示词中声明变量、施加类型约束、设置枚举值限制,甚至编写自定义验证逻辑。其核心机制是利用解码时的token屏蔽:在每一步生成时,LMQL会根据约束条件计算哪些token是允许的,然后将不允许的token的概率置为零,只从合法的token集合中采样。这种机制保证了最终输出在定义上就满足条件,而不是通过事后校验来修复。
理解LMQL的约束解码机制
要理解LMQL为什么能强制JSON格式,首先得明白它和普通提示词的区别。普通提示词只是给模型一段文本,模型自由生成后续内容,没有任何硬性限制。而LMQL把生成过程变成了一个带有变量的程序:你在提示词中定义占位符变量,例如[NAME]或[AGE],然后为这些变量附加约束条件。当模型生成到该变量位置时,LMQL会调用底层模型的解码器,根据约束条件动态计算候选token的掩码,确保只有满足约束的token才会被采样。
这种约束是在token级别实现的,不需要模型本身理解约束的语义。例如,如果你定义了type(AGE) == int,LMQL会在生成AGE变量时,屏蔽所有无法被解析为整数的token序列。这就好比给模型戴上了一副“语法眼镜”,它看到的世界里只有符合规则的路径可走。这种机制对JSON格式尤其有效,因为你可以精确控制花括号、引号、冒号等结构字符的生成位置,而只把需要模型自由发挥的部分留给变量。
import lmql
@lmql.query
def simple_constraint():
'''lmql
argmax
"Extract the capital of France: [CAPITAL]"
from
"openai/gpt-3.5-turbo"
where
len(CAPITAL) < 10
'''
上面的代码展示了一个最简单的LMQL查询:定义了一个变量CAPITAL,并约束其长度小于10。当模型生成CAPITAL时,LMQL会在每一步检查已生成的部分是否仍满足长度约束,如果当前token会导致总长度超过9,则这个token会被屏蔽。这种约束是确定性的,与模型本身的倾向无关,这就是LMQL能够强制格式的根本原因。
使用LMQL强制输出JSON格式
强制JSON格式是LMQL最典型的应用场景之一。最直接的做法是把整个JSON结构写成模板,将需要模型填充的值用变量表示,并给每个变量设置对应的类型约束。这样模型只生成变量的值,而花括号、引号、逗号等结构字符完全由模板提供,从根本上避免了格式错误。你不需要担心模型漏掉某个右括号,因为它根本没有机会生成括号。
下面是一个完整的示例,要求模型从一个文本中提取人物信息,并输出合法JSON。我们使用dataclass定义目标结构,然后通过json()类型约束来强制LMQL生成符合该结构的JSON字符串。
from dataclasses import dataclass
import lmql
@dataclass
class Person:
name: str
age: int
@lmql.query
def extract_person():
'''lmql
argmax
"Extract person info from the following text: 'John is 28 years old.'\n"
"Return the result as a JSON object: [PERSON]"
from
"openai/gpt-3.5-turbo"
where
PERSON is json(Person)
'''
执行这个查询时,LMQL会在生成PERSON变量时应用json(Person)约束。这意味着模型每一步生成的token都必须属于某个可能组成合法Person JSON对象的序列。如果模型试图生成一个不是数字的字符来填充age字段,该token会被屏蔽。最终得到的输出一定是一个类似{"name": "John", "age": 28}的合法JSON字符串。
这种方法的优势在于,即使模型在语义理解上出现偏差,输出的格式仍然严格符合定义。你可以放心地将结果直接传给json.loads(),不需要任何额外的解析容错处理。对于生产环境中的自动化流程来说,这可以避免大量由格式错误引发的故障。
枚举类型验证:把输出限制在预设选项内
除了基本类型约束,很多业务场景需要把字段值限定在枚举范围内,比如状态字段只能是active、inactive或pending。如果只是让模型自由生成,它可能会输出Active(大小写问题)、activo(错误拼写)或者进行中(语言不一致)。LMQL的枚举约束可以彻底解决这个问题。
在上面的JSON示例中,如果你希望status字段只能取三个值之一,可以定义一个带枚举字段的dataclass,并使用LMQL的in操作符进行约束。但更简单的方法是把枚举值直接作为模板的一部分,让模型在几个候选token之间选择。例如:
@lmql.query
def status_classification():
'''lmql
argmax
"Classify the sentiment of this review: 'The product works great and delivery was fast.'\n"
"The sentiment is: [SENTIMENT]"
from
"openai/gpt-3.5-turbo"
where
SENTIMENT in ["positive", "negative", "neutral"]
'''
在这个查询中,SENTIMENT变量被限制为只能从三个枚举值中选择一个。LMQL在解码时会屏蔽所有与这三个值不匹配的token序列。例如,如果模型一开始生成了p,那么下一步只能从o开始继续,因为只有positive以p开头;如果它生成了p o,那么剩下的路径就被锁定为s i t i v e。这种机制确保输出的字符串一定严格等于三个枚举值之一,不存在任何变体。
枚举约束还可以和JSON模板结合,对嵌套字段进行验证。例如,你要生成一个带有status字段的JSON对象,同时该字段只能取active或inactive,则可以将枚举约束应用到JSON模板中的对应变量上。以下代码展示了这种组合:
@lmql.query
def json_with_status():
'''lmql
argmax
"Generate a user status JSON with the following info: user_id=101, status=active\n"
"{\n"
" \"user_id\": [USER_ID],\n"
" \"status\": \"[STATUS]\"\n"
"}"
from
"openai/gpt-3.5-turbo"
where
type(USER_ID) == int and STATUS in ["active", "inactive"]
'''
在这个例子中,USER_ID被限制为整数,STATUS被限制为枚举值。模型只负责生成两个变量的值,整个JSON结构由模板固定。执行这个查询后,你可以直接得到一个合法、类型正确且枚举值受控的JSON字符串。如果你使用过传统提示词来构建类似功能,就会明白这种确定性保证有多么珍贵。
构建一个可靠的受约束生成管道
了解了基本用法之后,你可以把这些技术组合起来构建一个完整的受约束生成管道。假设你要从用户输入中提取订单信息,要求输出包含订单号、金额和状态的JSON,并且状态必须是pending、shipped、delivered之一。你可以在LMQL查询中同时使用类型约束和枚举约束,并将整个流程封装为一个函数,方便在应用层调用。
下面是一个综合实战案例,展示如何利用LMQL完成一个订单信息提取任务。注意代码中如何定义提示词模板、变量约束以及错误处理。
import lmql
import json
@lmql.query
def extract_order_info():
'''lmql
argmax
"Extract order information from the customer message: 'My order #12345 was shipped yesterday, total amount is $59.99.'\n"
"Return JSON with order_id, amount, and status.\n"
"{\n"
" \"order_id\": \"[ORDER_ID]\",\n"
" \"amount\": [AMOUNT],\n"
" \"status\": \"[STATUS]\"\n"
"}"
from
"openai/gpt-3.5-turbo"
where
len(ORDER_ID) > 0 and type(AMOUNT) == float and STATUS in ["pending", "shipped", "delivered"]
'''
# 调用并解析结果
result = extract_order_info()
order_json = json.loads(result)
print(order_json["status"]) # 必然是 "shipped"
在这个管道中,ORDER_ID是一个非空字符串,AMOUNT被约束为浮点数,STATUS被限制为三个枚举值之一。LMQL在生成时会严格执行这些约束,因此返回的字符串可以直接被json.loads()解析。即使输入文本中的状态是shipped,模型也不能生成shiped这样的错拼,因为它不在枚举列表中。这种强约束使得整个系统更加健壮,减少了后处理代码的复杂度。
需要注意的是,LMQL目前依赖后端模型对约束解码的支持,并非所有模型都原生支持token级别的屏蔽。一些托管API可能无法使用完整的约束功能,你需要选用支持该特性的模型或本地部署的模型。此外,过度严格的约束可能会降低生成质量,因为模型的选择空间被压缩了。在实际应用时,建议根据具体场景在约束强度和生成多样性之间找到平衡。
LMQL为结构化输出提供了一种全新的思路:把格式要求从提示词的“软约束”变成解码器的“硬约束”。通过JSON模板、类型检查和枚举验证的组合,你可以构建出极其稳定的生成管道,让大模型在需要严格格式的场景中变得真正可靠。如果你饱受格式错误之苦,不妨试试LMQL,它可能会成为你工具链中不可或缺的一环。