LocalAI 受限生成实战:用 BNF 语法(grammar)精确约束 LLM 输出格式
【免费下载链接】LocalAILocalAI is the open-source AI engine. Run any model - LLMs, vision, voice, image, video - on any hardware. No GPU required.项目地址: https://gitcode.com/GitHub_Trending/lo/LocalAI
LocalAI 的chat端点支持grammar参数,允许以 Backus-Naur Form(BNF)描述文法来"锁死"大语言模型的输出格式——无论是 JSON、YAML,还是只允许 "yes"/"no" 二选一。读完本文,你将掌握如何在请求中传入自定义 BNF 文法、理解文法在 LocalAI 源码中从 HTTP 请求到 llama.cpp 后端的完整传递链路,并了解response_format、函数调用等功能如何与受限生成协同工作。
一、功能概述:grammar 参数与 BNF 文法
chat端点支持grammar请求字段,其值是一段 BNF 文法。该特性使 LLM 只能生成严格符合用户自定义 schema 的输出,例如JSON、YAML或任何其他可以用 BNF 定义的格式。
在请求结构中,该字段的定义位于 OpenAI 兼容 Schema:
// A grammar to constrain the LLM output Grammar string `json:"grammar,omitempty" yaml:"grammar"` JSONFunctionGrammarObject *functions.JSONFunctionStructure `json:"grammar_json_functions,omitempty" yaml:"grammar_json_functions"`可以看到grammar是一个可选的字符串字段(omitempty),因此不会出现在转发给上游严格 Provider 的请求体中——这与 LocalAI 对 LocalAI 私有字段"零值不泄漏"的设计一致。
兼容性说明:该特性目前仅由使用 llama.cpp 后端的模型支持(受限于底层解码器对文法约束的实现)。完整兼容模型列表可参见 模型兼容性参考页。该能力源自 llama.cpp 项目对 grammar/logits 过滤的支持。
二、文法语法速览:BNF 规则支持的语法元素
对于更复杂的文法,可以定义多行 BNF 规则。语法解析支持以下元素:
| 语法元素 | 符号 | 说明 |
|---|---|---|
| 选择(Alternation) | \| | 在多个候选中任选其一 |
| 重复 | *、+ | *表示零次或多次,+表示一次或多次 |
| 可选 | ? | 元素可出现零次或一次 |
| 字符类 | [a-z] | 匹配某一类字符 |
| 字符串字面量 | "text" | 精确匹配指定文本 |
| 规则定义 | rule ::= ... | 命名规则,root为入口规则 |
文法以root规则为起点;请求体中可以通过\n换行符拼接多条规则。
三、实战示例(完整可复制)
示例 1:二值响应约束(yes / no)
以下示例将模型输出严格限制为 "yes" 或 "no",适用于需要严格控制回答格式的场景:
curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{ "model": "gpt-4", "messages": [{"role": "user", "content": "Do you like apples?"}], "grammar": "root ::= (\"yes\" | \"no\")" }'grammar参数被设置为 "yes"/"no" 的简单选择,无论上下文如何,模型响应都只能是这两个选项之一。
示例 2:强制 JSON 输出
也可以用文法强制 JSON 输出格式:
curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{ "model": "gpt-4", "messages": [{"role": "user", "content": "Generate a person object with name and age"}], "grammar": "root ::= \"{\" \"\\\"name\\\":\" string \",\\\"age\\\":\" number \"}\"\nstring ::= \"\\\"\" [a-z]+ \"\\\"\"\nnumber ::= [0-9]+" }'注意请求体中\\\"的转义层级:外层是 JSON 字符串的转义,内层是 BNF 字符串字面量中的引号,最终传给文法解析器的是"\"name\":"这样的字面量规则。
示例 3:强制 YAML 输出
同理,可以用文法强制 YAML 格式(此处为 fruits 列表):
curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{ "model": "gpt-4", "messages": [{"role": "user", "content": "Generate a YAML list of fruits"}], "grammar": "root ::= \"fruits:\" newline (\" - \" string newline)+\nstring ::= [a-z]+\nnewline ::= \"\\n\"" }'三个示例的共同点:root规则定义了输出的"骨架",子规则(string、number、newline)负责可复用的片段。文法只约束输出 token 序列,不改变提示词模板的生成过程。
四、源码纵深:grammar 从请求到后端的完整链路
4.1 入口:中间件读取请求字段
用户传入的grammar字段首先由请求中间件写入运行配置,见 请求中间件:
if input.Grammar != "" { config.Grammar = input.Grammar }4.2 chat 端点:文法的多个来源与优先级
chat 端点 中文法并非只能来自用户手写,实际存在多条生成路径,按请求内容触发:
response_format自动转文法:当请求携带response_format时,端点会将其转换为文法:type: "json_object"→ 直接套用内置的functions.JSONBNF(完整合法 JSON 文法);type: "json_schema"→ 将 schema 包装为JSONFunctionStructure,调用fs.Grammar(...)即时生成 BNF。
- 函数调用(Functions/Tools)自动生成文法:当请求需要走函数调用且未禁用文法(
NoGrammar为 false)或处于strict模式时,端点会把函数列表转换为 JSON 结构并调用jsStruct.Grammar(...)生成约束文法;非 strict 模式下还会追加一个answer(可通过NoActionFunctionName配置改名)"无动作"函数,允许模型直接回答而不调用工具。 grammar_json_functions字段:请求可直接携带 LocalAI 私有的JSONFunctionGrammarObject(一个 JSON Schema 结构),端点同样调用其Grammar()方法生成 BNF。
上述逻辑执行完毕后,config.Grammar被赋值,并在调试日志中输出(xlog.Debug("Grammar", ...)),便于排查文法是否生效。
4.3 后端投递:透传给 llama.cpp
运行配置中的Grammar最终被打包进发给后端的PredictOptions,见 后端选项组装:
pbOpts := &pb.PredictOptions{ ... Grammar: c.Grammar, ... }由于 llama.cpp 后端以独立 gRPC worker 进程运行,文法字符串经 backend.proto 定义的PredictOptions协议传递到 C++ 侧,由 llama.cpp 的采样器在逐 token 解码时进行 logits 过滤,从而保证每个输出的 token 都满足文法约束。这也解释了该特性的兼容边界:只有实现了 grammar 解码路径的 llama.cpp 系后端可用,其他后端会忽略该字段。
4.4 JSON Schema → BNF 的内部实现
自动路径中文法生成能力位于 pkg/functions/grammars 目录。其基础构件是 BNF 原始规则表:
PRIMITIVE_RULES = map[string]string{ "boolean": `("true" | "false") space`, "number": `("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space`, "integer": `("-"? ([0-9] | [1-9] [0-9]*)) space`, "string": `"\"" ([^"\\] | "\\" (["\\/bfnrt] | "u" ...))* "\"" space`, "null": `"null" space`, }这解释了为什么示例 2 中的string、number规则与手写版本行为不同:内置转换会生成处理转义、负数、指数形式的严格规则;源码注释中还特别说明,freestring规则若不允许\"和\\会产生歧义、实测效果显著变差——这类细节正是手写文法时容易踩坑的地方。
五、受限生成在 LocalAI 其他功能中的体现
grammar并非孤立特性,它还是多个 OpenAI 兼容能力的底层机制:
- OpenAI Functions / 函数调用:OpenAI Functions 文档 描述的 structured outputs 正是通过上文的"函数列表 → JSON 结构 → BNF 文法"路径实现的,模型输出被约束为合法的函数调用 JSON。
- Moderation 端点:
/v1/moderations的实现会先生成对应 moderation 响应 schema 的文法再约束输出(见 moderations 端点 处cfg.Grammar = grammar的赋值),保证返回内容严格符合 moderation schema。 - Completion 端点:
/v1/completions同样读取grammar字段,并在response_format为 JSON 时套用JSONBNF(见 completion 端点)。
从源码结构看,文法约束在模板层面还有配合逻辑:模板求值器 中config.Grammar != ""会参与函数调用模板的分支判断,即"是否携带文法"会影响最终 prompt 模板的形态。
六、使用限制与注意事项
- 后端限制:仅 llama.cpp 后端模型支持
grammar约束,使用前请确认模型的 backend 字段; - 文法正确性自负:手写 BNF 若有歧义或无法被解析,服务端会记录
Failed generating grammar错误日志并可能退化为无约束输出,建议先用简单文法验证再复杂化; - 与
response_format、functions的交互:三者都可能最终落到config.Grammar上,若同时传入grammar与函数定义,按 chat 端点的分支逻辑,函数生成的文法可能覆盖此前由response_format设置的值,使用时应避免混用; - 转义层级:curl 请求体中
\n、\"需要按"JSON 字符串 → BNF 字面量"两层转义书写,是实际调试中最常见的报错来源。
相关功能
- OpenAI Functions — 函数调用与结构化输出
- 文本生成 — 通用文本生成能力
- Moderation — OpenAI 兼容的安全分类,其响应被约束到 moderation schema
- 模型兼容性参考 — 各后端能力对照表
【免费下载链接】LocalAILocalAI is the open-source AI engine. Run any model - LLMs, vision, voice, image, video - on any hardware. No GPU required.项目地址: https://gitcode.com/GitHub_Trending/lo/LocalAI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考