llama.cpp 中 LLGuidance 实战指南:面向 JSON Schema 与 Lark 语法的结构化输出约束
llama.cpp 除了内置的 GBNF 语法约束外,还可选集成 LLGuidance——一个用 Rust 实现的高性能约束解码(constrained decoding / 结构化输出)库。本文基于仓库中的 LLGuidance 支持文档,结合 common/llguidance.cpp、common/sampling.cpp 的源码与 tests/test-grammar-llguidance.cpp 测试,完整讲解如何启用该编译选项、%llguidance 语法的接口约定、JSON Schema 的语义差异、token mask 的运行时实现,以及为什么 LLGuidance 选择 Lark 语法而不是直接复用 GBNF。
一、LLGuidance 是什么
LLGuidance 是一个专为大语言模型约束采样设计的独立库,最初作为 Guidance 库的后端开发,也可以脱离 Guidance 单独使用。它的核心能力包括:
- 支持 JSON Schema,覆盖面广、贴近规范语义;
- 支持任意上下文无关文法(CFG),采用 Lark 语法的一种变体书写;
- 性能非常高(原因见第四节),这是其词法器/解析器分离架构与一系列优化的结果。
代价是它由 Rust 编写,需要 Rust 工具链参与 llama.cpp 的构建过程。因此 llama.cpp 将其设计为一个默认关闭的可选编译开关,在 CMakeLists.txt 中定义:
option(LLAMA_LLGUIDANCE "llama-common: include LLGuidance library for structured output in common utils" OFF)
二、构建:启用 LLGuidance 支持
按 docs/llguidance.md 的说明,构建时打开 LLAMA_LLGUIDANCE 选项:
cmake -B build -DLLAMA_LLGUIDANCE=ON
make -C build -j
Windows 下将 make 替换为:
cmake --build build --config Release
前置条件是安装 Rust 编译器和 cargo 工具。
从源码构建流程看,common/CMakeLists.txt 在 LLAMA_LLGUIDANCE 开启后会做三件事:
- 通过 CMake 的
ExternalProject_Add拉取 llguidance 上游源码(当前固定到 v1.0.1 对应的提交d795912),并用cargo build --release --package llguidance编译出静态库; - 对 llama-common 目标定义编译宏
LLAMA_USE_LLGUIDANCE——这正是源码中所有 LLGuidance 分支的编译开关; - 将静态库
llguidance链接进 llama-common,并把target/release目录加入头文件搜索路径。Windows 下还会额外链接ws2_32 userenv ntdll bcrypt四个系统库。
也就是说,Rust 编译发生在 CMake 配置之后的构建阶段,产物是一个被 C++ 侧调用的 C ABI 静态库(接口头文件为 llguidance.h)。
三、接口设计:%llguidance 前缀与 -j 参数
LLGuidance 的接入不引入任何新的命令行参数,也不改动 common_params 结构(见 docs/llguidance.md "Interface" 一节)。它通过两条现有通道生效:
3.1 以 %llguidance 开头的文法字符串
当通过 --grammar(-gf 文件方式同理)传入的文法内容以 %llguidance 开头时,llama.cpp 会把它交给 LLGuidance 处理,而不是走内置的 GBNF 解析器。分支逻辑位于 common/sampling.cpp:
const std::string & grammar_str = common_grammar_value(params.grammar);
if (grammar_str.compare(0, 11, "%llguidance") == 0) {
#ifdef LLAMA_USE_LLGUIDANCE
grmr = llama_sampler_init_llg(vocab, "lark", grammar_str.c_str());
#else
GGML_ABORT("llguidance (cmake -DLLAMA_LLGUIDANCE=ON) is not enabled");
#endif
} else {
// 原有 GBNF 路径:llama_sampler_init_grammar / lazy patterns
}
两个细节值得注意:
- 传给 LLGuidance 的 grammar kind 固定为
"lark",即文法体按 Lark 变体语法解析; - 若使用
%llguidance文法但编译时未启用该选项,程序会直接 abort 并提示llguidance (cmake -DLLAMA_LLGUIDANCE=ON) is not enabled——在 common/llguidance.cpp 的降级实现中也有对应的警告输出。
因此你可以像使用 GBNF 一样使用 LLGuidance 文法,例如(示意):
llama-cli -m model.gguf -gf my_grammar.txt
# my_grammar.txt 内容以 %llguidance 开头,其后是 Lark 变体文法
对于已有的 GBNF 文法,可以用 LLGuidance 项目自带的 gbnf_to_lark.py 脚本将其转换为 Lark 风格,脚本通常还能自动处理终结符(大写)与非终结符(小写)的命名区分。
3.2 JSON Schema 请求(-j / -jf)
llama-cli 等工具用 -j(--json-schema)或 -jf(--json-schema-file)传入 JSON Schema 时,参数解析器调用 json_schema_to_grammar 将其转成文法字符串,入口见 common/arg.cpp。该函数的实现在 common/json-schema-to-grammar.cpp 中根据是否启用 LLGuidance 走完全不同的路径:
std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf) {
#ifdef LLAMA_USE_LLGUIDANCE
if (!force_gbnf) {
return "%llguidance {}\nstart: %json " + schema.dump();
}
#else
(void)force_gbnf;
#endif
return build_grammar(...); // 回退到内置的 GBNF 生成器
}
启用 LLGuidance 后,JSON Schema 会被原样 dump 拼进 %llguidance 文法里(start: %json <schema> 形式),由 LLGuidance 内部的 %json 规则解析,而不是先在 C++ 侧展开成 GBNF。这意味着:
- 同一份
-j参数,在未启用 LLGuidance 的构建上走内置 GBNF 生成器(功能子集),在启用后的构建上走 LLGuidance(更贴近规范); - common/chat.cpp 中 chat 接口的
inputs.json_schema同样经过json_schema_to_grammar,因此对话式调用也自动受益。
四、性能:token mask 计算成本
docs/llguidance.md 给出的实测数据(基于 JSON Schema Bench 基准):对于 128k 词表的 llama3 tokenizer,计算一次 "token mask"(即允许的 token 集合)平均消耗 50μs 单核 CPU 时间,p99 为 0.5ms,p100 为 20ms。
这个数量级的成本主要来自架构设计:
- 词法器(lexer)与解析器(parser)分离。JSON 等语言通常采用两阶段处理:先用正则词法器把字节流切成 lexeme,再由 CFG 解析器处理。词法器求值便宜得多,且 lexeme 数量比字节数少约 10 倍;
- LLM 的 token 往往与 lexeme 天然对齐,因此解析器实际只在不到 0.5% 的 token 上被真正调用,其余时间由词法器处理。
对照源码可以印证 mask 的使用方式:common/llguidance.cpp 中,apply 阶段通过 llg_matcher_get_mask / llg_matcher_compute_mask 拿到位图,然后逐 token 检查:
for (size_t i = 0; i < cur_p->size; ++i) {
auto token = cur_p->data[i].id;
if ((mask[token / 32] & (1 << (token % 32))) == 0) {
cur_p->data[i].logit = -INFINITY; // 不在允许集合内 → 直接屏蔽
}
}
被 mask 排除的 token 的 logit 被置为 -INFINITY,从而在后续 softmax/采样中概率为零。每次采样选定 token 后,llama_sampler_llg_accept_impl 调用 llg_matcher_consume_token 推进匹配器状态;reset 则调用 llg_matcher_reset 回到初始状态。
五、运行时实现:tokenizer 构建与采样器生命周期
深入 common/llguidance.cpp,LLGuidance 采样器(llama_sampler_llg)由四部分构成:词表指针 vocab、文法类型与文法文本、LlgTokenizer* 和 LlgMatcher*。
tokenizer 构建(llama_sampler_llg_new_tokenizer)是理解性能与正确性的关键:
- 对词表中每一个 token id 调用
llama_detokenize取其字节形式(普通 token 失败时以special标志重试;特殊 token 会在字节前加\xff前缀标记),并记录每个 token 的长度; - 以
llama_tokenize(封装为llama_sampler_llg_tokenize_fn)作为反查函数交给 LLGuidance; - EOS 取
llama_vocab_eot,若不存在则退回llama_vocab_eos。
该 tokenizer 会按词表做静态缓存(同一 vocab 只构建一次,克隆复用),避免每次创建采样器都全量 detokenize 一遍。llama_sampler_init_llg 还会做一次健全性断言:词表大小向上取整到 32 的倍数后乘以 4 字节,必须与 llg_matcher_get_mask_byte_size 返回的 mask 字节数一致——这保证了 mask 位图与词表一一对应。
其余生命周期操作都很直接:clone 通过 llg_clone_matcher / llg_clone_tokenizer 复制状态(支持并行采样链),free 释放 matcher 与 tokenizer。日志级别可通过环境变量 LLGUIDANCE_LOG_LEVEL 调整(见 common/llguidance.cpp,读取 cinit.log_stderr_level)。
六、为什么不直接复用 GBNF 格式
这是 docs/llguidance.md 单独设节的架构问题,答案的核心一句话是:GBNF 没有 lexer 的概念。
- 多数编程语言(含 JSON)都采用 lexer + CFG 解析器两阶段处理。lexer 基于正则、求值代价低;lexeme 数量比字节少约 10 倍,使得整体求值更快;
- LLM token 常与 lexeme 对齐,解析器介入的频率不到 0.5%;
- 代价是用户必须显式区分 lexeme(终结符)与 CFG 符号(非终结符)。Lark 的约定是:终结符名字大写,非终结符小写。
gbnf_to_lark.py脚本在很多场景下能自动完成这一转换。
这与第三节 3.2 的实现选择一致:既然 GBNF 表达不了 lexer,-j 启用 LLGuidance 后干脆不再把 Schema 降级翻译成 GBNF,而是让 LLGuidance 的 %json 规则原生处理。
七、JSON Schema 语义:与内置 GBNF 生成器的关键差异
LLGuidance 严格贴合 JSON Schema 规范,文档列出了三点与 llama.cpp 现有文法生成器的行为差异,这些差异在 tests/test-grammar-llguidance.cpp 中有逐条对应的测试用例:
| 行为 | 内置 GBNF 生成器 | LLGuidance | 测试佐证 |
|---|---|---|---|
additionalProperties |
默认按 false 处理 | 默认 true(需要收紧时显式写 "additionalProperties": false) |
"object properties, additionalProperties: true" 用例验证附加属性合法 |
| 空白符 | 受限 | 任意空白均允许(如 "a": 1 与 "a":1 均可) |
多个用例在 enum 值前后带空格的字符串上通过 |
properties 定义顺序 |
required 属性一律排前 | 保持声明顺序,与 required 与否无关 | "required + optional props each in original order" 用例:b 声明在 a 前,则 {"b": ..., "a": ...} 通过而反序失败 |
| 不支持的 Schema 关键字 | 可能静默忽略 | 直接报错,任何关键字都不会被静默忽略 | — |
测试文件 tests/test-grammar-llguidance.cpp 中 test_json_schema() 覆盖的 Schema 语义相当全面,可作为能力清单参考:
- 数值约束:
minimum/maximum/exclusiveMinimum/exclusiveMaximum(含负数边界、前导零如"01"被拒绝); - 字符串约束:
minLength/maxLength/pattern(含转义字符); - 类型与常量:
type: string/integer/boolean、const、enum(混合 string/null/number/array); - 对象语义:
properties、required、additionalProperties的 true/false 两种形态、属性顺序约束; - 数组语义:
minItems/maxItems、items多态(如type: ["array", "null"]); - 特殊 format:
date、uuid、time、date-time。
测试中还用 DISABLED_uniqueItems 标注了一个已知限制:uniqueItems 目前不支持(注释说明其实现代价过高,属于 TODO),使用时需要自行规避。
八、错误处理行为
文档明确说明当前策略:错误打印到 stderr,生成继续。源码可以确认这一语义——common/llguidance.cpp 中,compute_mask 返回非零时:
LOG_ERR("llg error: %s\n", llg_matcher_get_error(ctx->grammar));
llg_free_matcher(ctx->grammar);
ctx->grammar = nullptr;
return;
matcher 被释放后置空,之后的 apply 成为 no-op(约束解除,采样自由进行),因此约束失败不会中断推理,但输出将不再受约束保证。文档也提到未来可能会改进错误处理。文法创建阶段的错误(如语法不合法)则直接体现在 llg_matcher_get_error 上,llama_sampler_init_llg 返回 nullptr,上层 common_sampler_init 会抛出 "failed to parse grammar"。
九、测试与验证
该功能有专门的集成测试 tests/test-grammar-llguidance.cpp,且仅在选项开启时构建(tests/CMakeLists.txt):
if (LLAMA_LLGUIDANCE)
llama_build_and_test(test-grammar-llguidance.cpp ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-llama-bpe.gguf)
endif ()
其测试方法(match_string,tests/test-grammar-llguidance.cpp)忠实模拟了真实采样循环:对输入逐 token 做 "apply → 检查期望 token 的 logit 非负 → accept",最后检查 EOS 是否被允许以判定文法接受。测试组包括简单/复杂算术文法、特殊字符(多字节 emoji 按单字符计数)、* + ? 量词与 {n} / {n,} / {0,n} 重复量词、全套 JSON Schema 用例,以及一个把 llama_sampler_init_llg 与 llama_sampler_init_dist 串进 llama_sampler_chain 的集成用例,验证 LLGuidance 采样器在采样链中的组合行为。文法用例本身以 test_schema(内部拼成 %llguidance {}\nstart: %json <schema>,与 -j 的运行时形态一致)和 test_grammar(Lark 文法)两类组织。
小结
- 启用:
cmake -B build -DLLAMA_LLGUIDANCE=ON+ Rust 工具链;构建期自动以 cargo 编译上游静态库并链接进 llama-common; - 接口零侵入:
%llguidance前缀文法与-jJSON Schema 两条现有通道自动分流到 LLGuidance(kind 为lark),未启用时前缀文法会明确 abort、-j则回退内置 GBNF; - 语义更贴规范:
additionalProperties默认 true、任意空白、属性声明顺序保留、不支持的 Schema 显式报错;已知限制如uniqueItems尚不支持; - 性能来源:lexer/parser 分离,128k 词表下 mask 计算平均约 50μs;
- 失败不中断:运行期 mask 错误输出到 stderr 后解除约束继续生成;
- 可验证:
test-grammar-llguidance提供从词法到 JSON Schema 的完整回归测试,构建并运行该测试是确认环境配置正确的直接手段。
如果你想了解内置 GBNF 文法本身的语法细节,可参考 grammars/README.md;JSON Schema 到 GBNF 的内置转换逻辑则见 common/json-schema-to-grammar.cpp。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00