news 2026/9/7 23:42:41

llama.cpp 中 LLGuidance 实战指南:面向 JSON Schema 与 Lark 语法的结构化输出约束

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
llama.cpp 中 LLGuidance 实战指南:面向 JSON Schema 与 Lark 语法的结构化输出约束

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 -j

Windows 下将make替换为:

cmake --build build --config Release

前置条件是安装Rust 编译器cargo工具。

从源码构建流程看,common/CMakeLists.txt 在LLAMA_LLGUIDANCE开启后会做三件事:

  1. 通过 CMake 的ExternalProject_Add拉取 llguidance 上游源码(当前固定到 v1.0.1 对应的提交d795912),并用cargo build --release --package llguidance编译出静态库;
  2. 对 llama-common 目标定义编译宏LLAMA_USE_LLGUIDANCE——这正是源码中所有 LLGuidance 分支的编译开关;
  3. 将静态库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)是理解性能与正确性的关键:

  1. 对词表中每一个 token id调用llama_detokenize取其字节形式(普通 token 失败时以special标志重试;特殊 token 会在字节前加\xff前缀标记),并记录每个 token 的长度;
  2. llama_tokenize(封装为llama_sampler_llg_tokenize_fn)作为反查函数交给 LLGuidance;
  3. 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/booleanconstenum(混合 string/null/number/array);
  • 对象语义:propertiesrequiredadditionalProperties的 true/false 两种形态、属性顺序约束;
  • 数组语义:minItems/maxItemsitems多态(如type: ["array", "null"]);
  • 特殊 format:dateuuidtimedate-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_llgllama_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 23:42:02

Go sync.RWMutex 源码剖析:读写锁的设计与性能边界

读多写少是并发编程里出现频率最高的一类模型&#xff0c;缓存、配置中心、路由表、指标聚合&#xff0c;几乎到处都能碰到。Go 的 sync.RWMutex 就是为这个场景量身定做的读写锁&#xff0c;它允许大量读者同时持有读锁&#xff0c;只有写者需要独占。这篇东西我不打算只停留…

作者头像 李华
网站建设 2026/9/7 23:41:53

冠豪猪算法优化XGBoost回归实战:工业预测性能提升23%

1. 项目概述&#xff1a;当冠豪猪算法遇上XGBoost回归 去年在做一个工业设备剩余寿命预测项目时&#xff0c;传统XGBoost模型在噪声数据上的表现总是不尽如人意。直到尝试将冠豪猪优化算法&#xff08;Crested Porcupine Optimizer, CPO&#xff09;与XGBoost结合&#xff0c;测…

作者头像 李华
网站建设 2026/9/7 23:40:42

学生毕业离校系统-springboot

本项目为前几天收费帮学妹做的一个项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 基于springboot的学生毕业离校系统通过Mysql数据库连接数据库 http://localhost:80…

作者头像 李华
网站建设 2026/9/7 23:38:38

S7-200 SMART位读写库:基于间接寻址实现动态位操作

搞过 S7-200 SMART 通信项目的兄弟&#xff0c;应该都遇到过这种需求&#xff1a;报文里有一串状态位要解析&#xff0c;或者配方里存了一堆启停标志位&#xff0c;上位机不给你固定点位 V0.0、V0.1&#xff0c;而是直接给一个“第 N 个位”的序号让你去读写。比如标题里说的&a…

作者头像 李华
网站建设 2026/9/7 23:33:22

AI大模型教育行业落地指南:从技术底座到进校部署的关键路径

简介&#xff1a;《AI大模型教育行业白皮书》面向教育行业决策者、高校教师、AI产品与研究人员&#xff0c;系统梳理数智教育时代下从教育ICT建设、教育信息化到AI全面渗透的演进脉络&#xff0c;并围绕基础教育、高等教育、人才选拔与职业教育给出AI落地场景与实践路径。资源为…

作者头像 李华