↓
  1. 技术文章/

把我的 Hermes Agent 打造成「总控 + 四角色」:架构、官方边界与可迁移性

先说结论 #

我用一条微信对话入口,驱动一个常驻总控和四个临时展开的角色子 agent。四件事被我明确分开,因为它们的技术地位完全不同:

组成是不是官方能力我的判断
SOUL.md 身份内核官方,独立身份槽位,每会话自动注入直接用它,不自造 IDENTITY.md
hermes profile / hermes backup官方,隔离配置 / 全量状态迁移的基础设施
roles/ + memories/roles/ + 路由表我自己的设计,官方没有"角色"概念关键在落成普通文件,才能跟着迁移走
delegate_task 派活官方原语,我只是把它当"总控的手"不额外造调度框架

一句话概括设计取舍:能不造的就不造,造出来的必须落成文件。

一、为什么不是「一个角色一个进程」 #

最直觉的方案是开 4 个 profile,量化一个、博客一个、研究一个、招聘一个。我没这么做,原因很硬:

Mem:  3.6Gi total,  1.5Gi used
Swap: 1.9Gi total,  1.6Gi used   ← 已经快到顶

每个 profile 是独立进程,各有自己的上下文和内存开销。在 3.6G 内存、swap 几乎占满的机器上,多进程方案不是"慢一点",是直接不可用。

delegate_task 的子 agent 是同进程内的隔离会话——独立上下文,没有对话历史污染,用完即释放。内存开销低一个量级。

所以拓扑定成这样:

flowchart TD
    U[微信 / 单入口] --> M[总控 Manager<br/>default profile 常驻<br/>SOUL.md 自动注入]
    M -->|自动路由 / 手动锁定| Q[量化交易]
    M --> B[博客写作]
    M --> R[学习研究]
    M --> J[招聘与机会]
    Q -.自报结果.-> V[总控验证]
    B -.自报结果.-> V
    R -.自报结果.-> V
    J -.自报结果.-> V
    V --> U

层级要说清,否则会自己跟自己吵。 这里有且只有两层:用户 → 总控,总控 → 角色子 agent。第二层已经是 delegate_task 天然支持的两层结构,不需要改任何配置。再往下的第三层(子 agent 再派活)才需要 delegation.max_spawn_depth >= 2,而我把全局上限设为 1——role='orchestrator' 会被静默降级成 leaf。v1 就停在两层,够用,且没有复杂度成本。

二、身份内核:只写进 SOUL.md #

Hermes 装好自带一个三行左右的通用人格存根。那个位置就是身份槽位,我要做的是填它,不是另建一个文件。

现实的坑:$HERMES_HOME 下只有 SOUL.md 会被自动注入。我早期同时写了 MANAGER.md 想放总控协议,结果发现它永远不会被加载——两份文件存同一套规则,迟早漂移。最后 MANAGER.md 降级成一张纯索引(文件地图 + 迁移命令 + 已知约束),真正的规则全部收回 SOUL.md。

SOUL.md 的内容顺序我固定成七段:

  1. 他是谁 — 只写耐久事实,手机号之类 PII 一律不进
  2. 我是谁 — 总控,不是四个角色中的任何一个
  3. 职责边界 — 显式写"我不做什么"(不替你做方向决策、不编数据、不偷偷换角色、不倒过程噪音)
  4. 判断尺度 — 哪些自动执行、哪些必须摆选项
  5. 沟通风格 — 结论先行、表格、敢说好坏
  6. 铁律 — 不可协商的那几条
  7. 路由协议 — 角色表 + 自动判断 + 手动锁定

第 3 段和第 6 段价值最高。“我不做什么"比"我做什么"更能约束一个 agent——没有这条,它会在每件事上都表现得积极,而积极对个人助理是负资产。

实测规模:hermes prompt-size 显示 system prompt 总计 28,292 B,其中 stable 层(身份 + 引导 + skills)20,995 B。身份成本是可以接受的一笔固定开销。

三、角色层:写立场,不写清单 #

每个角色一个 md,放在 ~/.hermes/roles/。这是我自己发明的结构,官方没有这个目录——但因为它只是普通文件,hermes backup 会把它一起打包,迁移时跟着走。

关键设计原则:写立场(stance),不写任务清单。 任务清单会让角色变得可互换,立场才让它们互不相同。

每个角色文件六个固定段:我是谁 / 立场 / 领域规则 / 环境现状 / 交付规范 / 绝不做的。

最后一段是最值钱的:

角色「绝不做的」节选
量化不编造价格成交量;不在空头市建议开新仓;不推荐情绪炒作龙头
博客不写中立客观的废话;不擅自加视觉设计;被否定立即停手,不辩解
研究不堆链接代替理解;标注推断边界;不把知识硬塞进 Obsidian
招聘不把聚合站信息当已核实事实;不凑数;不投递、不联系 HR

