背景

我在服务器上跑着一个叫 Hachimi 的 AI 助手——基于 OpenClaw 平台, 接入 Telegram,平时帮我处理各种日常任务、控制 Windows Chrome 浏览器。 某天下午,她突然开始出 bug:每次调用工具,系统就把原始的 tool prompt 原封不动地打印出来。

症状分两种。第一种是工具调用的原始 JSON 直接打印出来:

{"type":"tool_use","id":"toolu_01...","name":"exec","input":{"command":"..."}}
{"type":"tool_result","tool_use_id":"toolu_01...","content":"..."}
...

第二种更诡异——回复里开始出现大量 </s> token:

你说得对,我刚才出了问题——回复里出现了大量 </s> token,这是模型异常的表现。

让我现在正常调用一个工具试试:
</s>
</s>
</s>

</s> 是序列结束标记(end-of-sequence token),正常情况下不应该出现在输出文本里。 它的出现说明上下文里混入了不属于当前对话的内容,模型在试图"结束"某个它以为还没结束的序列。

聊天里开始出现大段 JSON 和乱码,完全没法正常对话。 上下文被污染了,而且越滚越大,后面的消息几乎没有可用的 token window。 我试了几次 /new 重置会话,无效;试了重启 gateway,还是一样。

最后决定:重装

系统架构(重装前)

Telegram
    ↕
Hachimi(Claude Sonnet)
    ↕  OpenClaw Gateway(AWS EC2)
    ↕  WSL2 Node Host(Windows 本地)
    ↕  Windows Chrome CDP
         ↕  reverse SSH tunnel
    服务器本地端口

整套系统分散在三个地方:服务器上的 gateway、WSL2 里的 node host、 以及 Windows Chrome 的 CDP 接口。 重装意味着不只是重跑一个命令——所有这些连接都要重新拉起来。

02 症状与根因

具体表现是:每次 AI 调用工具(exec、web_search 等), Telegram 里就会收到原始的 tool call/result JSON 字符串, 而不是 AI 处理过的回复。

这类问题通常是上下文组装出了问题——tool result 被错误地插入消息序列, 导致后续轮次的 prompt 里带着上一轮的 raw 结构。 本质上是 session history 里出现了 tool_usetool_result 的错位, provider 端拒绝或报错,gateway 把错误原样返回。

重置会话无法解决,因为底层 session history 的存储格式已经损坏。 最干净的办法是从头来过。

有意思的是,OpenClaw 的 gateway 代码里本来就有一层防御: 在把消息发到 Telegram 之前,会主动过滤掉 <function_calls><function_response> 标签:

async sendMessage(chatId, text, options = {}) {
  text = text
    .replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "")
    .replace(/<function_response>[\s\S]*?<\/function_response>/g, "")
    .trim();
  // ...
}

说明这类污染不是第一次有人遇到——gateway 已经预设了清洗逻辑。 但当上下文损坏到一定程度,光靠过滤已经不够了, </s> 这类裸 token 不在过滤范围内,照样透传出来。

03 重装过程

Step 1 — 拉取 workspace

Hachimi 的所有配置文件、memory、自定义技能都存在一个私有 GitHub 仓库里。 这是最关键的一步——先把「记忆」拿回来。

cd /home/ubuntu/.openclaw/workspace
git init
git remote add origin https://github.com/<user>/<repo>.git
git fetch origin
git reset --hard origin/master
git branch --set-upstream-to=origin/master master

拉完之后,workspace 里就有了 MEMORY.mdSOUL.mdmemory/ 历史日记、以及几个自定义 skill(self-commit、self-followup、session-resume)。

Step 2 — 配置 openclaw.json

OpenClaw 的核心配置文件是 ~/.openclaw/openclaw.json。 新安装后这里是空白的,需要手动填回来。 主要包括:

  • 主模型配置

  • Gateway 端口和 auth token

  • Telegram bot token 和频道白名单

  • 各类 API key(LLM、搜索、语音识别、Notion 等)

  • memory-lancedb 插件(向量记忆)

大部分 key 之前存在 pass 里,直接读取。 顺便把所有 key 都备份进了 AWS SSM Parameter Store:

aws ssm put-parameter --name "/mybot/<key-name>" \
  --value "..." --type SecureString --overwrite

Step 3 — 重启 gateway,验证插件

systemctl --user restart openclaw-gateway
openclaw status

确认输出里有:

Memory   │ enabled (plugin memory-lancedb)
Telegram │ ON  │ OK

这次配置里有个小坑:memory-lancedb 的配置字段之前用的是 autoCaptureautoRecall 直接放在 entries 下, 但正确格式是放在 entries.memory-lancedb.config 里。 gateway 的 config validator 报错后才发现。

