llama.cpp 中 LLGuidance 实战指南:面向 JSON Schema 与 Lark 语法的结构化输出约束
【免费下载链接】llama.cppLLM inference in C/C++项目地址: https://gitcode.com/GitHub_Trending/ll/llama.cpp
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 -jWindows 下将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。
【免费下载链接】llama.cppLLM inference in C/C++项目地址: https://gitcode.com/GitHub_Trending/ll/llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考