文章目录
本文翻译整理自:GBNF Guide
https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md
关于 GBNF
GBNF(GGML BNF)是一种定义 formal grammars 的格式,用于在llama.cpp
约束模型输出。例如,您可以使用它 来强制模型生成有效的JSON,或者只用emojis说话。
examples/main
和examples/server
中以各种方式支持GBNF语法。
背景
Backus-Naur Form(BNF)是用于描述编程语言、文件格式和协议等形式语言语法的符号。GBNF是BNF的扩展,主要添加了一些现代正则表达式之类的功能。
基础
在GBNF中,我们定义了生产规则,指定如何将非终端(规则名称)替换为终端序列(字符,特别是Unicode code points)和其他非终端。生产规则的基本格式为: nonterminal ::= sequence...
.
示例
在深入之前,让我们看看grammars/chess.gbnf
中展示的一些功能,这是一种小型国际象棋符号语法:
# `root` specifies the pattern for the overall output
root ::= (
# it must start with the characters "1. " followed by a sequence
# of characters that match the `move` rule, followed by a space, followed
# by another move, and then a newline
"1. " move " " move "\n"
# it's followed by one or more subsequent moves, numbered with one or two digits
([1-9] [0-9]? ". " move " " move "\n")+
)
# `move` is an abstract representation, which can be a pawn, nonpawn, or castle.
# The `[+#]?` denotes the possibility of checking or mate signs after moves
move ::= (pawn | nonpawn | castle) [+#]?
pawn ::= ...
nonpawn ::= ...
castle ::= ...
非终端和终端
非终端符号(规则名称)代表终端和其他非终端的模式。它们必须是虚线小写单词,如move
、castle
或check-mate
。
终端是实际字符(代码点)。它们可以指定为序列,如"1"
或"O-O"
或范围,如[1-9]
或[NBKQR]
。
字符和字符范围
终端支持所有Unicode。Unicode字符可以直接在语法中指定,例如hiragana ::= [ぁ-ゟ]
,或者使用转义:8位(\xXX
)、16位(\uXXXX
)或32位(\UXXXXXXXX
)。
字符范围可以用^
否定:
single-line ::= [^\n]+ "\n"`
序列和替代品
序列中符号的顺序很重要。例如,在"1. " move " " move "\n"
中,"1. "
必须在第一个move
之前,等等。
用|
表示的替代项给出了不同的可接受序列。例如,在move ::= pawn | nonpawn | castle
中,move
可以是pawn
移动、nonpawn
移动或castle
。
括号()
可用于对序列进行分组,这允许在更大的规则中 嵌入替代方案,或将重复和可选符号(如下)应用于序列。
重复和可选符号
*
在一个符号或序列之后意味着它可以重复零次或多次(相当于{0,}
)。+
表示符号或序列应该出现一次或多次(等价于{1,}
)。?
使前面的符号或序列成为可选的(相当于{0,1}
)。{m}
重复先例符号或序列m
次{m,}
重复先前的符号或序列至少m
次{m,n}
在m
和n
次之间重复先例符号或序列(包括){0,n}
重复先例符号或序列最多n
次(包括)
评论和换行符
注释可以用#
指定:
# defines optional whitespace
ws ::= [ \t\n]+
允许在规则之间 以及嵌套在括号内的符号或序列之间 使用换行符。
此外,备用标记|
之后的换行符将继续当前规则,即使在括号之外也是如此。
根本规则
在完整语法中,root
规则总是定义语法的起点。换句话说,它指定了整个输出必须匹配的内容。
# a grammar for lists
root ::= ("- " item)+
item ::= [^\n]+ "\n"
后续步骤
本指南提供了一个简短的概述。查看此目录(grammars/
)中的GBNF文件 以获取完整语法的示例。您可以尝试使用:
./llama-cli -m <model> --grammar-file grammars/some-grammar.gbnf -p 'Some prompt'
llama.cpp
还可以提前或在每次请求时 将JSON模式转换为语法,见下文。
故障排除
语法目前有性能陷阱(见#4218)。
高效的可选重复
一个常见的模式是 允许重复一个模式x
多达N次。
虽然语义正确,但语法x? x? x?.... x?
(有N个重复)可能会导致极慢的采样。
相反,您可以编写x{0,N}
(或(x (x (x ... (x)?...)?)?)?
在早期 llama.cpp版本中 w/N-深度嵌套)。
使用GBNF语法
您可以使用GBNF语法:
- 在llama-server的完成端点中,作为
grammar
体字段传递 - 在llama-cli中,传递为
--grammar
&--grammar-file
标志 - 使用llama-gbnf-validator工具,针对字符串测试它们。
JSON模式→GBNF
llama.cpp
支持将 https://json-schema.org/ 的子集转换为GBNF语法:
- 在 llama-server :
- 对于任何完成终结点,作为
json_schema
主体字段传递 - 对于
/chat/completions
端点,在result_format
主体字段内传递(例如{"type", "json_object", "schema": {"items": {}}}
)
- 对于任何完成终结点,作为
- 在llama-cli 中,作为
--json
/-j
标志传递 - 提前转换为语法
- 在CLI中,带有示例/json_schema_to_grammar.py
- 在带有json-schema-to-grammar. mjs的JavaScript中(由服务器的Web UI使用)
查看测试以了解可能支持哪些功能(您还可以在#5978、#6659和#6555中找到使用示例)。
llama-cli \
-hfr bartowski/Phi-3-medium-128k-instruct-GGUF \
-hff Phi-3-medium-128k-instruct-Q8_0.gguf \
-j '{
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
}
},
"required": ["name", "age"],
"additionalProperties": false
},
"minItems": 10,
"maxItems": 100
}' \
-p 'Generate a {name, age}[] JSON array with famous actors of all ages.'
显示语法
您可以在命令行中使用以下命令转换任何架构:
examples/json_schema_to_grammar.py name-age-schema.json
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
item ::= "{" space item-name-kv "," space item-age-kv "}" space
item-age ::= ([0-9] | ([1-8] [0-9] | [9] [0-9]) | "1" ([0-4] [0-9] | [5] "0")) space
item-age-kv ::= "\"age\"" space ":" space item-age
item-name ::= "\"" char{1,100} "\"" space
item-name-kv ::= "\"name\"" space ":" space item-name
root ::= "[" space item ("," space item){9,99} "]" space
space ::= | " " | "\n" [ \t]{0,20}
以下也是已知限制的列表(欢迎贡献):
additionalProperties
默认为false
(产生更快的语法+减少幻觉)。"additionalProperties": true
可能生成包含未转义换行符的键。- 不支持的功能会被静默跳过。目前建议使用命令行Python转换器(见上文)来查看任何警告,并检查生成的语法/测试它w/llama-gbnf-validator。
- 不能在同一类型中混合
properties
w/anyOf
/oneOf
(#7703) - prefix Items已损坏(但items有效)
minimum
,exclusiveMinimum
,maximum
,exclusiveMaximum
:目前仅支持"type": "integer"
,不支持number
- 嵌套的
$ref
坏了(#8073) - 模式 必须以
^
开头,以$
结尾 - C++版本不支持远程
$ref
(Python和JavaScript版本获取https参考) string
格式缺少uri
,email
- 无
patternProperties
以及不太可能实现的其他不受支持的功能的非详尽列表(难以和/或太慢而无法支持无状态语法):
uniqueItems
contains
/minContains
$anchor
(参见取消引用)not
- 条件
if
/then
/else
/dependentSchemas
关于additionalProperties
警告:JSON模式规范声明object
默认接受附加属性。 由于这是缓慢的,似乎容易出现幻觉,我们默认没有额外的属性。 您可以在任何对象的架构中设置"additionalProperties": true
以显式允许附加属性。
如果您使用Pydantic生成模式,您可以使用每个模型类的extra
配置启用其他属性:
# pip install pydantic
import json
from typing import Annotated, List
from pydantic import BaseModel, Extra, Field
class QAPair(BaseModel):
class Config:
extra = 'allow' # triggers additionalProperties: true in the JSON schema
question: str
concise_answer: str
justification: str
class Summary(BaseModel):
class Config:
extra = 'allow'
key_facts: List[Annotated[str, Field(pattern='- .{5,}')]]
question_answers: List[Annotated[List[QAPair], Field(min_items=5)]]
print(json.dumps(Summary.model_json_schema(), indent=2))
显示 JSON schema & grammar
{
"$defs": {
"QAPair": {
"additionalProperties": true,
"properties": {
"question": {
"title": "Question",
"type": "string"
},
"concise_answer": {
"title": "Concise Answer",
"type": "string"
},
"justification": {
"title": "Justification",
"type": "string"
}
},
"required": [
"question",
"concise_answer",
"justification"
],
"title": "QAPair",
"type": "object"
}
},
"additionalProperties": true,
"properties": {
"key_facts": {
"items": {
"pattern": "^- .{5,}$",
"type": "string"
},
"title": "Key Facts",
"type": "array"
},
"question_answers": {
"items": {
"items": {
"$ref": "#/$defs/QAPair"
},
"minItems": 5,
"type": "array"
},
"title": "Question Answers",
"type": "array"
}
},
"required": [
"key_facts",
"question_answers"
],
"title": "Summary",
"type": "object"
}
QAPair ::= "{" space QAPair-question-kv "," space QAPair-concise-answer-kv "," space QAPair-justification-kv ( "," space ( QAPair-additional-kv ( "," space QAPair-additional-kv )* ) )? "}" space
QAPair-additional-k ::= ["] ( [c] ([o] ([n] ([c] ([i] ([s] ([e] ([_] ([a] ([n] ([s] ([w] ([e] ([r] char+ | [^"r] char*) | [^"e] char*) | [^"w] char*) | [^"s] char*) | [^"n] char*) | [^"a] char*) | [^"_] char*) | [^"e] char*) | [^"s] char*) | [^"i] char*) | [^"c] char*) | [^"n] char*) | [^"o] char*) | [j] ([u] ([s] ([t] ([i] ([f] ([i] ([c] ([a] ([t] ([i] ([o] ([n] char+ | [^"n] char*) | [^"o] char*) | [^"i] char*) | [^"t] char*) | [^"a] char*) | [^"c] char*) | [^"i] char*) | [^"f] char*) | [^"i] char*) | [^"t] char*) | [^"s] char*) | [^"u] char*) | [q] ([u] ([e] ([s] ([t] ([i] ([o] ([n] char+ | [^"n] char*) | [^"o] char*) | [^"i] char*) | [^"t] char*) | [^"s] char*) | [^"e] char*) | [^"u] char*) | [^"cjq] char* )? ["] space
QAPair-additional-kv ::= QAPair-additional-k ":" space value
QAPair-concise-answer-kv ::= "\"concise_answer\"" space ":" space string
QAPair-justification-kv ::= "\"justification\"" space ":" space string
QAPair-question-kv ::= "\"question\"" space ":" space string
additional-k ::= ["] ( [k] ([e] ([y] ([_] ([f] ([a] ([c] ([t] ([s] char+ | [^"s] char*) | [^"t] char*) | [^"c] char*) | [^"a] char*) | [^"f] char*) | [^"_] char*) | [^"y] char*) | [^"e] char*) | [q] ([u] ([e] ([s] ([t] ([i] ([o] ([n] ([_] ([a] ([n] ([s] ([w] ([e] ([r] ([s] char+ | [^"s] char*) | [^"r] char*) | [^"e] char*) | [^"w] char*) | [^"s] char*) | [^"n] char*) | [^"a] char*) | [^"_] char*) | [^"n] char*) | [^"o] char*) | [^"i] char*) | [^"t] char*) | [^"s] char*) | [^"e] char*) | [^"u] char*) | [^"kq] char* )? ["] space
additional-kv ::= additional-k ":" space value
array ::= "[" space ( value ("," space value)* )? "]" space
boolean ::= ("true" | "false") space
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
decimal-part ::= [0-9]{1,16}
dot ::= [^\x0A\x0D]
integral-part ::= [0] | [1-9] [0-9]{0,15}
key-facts ::= "[" space (key-facts-item ("," space key-facts-item)*)? "]" space
key-facts-item ::= "\"" "- " key-facts-item-1{5,} "\"" space
key-facts-item-1 ::= dot
key-facts-kv ::= "\"key_facts\"" space ":" space key-facts
null ::= "null" space
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? "}" space
question-answers ::= "[" space (question-answers-item ("," space question-answers-item)*)? "]" space
question-answers-item ::= "[" space question-answers-item-item ("," space question-answers-item-item){4,} "]" space
question-answers-item-item ::= QAPair
question-answers-kv ::= "\"question_answers\"" space ":" space question-answers
root ::= "{" space key-facts-kv "," space question-answers-kv ( "," space ( additional-kv ( "," space additional-kv )* ) )? "}" space
space ::= | " " | "\n" [ \t]{0,20}
string ::= "\"" char* "\"" space
value ::= object | array | string | number | boolean | null
如果您使用Zod,您可以让您的对象显式允许额外的属性w/nonstrict()
/passthrough()
(或显式不允许额外的道具w/z.object(...).strict()
或z.strictObject(...)
)但请注意, zod-to-json-schema 目前总是设置"additionalProperties": false
无论如何。
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const Foo = z.object({
age: z.number().positive(),
email: z.string().email(),
}).strict();
console.log(zodToJsonSchema(Foo));
Show JSON schema & grammar
{
"type": "object",
"properties": {
"age": {
"type": "number",
"exclusiveMinimum": 0
},
"email": {
"type": "string",
"format": "email"
}
},
"required": [
"age",
"email"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
age-kv ::= "\"age\"" space ":" space number
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
decimal-part ::= [0-9]{1,16}
email-kv ::= "\"email\"" space ":" space string
integral-part ::= [0] | [1-9] [0-9]{0,15}
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
root ::= "{" space age-kv "," space email-kv "}" space
space ::= | " " | "\n" [ \t]{0,20}
string ::= "\"" char* "\"" space
伊织 + NMT
2024-07-13(六)