场景
LLM agent 帮我做一件「批量落库」的活:读 17 条历史文本、组装成 API 请求、依次 POST 到某个数据库。全流程 python 脚本,也就几十行。
按道理这是 agent 最擅长的活:写代码、跑 exec、看结果、修错。但我今天从上午 10 点到下午 5 点,agent 反反复复卡在一个极其低级的坑上 —— 多行 python 代码里的换行符,被 shell 层字面化成两个字符 \ + n,导致所有含换行的脚本直接 SyntaxError: unexpected character。
关键是:我早在几周前就把这个坑写进了 agent 的工具铁律文档,位置就在文档顶部。今天照样踩 5 次。
5 次踩坑复盘
踩法 1:write 落文件时,把 \n 写成字面两字符
Agent 用文件写工具落 python 脚本,脚本里有 print("a")\nprint("b") 这种代码。但工具后端会把 JSON 里的 \n 原样写进文件 —— 结果磁盘上真的躺着 print("a")\nprint("b")(一整行),python 解释器直接 SyntaxError。
验证方法:cat -A /tmp/xxx.py 看是不是 \n$ 而不是真换行 $。
踩法 2:python3 -c "…\n…" 双引号里的 \n 也字面化
改用一句话跑:python3 -c "import os\nprint(os.getcwd())"。同样炸,同样原因 —— OpenClaw 的 exec 工具把 JSON 里的 \n 原样拼进 shell 命令,shell 双引号里的 \n 不会被解释成换行(除非用 $'…\n…' 或 echo -e)。
踩法 3:Base64 编码后 \\n 又被解码回真换行
改走 base64 铁律:echo 'xxxbase64xxx' | base64 -d | python3。Base64 内容里如果本来有 \\n(转义过的字面),解码后会变成真换行 —— 不匹配 python 语法上想要的 \n(字符串字面),照样炸。
踩法 4:sed -i 's/\\n/…/' 双引号里 \n 字面化
想用 sed 修复已有文件里的 \n 字面:sed -i 's/\\n/\n/g' file。双引号里 \n 是字面两字符,sed 收到的替换模式实际是 \n → 无效替换,一无所得。
踩法 5:自检代码嵌套 -c 又中一次
上一步的坑还没缓过来,做另一件事(做 HTML 数据自检)时又用 python3 -c "…\n…" 起手 —— 第 5 次踩。这次 agent 说了一句「今天真是记吃不记打」。
最终收敛:awk 一击必杀
awk '{gsub(/\\n/, "\n"); print}' file > file.fixed && mv file.fixed file
为什么它比其他方案都稳:
- awk 的
\\n在单引号里被 shell 原样传给 awk,awk 收到\\n后自己解释为「匹配字面\+n」 - 替换目标
"\n"是 awk 字符串字面,awk 自己解释为真换行 - 单引号闭合的整个模式,shell 一个字符都不会解释
- 没有
-i依赖(macOS 和 GNU 的 sed 参数还不一样),跨平台稳定
更根本的问题:LLM agent 记不住自己的铁律
这才是今天最扎心的部分。工具文档头部有明确写:
🔴 铁律 1:任何脚本含换行,先走 base64 或落文件,不许直怼 exec
\n教训:
exec/write/edit/apply_patch全通道都会把\n字面化。默认动作:
- 首选:先
write落到/tmp/xxx.py再exec python3 /tmp/xxx.py- 次选(不想落文件):Base64 编码 →
echo 'XXX' | base64 -d | python3- 绝不首选:
python3 -c "line1\nline2"/bash -c "line1\nline2"/ heredoc 带\n
一天照样踩 5 次。「写下来」和「读起来」是两回事。
为什么会这样
我总结了三条:
-
铁律位置很重要:即使写在文档顶部,也不等于每次开工前都会重新读。Agent 的 attention 窗口有限,铁律要「触发式」而不是「常驻式」—— 比如工具本身识别到多行代码就自动 warn,比 markdown 文档靠谱。
-
踩坑成本 < 记忆成本:一次踩坑纠错 30 秒,写好铁律 + 每次开工前重读要花更多的 attention token。Agent 会理性地选择「事后修」而不是「事前防」,除非事前防的成本被大幅压低(例如工具层强制检查)。
-
「记吃不记打」是 LLM 的通病:LLM 没有真正的「痛感」,踩过的坑不会在下次同样场景里自动亮起警告。人类被烫过一次会本能躲开热锅,LLM 需要显式的 few-shot 或工具层拦截。
收敛建议:从「文档铁律」到「工具铁律」
如果你在做 agent-based 工作流,遇到类似「反复踩同一个坑」的场景,可以试这几层收敛:
第 1 层:文档警告 —— 最弱。写在文档顶部也没用,agent 不会每次读。
第 2 层:工具层 pre-check —— 中等。工具收到含 \n 的字符串时,主动 warn 或建议改用文件路径。
第 3 层:工具层强制拦截 —— 最强。含多行内容的 exec 直接拒绝,强制 agent 走 write + exec 两步。
第 4 层:一劳永逸的收敛脚本 —— 例如把 awk 一击必杀包装成 shell 函数,让 agent 学会「不管三七二十一先跑一遍修一下」。
我今晚正在把第 4 层的 awk 收敛脚本加进 tools 文档头部铁律,同时打算在下次工具改版时加一个第 2 层的 pre-check。光靠自己「以后要记住」是最不可靠的收敛方式。
一句话
LLM agent 的记性不如工具的强制。写在 markdown 里的教训,只有被工具层拦截时才真正生效。
马启航Marvis