{ } JSONDock EN

AI · JSON 修复

AI 生成的 JSON 为什么总是坏的?

语言模型可以生成看起来完全合法、却通不过 JSON.parse 的 JSON。失败模式其实屈指可数——而与模型的输出不同,修复可以是确定性的。

llm-output.json
// 由 LLM 生成的用户档案 { 'name': 'Alice', "role": "engineer", "tags": ["dev", "json", "ai",], }
JSON.parse 抛出异常

一个错位的逗号,整份数据全部失效。

1语法无效
2报错指向错误的位置
3模型永远看不到这个错误
4重新生成既花时间又耗 token

你把 LLM 的输出粘贴到管道里,JSON.parse 抛出了异常。最常见的应对是重新生成一次:“把 JSON 修好。”有时候确实能修好;有时候它只是用不同的措辞再犯一次同样的错。这个循环远比它应有的更常见,因为它看起来像是随机的。但它并不随机——它遵循一个很小、很稳定的模式清单,而且修复时根本不需要让模型再试一次。

失败模式清单

错误不是无穷无尽的。我们见过的生成式输出中的每一种 JSON 失败,都落在下面的模式里。每一条都在 Node 20 上复现,并记录了 JSON.parse 的真实报错:

失败模式示例JSON.parse 报错
尾逗号{"a": 1,}Expected double-quoted property name in JSON at position 37
单引号{'a': 1}Expected property name or '}' in JSON at position 1
键名不加引号{a: 1}Expected property name or '}' in JSON at position 1
行注释字段前的 // 注释Expected property name or '}' in JSON at position 4
块注释字段间的 /* 注释 */Expected double-quoted property name in JSON at position 18
缺少逗号{"a": 1 "b": 2}Expected ',' or '}' after property value in JSON at position 17
字符串中的真实换行多行字符串值Bad control character in string literal in JSON at position 17
NaN / Infinity{"score": NaN}Unexpected token 'N', ... is not valid JSON
Markdown 代码块```json … ``` 包裹Unexpected token '`', "```json …
截断"tags": ["dev", "json"(未闭合)Expected ',' or ']' after array element in JSON at position 60
省略号["dev", "json", ...]Unexpected token '.', ... is not valid JSON

另外需要注意报错信息本身:它报告的位置是解析器第一次失去跟踪的地方,不一定就是偏差开始的地方。对象末尾的尾逗号会报成 “Expected double-quoted property name … at position 37”——比真正的错误位置还靠后一个字符。解析器只能告诉你它第一个迷路的位置,定位根因仍然要靠人工。

为什么 JSON 不给语法留任何余地

JSON 是主流数据格式里最严格的一种:只能用双引号、没有注释、没有尾逗号、不允许裸标识符、字符串不能换行。开发者常写的其他格式——Python 字典、JavaScript 对象、YAML、TOML——至少都会容忍其中的某一条规则。

语言模型是在混合了所有这些格式的文本上训练的。采样下一个 token 时,模型会给语法正确的选择分配一个很高但并非绝对的置信度。大多数时候它会落在正确的地方,偶尔会采到那个“看起来合理但错误”的 token:JSON 要求双引号的地方出现单引号、最后一个字段后面跟了个尾逗号、或者模型“好心”加上的一条注释。

两个特性决定了这不是偶然,而是结构性的:

  • 模型无法解析自己的输出。从发出 token 到把累积结果与 JSON 语法核对之间,不存在反馈回路。
  • 失败是零容忍的。JSON 没有任何容错:一个错位的逗号就能让整份数据失效,无论其余一万个字符多么正确。

所以真正的问题不是 AI 会不会输出损坏的 JSON,而是当它发生时,你的管道如何消化这次失败。

重新生成是个陷阱

“让模型自己修”是最常见的补救方式,但它有三个代价:

  • token 与延迟。每次重试都是一次完整的生成过程——按秒计的墙钟时间和额外的花费。
  • 不确定。同样的提示词这次可能修好 JSON,下次用另一种方式修坏。你无法围绕一个概率分布构建可靠的失败重试逻辑。
  • 内容漂移。重新生成可能悄悄改变数值——一个数字、一个名字、一个分类——而且没有任何 diff 会标出它。

内容本身出错时——模型漏掉了字段、误解了输入——重新生成才是正确的工具。但语法错误不是。语法失败有有限的清单,一个有限的算法就能处理。

确定性修复到底做了什么

确定性修复器应用一组固定的 token 级规则:去掉注释、把单引号和裸键名规范成双引号字符串、移除尾逗号、补上明显缺失的逗号、转义控制字符、闭合未闭合的结构、去掉 Markdown 代码块。同样的输入永远得到同样的输出,只需微秒级时间,不需要任何 API 调用。

失败模式修复器做了什么
尾逗号移除
单引号、未加引号的键名转换为双引号字符串
行注释、块注释剥离
缺少逗号补上
字符串中的真实换行转义为 \n
NaN / Infinity保留为 "NaN" / "Infinity" 字符串——保留原 token,而不是猜测
Markdown 代码块移除
截断的结构以最可能的意图补全括号
省略号丢弃
与重新生成的本质区别不消耗 token、没有延迟、没有不确定性。同样的输入永远产生同样的输出,这意味着修复结果可以测试、可以缓存。

修复器诚实止步的地方

修复器解决的是语法问题;它无法恢复模型从未发出的内容。在信任修复结果之前,有三个案例值得了解:

  1. 字符串在单词中间被截断。{"bio": "she works at 会被闭合为 {"bio": "she works at"}。句子剩下的部分已经消失——任何算法都无法恢复 token 流中不存在的文本。
  2. 缺失的分隔符是猜测。[1 2] 变成 [1, 2],因为逗号是最可能的意图——但这是修复器替你做的决定。
  3. 空值是被发明的。{"a": } 变成 {"a": null}null 并不在源数据里;把它当作事实之前,先检查一遍。

经验法则:把修复当作恢复网,而不是校验器。修复后的输出进入任何有状态的系统之前,先做校验。

预防优先

  • API 支持时请求结构化输出。OpenAI 的 response_format / JSON schema 模式Anthropic 的结构化输出Gemini 的 JSON 模式,都会在生成阶段强制产出合法 JSON。
  • 调低 temperature。设为 0 可以减少——但无法根除——变化。
  • 保持输出短小。截断是唯一会随输出长度放大的失败。一次要一条记录,而不是五十条。
  • 在边界处校验,重试一次。第二次仍然失败的话,用确定性修复,而不是陷入重新生成的循环。
  • 生产环境里,修复必须经过检查。永远不要把修复后的输出静默写入数据库而不经过校验。

JSONDock 如何修复 AI 输出

JSONDock 的修复标签页使用开源的 jsonrepair 引擎,然后用无损解析器重新格式化结果——修复语法时绝不会对数字进行舍入。假设一份 AI 输出既带尾逗号又含 64 位标识符:

{"orderId":900719925474099312345, "status": "paid",}

从修复标签页出来时,逗号已修复,标识符仍然是精确的 900719925474099312345

{
  "orderId": 900719925474099312345,
  "status": "paid"
}

这种无损行为正是我们的 JSON.parse 精度丢失指南 要讲清楚的事。原则相同:修复和格式化一样,永远不应该成为损坏数据的那一步。