news 2026/9/16 10:25:08

mistral.rs 中的 llguidance 受限生成:用 Lark + JSON Schema 文法强制「推理 + JSON」结构输出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
mistral.rs 中的 llguidance 受限生成:用 Lark + JSON Schema 文法强制「推理 + JSON」结构输出

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枚举、ParserFactorySequenceRecognizer在采样阶段逐 token 生效的底层链路。

什么是 llguidance 受限生成

受限生成(constrained decoding / structured output)的核心思想是:在每一步采样时,只允许"符合某个文法"的 token 通过 logits 屏蔽(masking),从而保证输出在形式上严格合法。mistral.rs 将该能力建立在 llguidance 之上——工作区根Cargo.toml固定使用llguidance = 1.2.0default-features = false,启用lark特性),并搭配toktrie_hf_tokenizers = 1.2.0toktrie = 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:不施加约束。

LlguidanceGrammarllguidance::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组合出一条完整的生成路径:

  1. 顶层 Lark 文法top

    GrammarWithLexer::from_lark(r#"start: "Reasoning: " /.+/ "\nJSON: " @myobj"#.to_string());

    这段 Lark 文法规定了输出的骨架:先输出字面量Reasoning:,然后是任意非空文本(/.+/,推理过程),换行后输出JSON:,最后以@myobj终结。@myobj是 llguidance 的文法引用语法:它指向grammars向量中name == "myobj"的另一个文法片段,解析时会被内联展开。

  2. 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() }

    该片段只声明namejson_schema两个字段(其余字段如lark_grammarregexebnf_grammarDefault),把一份 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], .. })被原样克隆为TopLevelGrammarConstraint::Llguidance分支不做转换),再由ParserFactory编译成解析器,包进llguidance::MatcherMatcher是有限状态机:每消费一个 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_grammarsllama_uses_parameters_keymistral_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::LlguidanceTopLevelGrammarParserFactory.create_parserllguidance::Matcher的链路,在每一步解码时屏蔽非法 token。本文示例展示了该链路的手工完整用法;同一模式也支撑着仓库内所有内置工具调用格式的受限解析,是理解 mistral.rs 结构化输出体系的切入点。

【免费下载链接】mistral.rsFast, flexible LLM inference项目地址: https://gitcode.com/GitHub_Trending/mi/mistral.rs

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

WebUploader分片上传与目录管理在工程日志系统的实践

1. 项目背景与需求解析在建筑工程管理领域&#xff0c;施工日志作为项目全周期的重要记录载体&#xff0c;其数字化管理一直存在三个典型痛点&#xff1a;首先是大型项目产生的日志文件体积庞大&#xff0c;单次上传经常因网络波动失败&#xff1b;其次是不同专业&#xff08;土…

作者头像 李华
网站建设 2026/9/16 10:21:39

微软BitNet:1-bit量化大模型CPU部署实践

1. BitNet&#xff1a;微软推出的轻量化大模型方案BitNet是微软研究院最新推出的一种轻量化大语言模型架构&#xff0c;它的核心创新在于通过1-bit量化技术大幅降低模型计算和存储需求。与传统的32位浮点模型相比&#xff0c;BitNet能在保持相当性能的同时&#xff0c;将模型体…

作者头像 李华
网站建设 2026/9/16 10:20:51

基于STM32的POE温湿度记录仪:SNMP历史数据与审计报表实现

机房运维这行干久了&#xff0c;最难堪的时刻不是设备故障本身&#xff0c;而是故障后给不出过程数据。我有一次处理机房空调停机&#xff0c;等赶到现场时设备已经高温自动关机&#xff0c;但原有监控系统只能告诉我"几点几分超过阈值"&#xff0c;之前三四个小时温…

作者头像 李华
网站建设 2026/9/16 10:20:11

windows 驱动实例分析系列: HidHide 驱动分析 - drivers 篇(一)

HidHide 驱动分析 - drivers 篇&#xff08;一&#xff09;&#xff1a;驱动框架与对象模型 一、目录概述 在 HidHide 的源码结构中&#xff0c;drivers/ 目录实际上与 HidHide/ 目录等同&#xff0c;是内核驱动模块 HidHide.sys 的完整实现所在。该驱动基于 Windows Driver Fr…

作者头像 李华
网站建设 2026/9/16 10:19:55

链表合并算法详解:迭代与递归双解法

1. 链表合并问题概述链表操作是算法面试中的常客&#xff0c;而合并两个有序链表更是基础中的基础。这道题看似简单&#xff0c;却蕴含着链表操作的核心思想。我在面试候选人时发现&#xff0c;能完整写出解法的人不少&#xff0c;但能清晰解释每一步操作意图的却不多。今天我们…

作者头像 李华