实战演示如何控制LLM的输出-以Guidance和Outlines为例

向量数据库大模型云通信
  1. 引言 =====

延续前文:

继续Agent智能体专题,今天主要从实践角度分享如何控制LLM的输出。

先简要回顾一下前文提到的约束解码。

picture.image

那什么是约束解码?约束解码是怎么实现的?

Constrained Decoding的一般步骤如下:

  1. 定义约束规则:首先,定义约束规则。比如正则表达式、上下文无关法(CFG)等形式化的语法规则,或者是一些自定义的逻辑规则。

  2. LLM 生成候选 Token:基于当前上下文(即已经生成的 Token 序列),LLM 会生成一个候选 Token 列表,并为每个 Token 赋予一个概率值。

  3. 约束检查(Mask Gen):根据预定义的约束规则,检查候选 Token 列表中的每个 Token,确保它们符合设定的规则。

  4. 过滤(Apply Mask):将不符合规则的 Token 的概率值设置为 0(或极小值),从而排除这些 Token。

  5. 采样:根据过滤后的概率分布,从剩余的候选 Token 中随机采样一个 Token,作为下一个生成的 Token。

  6. 重复步骤 2-5,直到生成完整的文本序列。

  7. 简介 =====

本文我们将聚焦于几种主流的约束解码开源框架,并从实践角度进行测评与分析。具体而言,我们将简要介绍 GuidanceOutlines 两个框架,并实测控制大型语言模型输出表现。

这两个框架代表了约束解码的发展方向:

  • 一方面是以交错解码为核心、强调模板逻辑与结构表达能力的 Guidance
  • 另一方面是以正则表达式与有限状态机驱动、侧重结构稳定性与部署可用性的 Outlines

我们将从其设计原理、约束策略、性能表现与使用体验等维度进行比较,为小伙伴们在实际应用中提供一些参考借鉴的经验。

  1. 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 输出结果

picture.image

根据上述的Prompt,我们输出结果较为稳定,同时输出速度较快。

3.3 局限性

  • 交错解码方法需要自定义语法,因此比单独的正则表达式更具局限性,且表达能力较弱。
  • 解码部分与分块预填充部分之间可能存在冲突,难以正确处理 Token 边界。
  • 解释器与后端之间的频繁通信会带来额外的开销。
  1. 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)  

结果:

picture.image

4.3 局限性

  • 由于有限状态机是在 Token 级别构建的,它在每一步只能通过一个 Token 来转换状态。因此,它一次只能解码一个 Token,这导致解码过程较为缓慢。
  • 上篇我们提及的XGrammar与 Outlines 和 llama.cpp 相比,XGrammar 的性能显著提升,参考速度如下图。

picture.image

小结

本文深入探讨了两种主流的约束解码框架——GuidanceOutlines ,从原理机制到实战应用,再到各自的优势与局限性,提供了系统性的评估。

Guidance 基于交错解码,通过模板语法精细控制生成过程,适用于需要逻辑控制与结构嵌套的场景;Outlines 则借助有限状态机,通过正则与 JSON Schema 等方式实现稳定、可控的结构化输出,更适用于格式规范明确的任务。

尽管它们在生成准确性、结构控制和兼容性方面表现出色,但也存在运行效率、复杂语法支持、Token 级别限制等问题。因此,在实际使用中,用户需根据任务需求、输出格式复杂度以及模型部署性能等多方面因素,选择合适的解码框架。

picture.image

0
0
0
0
关于作者

文章

0

获赞

0

收藏

0

相关资源
大规模高性能计算集群优化实践
随着机器学习的发展,数据量和训练模型都有越来越大的趋势,这对基础设施有了更高的要求,包括硬件、网络架构等。本次分享主要介绍火山引擎支撑大规模高性能计算集群的架构和优化实践。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论