Step 4 — 导入历史记忆

LanceDB 是 lazy init——gateway 启动后数据库是空的, 需要把之前的 memory 文件重新向量化入库。

NODE_PATH=/home/ubuntu/.npm-global/lib/node_modules \
OAI_KEY='sk-proj-...' \
node /tmp/import_memory.js

脚本遍历所有 memory/*.mdMEMORY.md, 按 800 字切块,调 OpenAI text-embedding-3-small,写入 LanceDB。 最终入库 150 条记录。

Step 5 — 重建 cron 任务

cron 任务没有存在 workspace 里(这是个教训),重装后全部丢失,需要重新添加:

openclaw cron add --name "my-task" \
  --cron "0 9 * * *" --tz "America/New_York" \
  --session isolated --announce --channel telegram \
  --timeout-seconds 300 \
  --message "执行任务..."

一个实用建议:把 cron 任务的定义也存到 workspace 的某个文件里, 重装后直接脚本化重建,而不是凭记忆一条条敲。

04 重连 WSL2 节点

连接方式

Hachimi 控制 Windows Chrome 的方式,是通过 WSL2 里跑的 OpenClaw node host, 加上一条 SSH reverse tunnel 把 Chrome CDP 端口映射到服务器:

# WSL2 本地:SSH tunnel + node host
ssh -fN -L <local-port>:127.0.0.1:<gateway-port> -i ~/.ssh/key.pem user@server
OPENCLAW_GATEWAY_TOKEN=xxx openclaw node run \
  --host 127.0.0.1 --port <local-port> --display-name "Windows WSL2"

# 服务器验证 Chrome CDP 可用
curl -s http://127.0.0.1:<cdp-port>/json/version

token 因为重装变了,之前 WSL2 的 systemd 服务里硬编码的旧 token 连接失败。 更新 token 之后恢复正常。

exec approval 的坑

Node exec 需要 approval——但 Telegram 频道不支持 approval UI。 默认情况下,从 Telegram 触发的 node exec 会直接报错:

Exec approval is required, but chat exec approvals are not enabled on Telegram.

解决方式是两边同时配:

WSL2 侧~/.openclaw/exec-approvals.json):

{
  "version": 1,
  "defaults": { "security": "full", "ask": "off", "askFallback": "full" },
  "agents": { "main": { "security": "full", "ask": "off", "askFallback": "full" } }
}

服务器侧openclaw.json):

"tools": {
  "exec": { "security": "full", "ask": "off" }
}

另一个坑:如果在 openclaw.json 里全局设置 tools.exec.host = "node", 所有 exec 都会路由到 node,包括需要在服务器本地跑的命令—— 这会导致 gateway restart 等操作也跑到 WSL2,形成循环死锁。 正确做法是按需在每次 exec 调用里指定 host=node,不设全局默认。

05 教训与改进

写一份重装手册

这次重装最大的问题是靠「记忆」——AI 的记忆,还有我自己的记忆。 重装过程里有好几个步骤是靠翻历史 memory 文件和 Telegram 记录拼凑的。

事后在 workspace 根目录新增了 SETUP.md, 把所有步骤、字段、命令、踩过的坑全部写进去。 下次重装按文档走就行。

用 AWS SSM 集中管理密钥

之前 key 分散在 passopenclaw.json、甚至 Telegram 聊天记录里, 重装时找起来非常麻烦。

这次把所有 key 统一存到 AWS SSM Parameter Store, 按服务分命名空间统一管理。

取用时:

aws ssm get-parameter \
  --name "/mybot/<key-name>" \
  --with-decryption \
  --query Parameter.Value --output text

EC2 上的 IAM Role 直接授权,不需要额外的 access key。 这是最后没做完的一步——当前还是靠 root key 访问,后续需要换成 Role-based。

精简 MEMORY.md

重装完之后发现 MEMORY.md 已经膨胀到 300 行, 里面混杂着「运行时要用的信息」和「只有重装才需要看的 setup 步骤」。

做了一次大清理:setup 相关内容全部迁移到 SETUP.mdMEMORY.md 只保留运行时快速查阅的内容(CDP 用法、常用命令、个人偏好、cron 任务等)。 顺便把 20 几个小碎片日记文件归档到 memory/archive/, 提炼出来的信息合并进 MEMORY.md

06 小结

整个重装过程大概花了两个小时,期间 Hachimi 全程参与—— 她在查自己的 memory 文件、找历史配置、调试连接、写恢复手册。 某种程度上,是她在帮自己重装自己。

系统复杂度越高,「可重装性」就越重要。 这次的教训是:任何靠记忆维持的状态,都是脆弱的。 文档、代码化的配置、版本控制——这些不只是工程实践, 对 AI 助手来说,也是持续存在的方式。