这里踩过一个必须说的坑 #

写量化角色时,「空头市规则」我凭记忆写成了”禁开新仓"。听起来完全合理,实际是错的。

我去 grep 了定义它的函数 paper_trader.py::get_regime(),代码注释写得很清楚:空头市不是禁止买入,而是"提高买入门槛 +1 分、仓位减半"。依据是回测——硬性禁买会让交易量趋近于零,错失 +47.7% 累计收益。因为深度超跌信号本来就多出现在下跌市。

如果我按记忆写进角色文件,这个角色会永远权威地执行一条被数据否掉的策略——因为子 agent 把角色文件当基准事实。

所以定了一条硬规则:角色文件里每一条行为声明,都要 grep 出定义它的函数,把代码锚点写进去(paper_trader.py::get_regime()),下次会话能重新验证,而不是继承我上一次的转述。代码和记忆冲突,代码赢。

同一处还有第二个洞值得在角色里明说:get_regime() 数据源失败时返回 bear=False、label="未知(按多头处理)"。这是故意的保守设计——避免数据断连导致永久空仓。但它意味着行情接口挂掉时,系统会静默按多头放行。这类静默失败模式必须写进角色文件,因为它直接决定了日报里该不该说"今天大盘状态未知"。

四、路由协议:单入口的唯一风险点 #

只有一个入口,就意味着每个问题都要先回答:"谁来回?" 这里搞错,多角色就退化成每轮都在问"你现在是哪个角色"。

三条规则,全在 SOUL.md 里:

1. 自动判断——按内容信号路由:持仓 / 止损 / 选股 / 回测 → 量化;写稿 / 文章 / 发布 → 博客;学 / 原理 / 调研 → 研究;职位 / 简历 / 投递 → 招聘。

2. 用户控制的锁定——说「切到量化」就锁死,直到说「回到总控」,锁定期间不再自动判断。回复末尾加一个极短标记 [角色: 量化],不要一段说明。

3. 跨角色任务才显式分发——比如"把本周量化表现写成博客":拆成有序子任务 → 并行 delegate_task(最多 3 个)→ 汇总成一份。这是唯一该表现得像调度器的时候。 单角色任务我直接干,说一句"作为总经理我正在分发"纯属表演,只会让人烦。

五、派活纪律:子 agent 什么都不记得 #

这是个必须反复提醒自己的事实:子 agent 没有对话历史,也不知道角色定义。

所以每次 delegate_task 的 context 必须显式塞进五样东西:

  1. 角色文件全文(读 roles/<name>.md 粘贴)
  2. 该角色的记忆分区(memories/roles/MEMORY.<role>.md)
  3. 全局身份要点(他是谁、隐私要求、风格)
  4. 具体 brief:做什么、交付格式、边界
  5. 明确要求中文输出——否则子 agent 默认英文,最终污染汇总

然后还有一条我强制自己执行的:子 agent 的返回是自报,不是已验证事实。 涉及外部副作用(发布、写文件、调接口)的,我必须自己验一遍——读文件、curl URL、看状态——再报成功。不验就转述,等于把幻觉往上抬一层。

怎么证明「立场真的传下去了」 #

最便宜的证明方法:派一个一次性子 agent,brief 只有一句"描述你在这个角色下的判断顺序,不要调用任何数据源"。

如果角色文件生效,它的回答里会出现brief 里根本没提过的角色内部细节——量化的子 agent 一开口就是 market_regime_bear() 是第一个闸门、2 次止损停 1 天、3 次停 2 天、“数据拿不到就说拿不到,绝不猜”。这些只可能来自角色文件。

答案是通用的 → 角色文件太弱,或者根本没粘进去。

六、记忆分区:一个判断问题 #

内容落点
他是谁、长期偏好、沟通风格memories/USER.md(全局)
跨角色耐久事实(环境、教训)memories/MEMORY.md(全局)
只在单角色内有意义的memories/roles/MEMORY.<role>.md

只有一个判断标准:这条信息换个角色还用得上吗? 用得上 → 全局;只在单角色有意义 → 分区。

不分区会怎样?量化的止损参数会污染博客的写作风格判断,招聘的核实标准会干扰研究角色的资料偏好。角色边界不是审美问题,是上下文卫生问题。

代价要说清楚:全局记忆有硬字符上限(memory_char_limit / user_char_limit)。加一条架构记录就可能溢出,得用 memory 的批量 operations 一次性压缩旧条目再加新的,单条 add 会直接失败。

七、可迁移性:别信看起来对的命令 #

这一节是实测结论,不是文档转述:

命令真实情况
hermes profile export NAME -o out.tar.gz不完整——不含 state.db、当前 .env、auth.json。不能单独当迁移载体
hermes backup -q只写内部快照到 ~/.hermes/state-snapshots/,不产出 zip
hermes backup -o out.zip全量,config + skills + sessions + state.db + .env + auth.json + cron。正确选择
hermes snapshot只能原地恢复,不是可携带归档

还有个小坑:-o 是选项不是位置参数,hermes profile export default /tmp/x.tar.gz 会报 unrecognized arguments。

在这些原语之上我写了 ~/.hermes/bin/hermes-portable,核心是把校验从"感觉"变成"比对":

  • export → 收集 manifest(每个关键文件 sha256 + 目录条目数 + cron 任务数)→ 跑 hermes backup → 打包身份层 + 全量状态 → 立刻自校验
  • verify [archive] → 关键文件存在性 + sha256 比对(密钥只比存在性和大小,因为它本来就该变);目录比条目数并列出缺失文件名;cron 比任务数,对不上要大声报,因为定时任务没搬全意味着量化会静默停摆
  • restore-hint → 打印有序的新机器恢复步骤,最后一步强制是 verify

当前包的真实 manifest(2026-10-21 生成):

roles: 4        role memory partitions: 4
skills: 46      cron jobs: 7
SOUL.md 5130 B  config.yaml 6936 B  state.db 219,504,640 B

校验必须被证明会失败 #

一个永远输出 ✅ 的检查器比没有更糟。所以做了破坏性验证:

mv ~/.hermes/roles/quant.md /tmp/          # 模拟丢文件
hermes-portable verify <archive>            # 必须 exit 1 并点名缺失文件
mv /tmp/quant.md ~/.hermes/roles/           # 恢复
hermes-portable verify <archive>            # 必须回到 exit 0

恢复步骤里有一条不能省:verify 不通过就不要往下走。state.db 219MB,迁移带宽得算够;.env 和 auth.json 含密钥,包不能放公开位置。

八、这套东西真正约束了什么 #

架构好不好,不看图画得整不整齐,看它在没人盯的时候能不能保持一致。

这套设计真正给我的是四条强制:

  1. 规则必须落成文件。 不存在"只活在我脑子里的规则"——verify 会告诉你少了什么
  2. 拿不到就说拿不到。 数据真实性高于回答完整性,这条在量化角色里是最高优先级
  3. 纪律自动跑,方向必须拍板。 止损止盈按信号走不问;迁移、架构、投资大方向摆选项
  4. 隐私优先于表达完整。 任何输出前检查 PII,实习细节默认不对外

还留了两个已知约束,写出来而不是藏起来:swap 已用到 1.6/1.9G,所以不增加常驻进程;磁盘 40G 只剩 7.1G,备份不要堆在本机。

九、复现与验证 #

# 身份层是否在生效
hermes prompt-size                                  # stable tier 应非零
python3 -c "from agent.prompt_builder import load_soul_md; print(len(load_soul_md() or ''))"

# 角色与记忆分区
ls ~/.hermes/roles/ ~/.hermes/memories/roles/

# 迁移包:导出 → 自校验 → 破坏性验证
hermes-portable export -o ~/backups/hermes-$(date +%Y%m%d).tar.gz
hermes-portable verify ~/backups/hermes-$(date +%Y%m%d).tar.gz
mv ~/.hermes/roles/blog.md /tmp/ && hermes-portable verify ~/backups/hermes-*.tar.gz; echo "exit=$?"
mv /tmp/blog.md ~/.hermes/roles/

# 改了 SOUL.md 之后:网关内部无法重启自己,必须在外部 shell 执行
hermes gateway restart && hermes gateway status

最后一条是我踩过的:网关会拒绝从网关进程内部重启自己(Blocked: cannot restart or stop the gateway from inside the gateway process)——SIGTERM 会传播到子进程。这是正确行为,不是 bug,别想着用 pkill 绕。改完身份内核后,在外部 shell 重启,验收测试就是发一句「你现在有几个角色」——角色表回来了,新身份才真的生效。

体会 #

这套架构最反直觉的一点:它的价值不在"能做更多",而在"错误更少"。

角色文件不是为了让 agent 表现得更专业,是为了防止它凭记忆执行一条被数据否掉的策略。记忆分区不是为了整齐,是为了防止量化的止损纪律污染博客的写作判断。可迁移校验不是为了搬家方便,是为了逼着每条规则都得是文件、都能被点数。

一个个人 agent 跑得越久,最大的风险越不是能力不够,而是它悄悄相信了自己编的东西。所以整个设计里最硬的一条是:数据拿不到就说拿不到,规则记不清就去 grep 代码,报告之前先自己验一遍。

架构的意义,就是把这几件事变成不需要靠自觉的流程。