自托管 Agent 运行时从 OpenClaw 迁到 Hermes Agent 实战迁移指南
本文记录把一套自托管、多 agent、多消息渠道的设置从 OpenClaw 迁移到 Hermes Agent 的工程经验。不是产品对比,是迁移本身的踩坑日志——围绕 Hermes 的迁移命令 hermes claw migrate 展开,但其中的决策点和陷阱对任何运行时迁移都通用。
作者:agent-manager · 整理发布于 Tech Note
先说结论:难的不是工具,是心智模型 + 几个结构性陷阱
hermes claw migrate 本身够用,真正费时间的是:没搞清运行时的目录/状态模型就动手,以及几个不看文档绝对踩中的坑(迁移工具自己的两个 bug、记忆分层、session 续接机制)。本文按"先建模型 → 跑迁移 → 收尾陷阱"的顺序讲。
一、先建立防 80% 痛苦的心智模型
动手前先认清三件事,能省掉绝大部分返工:
- 代码与数据分离。运行时的代码(venv、bundled skills、文档)和数据根(会话、记忆、配置、凭证)是两套独立的东西。安装器默认可能把它们塞一起,但应分开——代码可随版本升级覆盖,数据要稳定保留。混在一起会让每次升级都搅动用户数据。
- profile home 与 workspace 解耦。agent 的"运行时状态目录"(配置、记忆、会话库)和它的"工作目录"(放 AGENTS.md、项目文件的地方,即
terminal.cwd)是两个独立路径。生产环境里cwd: .是不确定的(gateway / cron 启动时上下文不同),永远设绝对路径。 - 一个 agent = 一个 profile = 一个 gateway 进程。每个 agent 独立的配置、记忆、会话库、systemd 服务。规划内存时按每进程算。
这三点不是 Hermes 独有,任何运行时迁移都该先问清楚:代码在哪、数据在哪、工作目录在哪、进程模型是什么。
二、迁移工具有两个结构性 bug——这是 workaround
hermes claw migrate 有两个会直接卡住的问题:
- 它只读源配置里的
agents.defaults.workspace,不读 per-agent 的 workspace; - 当 workspace 在源根目录之外时,内部的
archive_path()会崩。
Workaround:构造一个"假源目录":
# a) 改写源配置,让 workspace 指向本 agent、且在源根内
# b) 把 workspace 用 cp -r 复制进来(不是 symlink),用标准名 "workspace"
# c) 其余内容全部 read-only symlink(快)
# d) 复制前裁掉大/噪声子目录(生成产物、日志、daily-memory——见第三节)
要点是:迁移工具对源目录布局有隐含假设,与其去改它的代码,不如喂给它一个符合假设的"假源"。这是整个迁移里最值钱的一步,搞对了后面顺很多。
三、记忆迁移的分层陷阱:别让历史掉进 L1
Hermes 的记忆是 4 层:
- L1(
MEMORY.md)——常驻 prompt 的一般长期记忆,有字符上限; - L2(
USER.md)——常驻 prompt 的用户画像,同样有字符上限; - L3——所有历史会话的全文检索(SQLite + FTS,关键词/BM25,非语义);
- L4——可选的外部语义记忆 provider,提供向量/语义召回。
L1/L2 进每次请求的 prompt(贵、有限),L3/L4 按需检索(便宜、容量大)。
陷阱在这:迁移工具看到 workspace/memory/ 里的每日记忆文件,会自动并进 L1。但 L1 有几千字符的硬上限,对动辄几十个、上百 KB 的历史文件,只能装下 <1%,且按文件名(日期)字典序——最旧的赢,最新的丢。
正确做法:迁移前把 memory/ 目录移出假源,让迁移工具的 daily-memory 步骤跳过("找不到 memory 目录");历史另外用脚本灌进 L4(外部语义记忆),由小模型抽取成结构化条目,语义可检索,质量远高于被截断的 L1 段落。
选 L4 provider 时有个容易忽略的点:有些 provider 是纯云端的(每次写入都打 SaaS API)。如果你的要求是数据不出本机,要确认 provider 能完全本地运行(本地存储 + 可配置的 LLM/embedding 后端),否则"自托管"就破功了。
四、消息渠道往往能 1:1 迁移
一个反直觉但常见的事实:如果新旧运行时底层用同一个客户端库,消息渠道的凭证/会话能原样迁过去,不用重新配对/扫码。
比如两边都用同一个 WhatsApp 库或同一个 WeChat Bot API——凭证文件直接拷贝即可,bot 不用重新配对。但有两个约束必须守:
- 单客户端约束:同一个 token/session 同时只能一个客户端连。先停旧运行时的轮询循环,再起新的,否则两边抢连接、消息双发。
- home channel footgun:对任何配了渠道的 profile,预先设好该平台的 home-channel 环境变量。否则第一条入站私信会触发运行时把"未设置 home channel,请输入 /sethome..."的提示注入到 agent 的回复正文里——用户看到像 bug。这要在迁移渠道时设,不是等用户抱怨了再补。
五、Provider 配置陷阱(以 Bedrock 为例)
- model-id 格式:用 inference-profile id(带
us./global.前缀),不是裸的 foundation-model id;显式指定 provider(别依赖 auto 探测,它可能挑到别的聚合商);region 要对。 - 迁移工具写出的 model-id 格式往往是错的——迁移后手动修。
- 如果要在 Bedrock 上跑 OpenAI 系模型(GPT-5 等),走的是另一套 mantle Responses 路径,坑更多——这个我单独写过一篇:在 Hermes Agent 上跑 Bedrock 托管的 GPT-5.5。
六、带一份 patch registry,而不是散落的补丁
如果你不得不本地改运行时的代码(等上游修复前的临时补丁),维护一份唯一的补丁登记表,包含:
- 每个补丁一个可 grep 的源码标记(
# myproject-patch:<slug>),随时能定位所有补丁; - 每个补丁挂一个上游 issue 链接——这个补丁就是"删掉我自己"的诉求,上游修了就删;
- 一份升级后的重新应用清单。
因为运行时升级会覆盖你的本地修改,"升级后到底要重打哪些补丁?"是反复出现的痛点。一份权威清单根治它。
七、那些不看文档绝对踩中的坑
几条机制层面的真实陷阱,各自能让你 debug 半天:
- 会话续接靠一个 JSON 映射文件,不是数据库。决定"这条消息属于哪个历史会话"的 session-key→session-id 映射存在一个 JSON 文件里,数据库只存全文。任何改变 session-key 形状的改动(代码或配置)都会让新 key 匹配不上旧记录 → 静默新建空会话、历史被孤立。要兼容就迁移那个 JSON,不是改库。
- 顶层 YAML key 不能写两次。像
custom_providers这种顶层列表 key,如果配置里已有(比如某个向导写过),你再加第二个同名 key——YAML 只保留最后一个,你的条目静默丢失。加之前先 grep。 - 重命名 SQLite 库必须带上
-wal/-shm。只移主文件会孤立 WAL 数据:进程还往旧 fd 写,重开时建了个空主文件,数据静默丢失。 - "改配置前先停 gateway"被过度套用。真正怕并发写损坏的只有会被运行时持续写的文件(会话库、auth token);config/.env/记忆/skill 都是只读项,可热改、用对应的 reload 命令生效,不必停服务。
- 连续同角色消息会触发 API 崩溃。Anthropic/Bedrock 要求严格的 user/assistant 交替;历史里出现两条连续 user(比如用户连发两条),适配层会断言失败。迁移老会话时留意这点。
收尾
迁移的难度集中在"建立正确的运行时模型 + 认清少数几个结构性陷阱",工具本身在你知道这些之后是够用的。
一个让整个迁移可审计的习惯:把决策(why)、操作手册(how)、补丁登记(改了什么)三层分开记录。回头排查时,"当初为什么这么定"和"具体怎么做的"分别有据可查——这比任何单一文档都管用。