[控场AI]
· 12 分钟阅读· 6,190 字

一个采样设置,如何让LLM的JSON输出合格率从75%暴跌到4%

一个采样设置,如何让LLM的JSON输出合格率从75%暴跌到4%

一个no_repeat_ngram_size设置让Llama 3.2 1B的JSON合格率从75%跌至4%。

作者在真实浏览器中用Transformers.js和WebGPU测试了三款小模型(SmolLM2 360M、Qwen2.5 0.5B、Llama 3.2 1B),共432次JSON生成。消融实验显示,temperature基本无害,repetition_penalty仅轻微下降,而no_repeat_ngram_size: 3单独启用就把Llama的schema合法率从75%打到4%,因为它把提示词也计入历史,禁止了JSON必需的重复token序列,模型只能绕路写出带空格、单引号或改名的键。四步解析阶梯可把解析率从29%提升到44%,但修不了截断循环和根本没有JSON的回复。示例提示词还导致21/288条回复泄漏示例值。作者建议结构化调用关闭这两个参数、用贪心解码、加schema校验与重试,最优解是约束解码。

一个采样设置,如何让LLM的JSON输出合格率从75%暴跌到4%

如果你正在用开源小模型做结构化输出(比如让模型返回JSON),这篇实测可能会帮你省下几个小时的调试。一位开发者在真实浏览器环境中,用三款小模型跑了432次JSON生成,发现一个常见的防重复设置 no_repeat_ngram_size: 3 会把Llama 3.2 1B的JSON合格率从75%直接打到4%。问题不在模型能力,而在采样器禁止了模型写出schema需要的token。

rss source: LLM JSON output: one setting dropped valid JSON from 75% to 4%

实验设置:432次回复,三个小模型,一个真实浏览器标签页

作者在自建的BEINGS聊天室中测试了三个模型:SmolLM2 360M Instruct、Qwen2.5 0.5B Instruct和Llama 3.2 1B Instruct(q4f16)。全部通过Transformers.js 4.2.0在WebGPU上以4-bit权重运行,和线上产品用的是同一套固定构建。

测试包含三个任务,每个任务8个输入:给聊天消息打上说话人、四种情绪之一和回复;从消息中提取姓名、邮箱和电话;把购物笔记转成带整数数量的物品列表。每个输入都有标准答案,因此可以分别评估三件事:能否解析、是否符合schema、答案是否正确。

对比两种采样配置:一种是聊天室线上用的配置(开启采样,temperature 0.75,top_p 0.9,top_k 40,repetition_penalty 1.12,no_repeat_ngram_size 3),另一种是纯贪心解码(关闭采样,repetition_penalty 1.0,no_repeat_ngram_size 0)。

Transformers.js、WebGPU 与 4-bit 量化

Transformers.js 是 Hugging Face 推出的 JavaScript 移植版推理库,让模型无需后端即可直接在浏览器中运行。它依赖 WebGPU 获得 GPU 加速,WebGPU 是浏览器提供的新一代图形与计算 API,相较 WebGL 更适合通用计算,但仍可能因驱动或显存问题触发「设备丢失」(device lost)。

4-bit 量化把模型权重从 16 位浮点压缩到 4 位整数(如 q4f16 表示权重 4-bit、部分计算用 fp16),可大幅降低显存占用与下载体积,使 1B 级模型能在消费级设备运行。代价是精度损失,可能影响指令遵循与格式稳定性。测试在真实浏览器标签页中进行,更贴近实际部署环境,但也引入了设备丢失等运行时不确定性,作者因此对失败批次做了重跑。

聊天设置下,72次回复无一符合schema

使用完整聊天设置和示例提示词时,三个模型没有一次回复符合schema:0/72。换成贪心解码加同样的提示词,25/72符合,仅Llama一个模型就从0/24提升到18/24。

作者随后在贪心解码基础上逐个打开聊天设置,每次只加一个参数,每组24次回复。结果如下:

添加的设置Llama 3.2 1BSmolLM2 360M
无(贪心)18/24 (75%)7/24 (29%)
仅 temperature: 0.7519/24 (79%)9/24 (38%)
仅 repetition_penalty: 1.1216/24 (67%)9/24 (38%)
仅 no_repeat_ngram_size: 31/24 (4%)0/24 (0%)
全部聊天设置0/240/24

Temperature基本无害,甚至让Llama多对了一次。重复惩罚只造成轻微下降。真正的元凶是n-gram禁令,单独启用就足以摧毁JSON输出。

根因:no_repeat_ngram_size禁止了schema必需的token

no_repeat_ngram_size: 3 的规则是“永远不要产生任何已经出现过的3个token序列”。在Transformers.js的实现中,任何会构成重复3-gram的token,其分数会被设为负无穷,永远无法被选中。

