如果你经常使用大语言模型(LLM)来完成各类任务——例如编写代码、生成结构化数据或调用API——那么你大概率已经熟悉json_mode或“函数调用”这类功能。这些功能的底层其实解决了一个非常关键的问题:让LLM的输出完全按照我们指定的格式进行。尤其是JSON这种机器可读的结构化数据,一旦格式出现错乱,后续的解析就会变成灾难。

LLM生成内容的核心机制
众所周知,当前大语言模型生成内容的过程本质上是基于概率模型的——它会逐步预测每一个token。每个token的选择,最终由一个概率分布来决定。换句话说,下一个token是根据上下文“推测”出来的。
这种机制赋予了LLM极大的灵活性,使其能够生成多种多样的文本。然而,灵活性也带来了代价:当我们期望得到某种特定格式的输出(例如JSON)时,该怎么办?
通过概率分布约束实现强制格式
除了让LLM自由发挥,我们还可以通过技术手段来限制其行为。今天重点介绍一种非常实用的方法——人为干预概率分布,强制LLM生成符合特定语法规则的内容。
简单来说,就是将那些不符合目标格式的token的概率直接置为0,从而确保LLM在生成过程中不会偏离预期路线。这种做法在实际应用中已经非常普遍,例如json_mode、结构化输出(structured output)以及函数调用(function calling)等。
GitHub上的开源项目llama.cpp提供了一个名为grammars/json.gbnf的文件(GitHub 仓库地址:llama.cpp 的 JSON 语法规则定义),专门用于定义JSON语法的规则。
root ::= object
value ::= object | array | string | number | ("true" | "false" | "null") ws
object ::=
"{" ws (
string ":" ws value
("," ws string ":" ws value)*
)? "}" ws
array ::=
"[" ws (
value
("," ws value)*
)? "]" ws
string ::=
"\"" (
[^"\x7F\x00-\x1F] |
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F]{4}) # escapes
)* "\"" ws
number ::= ("-"? ([0-9] | [1-9] [0-9]{0,15})) ("." [0-9]+)? ([eE] [-+]? [0-9] [1-9]{0,15})? ws
# Optional space: by convention, applied in this grammar after literal chars when allowed
ws ::= | " " | "\n" [ \t]{0,20}
通过这种方式,LLM在生成JSON格式的响应时,能够严格遵循这些语法规则,不会出现任何偏差。
如果你想利用自定义的JSON结构,可以参考下面的代码示例:
from llama_cpp import Llama, LlamaGrammar
# Define your GBNF grammar
grammar_string = r"""
root ::= "{" pair ("," pair)* "}"
pair ::= string ":" list
list ::= "[" value ("," value)* "]"
value ::= string | number
string ::= "\"" [a-zA-Z0-9_]+ "\""
number ::= [0-9]+
"""
# Create a LlamaGrammar object
my_grammar = LlamaGrammar.from_string(grammar_string)
# Initialize Llama model
llm = Llama(model_path="C:/Users/sride/PycharmProjects/gbnf_implemen/llama-2-7b.Q4_K_S.gguf")
# Generate constrained output
prompt = "Give me list of fruits"
output = llm(prompt, max_tokens=100, temperature=0.7, grammar=my_grammar)
print(output['choices'][0]['text'])