
llama.cpp 中 LLGuidance 实战指南面向 JSON Schema 与 Lark 语法的结构化输出约束【免费下载链接】llama.cppLLM inference in C/C项目地址: https://gitcode.com/GitHub_Trending/ll/llama.cppllama.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_LLGUIDANCEON 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.cppconst 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_LLGUIDANCEON) is not enabled); #endif } else { // 原有 GBNF 路径llama_sampler_init_grammar / lazy patterns }两个细节值得注意传给 LLGuidance 的grammar kind 固定为lark即文法体按 Lark 变体语法解析若使用%llguidance文法但编译时未启用该选项程序会直接 abort并提示llguidance (cmake -DLLAMA_LLGUIDANCEON) 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/-jfllama-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.5msp100 为 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.cppLLGuidance 采样器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作为反查函数交给 LLGuidanceEOS 取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: falseobject 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]特殊 formatdate、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.txtif (LLAMA_LLGUIDANCE) llama_build_and_test(test-grammar-llguidance.cpp ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-llama-bpe.gguf) endif ()其测试方法match_stringtests/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_grammarLark 文法两类组织。小结启用cmake -B build -DLLAMA_LLGUIDANCEON Rust 工具链构建期自动以 cargo 编译上游静态库并链接进 llama-common接口零侵入%llguidance前缀文法与-jJSON Schema 两条现有通道自动分流到 LLGuidancekind 为lark未启用时前缀文法会明确 abort、-j则回退内置 GBNF语义更贴规范additionalProperties默认 true、任意空白、属性声明顺序保留、不支持的 Schema 显式报错已知限制如uniqueItems尚不支持性能来源lexer/parser 分离128k 词表下 mask 计算平均约 50μs失败不中断运行期 mask 错误输出到 stderr 后解除约束继续生成可验证test-grammar-llguidance提供从词法到 JSON Schema 的完整回归测试构建并运行该测试是确认环境配置正确的直接手段。如果你想了解内置 GBNF 文法本身的语法细节可参考 grammars/README.mdJSON Schema 到 GBNF 的内置转换逻辑则见 common/json-schema-to-grammar.cpp。【免费下载链接】llama.cppLLM inference in C/C项目地址: https://gitcode.com/GitHub_Trending/ll/llama.cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考