关键在于 input_ids 包含了整个提示词。生成循环从完整提示词开始,新token不断追加。而JSON列表天然包含重复结构,比如 {"name": ..., "qty": ...}, {"name": ..., "qty": ...},同样的token组合反复出现。更糟的是,示例提示词已经写过一遍 "name": " 和 "qty":,这些3-token序列在模型开口之前就被禁掉了。

模型无法拒绝回答,只能选择次优路径——绕路。真实回复中出现了 " qty"(带前导空格)、".name"、单引号包裹的 'name'、用 "quantity" 代替 "qty"、用 "type" 代替 "name"。每一个都是绕过被禁3-gram的最短路径。危险的是,有些绕路后的JSON仍然能被解析,但你的代码读取 item.qty 时只会得到 undefined。

模型并非不擅长JSON,而是采样器禁止了schema要求的精确token,提示词中的示例让情况更糟。

n-gram 与 no_repeat_ngram_size 的机制

n-gram 指文本中连续出现的 n 个 token 序列。no_repeat_ngram_size: 3 意味着解码器在生成每个新 token 时,会检查「当前 token 与之前两个 token 组成的 3-gram」是否在已生成的历史中出现过;若出现过,该 token 的 logit 被置为 -inf,从而被硬性禁止。这与 repetition_penalty 的软性降权不同,是一种不可违反的约束。

问题在于,多数实现(包括 Transformers.js)统计历史时把整个 input_ids 都算进去,而 input_ids 包含提示词本身。因此提示词里出现过的任何 3-gram 都会在生成阶段被永久封禁。对于 JSON 这类高度模板化、键名与标点反复出现的格式,这种封禁直接切断了模型写出合法结构的路径。该参数原本用于抑制长文本生成中的复读循环,适合自由文本,却不适合结构化输出。

重复惩罚也会惩罚你的提示词

repetition_penalty 是更温和的机制,出自CTRL论文(Keskar等人,2019)。它会把已出现token的分数除以惩罚系数。Transformers.js的源码注释明确写道:对于decoder-only模型,考虑的token包括提示词。

这意味着schema中的每个引号、冒号和键名,再次出现的概率都会被轻微压低。1.12的系数是轻推而非禁止,数据也吻合:Llama从18/24降到16/24。但作者仍建议结构化调用时设为1.0,毕竟没有理由惩罚你明确要求的标点符号。

很多应用可能在共享配置中一次性设置了这些参数,所有结构化调用都默默继承了它们。值得在代码库里搜索一下。

解析阶梯能救回43条回复,但救不了全部

