前几天接口偶发 500。日志里只有一行 JSONDecodeError,把返回体打出来才看见,模型在 JSON 前面多说了一句「好的,结果如下」,后面那串东西本身完全正确。
这种事不值得为它改提示词。我决定写个容错解析,动手前先量一量到底有多少种坏法。
18 条样本是我手写的
先把丑话说在前面。这 18 条并非从某家接口抓下来的原始返回,是我按自己见过的坏法一条条写出来的。
清单是这样的,markdown 围栏、前后多说话、尾逗号、单引号、Python 的 None 和 True、注释、全角引号、BOM、被截断、两个对象连在一起、字符串里有真换行、千分位、Windows 路径。
所以下面测的是解析器扛不扛得住这类坏法,它证明不了某家模型一定这么返回。样本、判定表和 5 种写法的完整代码都在文末,谁都能重跑一遍。

判定要分三档
大多数对比只算解析成不成功,这里多算一档。第一档解析成功且值与预期一致,第二档解析成功但值是错的,第三档直接抛异常。
第二档才是真正难查的那种,因为它在日志里长得很干净。程序不报错,拿着错值继续往下跑,出问题的地方离出错的地方可能隔了三个函数。
后面每一格都按这三档判。
直接 json.loads 只过 1 条
S1 就是一行 json.loads。18 条里对 1 条,抛异常 16 条。
失败得最彻底的地方在于,哪怕 JSON 前面只多了「好的」两个字,它也直接抛。很多人以为它会自动忽略前后的杂质,它不会,它要求整串字符串就是合法 JSON,多一个字符都不行。
这一点怪不到它头上。json.loads 从来没承诺过要处理模型的口头禅,是我把它放错了位置。
从 7 条到 13 条,再到 14 条
S2 换个思路,找第一个左大括号和最后一个右大括号,把中间那段交给 json.loads。18 条里对 7 条,markdown 围栏、前后加话、BOM 这三种最常见的都能过。
尾逗号和单引号它过不了,这两类的毛病长在括号里面,截掉外面那层没用。还有个地方容易忽略,S2 取的是最后一个右大括号,碰到两个对象连在一起,它会把两个缝成一段,照样抛异常。
S3 在 S2 基础上多一道正则清洗,去围栏、去注释、全角标点转半角、None 转 null、去尾逗号、补齐括号。18 条里对 13 条,抛异常 4 条。单次 16.35 微秒,S1 是 3.67 微秒,差了 4.5 倍。
这个倍数看着吓人,实际没意义。一次请求里解析 20 万次才 3 秒,正常业务一天解析不了那么多回。慢不构成选型理由,能不能救回来才是。
S5 把 S3 的清洗留下,把取最后一个右大括号换成数到括号配平就停。18 条里对 14 条,多救回来的那一条正是两个对象连在一起,5 种写法里只有它能处理。代价是单次 18.67 微秒,比 S3 多 2.32 微秒。

自己写状态机,反而只有 10 条
这一条我一开始没料到。S4 是一个字符一个字符扫的状态机,理论上它比正则更稳,因为它知道此时此刻自己在不在字符串里面。结果 10 条,比正则少 3 条。
查下来三个原因。第一,我只写了半角引号到双引号的映射,全角引号那条直接漏掉。第二,它从第一个左大括号开始扫,前面的解释文字里如果带反引号,围栏残留会让它切错位置。第三,碰到后面还有一句话那种,它在右大括号就停了,可那句话里的三个反引号还留着。
写得更复杂,覆盖的坏法反而更少。状态机要穷举的分支比正则多,我漏了几个。
最危险的格子不是失败
表里有 4 条,5 种写法全部救不回,字符串里有真换行、千分位数字、Windows 路径、字符串里没转义的引号。
共同点是原文信息已经丢了。字符串里那个真换行,你没法判断它本来就要换行,还是某个转义序列被截断后留下的残片。修补的前提是有信息可补,这 4 条没有,所以再好听的写法也没用。
还有一条更麻烦。Windows 路径那一行,5 种写法全部成功。样本长这样,`{"p":"C:\new\table"}`。
这是合法 JSON,里面的 `\n` 是换行,`\t` 是制表符。于是解析器兴高采烈地返回一个值,中间夹着一个换行和一个制表符。
它不报错,不告警,日志里干干净净。等你发现路径莫名少了一段,已经是三天后。这条把我原来那套「解析成功就算成功」的判断整个废掉了。

我现在的写法
模型那边先开 JSON 模式,能吐结构化输出就让它吐,这一步能消掉大半问题。
解析按 S5 写,多 2.32 微秒换 11 条容错,划算。解析失败就重试,最多两次,第二次把原始返回和报错位置一起塞回给模型。
最要紧的一条是解析成功之后必须校验字段。字段缺了、类型不对、值不在允许范围里的,一律按失败处理,走重试。上面那个 Windows 路径的洞,只有在字段校验这一层才拦得住,解析器永远拦不住,因为它认为自己做对了。
样本表和 5 种写法在 `_exp/exp_json.py`,改一下样本表就能加自己的坏法。你们那边最常遇到的是哪一种?我这边排第一的是围栏包一层。