AI 基础入门 · 第21课
结构化输出:为什么自由文本不能当程序读,怎么让模型吐 JSON
你在聊天框里跟 AI 聊得很欢,但程序员最怕的一幕是:让模型"返回一个 JSON",它却写成 好的,结果如下:{...},或者少个逗号、多句解释,你的 JSON.parse() 直接崩。这一课讲清楚为什么自由文本不能直接当程序数据用,以及业界的三板斧:JSON 输出模式(json mode)、JSON Schema 约束、function calling 的 json mode,最后讲 llama.cpp 里 --grammar 用 EBNF 文法到底是怎么从根上把模型"逼"成只吐合法 JSON 的。这是 Agent(第10课)能调用工具的地基。
① 小白第一课怎么学(4 步走,约 65 分钟)
这一课偏工程,重点是理解"约束从哪来",不是背语法。
1先理解痛点(10 分钟)
读②③:自由文本为啥不能直接 parse。
2三板斧对照(20 分钟)
读④:json mode / JSON Schema / function calling。
3看懂 grammar 原理(15 分钟)
读⑤:EBNF 在采样时干了什么。
4刷题自测(20 分钟)
做⑦⑩,错题回⑥看易错点。
本课小目标学完你要能:① 说清为什么 prompt 里写"请输出 JSON"不可靠;② 区分 json mode 和 JSON Schema;③ 解释 llama.cpp 的 --grammar 是怎么在采样阶段把非法 token 概率清零的。
② 一图看懂:从"自由文本"到"可解析 JSON"
读法:左边是"裸奔"——模型爱说啥说啥,后端 parse 必崩;中间是三层约束,越往下越严;右边是结果——输出变成程序能直接消费的数据。关键洞察在最下面:约束不是事后修字符串,而是在采样时就挡住非法 token。
③ 本质直觉:自由文本是"人话",程序要的是"表格"
为什么不能靠 prompt 里写一句"请输出 JSON"?因为大模型本质是在"预测下一个最可能的 token"(第12课)。它没有"必须遵守格式"的硬约束,只是被你提醒了"大概该输出 JSON"。心情好就规规矩矩,心情不好就加一句"以下是结果:"、或者在末尾补一句"希望对你有帮助"、或者把引号打成中文引号 “”——这些对人来说无所谓,对 JSON.parse() 来说全是致命错误。
核心矛盾
人话 = 连续、自由、可带修饰;程序数据 = 严格、固定、零容错。
让"说人话的模型"直接吐"零容错的数据",中间必须加一层硬约束。
那这层硬约束加在哪?回忆第12课:模型每生成一个 token,都会先算出一整张"下一个词的概率表"(logits),再按温度采样。结构化输出的所有技巧,本质都是同一件事——在采样前,把"不符合格式的那些 token"的概率强行改成 0。
· 比如现在语法上必须是个开引号 ",那就把整张概率表里所有不是 " 的 token 全部清零,模型只能选 ";写完字符串该闭引号了,就只允许 ";接下来该是冒号了,就只允许 :。这样一步步"牵着走",出来的必然是合法 JSON。
一句话本质结构化输出不是"求模型好好写",而是"在采样器门口设了个安检":不符合格式的 token 一律不准出门。模型的自由发挥,被限定在格式允许的格子里。
④ 完整体系:三板斧 + llama.cpp grammar
四种约束方式对比(由弱到强)
| 方式 | 怎么触发 | 约束到什么程度 | 可靠性 |
| Prompt 提示 | 在 prompt 里写"请只输出 JSON" | 无硬约束,全靠模型自觉 | 低,经常翻车 |
| JSON mode(json_object) | 请求体加 "response_format":{"type":"json_object"} | 只保证输出是合法 JSON,字段不固定 | 中高 |
| JSON Schema | response_format 里塞 json_schema,规定字段名/类型/枚举 | 连有哪些字段、什么类型、取值范围都卡死 | 高 |
| function calling 的 json mode | 传 tools 数组,模型决定调哪个函数、填什么参数 | 函数名 + 参数结构都由 Schema 限定 | 高(Agent 核心) |
| llama.cpp --grammar(EBNF) | 命令行 --grammar grammar.gbnf | 用文法精确描述任何输出结构,不止 JSON | 最高,本地引擎级 |
JSON Schema 长什么样
{
"type": "object",
"properties": {
"city": { "type": "string" },
"temp": { "type": "number" },
"weather": { "type": "string", "enum": ["晴","阴","雨"] }
},
"required": ["city","temp"]
}
这段 Schema 等于告诉模型:你必须输出一个对象,里面要有 city(字符串)和 temp(数字),weather 只能是晴/阴/雨三个之一。后端收到后直接 JSON.parse 取字段,零容错。
function calling 和普通 json mode 的关系
Function calling 本质就是更具体的结构化输出:你不只要求"吐 JSON",而是要求"从给定的几个函数里选一个,并按它的参数 Schema 填好 JSON"。模型吐出的不是给人看的话,而是一段"调哪个函数、参数是什么"的结构化指令——这正是第10课 Agent 能调工具的原理。
json mode ≠ JSON Schema很多人混淆。json mode 只保证"大括号配对、逗号正确",里面放什么字段它随便编;JSON Schema 才把字段名、类型、枚举值都钉死。要让后端稳定取 data.temp,必须用 Schema,光开 json mode 不够。
⑤ llama.cpp 的 --grammar / EBNF 原理
什么是 EBNF 文法约束
EBNF(扩展巴科斯范式)是一种用文字描述"合法字符串长什么样"的形式语言。
llama.cpp 的 --grammar 参数接受一个 .gbnf 文法文件,它告诉引擎:接下来的输出必须符合这套语法。例如一个最简单的"城市+温度"文法:先写一个 {,然后 "city",冒号,一段字符串,逗号,"temp",冒号,一个数字……
它在采样时具体干了什么
关键就在第12课那张 logits 概率表。
引擎每算完一步 logits,文法解析器就看:"按语法,下一个位置允许出现哪些 token?"然后把所有不允许的 token 的 logit 改成 -∞(概率 0),再喂给采样器。于是模型在"被允许的候选"里选,选完文法推进到下一个状态,如此往复。这不是事后修补,是生成的同时就被文法牵着走。
为什么本地引擎要自己实现 grammar
云端 API 的 json mode 是服务端黑盒帮你做的。
但你本地用 llama.cpp 跑 GGUF(第15、20课)时,没有云端帮你约束,所以 llama.cpp 自己内置了一套 GBNF 文法引擎,用 --grammar 参数手动挂上。OpenAI 兼容的 response_format 在 llama.cpp server 里也会被自动转成等价文法。
# 一个极简 JSON 文法片段(llama.cpp .gbnf 风格示意)
root ::= "{" ws "\"city\"" ws ":" ws string ws ","
ws "\"temp\"" ws ":" ws number "}" ws
string ::= "\"" [^"]* "\""
number ::= [0-9]+ ("." [0-9]+)?
ws ::= [ \t\n]*
应用场景清单① 让模型从简历里抽出固定字段入库;② 让模型输出分类标签供程序路由;③ function calling 让 Agent 调天气/日历/搜索工具;④ 强制输出 SQL、表格、评分(1~5 分)这类机器要直接消费的结构。
⑥ 高频错误诊断(4 条)
错误 1:只在 prompt 里写"请输出 JSON"就上线这是最常见的坑。没有硬约束时,模型偶尔会加解释文字、用中文引号、漏逗号,JSON.parse 在生产环境随机崩溃。必须开 json mode / JSON Schema。
错误 2:以为开了 json mode 字段就固定了json mode 只保证"是个合法 JSON",字段名模型仍可能乱编。要固定字段必须传 JSON Schema,写明 required 和类型。
错误 3:事后用正则去"修"模型输出很多人写一堆正则想把 好的:{...} 里的大括号抠出来。这很脆弱,遇到嵌套括号就错。正确做法是从采样端约束,而不是事后擦屁股。
错误 4:把 grammar 当成"改模型权重"grammar 不改变模型参数,也不改变它的知识,只是在采样时过滤候选 token。模型仍然可能"在合法 JSON 里填错误的内容"(比如把温度写成 -999)——格式对了,不代表内容对了。
⑦ 考点真题演练(4 题)
考点分布
| 考法 | 出题形式 | 应对 |
| 约束位置 | 问约束加在哪一步 | 采样前清零非法 token |
| json mode vs Schema | 问哪个固定字段 | JSON Schema |
| function calling | 问它本质是什么 | 更具体的结构化输出 |
| grammar 原理 | 问 --grammar 怎么生效 | 按 EBNF 过滤 logits |
真题基础1. 为什么只在 prompt 里写"请输出 JSON"不可靠?
真题中档2. 想让模型固定输出 city、temp 两个字段且 temp 必须是数字,应该用?
真题中档3. llama.cpp 的 --grammar 约束,本质是在哪一步生效?
真题拔高4. 关于 function calling,正确的理解是?
⑧ 必背知识点卡
核心矛盾:人话自由 vs 程序数据零容错 中间必须加硬约束
约束本质:采样前把非法 token 的 logits 清零 边生成边挡,不是事后修
json mode:response_format type=json_object,只保证合法 JSON 字段不固定
JSON Schema:规定字段名/类型/枚举/required 字段也钉死
function calling:更具体的结构化输出,吐"调哪个函数+参数" Agent 地基
llama.cpp grammar:--grammar x.gbnf,EBNF 文法逐 token 约束 本地引擎级
易错:格式对 ≠ 内容对 grammar 不保证事实正确
⑨ 应用输出:写一个"抽天气"的结构化请求
实战场景:让模型把"明天南京大概28度,有点小雨"抽成固定 JSON
① 不要只写"请输出 JSON"——加 response_format,并附上 JSON Schema,要求 city、temp(数字)、weather(枚举晴/阴/雨)。
② 请求体:{"model":"...","messages":[...],"response_format":{"type":"json_schema","json_schema":{"schema":{...}}}}。
③ 后端:直接 JSON.parse(content),取 data.city、data.temp 入库,不用写正则清洗。
④ 本地跑 llama.cpp:用 --grammar weather.gbnf 挂上等价文法,离线也能拿到同样保证。
口述全链路"自由文本人能读、程序读不了,因为它有废话还可能缺逗号。解法不是事后用正则擦屁股,而是在采样那一步,按文法把非法 token 的概率清零:该写引号就只许引号,该写数字就只许数字。json mode 只保证合法 JSON,JSON Schema 连字段都钉死,function calling 则进一步规定调哪个函数——这就是 Agent 能可靠调用工具的地基。"
⑩ 分层练习 16 题(基础 6 + 中档 5 + 拔高 5)
▍基础 6 题
基础1为什么大模型的自由文本不能直接当程序数据用?
因为它自由、可能带废话、标点不规范(中文引号、漏逗号),而程序 JSON.parse 零容错,遇到就崩。
基础2json mode 的触发参数长什么样?
请求体加 "response_format":{"type":"json_object"}。
基础3json mode 保证了什么、没保证什么?
保证输出是语法合法的 JSON;没保证有哪些字段、字段类型——那是 JSON Schema 的事。
基础4JSON Schema 里
required 字段干嘛用?
列出哪些字段必须出现,缺了就不合法,避免模型漏掉关键数据。
基础5llama.cpp 挂文法用哪个命令行参数?
--grammar xxx.gbnf。
基础6function calling 吐出的内容由谁真正执行?
模型只吐"调哪个函数、参数是什么"的结构化 JSON,真正执行函数的是你自己的后端代码,模型不碰你的系统。
▍中档 5 题
中档7结构化约束为什么说是"在采样阶段"生效?
因为模型每步都要从 logits 采样下一个 token。约束就是在采样前,按文法把当前位置不允许的 token 的 logits 改成 -∞,让模型只能在合法候选里选。
中档8为什么不推荐用正则事后"抠" JSON?
模型输出可能嵌套括号、带解释、标点杂糅,正则很难覆盖所有情况,脆弱且误判率高。从采样端约束才能从根上保证合法。
中档9json mode 和 JSON Schema 到底差在哪?
json mode 只约束"是不是合法 JSON";JSON Schema 进一步约束"必须有哪些字段、每个字段什么类型、枚举值是什么"。要后端稳定取字段必须用 Schema。
中档10EBNF 是什么?和 grammar 什么关系?
EBNF(扩展巴科斯范式)是描述合法字符串结构的形式语言;llama.cpp 的 .gbnf 文法文件就是用 EBNF 写的,引擎据此逐 token 约束输出。
中档11"开了 grammar 模型就不会输出错误信息了"对吗?
不对。grammar 只保证格式合法(是个结构正确的 JSON),不保证内容正确(温度可能编错)。格式对和事实对是两回事。
▍拔高 5 题
拔高12function calling 和普通结构化输出的关系?为什么说它是 Agent 的地基?
function calling 是结构化输出的特化:你传一组工具定义(名字+参数 Schema),模型按 Schema 吐出该调哪个工具、参数填什么。后端拿到这个结构化指令去真正执行,再把结果喂回模型——循环起来就是 Agent(第10课)。
拔高13为什么云端 API 帮你做了 json mode,本地 llama.cpp 还要自己实现 grammar?
云端是服务端黑盒,它在自己的推理栈里做了 logits 过滤;你本地跑 GGUF 时没有这个黑盒,所以 llama.cpp 内置 GBNF 文法引擎,用 --grammar 手动挂上,在自己的采样循环里做同样的过滤。
拔高14grammar 约束会不会降低输出质量或速度?
它只是把非法候选清零、重新归一化概率,计算开销很小;在格式严格的场景下反而省去了重试和清洗,端到端更快。它不限制"在合法范围内选哪个",所以合法内容的质量不受损。
拔高15想让模型输出一个枚举:评分只能是 1~5 的整数,Schema 怎么写?
给 score 字段声明类型为整数并用枚举或范围限制,例如 "score":{"type":"integer","enum":[1,2,3,4,5]};配合 grammar,模型在该位置只能吐出 1~5 之一。
拔高16结合第12课采样,讲清"温度高"和"结构化约束"是否冲突?
不冲突。温度是在"合法候选"集合内调节随机性;结构化约束是先把非法候选剔除。流程是:先按文法清零非法 token → 再在剩余合法 token 上按温度/top_p 采样。温度高只是让它在合法格式内更灵活,不会逃出格式。
⑪ 记忆口诀 + 7 天复习计划
三句口诀
① 自由文本人能读,程序数据零容错。
② 约束不在事后修,采样门口清非法 token。
③ json mode 保合法,Schema 钉字段,grammar 牵着走,function calling 调工具。
| 天 | 任务 | 自检 |
| 第 1 天 | 读②③,理解"采样时清零"原理 | 说清约束加在哪步 |
| 第 2 天 | 背知识点卡 + 基础 1-6 | 基础全对 |
| 第 3 天 | 读④⑤,做中档 7-11 | 区分 json mode/Schema |
| 第 4 天 | 做拔高 12-16 | 讲清 grammar 与采样关系 |
| 第 5 天 | 做⑦真题 4 题 | 限时每题 2 分钟 |
| 第 6-7 天 | 合上书口述三句口诀和四层约束 | 不看资料全说对 |