LLM JSON 修复(INV-JSON-01)

wesgine 的 internal/infra/llmjson 包将模型输出的近似 JSON 修成合法 JSON,同时保证字符串值不被改变。


核心问题

LLM 经常输出"几乎合法"的 JSON:

{"command":"grep -n 'a,]b' f.go","n":1,}
//                                    ^ 尾部逗号

修复必须只删尾部逗号,不动字符串里的 ,]。


位置安全原则

每一遍改写必须位置安全:只动字符串字面量之外的字节。

反例:removeTrailingCommas 曾逐字扫全文删"逗号后跟 }/]":

输入: {"command":"grep -n 'a,]b' f.go","n":1,}
错误: {"command":"grep -n 'a]b' f.go","n":1}
                           ^ 字符串内容被改了!

修好之后跑的是 grep -n 'a]b' f.go——这不是罕见输入,每个 JSON 定界符也是正则里的普通字符。


修复管线

步骤作用位置
NormalizeOutsideStrings全角标点 → ASCII字符串外
removeTrailingCommas删尾部逗号字符串外
escapeBareControlsInStrings转义控制字符字符串内(唯一例外)
balanceBrackets补缺的 }/]字符串外

字符集约束

NormalizeOutsideStrings 可以收入的字符

引号是判断"在不在字符串里"的依据,先改引号等于先毁掉判据。

控制字符

JSON 禁止全部 32 个控制字符,不只是 \n/\t。

只处理常见两个时,Windows 换行留下裸 \r,穿过每一道修复仍然非法,症状读起来像截断。


escapeBareControlsInStrings

唯一动字符串内部的改写,豁免理由是保值:

裸换行 → \n     (unmarshal 成同一字节)
裸 tab → \t     (unmarshal 成同一字节)
裸 \r  → \r     (unmarshal 成同一字节)

不是"反正在字符串里",是改写后解码值不变。


验证方法

json.Valid 是最低门槛不是充分条件:

正确的断言落在解码后的值上:

// fixture 故意把 JSON 定界符放进字符串值里
input := `{"cmd":"grep 'a,]b'","n":1,}`
result := Repair(input)
var m map[string]interface{}
json.Unmarshal([]byte(result), &m)
assert(m["cmd"] == "grep 'a,]b'")  // 值未变

回归测试

TestRepair_PreservesStringContent:


闸门

代码审查时的检查清单:

  1. 新的改写遍是否追踪字符串边界?
  2. 字符集是否包含引号类字符?
  3. 测试是否断言解码后的值?
  4. 回归 fixture 本身是否非法?(合法 fixture 让测试恒绿)

相关文档