- 引言 =====
延续前文:
继续Agent智能体专题,今天主要从实践角度分享如何控制LLM的输出。
先简要回顾一下前文提到的约束解码。
那什么是约束解码?约束解码是怎么实现的?
Constrained Decoding的一般步骤如下:
-
定义约束规则:首先,定义约束规则。比如正则表达式、上下文无关法(CFG)等形式化的语法规则,或者是一些自定义的逻辑规则。
-
LLM 生成候选 Token:基于当前上下文(即已经生成的 Token 序列),LLM 会生成一个候选 Token 列表,并为每个 Token 赋予一个概率值。
-
约束检查(Mask Gen):根据预定义的约束规则,检查候选 Token 列表中的每个 Token,确保它们符合设定的规则。
-
过滤(Apply Mask):将不符合规则的 Token 的概率值设置为 0(或极小值),从而排除这些 Token。
-
采样:根据过滤后的概率分布,从剩余的候选 Token 中随机采样一个 Token,作为下一个生成的 Token。
-
重复步骤 2-5,直到生成完整的文本序列。
-
简介 =====
本文我们将聚焦于几种主流的约束解码开源框架,并从实践角度进行测评与分析。具体而言,我们将简要介绍 Guidance 和 Outlines 两个框架,并实测控制大型语言模型输出表现。
这两个框架代表了约束解码的发展方向:
- 一方面是以交错解码为核心、强调模板逻辑与结构表达能力的 Guidance ;
- 另一方面是以正则表达式与有限状态机驱动、侧重结构稳定性与部署可用性的 Outlines 。
我们将从其设计原理、约束策略、性能表现与使用体验等维度进行比较,为小伙伴们在实际应用中提供一些参考借鉴的经验。
- Guidance:基于交错解码 ===================
Guidance 提供了一组基于交错解码(Interleaved-based Decoding)的语法规则,并以 llama.cpp 作为后端。在这种方法中,给定的 JSON Schema 可以拆分成多个部分,每个部分包含一个分块的预填充部分(chunked prefill)或一个受限解码部分(constrained decoding)。这些部分交替执行。由于分块预填充可以在一次前向传递中处理多个 Token,因此它比逐个 Token 解码更为高效。
官方文档:
https://guidance.readthedocs.io/en/latest/index.html
3.1 Guidance 的原理
guidance
是一个用于控制大型语言模型(LLMs)的库。它的设计初衷是使语言模型的控制更为高效和有效。这是通过编写引导程序(guidance programs)实现的, 这些程序允许你将文本生成、提示以及逻辑控制交织在一起,形成一个与语言模型处理文本的方式相匹配的连续流程。
引导程序基于Handlebars模板语言的简单、直观语法,但具有一些独特的功能。它们有一个与语言模型处理token顺序直接对应的独特线性执行顺序。这意味着在执行过程中的任何时刻,都可以使用语言模型来生成文本(使用**{{gen}}
命令)或进行逻辑控制流决策(使用 {{#select}}...{{or}}...{{/select}}
**命令)。生成和提示的交织可以使输出结构更精确,从而提高准确性,同时也产生清晰、可解析的结果。
guidance
通过一个token备份模型,然后允许模型向前移动,同时限制它仅生成前缀与最后一个token匹配的token,从而消除这些偏差。这种“token修复”过程消除了token边界偏差,并允许自然地完成任何提示。
3.2 Guidance实践
3.2.1 guidance安装
pip install guidance
3.2.2 约束生成
guidance 支持使用 selects、正则表达式、上下文无关文法(CFG)等多种方式来约束生成内容。例如,下面的代码展示了如何使用 f-strings, select 在两个选项之间进行选择:
from guidance import models, gen, user, assistant, system, select
path = "Qwen/Qwen2.5-3B-Instruct/"
qwen = models.Transformers(path, torch\_dtype="auto", device\_map="auto")
system\_prompt = """\
请用 JSON 的组织句子中的主语、谓语、宾语,并请判断句子的情绪(友好、中性、敌意),用下面的格式:
{"主语": str, "谓语": str, "宾语": str, "情绪": enum["友好", "中性", "敌意"]}
"""
user\_prompt = "你什么意思,这样开车?\n使用'```json'开始并在最后补充解释,为什么这么做"
with system():
lm\_sys = qwen + system\_prompt
with user():
lm = lm\_sys + user\_prompt
with assistant():
lm = lm + '{"主语": "' + gen(stop='"') + '", "谓语": "' + gen(stop='"') + '", "宾语": "' + gen(stop='"') + '", "情绪": "' + select(["友好", "中性", "敌意"]) + '"}'
print(lm)
3.2.3 输出结果
根据上述的Prompt,我们输出结果较为稳定,同时输出速度较快。
3.3 局限性
- 交错解码方法需要自定义语法,因此比单独的正则表达式更具局限性,且表达能力较弱。
- 解码部分与分块预填充部分之间可能存在冲突,难以正确处理 Token 边界。
- 解释器与后端之间的频繁通信会带来额外的开销。
- Outlines:广泛使用的主流框架 =====================
Outlines 是一个帮助用户以简单和稳定方式使用 LLM 的 Python 库,能够基于regex(正则化表达式)、json、grammar 实现 structured generation, 它允许开发者以简单而健壮的方式(具有结构化生成)使用 LLM,并且已经被许多公司在生产环境中使用。
官方文档:
https://www.aidoczh.com/outlines/index.html
4.1 Outlines实现原理
通过将 JSON Schema 转换成正则表达式,然后基于该正则表达式构建有限状态机(FSM, Finite State Machine),引导 LLM 的生成。在 FSM 的每个状态下,我们可以计算允许的转换,并确定可接受的下一个 Token。这样,我们可以在解码过程中跟踪当前状态,并通过对输出应用 logit 偏置来过滤掉无效的 Token。具体可以参见论文:《Efficient Guided Generation for Large Language Models》。
基于 FSM 的方法利用广义正则表达式来定义低层次的规则,这些规则可以应用于多种语法,如 JSON 架构、IP 地址和电子邮件等。
Outlines具有以下特点:
- 让 LLM 生成有效的 JSON
- 用于 vLLM 部署 LLM 服务
- 使 LLM 遵循正则表达式约束的生成格式
- 提供强大的 Prompt Templating:通过 Prompt 模板更好地管理复杂的 Prompt
4.2 实践
4.2.1 环境安装
安装Outlines模块:
pip install outlines
4.2.2 使用正则表达式控制模式生成格式
import outlines
model = outlines.models.transformers("Qwen/Qwen2.5-3B-Instruct/")
prompt = """
你是一个聪明的AI助手。我们需要分配给每一个高考学生一个任意的高考编号,以下是高考学生编号的注意事项:
-- 1. 高考学生号需要由当地省份的英文大写字母缩写+一串6位的数字组成。比如广东考生433号,GD000043.
-- 2. 请确保生成的学生编号不允许出现重复。
请按要求生成高考学生的编号。
"""
generator = outlines.generate.text(model)
unstructured = generator(prompt, max\_tokens=30)
generator = outlines.generate.regex(
model,
r"([A-Z]{2,3}[0-9]{6})",
sampler=outlines.samplers.greedy(),
)
structured = generator(prompt, max\_tokens=30)
print("unstructured:\n", unstructured)
print("structured:\n", structured)
结果:
unstructured:
"GD123456",
"SD234567",
"JS345
structured:
GD000001
可以看出经过Outlines限制,能减少模型输出非必要内容。但Regex正则提取的方法更加适用于生成 ip地址、电话等较为固定的文本数字格式。
4.2.3 输出定义Json结构的function call范式
我们通过JSON Schema规范的字符串传递给模型生成需要的Json结果。
完整代码如下:
from pydantic import BaseModel
from outlines import models
from outlines import generate
model = models.transformers("Qwen/Qwen2.5-3B-Instruct/")
schema = """
{
"title": "User",
"type": "object",
"properties": {
"取件码": {"type": "string"},
"取件地址": {"type": "string"}
},
"required": ["取件码", "取件地址"]
}
"""
generator = generate.json(model, schema)
result = generator(
"请生成一个带取件码、取件地址的快递提醒通知。取件码一般为6位随机数字"
)
print(result)
结果:
4.3 局限性
- 由于有限状态机是在 Token 级别构建的,它在每一步只能通过一个 Token 来转换状态。因此,它一次只能解码一个 Token,这导致解码过程较为缓慢。
- 上篇我们提及的XGrammar与 Outlines 和 llama.cpp 相比,XGrammar 的性能显著提升,参考速度如下图。
本文深入探讨了两种主流的约束解码框架——Guidance 和 Outlines ,从原理机制到实战应用,再到各自的优势与局限性,提供了系统性的评估。
Guidance 基于交错解码,通过模板语法精细控制生成过程,适用于需要逻辑控制与结构嵌套的场景;Outlines 则借助有限状态机,通过正则与 JSON Schema 等方式实现稳定、可控的结构化输出,更适用于格式规范明确的任务。
尽管它们在生成准确性、结构控制和兼容性方面表现出色,但也存在运行效率、复杂语法支持、Token 级别限制等问题。因此,在实际使用中,用户需根据任务需求、输出格式复杂度以及模型部署性能等多方面因素,选择合适的解码框架。