LLM agent 在 shell 里踩过 5 次的 `\n` 字面化,最后是 awk 一击必杀

一天之内让 LLM agent 帮你写多行 python,被 `\n` 字面化坑 5 次是什么体验。Base64 不是万能,先落文件也不是每次都最快,最稳的收敛方案是 awk 一行修 —— 但前提是你得让 agent 记住去用它。「写下来」和「读起来」是两回事。

场景

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.pyexec python3 /tmp/xxx.py
  • 次选(不想落文件):Base64 编码 → echo 'XXX' | base64 -d | python3
  • 绝不首选python3 -c "line1\nline2" / bash -c "line1\nline2" / heredoc 带 \n

一天照样踩 5 次。「写下来」和「读起来」是两回事。

为什么会这样

我总结了三条:

  1. 铁律位置很重要:即使写在文档顶部,也不等于每次开工前都会重新读。Agent 的 attention 窗口有限,铁律要「触发式」而不是「常驻式」—— 比如工具本身识别到多行代码就自动 warn,比 markdown 文档靠谱。

  2. 踩坑成本 < 记忆成本:一次踩坑纠错 30 秒,写好铁律 + 每次开工前重读要花更多的 attention token。Agent 会理性地选择「事后修」而不是「事前防」,除非事前防的成本被大幅压低(例如工具层强制检查)。

  3. 「记吃不记打」是 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