即使采样设置正确,小模型仍会把JSON包在散文或代码块里,JSON.parse 会直接抛错。作者设计了一个四步解析阶梯:

  1. 直接解析
  2. 去掉代码围栏后解析
  3. 提取第一个配对的 {...} 或 [...] 后解析
  4. 修复常见语法问题后解析(丢弃 // 注释、给裸键加引号、单引号换双引号、映射Python的True/False/None、去除尾逗号、闭合未关闭的括号)

在288条主运行回复中,严格 JSON.parse 接受了84条(29%),阶梯提升到127条(44%)。对Llama贪心解码加普通提示词的情况,从14/24提升到24/24。

但44%到100%之间的差距才是重点。阶梯能修复JSON的包装方式,却修不了那些根本没包含JSON的回复。

解析失败的真正原因:截断和根本没有JSON

在204条解析失败的回复中,代码围栏和“Here is your JSON:”这类散文只有43条。最大的两个原因无法靠解析器解决:

  • 在token限制处截断(62条):主要不是长答案,而是循环。Qwen反复输出 "jars", "jars", "jars" 直到耗尽token。
  • 完全没有JSON(61条):纯散文、一个“提取联系信息”的Python函数、道歉,或者把输入原样返回。

Qwen2.5 0.5B值得单独说明:96次回复中0次直接解析成功,4次经阶梯成功,0次符合schema。贪心回复大多回显用户消息然后进入循环。作者强调这不一定是Qwen2.5的普遍问题,可能是这个量化构建的问题,但提醒要测试你实际部署的那个文件。

合法JSON不等于正确JSON

符合schema和答案正确是两个数字。Llama最佳配置(贪心+示例)给出18条schema合法回复,但只有16条正确。两类问题:

  • 示例泄漏:被要求提取“Call me back on (555) 018-2299. Thanks, Tom Okafor”的联系方式时,Llama返回了示例中的名字和邮箱,配上了Tom的电话。在288条示例提示词回复中,21条包含从示例中抄来的值。普通提示词下144条中只有1条。
  • 类型漂移:"qty": "1" 作为字符串,缺失的 "email" 键而不是 null。JSON Schema验证器能抓住这些,JSON.parse 永远不会。

示例提示词对Llama仍然是更好的选择(16正确 vs 普通提示词的12正确),但对SmolLM2更差(3 vs 7)。小模型倾向于复制示例而非从中学习。如果使用示例,让示例值明显是假的(比如 "NAME"、0),这样泄漏容易发现。

JSON Schema 验证与类型漂移

JSON.parse 只检查文本是否为合法 JSON,不检查结构是否符合预期。JSON Schema 是一种描述数据形状的规范,可规定必填键、字段类型、枚举取值等,配合验证器(如 Ajv)能在解析后进一步把关。

文中「类型漂移」指模型输出的结构看似正确但类型不符,例如数量被写成字符串 "1" 而非整数 1,或缺失字段用缺键代替 null。这类错误 JSON.parse 完全无法察觉,下游代码读取时才会静默失败。因此在解析阶梯之后增加 schema 校验,并检查类型而非仅键名存在性,是保证数据可用的必要环节。

对GPT、Claude和大模型意味着什么

这次测试是360M到1B参数模型在浏览器中的表现,失败率会比前沿API高。但作者认为采样器的发现对任何自行运行开源模型的人都适用:n-gram禁令是机械规则,不关心模型有多聪明。更大的模型只是找到更巧妙的绕路方式,这可能更糟,因为它们看起来是对的。

能跳过猜测的话就跳过。约束解码把每个下一个token限制在能保持输出符合语法或schema的范围内,从构造上杜绝格式错误。OpenAI Structured Outputs、llama.cpp GBNF语法和Outlines都提供这种能力。如果不能使用,解析阶梯加schema校验加一次重试是实际的底线。

约束解码(Constrained Decoding)

约束解码是在每一步采样时,把候选 token 限制在「能使输出继续符合给定语法或 JSON Schema」的集合内。实现方式通常是把 schema 编译成有限状态机或上下文无关文法,再据此动态屏蔽非法 token。这样模型从构造上就不可能生成格式错误的输出,无需事后解析修补。

常见方案包括 OpenAI 的 Structured Outputs、llama.cpp 的 GBNF 语法文件、以及 Outlines 库。其局限在于:只保证语法与结构合法,不保证语义正确(如字段值是否准确),也不解决截断或模型拒绝回答的问题;复杂的嵌套 schema 可能拖慢解码速度。对自行部署开源模型的团队,这是比调采样参数更根本的解决方案。

今天就能在项目里做的事

  1. 搜索 no_repeat_ngram_size 和 repetition_penalty。如果结构化调用从共享配置继承了这两个参数,覆盖它们:no_repeat_ngram_size: 0、repetition_penalty: 1.0。
  2. 对JSON使用贪心解码或低温度。温度在这里没造成伤害,但贪心让失败可复现,便于修复。
  3. 用阶梯解析。记录哪一步被用到;第4步比例上升是早期预警。
  4. 解析后对照schema验证,检查类型而非仅键名。
  5. 失败后带错误信息重试一次。两大失败类型(循环和无JSON)是解析器修不了的。
  6. 让示例值明显是假的,并测试泄漏。
  7. 为列表提高token上限。截断回复是唯一应该闭合括号的场景,但你会静默丢失条目。

关键数字汇总

指标数值
生成并评分的回复432(288主运行+144消融)
Llama 3.2 1B schema合法,贪心+示例18/24 (75%)
同上,仅添加 no_repeat_ngram_size: 31/24 (4%)
三个模型,聊天设置+示例提示词0/72
288条主回复直接解析成功84 (29%)
经四步阶梯后解析成功127 (44%)
Llama贪心+普通提示词经阶梯24/24
泄漏示例值的回复288条示例提示词回复中21条
Qwen2.5 0.5B schema合法0/96

一个诚实的说明:Chrome的WebGPU设备丢失过两次,作者重跑了Llama的整批并让脚本在设备丢失时重载模型重试,所有数字都来自完整的数据集。

对于BEINGS的下一步,作者计划让生物动作以JSON返回,为结构化调用使用独立的贪心、无惩罚配置,而聊天保留防重复设置。75%能否在真实房间流量中保持,值得关注。

如果你手头正好有一条坏掉的LLM回复,可以试试作者提供的免费JSON修复工具,它运行同样的四步阶梯,并告诉你哪一步修好了它。

#LLM JSON输出#no_repeat_ngram_size#结构化输出#采样参数#小模型JSON#Transformers.js#约束解码#JSON解析修复
分享:

相关推荐