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:
- 不追踪边界的实现能过其余全部测试
- 只挂在这一条
闸门
代码审查时的检查清单:
- 新的改写遍是否追踪字符串边界?
- 字符集是否包含引号类字符?
- 测试是否断言解码后的值?
- 回归 fixture 本身是否非法?(合法 fixture 让测试恒绿)
相关文档
- 行式输入安全 →
line-input-safety.md - 子进程组管理 →
exec-process-group.md