
一次工具调用解析失败,报的是 Unterminated string in JSON at position 34。排查按顺序走:先看这次生成是怎么收尾的,再看拼出来的字符串缺在哪,最后试补。下面这份 chunk 序列是按流式响应的字段形状手写的桩,模型侧行为这台机器上没法实测;拼装与解析的结果都是本机跑出来的。
先看 finish_reason
== 拼完之后拿到什么 ==
finish_reason : length
工具调用个数 : 1
工具名 : create_order
arguments 字符串 : "{\"sku\":\"A-1001\",\"qty\":12,\"addr\":\"深"
arguments 长度 : 34
finish_reason 是 length,不是 tool_calls,也不是 stop。工具名 create_order 和调用 id 都在,arguments 长度 34。这个字段说明生成是被 token 上限截断的,模型自己并没有收尾。
按这份回放看,finish_reason 只在最后一个 chunk 里出现,前面几次都是 null,所以要等流结束才能确认截断。想在流中间就止损,只能自己统计已经收到的长度,或者给这次调用单独设一个更紧的上限。
拼出来的字符串缺了什么
== 直接解析这份 arguments ==
JSON.parse 抛错 : SyntaxError: Unterminated string in JSON at position 34 (line 1 column 35)
报错位置是 34,正好是字符串末尾,含义是「引号还没闭合就到了结尾」。JSON 解析器只报第一个错误,它报的是结构还没成型,和字段校验无关。
先试补括号
== 补上收尾括号再解析(常见的兜底写法)==
补完是 : {"sku":"A-1001","qty":12,"addr":"深}
还是抛错 : SyntaxError: Unterminated string in JSON at position 35 (line 1 column 36)
只补花括号是最常见的兜底写法,这一步还是抛错,位置从 34 挪到 35,等于白补。原因在截断点落在字符串内部:花括号补齐了 qty 那一层,addr 的引号仍然开着。
连引号一起补:解析成功,值是错的
== 连开着的引号一起补 ==
补完是 : {"sku":"A-1001","qty":12,"addr":"深"}
解析结果 : {"sku":"A-1001","qty":12,"addr":"深"}
字段齐全吗 : qty = 12 addr = "深"
把缺的引号和花括号都补上,JSON.parse 通过了,拿到一个「合法」对象:sku 和 qty 都在,addr 是「深」。原本要写的是「深圳」。这一份传下去不会报错,落库也不会报错,只有在页面上看到地址少一个字的时候才会有人回头查。
顺便记一下这类修复函数的适用边界:截断点落在结构层时,补括号只补结构,值不受影响;落在字符串内部时,补出来的字符串照样「合法」,值却短了一截。判断依据是 JSON.parse 的报错消息里有没有 Unterminated string,而不是补完之后能不能解析成功。
这就是补 JSON 最危险的地方:它修改的不是结构,是内容。字段少一个会立刻报错,值被截掉一半反而一路绿灯。
两种收尾的对照
== 两种收尾的对照 ==
正常收尾 (stop) 长度 37 解析成功 → {"sku":"A-1001","qty":12,"addr":"深圳"}
被截断 (length) 长度 34 解析失败 → Unterminated string in JSON at position 34 (line 1 column 35)
正常收尾的 37 个字符解析出完整字段,被截断的 34 个字符直接被拒。三次实验的差别只在一个 finish_reason,字符串前面 34 个字符完全一样。
三种处理方式
先看 finish_reason,值是 length 的时候把这次工具调用整个丢掉,要求重新生成。这一条几乎没有代价,被截断的调用本来就没有完整参数可用。
请求侧要留余量:max_tokens 得同时装下回答和工具参数,参数长的工具尤其容易被挤。参数一长,length 出现的概率就跟着涨。
确实要兜底修复的话,按报错位置分:报错落在结构层(缺括号)可以补,报错落在字符串内部(Unterminated string)直接放弃,因为补出来的值可能是错的。我的取舍是不做静默修复,截断就失败重试,补出来的对象从字面上看不出问题,一旦落库就要靠人工核对才能发现。
重试之前还要确认这次调用有没有真的发出去。参数解析失败的情况下不会发出请求,这一层是安全的;但如果是解析成功之后请求超时,重试就有可能生成第二单,写操作的工具要带一个调用侧的幂等键来处理这种重复。
模型在什么条件下会把参数写这么长、length 出现的频率有多高,这两点这台机器上没实测,只能确认解析侧的行为:被截断的 arguments 修不好,能修的那次也会改值。