GBNF Guide


本文翻译整理自:GBNF Guide
https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md


关于 GBNF

GBNF(GGML BNF)是一种定义 formal grammars 的格式,用于在llama.cpp 约束模型输出。例如,您可以使用它 来强制模型生成有效的JSON,或者只用emojis说话。

examples/mainexamples/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 ::= ...

非终端和终端

非终端符号(规则名称)代表终端和其他非终端的模式。它们必须是虚线小写单词,如movecastlecheck-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}mn次之间重复先例符号或序列(包括)
  • {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语法:


JSON模式→GBNF

llama.cpp支持将 https://json-schema.org/ 的子集转换为GBNF语法:


查看测试以了解可能支持哪些功能(您还可以在#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
  • 不能在同一类型中混合propertiesw/anyOf/oneOf#7703
  • prefix Items已损坏(但items有效)
  • minimumexclusiveMinimummaximumexclusiveMaximum:目前仅支持"type": "integer",不支持number
  • 嵌套的$ref坏了(#8073
  • 模式 必须以^开头,以$ 结尾
  • C++版本不支持远程$ref(Python和JavaScript版本获取https参考)
  • string 格式缺少uriemail
  • patternProperties

以及不太可能实现的其他不受支持的功能的非详尽列表(难以和/或太慢而无法支持无状态语法):


关于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(六)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程乐园

请我喝杯伯爵奶茶~!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值