归档/2026.04.25·AI Agent

修复 LLM 输出的代码中经常出现多余引号的问题

我们在做 Agent 项目的时候,碰到了一个让人摸不着头脑的 bug:模型生成的代码有时候能跑,有时候报 JSON 解析错误,复现概率还挺高。

问题出在哪

我们的 Agent 需要让模型动态生成 Python 代码。代码通过 function calling 传递——模型调用工具时,把生成的代码放在参数里,后端解析后执行。

某天执行日志里开始频繁出现这个错误:

params is not valid JSON
  Received: {
    \\\"hwnd\\\": 133386,
    \\\"keys\\\": \\\"火箭{ENTER}\\\",
    \\\"explanation\\\": \\\"...\\\"
  }

同一个工具,同样的参数格式,有时成功,有时失败。翻了半天日志,发现规律:成功的调用里 params 是 JSON 对象,失败的调用里 params 是模型自己序列化出来的 JSON 字符串。

正常的调用:

{
  "method": "send_keys",
  "params": {
    "hwnd": 133386,
    "keys": "火箭{ENTER}",
    "explanation": "在搜索框中输入联系人名称"
  }
}

出问题的调用:

{
  "method": "send_keys",
  "params": "{
    \\n  \\\"hwnd\\\": 133386,
    \\n  \\\"keys\\\": \\\"火箭{ENTER}\\\",
    \\n  \\\"explanation\\\": \\\"在搜索框中输入联系人名称\\\"
    \\n}"
}

params 本该是对象,模型给整成了一个带双重转义的字符串。后端 JSON.parse 拿到的是字面量 \\n\\" ,不是换行符和引号,解析当然失败。

为什么会这样

Function calling 通过 JSON 传参,JSON 规范要求字符串里的双引号转义为 \"、换行符转义为 \n。这层转义正常情况下由 SDK 处理,开发者直接把值传进去就好。

但模型有时候会在内容层再多加一层:生成代码时,把 " 写成 \",把换行写成 \n。SDK 再序列化一次,就变成了双重转义:

"  →  \"  →  \\\"
\n →  \n  →  \\n

本质上是模型把传输层的转义规则错误地应用到了代码内容上——它"记住"了 JSON 字符串里引号要加反斜杠,但没意识到 SDK 已经帮它做了这一步。

不止我们遇到过

查了一圈,发现 Aider 和 OpenCode 都踩过同样的坑。

Aider 早在 2023 年就做过专项 benchmark,在 133 道 Exercism Python 练习题上对比了 function calling 和纯文本两种传代码的方式,结论是:

"Function calls performed worse than the whole file method, for all the models."

GPT-3.5 在 function calling 模式下甚至会直接忽略 JSON schema,产生幻觉式的函数调用。研究者的最终决定是不采用 functions API,Aider 的 function calling 版本代码编辑器此后全部废弃。

2024 年他们又做了一篇 LLMs are bad at returning code in JSON,在 Claude 3.5 Sonnet、DeepSeek Coder V2、GPT-4o 等模型上验证了同样的结论:

"LLMs produce lower quality code if they're asked to return it as part of a structured JSON response."

让模型在 JSON 里返回代码,它要分心处理转义逻辑,代码质量会整体下降,语法错误率也会上升。

OpenCode 的 issue #34652 记录的是另一个变体:Claude 通过原生 Anthropic provider 调用工具时,会把本应是数组的参数以 JSON 字符串的形式传过来:

SchemaError(Expected array, got "[{\"content\":...,\"status\":...,\"priority\":...}]")

got 后面是一个带转义引号的字符串,不是真正的数组。模型多做了一次 JSON 序列化。这个 issue 被标记为间歇性复现、无法从用户侧规避,至今还在等官方修复。

Aider 和 OpenCode 的思路

两个项目的处理方向不同。

Aider 选择从根本上绕开:彻底放弃 function calling,改用纯文本传代码。模型输出 Markdown 代码块,接收端用正则提取。模型只需要"写代码",不用管序列化格式,转义问题自然消失。

OpenCode 的方向是在 schema 校验层做兼容:收到字符串时先 JSON.parse 一次,解析成功就继续走。不改变模型的输出行为,只是在后端多做一层容错。

我们的做法

我们参考了 Aider 的思路,但不想完全放弃 function calling,所以加了两层处理。

第一层是预防。commit_code_patchpatch 字段 description 里,明确要求模型必须用 codeblock 包裹代码:

```python
# 你的代码
```

这样做有两个效果:模型进入"写代码"的心智模型而不是"生成 JSON 字符串";Markdown codeblock 内部不需要任何转义,写什么就是什么。

另外在 prepare_code_build(生成代码前必须调用的工具)的返回值末尾加了一条格式提醒,每次生成前强化一次。

接收端新增 codeblock 提取逻辑,没有 codeblock 时向后兼容,走原有逻辑。

第二层是兜底。 即使 prompt 引导到位,模型偶尔也可能不按格式来。我们在 AST 校验环节做了增强:校验失败时,错误信息附带报错行号和对应的代码上下文,而不是只返回一个笼统的 SyntaxError。

SyntaxError at line 14:
    result = f"value: {data[\"key\"]}"
                                ^
invalid syntax (possibly due to extra backslash escaping)

模型看到精确的行号和出错代码,下一轮自行修复的成功率高很多。

改完之后的效果

用一个容易触发转义问题的输入做了 A/B 测试,模型是 Gemini Flash,各跑 20 次并发执行:

指标修复前修复后
AST 通过率9/20(45%)20/20(100%)
codeblock 使用率0/20(0%)19/20(95%)
多余反斜杠转义11/20(55%)0/20(0%)

codeblock 使用率是 95% 而不是 100%,说明 prompt 引导本身不是万能的——所以才需要 AST 兜底,两层加在一起,才能把问题压到可接受的范围。