
模型把结果写成 JSON 的时候,包法并不固定:有时裸着,有时外面套一层围栏,有时前后还各带一句说明。为了让解析走同一条路,代码里通常先剥一层。剥法写错有两种后果,报错算轻的,怕的是解析成功而拿到的不是答案。12 条样例分别跑了六种剥法,成绩差得很开。
== 汇总:解析成不成功 ==
整串直接 parse: 成功 1 / 失败 11(共 12 条)
按行删掉围栏行: 成功 7 / 失败 5(共 12 条)
严格 json 围栏: 成功 6 / 失败 6(共 12 条)
第一个围栏: 成功 8 / 失败 4(共 12 条)
从{切到}: 成功 9 / 失败 3(共 12 条)
逐个候选试解析: 成功 11 / 失败 1(共 12 条)
最后一行是后面要给的写法。前面五种从 1 条到 9 条不等。光看这张表还不够,把值和预期对一遍,会多出一列:
== 汇总:内容对不对(解析成功但值不对的最危险) ==
整串直接 parse: 对 1 / 值不对 0 / 解析失败 11
按行删掉围栏行: 对 7 / 值不对 0 / 解析失败 5
严格 json 围栏: 对 5 / 值不对 1 / 解析失败 6
第一个围栏: 对 7 / 值不对 1 / 解析失败 4
从{切到}: 对 9 / 值不对 0 / 解析失败 3
逐个候选试解析: 对 11 / 值不对 0 / 解析失败 1
「解析成功但值不对」这一列是两个数字,落在下面要批的第三、第四种写法上。
错法一:整串丢给 JSON.parse
12 条里成功 1 条。只有裸 JSON 那条能过,带说明的,带围栏的,带收尾句的,全部抛错。这种失败至少看得见:报错的原文和位置都在,排查花不了多少时间。
错法二:按行删掉围栏那一行
成功 7 条。它把以三个反引号开头的整行删掉,前面的说明句和后面的收尾句都留着。碰到最常见的那种包法(一句说明加一段围栏),剥完第一行还是中文,JSON.parse 立刻报错。
它失败在最常见的输入上,成功率却不低,因为它对首尾的杂物没有区分:围栏行删了,说明行没删。
错法三:严格匹配 json 围栏
成功 6 条,是五种里最低的。这个写法要求标记正好是小写 json,前后围栏各占一行。模型把标记写成大写、写成 js、或者忘了写收尾那行,它一条都剥不出来。
它的问题还不止低成功率,先给示例再给答案的输入上,它会取到示例:
严格 json 围栏: 成功 -> {"city":"城市名","temp":0}
剥出来的是示例里的字段,城市名是个占位符,温度是 0。解析成功,值是错的,而且错得没有痕迹。
错法四:取第一个围栏
成功 8 条,比上一版宽松,标记写什么都能剥。它同样在示例那一条上取到 0 度,另外还有一条会切坏:
第一个围栏: 失败 -> Unterminated string in JSON at position 10 (line 1 column 11)
这一条的字段值里本身写着三个反引号,围栏匹配在字符串中间就收了尾,剥出来的片段少半个引号。字符串里出现围栏是很常见的,模型解释自己怎么输出的时候就会写上。
错法五:从第一个花括号切到最后一个
成功 9 条,是五种里最高的,而且没有一条值不对。它栽在两头都有花括号的输入上:说明里写了一个花括号占位符,或者前后有两段围栏,切出来的范围跨过了中间那些字符,报错是 JSON 后面还有非空白字符。
它的失败比前两种舒服,至少是报错,不是悄悄给错值。
正确写法:把候选排成一队逐个试
做法是把所有可能的候选排成一队:先认出标记是 json、或者没写标记的那些围栏块,从最后一块往前排,再把括号扫描的结果放在队尾。逐个丢给 JSON.parse,第一个解析成功的就用。
从最后一块往前排是关键一步。模型先给示例再给答案时,答案在后面,取第一块就是取到示例。括号扫描放在队尾是为了兜住没有围栏、或者围栏没闭合的输入。
括号扫描本身也要按字符串处理:遇到引号就进字符串状态,字符串里跳过后面的反斜杠和花括号,只统计裸露的括号配对。少了这一步,值里面出现花括号就会提前收尾。
有一类是真修不了的
样例里最后一条,值中间有一个没有转义的引号。六种写法全部失败,逐个候选那一版也没能救回来。那已经不是包法的问题,是模型写出来的 JSON 本身不合法。
这种输入只能交给更宽松的解析器,或者把原始输出整段打回给模型重写一次。判断依据是错误位置落在字符串里面还是外面:括号不配对、围栏剥不干净,属于包法问题;引号嵌套错、缺逗号,属于内容问题。
这段逻辑我现在的写法是:解析失败时把原始输出整段连同试过哪些候选一起打出来,再决定是重试还是补救。只看错误信息,很难分清是包法的问题还是内容的问题。