作者用300个真实LLM故障输出样本对7款JSON解析器进行基准测试,结果显示json-repair综合最优(88.3%完全匹配),而标准json.loads仅8.3%。
我维护了一个叫 jsonshim 的标准库工具,专门用来从语言模型的回复中提取 JSON。上周我为这项工作构建了一个包含 300 个用例的合规测试套件 MALFORMED-300,并公布了它在我自己写的解析器上发现的结果:18 个失败,其中 5 个是模型本应什么都没生成却凭空生成了值。
接下来显而易见的问题——也是我一直回避的问题——是:它在实际使用中与大家真正会用的那些库相比表现如何?
所以我全部跑了一遍。每个只跑一次。以下是结果。
七个解析器,300 个带标签的用例,Python 3.12.3,各跑一次,没有事后调优。
__version__ 为 3.0.5;但安装它的 pip 分发包报告的是 3.0.6。表中版本取自已安装的模块。设两列是因为这些库的设计目标并不相同,用一列会在不知不觉中惩罚其中某些库——因为那是作者有意做出的决策。
套件的契约要求对全部 300 个结果评级:其中 25 个用例是不可恢复的——模型输出了 {,或者一个裸露的 <redacted> 占位符——这些只能通过拒绝来通过。在那些情况下返回 {} 是失败,不是接近通过。
275 个可恢复的抛出会把问题扔掉,只问当有东西可取时库返回了什么。
在 275 个可恢复用例上,json-repair 击败了我写的工具。264 比 262。
jsonshim 在全部 300 列中凭借拒绝策略 alone 领先。它拒绝了 25 个不可恢复用例中的 20 个;json-repair 只拒绝了 1 个,因为 json-repair 的设计理念是总要给你点什么。如果你在渲染 UI,更愿意展示一张空卡片而不是一个错误,这确实是一个合理的设计。
但如果解析后的对象要写入数据库或用于工具调用,这就是错误的设计。一个凭空生出的值不会抛出异常。不会记录日志。它到达时看起来和真实数据一模一样。套件中最严重的单个案例是 {"user_id": <redacted>} 变成了 {"user_id": "<redacted>"}——一次脱敏变成了用户 ID。我自己的工具在这个案例上仍然失败,而且我让它继续失败,因为看到分数后再修复会把 94.0% 从一个测量值变成一种声明。
所以表格的诚实解读不是"这个最好"。而是:选择你管道能承受的拒绝策略,然后选择恢复率。
json5、pyjson5、demjson3 和 dirtyjson 在这方面并不差。它们在做不同的工作。
它们是方言解析器。它们读取的是比严格 JSON 更宽松的语法,所以它们能精确处理那些属于语法问题的类别——所有四个库在未加引号的键上都是 25/25,尾随逗号 20/25,单引号 16/25,注释 16/25——而在代码围栏、散文包装和截断上得分均为零。
这个零不是缺陷。当模型回复
Thought: the user wants structured output.
Action:
```json
{"goal": "triage inbox"}
时,它并没有产生格式错误的 JSON。它产生的是内含 JSON 的散文。在某东西先找到跨度之前,没有什么语法可以宽容的。定位 payload 和解析 payload 是两个不同的问题,七个库中只有两个同时做这两件事。
这意味着常见的建议——"用 json5 就好"——回答的是大多数 LLM 输出并不会问的问题。
## 没有库能赢下每个类别
json-repair 在包装器上以 25 比 17 取胜——那是我最弱的类别却是它最强的。我在 Python 字面量上以 25 比 21 取胜。看网格图,不要看总分。
这是决定这张表是否有价值的部分。
每个库都通过各自有文档记录的入口调用,在各自有文档记录的宽容模式下运行(如果有的话)。adapters.py 故意写了一百行左右,这样你一分钟就能审查完。
没有输入是预先清理的。每个解析器看到的是完全相同的原始字符串。
唯一的后处理是将库特定的容器类型和哨兵类型规范化为普通的 dict/list/None,所以比较的是值本身,而不是哪个类包装了它们。
各跑一次。没有任何尝试、调整和重新运行。
在给任何第三方库打分之前,测试工具必须精确复现一个更早前单独发布过的 jsonshim 分数——282/300,5 个 invented,4 个 false refusals。它做到了。这是我信任其他六行的唯一原因。
语料库真值是通过构造产生的:一个检查点渲染器在格式错误的文本存在之前就发出期望值,所以从来没有解析器被咨询过正确答案。
当库和套件对某个用例应该返回什么意见不一致时,套件是一个声明过的评分契约,而不是法律。`python3 score.py --spec` 会完整打印出来,真值也会一同发布,所以不一致是可核查的而不是可争辩的。
测试工具、打分器和 30 个分层用例是公共领域。不需要账号,不需要邮箱。
curl -O https://toolkitlabs.org/malformed300/sample30.jsonl curl -O https://toolkitlabs.org/malformed300/score.py python3 score.py --corpus sample30.jsonl --parser json # the control python3 score.py --corpus sample30.jsonl --parser yourmodule:recover
score.py 在相对于保存的基线出现回归时以 exit 2 退出,所以它可以 gate CI——这是拥有这一切的真正原因。一个在依赖升级后悄悄从 94% 跌到 88% 的解析器不是你能从测试中发现的。
完整结果、全部七个库的逐类别网格以及原始运行记录:toolkitlabs.org/leaderboard。页面从 leaderboard.json 生成,所以上面的任何数字都不会偏离产生它的那次运行。
如果你的解析器在样本上的位置很有意思,我真的想知道——包括它是否击败了我的。