mistral.rs 中的 llguidance 受限生成:用 Lark + JSON Schema 文法强制「推理 + JSON」结构输出
【免费下载链接】mistral.rsFast, flexible LLM inference项目地址: https://gitcode.com/GitHub_Trending/mi/mistral.rs
本文围绕 mistral.rs 的官方示例文档 llguidance 展开,完整讲解如何用 llguidance 文法对 LLM 生成过程施加约束:以一段 Lark 文法包裹推理文本、再以@myobj锚点引用一个 JSON Schema 子文法,使模型输出必然形如Reasoning: …\nJSON: {"answer": "Yes"|"No"}。读完本文,你将掌握该示例的完整代码与运行方式,并理解文法是如何经由Constraint枚举、ParserFactory与SequenceRecognizer在采样阶段逐 token 生效的底层链路。
什么是 llguidance 受限生成
受限生成(constrained decoding / structured output)的核心思想是:在每一步采样时,只允许"符合某个文法"的 token 通过 logits 屏蔽(masking),从而保证输出在形式上严格合法。mistral.rs 将该能力建立在 llguidance 之上——工作区根Cargo.toml固定使用llguidance = 1.2.0(default-features = false,启用lark特性),并搭配toktrie_hf_tokenizers = 1.2.0与toktrie = 1.4.0两个依赖,把 Hugging Face tokenizer 的字节级词表编译成 token 字节前缀树(TokTrie),供文法匹配器在字节层面精确对齐 token 边界。
从 Constraint 枚举 可以看到 mistral.rs 支持的全部受限生成入口:
pub type LlguidanceGrammar = llguidance::api::TopLevelGrammar; /// Control the constraint with llguidance. pub enum Constraint { Regex(String), Lark(String), JsonSchema(serde_json::Value), Llguidance(LlguidanceGrammar), None, }Regex/Lark/JsonSchema:单一文法的便捷入口,内部会各自转换成一个TopLevelGrammar;Llguidance:完整入口,直接传入TopLevelGrammar(即本文示例使用的形式),允许携带多个文法片段并通过名字相互引用;None:不施加约束。
LlguidanceGrammar是llguidance::api::TopLevelGrammar的类型别名,其结构为grammars: Vec<GrammarWithLexer>加一个可选的max_tokens,这正是下面示例中vec![top, schema]的来源。
示例代码逐段解析
示例源码位于 mistralrs/examples/advanced/llguidance/main.rs,在 mistralrs/Cargo.toml 中注册为name = "llguidance"的 example,运行命令为:
cargo run --release --example llguidance -p mistralrs完整代码如下:
use anyhow::Result; use mistralrs::{ llguidance::api::GrammarWithLexer, IsqBits, LlguidanceGrammar, ModelBuilder, PagedAttentionMetaBuilder, RequestBuilder, TextMessageRole, }; use serde_json::json; #[tokio::main] async fn main() -> Result<()> { let model = ModelBuilder::new("google/gemma-4-E4B-it") .with_auto_isq(IsqBits::Four) .with_logging() .with_paged_attn(PagedAttentionMetaBuilder::default().build()?) .build() .await?; let top = GrammarWithLexer::from_lark(r#"start: "Reasoning: " /.+/ "\nJSON: " @myobj"#.to_string()); let schema = GrammarWithLexer { name: Some("myobj".to_string()), json_schema: Some(json!({ "type": "object", "properties": { "answer": {"type": "string", "enum": ["Yes", "No"]}, }, "required": ["answer"], "additionalProperties": false, })), ..Default::default() }; let request = RequestBuilder::new() .set_constraint(mistralrs::Constraint::Llguidance(LlguidanceGrammar { grammars: vec![top, schema], max_tokens: None, })) .set_sampler_max_len(100) .add_message( TextMessageRole::User, "If all dogs are mammals, and all mammals are animals, are dogs animals?", ); let response = model.send_chat_request(request).await?; println!("{}", response.choices[0].message.content.as_ref().unwrap()); Ok(()) }第一步:加载模型
ModelBuilder::new("google/gemma-4-E4B-it")指定模型(此处为 Google 的 gemma-4-E4B-it),后续链式方法配置推理环境:
.with_auto_isq(IsqBits::Four):启用自动 ISQ(In-Situ Quantization)在线 4-bit 量化,无需预先量化权重即可降低显存占用;.with_logging():开启运行日志;.with_paged_attn(PagedAttentionMetaBuilder::default().build()?):启用 PagedAttention 分页注意力以优化 KV cache 内存管理;.build().await?:异步完成模型加载。
模型加载本身与约束生成无关,任何支持send_chat_request的文本模型都可以替换。
第二步:构造「Lark 外壳 + JSON Schema 内核」双文法
这是示例的核心,也是 llguidanceTopLevelGrammar的典型用法——用两个GrammarWithLexer组合出一条完整的生成路径:
顶层 Lark 文法
top:GrammarWithLexer::from_lark(r#"start: "Reasoning: " /.+/ "\nJSON: " @myobj"#.to_string());这段 Lark 文法规定了输出的骨架:先输出字面量
Reasoning:,然后是任意非空文本(/.+/,推理过程),换行后输出JSON:,最后以@myobj终结。@myobj是 llguidance 的文法引用语法:它指向grammars向量中name == "myobj"的另一个文法片段,解析时会被内联展开。JSON Schema 子文法
schema:GrammarWithLexer { name: Some("myobj".to_string()), json_schema: Some(json!({ "type": "object", "properties": { "answer": {"type": "string", "enum": ["Yes", "No"]}, }, "required": ["answer"], "additionalProperties": false, })), ..Default::default() }该片段只声明
name与json_schema两个字段(其余字段如lark_grammar、regex、ebnf_grammar走Default),把一份 JSON Schema 编译成受限生成器:answer字段必填、取值只能是"Yes"或"No",且禁止额外属性。
两段文法合起来保证了输出必然形如:
Reasoning: <自由推理文本> JSON: {"answer": "Yes"}其中自由文本部分不受约束(模型可自由发挥推理),JSON 部分则被逐 token 强制合法。
第三步:组装请求并发送
let request = RequestBuilder::new() .set_constraint(mistralrs::Constraint::Llguidance(LlguidanceGrammar { grammars: vec![top, schema], max_tokens: None, })) .set_sampler_max_len(100) .add_message( TextMessageRole::User, "If all dogs are mammals, and all mammals are animals, are dogs animals?", ); let response = model.send_chat_request(request).await?;set_constraint(Constraint::Llguidance(...)):把双文法挂到请求上,max_tokens: None表示文法侧不额外限制 token 数;set_sampler_max_len(100):采样器层面的最大生成长度 100 token,作为硬性截断上限;add_message(TextMessageRole::User, ...):追加一条用户消息(一段三段论推理题);send_chat_request返回Response,示例直接打印choices[0].message.content。
底层实现:文法如何变成逐 token 的 Matcher
理解示例的行为,需要看 mistral.rs 内部的三条链路。
词表 → 字节前缀树:build_llg_factory
pipeline/llg.rs 中的build_llg_factory在模型初始化时为每个 tokenizer 构建一个全局共享的ParserFactory:
- 先把 tokenizer 的 decoder 规整为单个
ByteLeveldecoder,保证文法与字节流对齐; - 收集所有 special token(
at.special为真的 added tokens); - 通过
toktrie_hf_tokenizers::ByteTokenizer::from_tokenizer(tokenizer)得到 token 字节表token_bytes,并对可能被漏标的 special token 手动补上SPECIAL_TOKEN_MARKER前缀(源码注释说明这是针对 Tekken 类 tokenizer 的修复,否则toktrie构建器可能不给特殊 token 打标记); - 最终
TokTrie::from(&info, &token_bytes)+ParserFactory::new_simple(&env)产出Arc<ParserFactory>,缓存在 pipeline metadata 中(metadata.llg_factory)。
文法 → Matcher
llg.rs 的后两个函数 完成约束到匹配器的转换:
pub fn llg_grammar_from_constraint(constraint: &Constraint) -> Result<Option<TopLevelGrammar>> { let grm = match constraint { Constraint::Regex(regex) => TopLevelGrammar::from_regex(regex), Constraint::Lark(lark) => TopLevelGrammar::from_lark(lark.clone()), Constraint::JsonSchema(value) => TopLevelGrammar::from_json_schema(value.clone()), Constraint::Llguidance(value) => value.clone(), Constraint::None => return Ok(None), }; Ok(Some(grm)) } pub fn constraint_from_llg_grammar( factory: &ParserFactory, grm: TopLevelGrammar, ) -> Result<llguidance::Matcher> { let parser = factory.create_parser(grm)?; Ok(llguidance::Matcher::new(Ok(parser))) }也就是说,示例中手写的Constraint::Llguidance(LlguidanceGrammar { grammars: vec![top, schema], .. })被原样克隆为TopLevelGrammar(Constraint::Llguidance分支不做转换),再由ParserFactory编译成解析器,包进llguidance::Matcher。Matcher是有限状态机:每消费一个 token 就步进一次,采样阶段用它查询"当前状态下哪些 token 合法",从而在 logits 上屏蔽非法 token。
每步采样中的挂载点
sequence.rs 中的 SequenceRecognizer 表明每条推理序列携带的识别器只有两种形态:
pub enum SequenceRecognizer { Llguidance(Box<llguidance::Matcher>), None, }在 pipeline/sampling.rs 中,请求带着约束进入解码循环后,会执行crate::pipeline::llg::constraint_from_llg_grammar(factory, grm)并赋值seq.recognizer = SequenceRecognizer::Llguidance(Box::new(matcher));此后每一步采样都会先经过该 Matcher 过滤候选 token。同一套机制还服务于流式工具调用:当检测到模型开始输出工具调用片段时,tool_call_state会动态激活一个"续文法"(continuation grammar)——若构建失败或 llguidance 不可用,源码会以tracing::warn!("Cannot force required tool call: llguidance is unavailable")降级为无约束继续。
同一模式在仓库中的规模化应用
示例里"一个 Lark 外壳文法 +@引用的 JSON Schema 子文法"的组合方式,正是 mistral.rs 内置工具调用解析器共用的骨架。tools/grammar.rs 的build_json_format_grammar与示例结构一一对应:
pub(crate) fn build_json_format_grammar( lark: String, tools: &[Tool], args_key: &str, is_array: bool, ) -> TopLevelGrammar { let top = GrammarWithLexer::from_lark(lark); let schema = json_body_schema(tools, args_key, is_array); let json_body = GrammarWithLexer { name: Some("json_body".to_string()), json_schema: Some(schema), ..Default::default() }; TopLevelGrammar { grammars: vec![top, json_body], max_tokens: None, } }区别仅在于子文法名叫json_body、schema 由工具列表动态生成。该文件末尾的单元测试(qwen_grammar_has_two_grammars、llama_uses_parameters_key、mistral_nemo_is_array等,见 grammar.rs 测试模块)验证了各模型格式(Qwen、Llama、Mistral Nemo、Hunyuan、DeepSeek、Gemma4、Harmony、Liquid、Atem)下外壳文法与子文法的组合形态,可以直接作为阅读时确认文法结构的参照。
相关资源与运行提示
- Rust 示例源码:mistralrs/examples/advanced/llguidance/main.rs;对应文档页由 docs/scripts/render_examples.py 从示例源码自动生成(文档中亦有此说明,修改行为应改示例源码而非文档);
- Python 侧同款示例:examples/python/llguidance.py 与 server 侧 examples/server/llguidance.py,可在 Python API 与服务端场景复用同一套文法思想;
- 库再导出路径:
mistralrs门面 crate 通过 pub use mistralrs_core::llguidance 再导出llguidance,因此示例中use mistralrs::llguidance::api::GrammarWithLexer可用; - 更简单的入口:如果只需要单段文法,可直接用
Constraint::Regex("...".to_string())、Constraint::Lark("start: ...".to_string())或Constraint::JsonSchema(json!(...)),无需手工组装TopLevelGrammar;需要多片段互相引用(如本文的@myobj)时才必须使用Constraint::Llguidance。
小结
mistral.rs 的 llguidance 受限生成把"结构化输出"下沉到了采样层:用户只需提供文法(Lark 外壳 + 命名的 JSON Schema 子文法),运行时经由Constraint::Llguidance→TopLevelGrammar→ParserFactory.create_parser→llguidance::Matcher的链路,在每一步解码时屏蔽非法 token。本文示例展示了该链路的手工完整用法;同一模式也支撑着仓库内所有内置工具调用格式的受限解析,是理解 mistral.rs 结构化输出体系的切入点。
【免费下载链接】mistral.rsFast, flexible LLM inference项目地址: https://gitcode.com/GitHub_Trending/mi/mistral.rs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考