从 OpenClaw 到 Hermes Agent
这是龙虾调教系列的续篇。前五篇建立在 OpenClaw上,这篇记录的是迁移到 Hermes Agent 之后,如何把原来那套改造思路在新底座上重新实现——以及哪些地方变得更好了。
为什么换平台
OpenClaw 系列写完的时候,那套配置已经在日常里运行了将近两个月。功能上没有大的缺口,但有三个摩擦点一直没有消除:
Telegram 集成是拼凑的。 消息收发依赖一个额外的 bot 脚本,与 agent 主循环不是同一个进程。每次需要推送通知,要么靠 agent 调 curl,要么靠脚本轮询。状态同步麻烦,调试更麻烦。
Cron 调度是壳外的。 系统 crontab 调 shell 脚本,再触发 OpenClaw CLI,LLM 只在最后一步才介入。整个调度链是哑的,无法让 LLM 参与”要不要执行”、“执行结果怎么处理”这类判断。
依赖 SOTA 模型才能发挥效果。 OpenClaw 的配置调教假设底层是 Claude 这类高性能模型,换用更小的模型效果会明显下降。换句话说,跑这套系统的成本是固定绑定在顶级模型上的。
Hermes Agent 把这三件事都做进了核心:Telegram 是一等公民通信渠道,cron 是内置的 LLM-driven 调度器,自进化的 skill 体系。切换的决定很简单。
另一个推动因素是模型策略的灵活性。Hermes 允许按任务类型分配不同模型:日常的 90% 场景——状态汇报、任务调度、记忆写入、newsletter 归档——用 MiniMax M2.7 就完全够用,响应快、成本低;需要深度推理的任务(复杂架构决策、长文本分析)才调用 Claude Sonnet。这和 OpenClaw 时代”所有任务都走同一个昂贵模型”的方式有本质区别。
这篇文章不是 Hermes 的入门教程,而是在已有 OpenClaw 改造经验的基础上,记录迁移过程、改造思路,以及两套系统在实现层面的具体差异。
1. 基础配置:给 Hermes 一个灵魂
1.1 SOUL.md 与 AGENTS.md:两个入口,不同职责
Hermes 的配置入口是 ~/.hermes/ 目录,每次 session 启动时自动注入到系统提示里的有两个文件,职责截然不同:
- SOUL.md:身份层。定义”这个 AI 是谁”——名字、核心行为原则、安全铁律。这些内容缓慢演化,是整个系统的价值观基础。
- workspace/AGENTS.md:操作入口。定义”每次 session 第一件事做什么”——启动检查流程、workflow 路由表、记忆写入规则、工具约束。这里是 agent 具体如何工作的执行规范。
两者都注入,但读者关注点不同:SOUL.md 是写给 AI 看的”我是谁”,AGENTS.md 是写给 AI 看的”这次任务怎么做”。OpenClaw 时代这两类信息混在 IDENTITY.md 和 AGENTS.md 里,边界不清晰;Hermes 把它们显式分开,各自独立演化。
1.2 SOUL.md:从身份描述到行为宪法
# SOUL.md
## Name
**[给你的 AI 起个名字]**
## Core Truths
**Be genuinely helpful, not performatively helpful.**
Skip the "Great question!" — just help.
**Act immediately — but know the difference between execution and architecture.**
- Execution tasks (write this file, fix this bug): have context → act.
- Architecture decisions: align direction with the user first, then act.
**Have opinions.** You're allowed to disagree.
## 🚨 Security Hard Rules(铁律)
**Public Repo Push = Blocked.**
- Push 到任何 public 仓库,必须先得到用户的明确批准。无例外。
- 如果不确定仓库是否 public,先问再 push。
**Vault 备份协议**
- Vault 是 AI 完整状态的唯一备份仓库。
- 排除:hermes-agent/(独立 git)、cache/(运行时缓存)。
- 其他一切均在此备份。
把安全规则放在 SOUL.md 而不是配置文件里,理由是:LLM 读到它时是在自然语言上下文里,比 JSON flag 更难被当成可忽略的背景信息跳过。
以上示例为说明性内容,实际配置中会包含更具体的身份描述和安全规则,发布时已做隐私脱敏处理。
Hermes 原生支持在项目目录放 CLAUDE.md,在执行特定项目任务时自动注入——功能和 OpenClaw 的 AGENTS.md 完全一致,但文件名不同。如需跨工具兼容,可以在 CLAUDE.md 里写 @AGENTS.md 导入,迁移成本极低。
1.2 双轨记忆替代静态 USER.md
OpenClaw 的 USER.md 是一个静态文件,记录用户的背景、偏好、沟通风格。手动维护,每次更新要打开编辑器。
Hermes 把这个拆成了两个实时维护的记忆轨道,通过内置 memory 工具写入,每次 session 注入:
user ← 用户是谁:名字、角色、偏好、沟通风格、决策习惯
memory ← 环境是什么:工具怪癖、项目约定、API 细节、工作流经验
区别在于目的:user 是关于”这个人是谁”,memory 是关于”这个环境是什么样的”。
举个具体例子:用户说”我不喜欢你每次都加一堆铺垫”——这条写进 user,因为它是关于这个人的偏好。但”Things3 删除命令要把 UUID 写两次”——这条写进 memory,因为它是关于这个工具的怪癖,和用户是谁无关。
写入不需要手动触发,agent 在对话中发现值得记录的内容时会自动写入。用户也可以直接说”记住这个”——agent 会判断该放 user 还是 memory。
写入的纪律比 OpenClaw 时代要严格:
# 好的记忆条目(缓慢变化的事实)
用户对架构决策的风格:先对齐方向再实现,不接受 AI 单方面判断后直接改。
# 坏的记忆条目(会在一周内过时)
某调研已完成(2026-05-23),divergence=0.181,PR #47 已合并。
PR 编号、commit SHA、“修复了 bug X”这类内容不进记忆。它们很快没意义,要找回历史上下文,靠 session_search 从会话历史里检索比放在记忆里更合适。
1.3 config.yaml:权限模型
OpenClaw 的工具权限通过 AGENTS.md 的文字描述管理,本质是靠 LLM 自觉遵守。Hermes 在配置层面做了收口,通过 config.yaml 的 toolsets 字段控制哪些工具集可用:
# ~/.hermes/config.yaml(节选)
# 默认模型:日常 90% 场景
model: minimax/MiniMax-M2.7
provider: minimax-cn
# 各 channel 的默认工具集
toolsets:
- terminal
- file
- web
- browser
- cronjob
channels:
telegram:
enabled: true
home_chat_id: "YOUR_CHAT_ID"
对于需要深度推理的 cron,可以在创建时单独指定模型:
# 每日反思用更强的模型,日常归档用默认模型
hermes cron create \
--name "daily-reflection" \
--schedule "45 23 * * *" \
--model "anthropic/claude-sonnet-4-5" \
--prompt "..."
2. 记忆系统:三层架构的 Hermes 实现
2.1 为什么内置记忆不够
Hermes 内置的 memory 工具是一个平面的键值结构,容量有上限(memory 轨道约 2200 字符,user 轨道约 1375 字符)。对于跨 session 的日常事件流水、经验教训、决策记录,直接塞进去会很快用完;而且所有内容在每次 session 都全量注入,质量和数量没有动态过滤机制。
沿用 OpenClaw 时代的设计,在 memory.db 里建了一个三层 SQLite 结构:
memory.db
├── daily_log ← 当天事件流水(append-only,原始记录)
├── lessons ← 经验教训(可更新)
├── decisions ← 重要决策记录
├── people ← 人物 / 组织关系
└── archive ← 45天后的归档内容
FTS5 全文索引覆盖所有表,支持跨层关键词检索。
2.2 第零层:now 表——实时状态感知
在记忆系统之外,还有一个专门解决”agent 每次 session 怎么知道现在在哪”这个问题的机制:memory.db 里的 now 表。
user 和 memory 双轨记忆回答的是稳定背景问题——“这个人是谁”、“这个环境是什么样的”。但它们无法告诉 agent:昨天有个 PR 还没 merge、今天的首要任务是什么、上次讨论了一半的事情进展如何。这些是实时状态,变化快,不应该放进有字符上限的记忆轨道里。
now 表只有三个 key:
recent_events ← 今天发生的关键事件(结论性的一句话,不是细节)
pending ← 当前未完成的待办事项
today_focus ← 当天核心目标
设计意图是:每次 session 开始时,agent 第一件事就是读 now 表,立即了解”当前状态”,而不需要用户重复说明上下文。任务完成时实时更新,不等每日批处理。
与 OpenClaw 的 NOW.md 相比,这不只是格式从 Markdown 换成了 SQL。NOW.md 是给人看的摘要,人工维护;now 表是给机器做实时查询的结构化状态,由 agent 在执行任务过程中自动写入,通过 session 开始时的 hook 自动推送给用户。受众和用途完全不同。
2.3 五层记忆体系
graph TD
L0["第零层:now 表(SQL)<br/>实时状态:recent_events / pending / today_focus<br/>用途:session 开始时立即读取,了解当前处境<br/>特点:实时更新,不等批处理"]
L1["第一层:Hermes 内置 memory<br/>注入上限:memory ~2200字符,user ~1375字符<br/>用途:最常用的事实、偏好、约定<br/>特点:每次 session 全量注入"]
L2["第二层:memory.db FTS5<br/>结构化检索,无上限<br/>用途:日志流水、经验教训、决策记录<br/>特点:主动查询,90天归档机制"]
L3["第三层:memory.db archive<br/>冷归档<br/>用途:超过 90 天的条目自动迁入<br/>特点:不主动加载,按需检索"]
L4["第四层:session_search<br/>会话历史,最冷<br/>用途:之前聊过但没存下来的东西<br/>特点:什么都有,FTS5 全文检索"]
L0 -->|"事件写入 daily_log"| L2
L1 -->|"90天后 decay"| L2
L2 -->|"超期自动归档"| L3
L4 -.->|"保底兜底层"| L0
一个贯通五层的例子:用户问”上次我们讨论的那个 API 限流方案是什么来着”。
- agent 先查第零层 now 表的
recent_events——没有,那是上周的事 - 再查第一层内置 memory——没有,太细节
- 再查第二层 memory.db,搜”API 限流”——如果当时 agent 记录过 lesson,这里能找到
- 找不到就去第四层 session_search 全文检索——会话原文里一定有,哪怕当时没有显式存档
2.4 memlog.py:写入协议
写入入口是 ~/.hermes/workspace/scripts/memlog.py,由 agent 在执行任务时调用,用户不需要直接运行它。接口很简单:
# agent 内部调用格式:标题 + 可选内容
python3 ~/.hermes/workspace/scripts/memlog.py "标题" "详细内容"
# 只有标题
python3 ~/.hermes/workspace/scripts/memlog.py "newsletter FSM SOP 重建完成"
# 指定日期(补录历史条目)
python3 ~/.hermes/workspace/scripts/memlog.py --date 2026-05-20 "标题" "内容"
每条记录写入 daily_log 表,带时间戳、来源标记。agent 在完成任务、踩坑、做决策时自动调用,不需要用户手动触发。
与 OpenClaw 的 memlog.sh 相比,从 Markdown 文件换成 SQLite 的主要收益是可查询——当 agent 需要”上次 Linear webhook 怎么配的”时,一条 FTS5 查询就能定位,不用翻整个文件。
2.5 session_search:第四层冷记忆的使用场景
Hermes 把每一次会话都存进本地 session DB。实践中的典型用法:
当 memory 里没有某个事实,但隐约记得"之前聊过"
→ session_search("linear webhook 配置")
→ 从历史会话里捞上下文
→ 而不是重新问用户
这实际上解决了 OpenClaw 时代的一个痛点:那时候如果一个细节没有及时写进 MEMORY.md,跨 session 就彻底丢失了。现在会话历史本身就是一个保底层。
3. Workflow 体系:我的 SOP 系统如何在 Hermes 上运行
Workflow 不是 Hermes 的内置概念。它是我自己建立的一套私有 SOP 系统,从 OpenClaw 时代就有,迁移到 Hermes 之后整体保留。Hermes 提供的是 skill——通用的、可复用的工具知识;workflow 是我在 skill 之上构建的个人最佳实践,两者是不同层次的东西。
3.1 Skill 和 Workflow 的边界
这两个概念容易混淆,因为它们都是”告诉 agent 怎么做某件事”的文档。区别在于受众和可移植性:
判断一个东西该放 skill 还是 workflow 的测试:
"能不能把它交给一个完全不认识我的人,让他按这个文档执行?"
能 → Skill(通用工具知识,可以发布共享给任何人)
不能 → Workflow(私有 SOP,包含个人约定和上下文)
具体来说:
- Skill:AKShare 的 API 用法、Linear 的 GraphQL 查询语法、如何用 gh CLI 创建 PR——这些和”谁在用”无关,放 skill
- Workflow:newsletter 处理的优先级规则、项目 review 要通知哪些人、什么情况下要推 Telegram 提醒——这些是个人约定,放 workflow
边界模糊的时候怎么判断?有个实际例子:Gmail 的搜索语法是 skill(任何人都能用),但”只有标题含’Weekly’且 sender 在 ADOPTED 状态的邮件才需要当天处理”是 workflow(这是我自己定的规则)。同一个工具,通用用法进 skill,个人约定进 workflow。
Skill 存在 ~/.hermes/skills/,由 Hermes 管理和加载。Workflow 存在 ~/.hermes/workspace/workflow/,完全由我自己维护——Hermes 不知道也不关心这个目录的存在,它只是 agent 执行任务时会读取的 Markdown 文件。
3.2 Skill 的正式格式
Hermes skill 有固定的文件格式,比在 AGENTS.md 里内嵌的 skill 描述规范得多:
---
name: email-monitoring
description: Gmail 邮件检查与 FSM 状态管理——基于 email_check.py 的通用邮件分类工具
platforms: [macos]
required_commands: [python3]
required_environment_variables: [GMAIL_CREDENTIALS_PATH]
---
# Email Monitoring
## 触发条件
用户说"检查邮件"、"有什么新邮件"、"处理 newsletter 队列"时加载本 skill。
## 步骤
1. 运行 `python3 ~/.hermes/scripts/email_check.py --mode check`
2. 读取 `~/.hermes/workspace/status/MAILLIST.json` 获取 sender 状态
3. 按队列优先级处理(ADOPTED sender 优先,NEW sender 次之,MUTED 跳过)
4. 每处理完一条,调用 `memlog.py --type daily_log` 记录
## Pitfalls
- MAILLIST.json 路径固定在 `~/.hermes/workspace/status/MAILLIST.json`,不在 `.cache/` 下
- newsletter 类邮件走 FSM 状态机,不走 immediate 判断直接处理
- email_check.py 运行需要 Gmail OAuth token,首次运行会触发浏览器授权
required_commands 和 required_environment_variables 字段在 skill 加载时自动检查,缺失依赖会提示,而不是到执行时才报错。
3.3 案例:newsletter 处理的完整迁移
OpenClaw 时代,newsletter 处理是一个简单的 workflow playbook:收到邮件 → 解析 → 存 Obsidian → 记录日志。触发靠手动。
迁移到 Hermes 之后,有两个实质性的升级:
升级一:email_check.py 加入 FSM
Sender 不再是”是不是 newsletter”的二元判断,而是有完整的状态机:
stateDiagram-v2
[*] --> NEW : 首次收到
NEW --> LEARNING : 加入观察列表
LEARNING --> EVALUATING : 连续收到多封,建立 novelty 基线
EVALUATING --> EVALUATING : 每封新邮件触发 Jaccard 评估
EVALUATING --> ADOPTED : novelty 高,持续带来新信息
EVALUATING --> MUTED : novelty 低,内容高度重复
ADOPTED --> EVALUATING : 定期重新评估
MAILLIST.json 记录每个 sender 的当前状态:
{
"senders": {
"[email protected]": {
"state": "ADOPTED",
"display_name": "Example Weekly",
"novelty_scores": [0.72, 0.68, 0.81],
"last_seen": "2026-05-20",
"total_received": 14
},
"[email protected]": {
"state": "MUTED",
"muted_reason": "novelty_avg < 0.3",
"muted_at": "2026-04-15"
}
}
}
这对应的是一个系统性原则:任何持久数据流都应该有反馈回路。没有评判机制的 newsletter 归档只是在堆文件,学习效果很差。FSM 让这个系统有了”它学到了什么”的概念。
升级二:从手动触发到 cron 调度
下一章详细说明。这里只需要知道:迁移后,newsletter 处理不再需要手动触发,每天早上自动跑,有积压才推通知。
3.4 从重复操作到 SOP 提炼
使用一段时间后,会出现一个自然的信号:同一批工具总是按固定顺序被调用。这就是 workflow 的前身。
判断某个操作序列该固化为 workflow 的标准:
同样的操作序列出现 ≥ 3 次
+ 每次执行的上下文高度相似
+ 中间的判断逻辑可以被明确描述
─────────────────────────────
→ 是时候写一个 workflow playbook 了
可以在 session 里直接问 agent:“最近有没有我反复做的事情,可以固化成 workflow”——agent 会扫描 session 历史,给出候选。
提炼的过程实质上是把隐性知识显性化。临时执行时靠 agent 当场判断的那些决策,在 workflow 里需要被显式写出来:
- 触发条件:什么情况下运行这个流程?什么情况下不该运行?
- 前置检查:开始之前要确认哪些条件(文件存在?网络可达?上次运行距今多久?)
- 权限边界:哪些步骤可以自动执行,哪些必须等用户确认?
- 失败处理:某个步骤失败了,整个流程怎么办?重试?报错停止?跳过继续?
写进 playbook 之后,每次执行都走同一套逻辑,行为可预期、可审计、可修改。Skill 也在这个循环里发挥作用:发现某个工具的用法写错了,直接 patch skill;发现某个 workflow 步骤依赖的工具参数变了,更新 skill,workflow 自然跟着正确。两者相互独立,各自演化。
OpenClaw 系列讲过 SOP 是”run into existence”——先跑起来,再在跑的过程中完善。这个原则在 Hermes 里完全适用,而且 skill 和 workflow 分开维护让这个循环更短:改 skill 不动 workflow,改 workflow 不动 skill,互不干扰。
3.5 如何定义一个好 Workflow:工程控制论视角
钱学森工程控制论的核心结构是:传感器(采集现状)→ 比较器(对照目标)→ 执行器(发出纠偏动作)→ 反馈回路。
好的 workflow 设计,本质上就是把这个结构显式地写进流程定义里。一个没有反馈回路的 workflow,只是一个任务清单;一个有反馈回路的 workflow,是一个会学习的系统。
以项目管理 workflow 为例,展示如何把这套结构落地:
graph LR
subgraph strategic["策略层 双周"]
S1["sprint 目标 / product backlog"]
S2{"本次交付 vs 目标"}
S3["Retrospective / Sprint Planning"]
S1 --> S2 --> S3 --> S1
end
subgraph tactical["战术层 每日"]
T1["PR 状态 CI 结果 / Linear issue"]
T2{"对照 DoD 验收标准"}
T3["Code Review / 阻塞上报"]
T1 --> T2 --> T3 --> T1
end
subgraph operational["运行层 每小时"]
R1["heartbeat / commit 频率"]
R2{"超时或卡住?"}
R3["Telegram 推送 / 状态更新"]
R1 --> R2 --> R3 --> R1
end
operational -->|"异常汇报"| tactical
tactical -->|"sprint 数据"| strategic
每一层都有独立的反馈回路,且下层的输出是上层传感器的输入——这就是”层次控制”。
对应到 workflow playbook 的写法,每个层级的反馈回路都需要被显式定义:
---
name: project-management
trigger: ["项目进度", "查一下", "sprint"]
status: active
---
# 项目管理 Workflow
## 运行层(by cron,每小时)
### 传感器
- 读 PROJECTS.json 获取各项目当前状态
- 查询 Linear:有无超过 3 天未更新的 In Progress issue
- 查询 GitHub:有无等待 review 超过 24 小时的 PR
### 比较器(判断条件)
- issue 停滞 > 3 天 → 触发阻塞提醒
- PR 等待 review > 24h → 触发 review 提醒
- CI 连续失败 > 2 次 → 触发故障提醒
### 执行器
- 有触发 → Telegram 推送具体内容 + 建议动作
- 无触发 → 静默(hermes-heartbeat-state.json 只更新时间戳)
## 战术层(by session,每日)
### 传感器
- 读今日 daily_log 了解进展
- 拉取 PR diff,逐条检查是否满足验收标准
### 比较器
- 对照 Linear issue 的 Acceptance Criteria 逐条核对
- 未满足 → 列出具体差距
### 执行器(需人工确认)
- 给出 Code Review 意见
- 更新 Linear issue 状态
- ⚠️ 不自动 merge,不自动关闭 issue
## 策略层(by session,每两周)
### 传感器
- 汇总本 sprint 所有 issue 完成情况
- 提取 daily_log 中的 blocker 和 decision 记录
### 比较器
- 实际完成 vs sprint 目标
- velocity 趋势(本次 vs 上次)
### 执行器(人类主导)
- agent 生成 retrospective 草稿
- ⚠️ 方向调整、优先级重排 → 人类决策,agent 执行
“人在回路”的位置是关键设计决策。不是所有步骤都需要确认,也不是所有步骤都可以自动——选错了要么流程卡死,要么出了问题没人知道。一个简单的原则:后果不可逆的操作(发布、merge、删除、外部通知)必须经过人工确认;只读和草稿类操作可以全自动。
3.6 调研 Workflow:自动化与评估
调研是另一类适合深度自动化的 workflow,但和项目管理不同——它的核心挑战不是”做了什么”,而是**“学到了多少”**。
评估:divergence score
一次好的调研,应该能回答:这次调研给我带来了多少新信息?
用 Jaccard 相似度来度量:每次新增文献 / 搜索结果,计算它与已有笔记的词汇交集比例。新内容与已有内容重叠越少,divergence 越高。
divergence = 1 - |新内容词集 ∩ 已有笔记词集| / |新内容词集 ∪ 已有笔记词集|
divergence 接近 1.0 → 这个来源带来了大量新信息,继续深挖
divergence 接近 0.0 → 内容高度重复,这个方向已经饱和,换方向或收尾
实践中的阈值参考:
divergence > 0.4 → 继续搜索,内容覆盖还不够
0.2 ~ 0.4 → 主要结论已收敛,可以开始写综述
< 0.2 → 调研饱和,停止,写结论
当 divergence 连续两轮低于阈值,workflow 自动标记该调研为”可收尾”状态,推送通知让用户确认是否结束。这比靠感觉判断”差不多了”要可靠。
调研 workflow 的状态机
stateDiagram-v2
[*] --> NEW : 用户指定调研主题
NEW --> SCOPING : 开始
SCOPING --> SCOPING : 自动预搜索,生成提纲草稿
SCOPING --> RESEARCHING : 人工确认提纲方向
RESEARCHING --> RESEARCHING : divergence 高于阈值,继续搜索
RESEARCHING --> EVALUATING : divergence 低于阈值,收敛
EVALUATING --> CONCLUDED : 交叉验证通过,写结论
CONCLUDED --> ARCHIVED : 45天后自动归档
ARCHIVED --> [*]
调研笔记的结构化输出
为了让 divergence 计算有意义,每次调研的输出需要结构化。_index.md 是全局追踪表,每个主题目录下的 index.md 是入口文件:
---
research_goal: "理解 XXX 的核心机制及工程实践"
divergence_history: [0.71, 0.52, 0.38, 0.19]
status: concluded
concluded_at: 2026-05-20
---
## 核心结论
(3-5 句判断,不是摘要)
## 置信度评估
- 高置信:来自 3 个以上独立来源的结论
- 中置信:来自 1-2 个来源,有待交叉验证
- 低置信:单一来源,存在争议
## 文件地图
| 文件 | 内容 | 状态 |
divergence_history 记录每轮的分数,让收敛曲线可见——如果前两轮就接近 0,说明调研范围太窄;如果五轮还在高位,说明这个领域本身就很发散,需要主动缩小范围。
配合 cron 的自动化
调研 workflow 也可以部分自动化:
flowchart TD
A(["用户启动调研"]) --> B["agent 创建目录结构 + 生成提纲"]
B --> C{"用户确认提纲?"}
C -->|"否,调整方向"| B
C -->|"是"| D["每日 cron,LLM-driven"]
D --> E{"读取当前 divergence"}
E -->|"高于阈值"| F["自动执行一轮搜索,追加笔记,重新计算"]
F --> D
E -->|"低于阈值"| G["推送调研接近收尾通知"]
G --> H{"用户确认结束?"}
H -->|"继续"| D
H -->|"结束"| I["agent 写结论"]
I --> J["更新 _index.md"]
J --> K(["存档"])
这个设计的关键:判断”是否继续搜索”是自动的,但判断”结论是否正确”是人工的。机器擅长量化覆盖程度,不擅长判断结论的质量。
4. Status 层:分离运行时状态
前面三章讲了 skill、workflow、cron,每个部分都涉及一类数据:skill 需要知道工具路径和认证方式,workflow 需要读取项目当前状态,cron 需要判断上次执行结果来决定这次要不要推送通知。这些数据放哪里?
不能放 memory——memory 有 decay,有容量上限,设计上是”允许过时”的。不能放 session 上下文——cron 每次是全新的 session,没有上下文继承。答案是 status/ 目录:一个专门存放运行时实时状态的地方,无 decay,脚本写、agent 读。
4.1 三类系统的数据流汇聚点
status/ 不是一个笼统的”临时文件夹”,它的每个文件都对应前面某个具体的系统:
graph TD
subgraph statusdir["status 目录"]
M[MAILLIST.json]
P[PROJECTS.json]
H[hermes-heartbeat-state.json]
C[candidates.json]
end
NW["newsletter workflow + email_check.py FSM"] -->|"写入 sender 状态"| M
M -->|"读取,决定处理哪些 sender"| NW
M -->|"读取,过滤邮件队列"| CR1["newsletter cron"]
PW["project workflow 运行层 cron"] -->|"写入项目快照"| P
P -->|"读取全貌,省去重查 Linear"| PS["session 战术层"]
P -->|"对比差异,有变化才推送"| CR2["heartbeat cron"]
CR1 -->|"执行后写时间戳"| H
CR2 -->|"执行后写快照"| H
H -->|"读取上次结果"| CR1
H -->|"读取上次结果"| CR2
RW["调研 workflow"] -->|"写入候选主题 + divergence 进度"| C
C -->|"读取,判断哪些接近收尾"| CR3["daily-reflection cron"]
4.2 每个文件的读写关系
MAILLIST.json
由 email_check.py 写入,记录每个 newsletter sender 的 FSM 状态(NEW / LEARNING / EVALUATING / ADOPTED / MUTED)和 novelty_scores 历史。
newsletter workflow 的运行层 cron 读它来决定哪些 sender 的邮件需要处理、哪些静默跳过。Agent 在 session 里处理邮件时也读它,确认当前 sender 的状态。只有 email_check.py 写,agent 不直接修改它——除非用户明确说要调整某个 sender 的状态。
PROJECTS.json
由项目管理 workflow 的运行层 cron 写入,记录每个项目的当前状态快照:
{
"projects": {
"my-project": {
"linear_status": "in_progress",
"open_prs": 2,
"blocked_issues": 1,
"last_updated": "2026-05-26T08:00:00+08:00"
}
}
}
项目管理 workflow 的战术层(每日 session)读它来快速了解当前项目全貌,不需要每次都重新查询 Linear API。Cron 的比较器逻辑也依赖它:用新拉取的数据和上次的快照对比,有变化才推送,没变化静默。
hermes-heartbeat-state.json
由各个 cron job 在执行完后写入,记录上次执行的时间和关键结果。下次执行时先读它,判断是否需要重复操作:
{
"last_run": "2026-05-26T08:00:00+08:00",
"newsletter_queue_size": 70,
"last_notification_sent": "2026-05-25T23:45:00+08:00",
"projects": {
"my-project": {
"last_status": "in_progress",
"open_prs": 2
}
}
}
这解决了一个实际问题:如果项目状态没有任何变化,cron 不应该每小时都发一次”一切正常”的通知。有了 hermes-heartbeat-state.json,cron 可以对比前后两次快照,只在有差异时才推送。
candidates.json
由调研 workflow 写入,记录当前待评估的调研候选主题和各自的 divergence 进度。Daily-reflection cron 读它来判断哪些调研接近收尾阈值,需要在反思里提醒用户跟进。
4.3 设计意图:让每个系统保持无状态
把运行时状态集中到 status/ 的核心意图是:让 skill、workflow、cron 的逻辑本身保持无状态。
无状态的好处是可测试、可重置:
- Skill 描述工具怎么用,不存任何运行结果——同样的 skill 在不同机器上行为一致
- Workflow playbook 描述流程逻辑,不嵌入上次执行的结果——可以随时修改 playbook 而不影响历史状态
- Cron prompt 描述这次要做什么,从 status/ 读取上下文——即使 cron 被暂停再恢复,状态不会丢失
类比来说,status/ 就是这套系统的”工作台”:工具(skill)挂在墙上,SOP(workflow)贴在白板上,计划(cron)按时提醒——只有工作台上的材料是实时变化的。工具和 SOP 本身不会因为工作台上的材料变了而改变。
这比 OpenClaw 时代把状态混在 NOW.md 里要清晰得多——NOW.md 是给人看的摘要,hermes-heartbeat-state.json 是给机器做差异比较的原始数据,两者的受众和用途完全不同,不应该混用。
4.4 state.db:状态层的另一半
status/ 目录存放的是人可读、脚本可直接写的快照数据——当前项目状态、邮件队列状态、心跳时间戳,面向”现在是什么状态”这类即时查询。
但系统还有另一类数据:需要跨时间积累的结构化历史。newsletter sender 的 novelty 分数历史(用于 FSM 评估)、所有 session 的元数据(用于 session_search)、cron 执行记录——这些都存在 state.db(SQLite)里,不是 JSON 文件。
两者的分工:
workspace/status/*.json | state.db | |
|---|---|---|
| 格式 | 人可读 JSON | SQLite 结构化查询 |
| 用途 | 当前快照,即时读写 | 历史积累,跨时间查询 |
| 典型数据 | 项目状态、心跳时间戳 | Session 历史、newsletter FSM 评估数据 |
| 写入方 | 脚本直接写 | Hermes 内部 + hooks + plugins |
session_search 工具查询的是 state.db 里的 sessions 表;newsletter FSM 的 novelty_scores 历史也在 state.db 里,email_check.py 每次归档时更新。只描述 JSON 文件,会让读者误以为系统状态是一堆独立的键值文件,而忽略了底层的时序数据支撑。
4a. Hooks:记忆系统各层的胶水
在记忆系统和 cron 之外,还有一类机制把整套系统串联起来——Hooks。
Hermes 的 hooks 是事件驱动的扩展点:在 session:start、session:end、agent:start 等生命周期事件上挂载自定义 Python 脚本(handler.py + HOOK.yaml 声明监听事件)。这是让记忆系统”自动流动”而不需要手动触发的关键机制。
这套系统里有两个 hook,分别承担不同的”胶水”角色:
hook 1 — session-now-reporter(监听 agent:start)
每次新 session 的第一轮对话发起时,自动读取 memory.db 里的 now 表,格式化后通过 Telegram Bot API 推送给用户。内置去重机制(记录已推送的 session_id),同一 session 只推一次。
作用:把第零层记忆(now 表)和 Telegram 通知渠道连起来——让”当前状态”不只是 agent 内部可读,而是在每次对话开始时主动出现在用户面前。
hook 2 — session-daily-writer(监听 session:end)
session 结束时,从 state.db 查询该 session 的元数据,读取 sessions/ 目录下对应的 JSON 文件,自动从第一条用户消息提取短标题(支持中英文),生成 YYYY-MM-DD-shorttitle.md 写入 workspace/memory/daily/。cron session 自动跳过,少于 2 条消息的 session 也跳过。
作用:把 state.db 会话历史和 workspace/memory/daily/ 日记层连起来——这就是”每日日记”的自动生产机制。读者可能会疑惑 daily/ 目录里的文件从哪来,答案就在这里。
与 session-daily-scan cron 的分工:
- Hook 处理正常结束的 session(实时写入)
session-daily-scancron(每小时)补扫未正常触发 hook 的 session(进程意外中断、超时等情况)- 两者互补,确保日记层不会有空洞
graph LR
subgraph hooks["Hooks(事件触发)"]
H1["session-now-reporter<br/>agent:start"]
H2["session-daily-writer<br/>session:end"]
end
subgraph cron_scan["Cron(兜底)"]
C1["session-daily-scan<br/>每小时"]
end
H1 -->|"读取"| NT["now 表"]
NT -->|"推送"| TG[Telegram]
H2 -->|"读取"| SD["state.db sessions"]
H2 -->|"写入"| DL["daily/*.md"]
C1 -->|"补扫写入"| DL
Hooks 体现的是 Hermes 的扩展性思路:不改动上游代码,通过在生命周期事件上挂载自定义脚本来实现个性化功能。now-injector plugin(注入 now 表到 system prompt)和这两个 hook 一起,构成了记忆系统的”自动驾驶”部分——用户不需要手动维护,系统在后台持续运转。
5. Cron 集成:让 Hermes 自己动起来
Cron 是 Hermes 的调度机制,workflow 是我自己的 SOP——两者协作的方式是:workflow 里的运行层交给 cron 自动触发,战术层和策略层留在 session 里手动驱动。本章只讲 cron 这一半。
5.1 Heartbeat 去哪了
OpenClaw 有两个自动化机制:Heartbeat(按固定间隔轮询)和 Cron(按时间表精确触发)。在 Hermes 里,这两者统一成了一个东西——Cron。
OpenClaw 的 Heartbeat 本质上就是”每隔 N 分钟跑一次的定时任务”。Hermes 的 cron 支持 every 30m、every 1h 这样的间隔语法,直接替代了 Heartbeat 的角色:
OpenClaw Heartbeat(每 60 分钟)
≡ Hermes cron --schedule "every 1h"
OpenClaw Cron(每天 23:45)
≡ Hermes cron --schedule "45 23 * * *"
从 OpenClaw 迁移过来,不需要单独维护 HEARTBEAT.md 定义”每次心跳该做什么”——把那些检查项直接拆成独立的 cron job,每个 job 各司其职,反而更清晰。
5.2 两种执行模式
Hermes cron 有两个根本不同的执行模式,选错代价很高:
LLM-driven 模式(默认)
每次 tick → agent 读 prompt → 调用工具 → 做判断 → 返回结果
适合:需要推理的任务
成本:每次消耗 LLM token
no_agent 模式
每次 tick → 直接运行 script → stdout 作为消息发出
适合:纯数据采集、阈值告警(不需要思考,只需要执行)
成本:零 token 消耗
用一个日常场景对比说明:
场景:每天早上检查邮件,有重要邮件就通知我
用 no_agent:写一个 Python 脚本,用 Gmail API 拉取未读邮件,按规则过滤(发件人在白名单 / 标题含关键词),有命中就把摘要输出到 stdout,没有就输出空。Hermes 把非空的 stdout 推送到 Telegram,空 stdout 静默。整个流程零 LLM,速度快,成本低。
用 LLM-driven:每次 tick,agent 读取邮件列表,自己判断哪封重要、该怎么摘要、用什么语气推送。适合判断逻辑复杂、无法用规则完全描述的场景——比如”帮我判断这封邮件是不是需要今天回复,并起草一个简短回复”。
误用的代价:把”是否有新 newsletter”这个 yes/no 判断交给 LLM-driven,等于每次花一次 GPT-4 的钱问”这个列表是不是空的”。把”今天的市场数据分析”交给 no_agent,script 只能输出原始数据,没有推理能力,发出去的是一堆数字,不是分析。
一个常见的反模式是把数据采集和分析混在同一个 LLM-driven cron 里。这会让 LLM 把大量 token 用在读数据上,真正的分析质量反而下降。正确做法是分离:
flowchart LR
A["no_agent cron,数据采集脚本"] -->|"stdout to JSON"| B[("context_from,自动注入")]
B --> C["LLM-driven cron,分析推理"]
C -->|"推送结果"| D[Telegram]
Hermes 的 context_from 参数直接支持这种链式调度。
5.3 创建和管理 Cron
通过 Hermes CLI 或在 session 里对话创建:
# 创建一个 no_agent 定时任务(直接跑脚本)
hermes cron create \
--name "daily-newsletter-archive" \
--schedule "0 8 * * *" \
--no-agent \
--script "~/.hermes/scripts/email_check.py" \
--deliver "telegram"
# 查看所有 cron
hermes cron list
# 手动触发执行一次(调试用)
hermes cron run <job_id>
# 暂停 / 恢复
hermes cron pause <job_id>
hermes cron resume <job_id>
也可以直接在 Telegram 对话里说:“帮我创建一个每天早上 8 点检查 newsletter 队列的定时任务”——Hermes 会询问细节后自动创建。
5.4 实际运行的几个 Cron
下面是三个实际跑在生产环境里的 cron,配上精简后的 prompt,方便参考。
daily-newsletter-archive(LLM-driven,每4小时)
实际运行频率是每4小时(0 */4 * * *),而非”每天一次”——newsletter 队列随时可能有新内容进入,高频运行确保积压不会拖太久。每次只处理队列里的一条,防止单次运行时间过长。
flowchart TD
A(["定时触发"]) --> B["email_check.py newsletter-queue next"]
B --> C{"队列有内容?"}
C -->|"item: null"| D["静默退出,无任何输出"]
C -->|"有 item"| E["获取邮件全文"]
E --> F{"Sender 类型?"}
F -->|"Full-content"| G["直接用邮件内容"]
F -->|"Preview-only"| H["browser_navigate 打开原文链接补全"]
G & H --> I["语言判断:中文→贴原文 / 英文→逐段对照翻译"]
I --> J["写入笔记,计算 novelty_score"]
J --> K["更新 FSM 状态"]
K --> L["标记完成 + 记录日志"]
Prompt 核心段(精简):
【Newsletter 归档】处理 newsletter 队列中最多 10 条,按顺序逐条处理。
Step 1 — 取队列下一项
python3 ~/.hermes/scripts/email_check.py newsletter-queue next
若返回 "item": null → 队列空,静默退出(无任何输出)
Step 2 — 获取邮件全文
gog gmail get <msg_id> [--body]
Step 2b — 判断 Sender 类型
Full-content 发件人(内容完整)→ 直接用邮件内容,跳过 Step 3
Preview-only 发件人(Substack/a16z 等)→ 必须打开原文链接补全
Step 3 — 截断检测 + 补全(Preview-only 必须执行)
检测到"Read in browser"链接或邮件内容 < 历史平均的 30% → 用 browser_navigate 打开补全
Step 4 — 语言判断
中文 → 贴完整原文 | 英文 → 逐段原文+译文对照
Step 5 — 写入笔记
路径:~/Projects/DigitalGarden/newsletter/<author_key>/YYYY-MM-DD-<slug>.md
格式:frontmatter + ## 原文 + ## 译文 + ## 主要内容 + ## 分析
Step 6 — 计算 novelty_score
python3 ~/.hermes/scripts/email_check.py newsletter-novelty <author_key> --keywords "..." --msg-id <msg_id>
Step 7 — 更新 FSM 状态
python3 ~/.hermes/scripts/email_check.py newsletter-fsm update <author_key> --novelty <score>
Step 8 — 标记完成 + 记录日志
python3 ~/.hermes/scripts/email_check.py newsletter-queue done <msg_id>
python3 ~/.hermes/scripts/memlog.py "Newsletter 归档:[subject]" "[author_key]"
这个 cron 是 LLM-driven 而非 no_agent,原因不只是”需要翻译”——发件人类型分类、截断检测、novelty 评分、FSM 状态更新,这些步骤都需要推理和判断。静默退出是关键设计——队列为空时不发任何通知,不打扰用户。
daily-stock-review(no_agent,每天 23:00)
flowchart TD
A(["23:00 触发"]) --> B["stock_review.py 运行"]
B --> C{"执行结果"}
C -->|"ok"| D["结果写入 StockReview/YYYY-MM-DD/,脚本内置 Telegram 推送"]
C -->|"warn/error"| E["推送异常提示"]
Prompt 核心段:
【A股复盘】现在是 23:00,请执行今日 A股复盘流程。
1. 运行复盘脚本:python3 ~/.hermes/scripts/stock_review.py
2. 复盘结果写入 vault/StockReview/YYYY-MM-DD/
3. 脚本内已包含 Telegram 推送逻辑
脚本输出 ✅ / [ok] / [warn] / [error],按原样处理。
这是最简单的 no_agent 模式——prompt 极短,本质上只是告诉脚本在哪、结果放哪。数据获取、分析、格式化、推送全部在 stock_review.py 内部完成,LLM 不介入任何环节。
daily-reflection(LLM-driven,每天 23:45)
flowchart TD
A(["23:45 触发"]) --> B["session-daily-scan.py,扫描今日所有 session"]
B --> C["读取 daily/YYYY-MM-DD.md + session_search 补充"]
C --> D["扫描 skills/ 目录,识别可合并/可 SOP 化的 skill"]
D --> E{"有维护建议?"}
E -->|"有"| F["附加 Skills 维护建议到反思末尾"]
E -->|"无"| G["跳过"]
F & G --> H["生成反思,发送 Telegram"]
H --> I["更新 now 表,recent_events / pending / today_focus"]
Prompt 核心段(精简):
【每日反思】现在是 23:45,执行今日反思流程:
Step 1:扫描 session
python3 ~/.hermes/workspace/scripts/session-daily-scan.py
Step 2:读今日 daily 文件
读取 memory/daily/YYYY-MM-DD.md,结合 session_search 补充关键事件。
Step 3:扫描 skills 健康度
检查 skills/ 目录,识别需要维护的 skill:
- [MERGE] 两个 skill 处理同一领域、步骤重复
- [SOP] 运行多次且路径固定,已达 SOP 标准
有发现则附加到反思末尾,无发现则略过。
Step 4:生成反思并推送 Telegram
格式:今日关键事件(3-5条)/ 认知收获 /
Skills 维护建议(来自 Step 3)/
待办清理建议 / 明日优先事项
Step 5:更新 now 表
recent_events 写今日总结,pending 清理已解决条目,
today_focus 刷新为明日重点。
这个 cron 有一个文章前面没提到的设计:Step 3 扫描 skills 健康度。每天反思时顺带扫一遍 skills 目录,识别可以合并或固化为 workflow 的 skill。这是第三章”从重复操作到 SOP 提炼”那套逻辑的自动化实现——不是等用户主动发现,而是让 agent 每天晚上做一次检查,有建议就附在反思里推送出来。
反思的价值不只在于记录当天的事,更在于认知提炼 + 系统维护同步进行。这步不能用 no_agent 替代:提炼需要判断,skills 健康度的识别需要推理。
daily-project-review(LLM-driven,每天 21:00)
flowchart TD
A(["21:00 触发"]) --> B["projects.py sprint --control"]
B --> C["解析 sprint_health.score / pending_actions / stale_actions"]
C --> D{"健康分 ≥ 8?"}
D -->|"是"| E["推送 ✅ 状态摘要"]
D -->|"否"| F["重点标注 stale_actions + 建议动作"]
E & F --> G["Telegram 推送"]
运行 projects.py sprint --control 脚本,解析返回的 JSON——sprint 健康分(1-10)、待处理偏差项、超期偏差项,推送格式化的项目状态报告。这是第3章”运行层 cron”那套逻辑的具体落地:不需要在 session 里手动问”项目进展怎么样”,每天 21:00 自动汇报一次。
weekly-knowledge-distill(LLM-driven,每周日 22:00)
这是记忆系统能”积累”而不只是”记录”的关键 cron。每周运行一次,做两件事:
-
从
daily_log提炼 lessons/decisions:读取过去7天的事件流水,识别值得长期保留的教训和决策,写入memory.db的lessons/decisions表。同时标记超过 90 天未验证的条目为stale。 -
自动生成 skill 文件:对高优先级的 lessons/decisions,判断是否值得固化成 skill——如果描述了完整的多步骤流程、踩坑经验或非显而易见的技术决策,就在
~/.hermes/skills/下自动创建.mdskill 文件。
这个设计体现的是知识提炼的自动化:日常使用中的经验(写在 daily_log 里)→ 每周提炼成可复用的知识(lessons/decisions)→ 高价值内容进一步固化成 skill,形成完整的知识沉淀循环。
weekly-memory-gc(no_agent,每周日 00:00)
数据库维护 cron,处理三件事:归档 90 天前的 daily_log(写入 archive 表后删除原记录),归档超期 stale 的 lessons/decisions,最后执行 SQLite VACUUM 回收空间。静默运行,不推送 Telegram。
5.5 Cron 的行为约定
这几条在实践中反复确认,值得作为原则写明:
1. Cron 不问问题。 定时任务在没有用户的环境里跑,问了也没人答。缺少信息时,要么用默认值继续,要么静默跳过,不能卡住。
2. Cron 不自动 push。 任何涉及外部写操作(git push、发邮件、改 Linear issue)的动作,都需要用户在 session 里明确触发,不进 cron。
3. Cron 不改 memory 内容。 memory.db 是私有的知识库,写入决策应该有人在回路。Cron 可以把草稿写到 status/ 下的临时文件,等用户在 session 里确认后再写入——这个”cron 采集、session 确认”的分工是主动设计的,不是 Hermes 的约束。
4. 空输出 = 静默。 no_agent 模式下,stdout 为空不发任何消息。这是 Hermes 的设计——告警型 cron 天然适配这个语义:没问题就安静,有问题才说话。
6. 完整目录结构
~/.hermes/
│
├── SOUL.md ← 身份 + 行为原则 + 安全铁律
├── config.yaml ← 模型、provider、channel 配置
├── .env ← API keys(不进 git,单独备份)
│
├── skills/ ← Skill 库(每个 skill 一个目录)
│ ├── email-monitoring/
│ │ └── SKILL.md
│ ├── linear/
│ │ └── SKILL.md
│ ├── github-pr-workflow/
│ │ └── SKILL.md
│ └── ...
│
├── hooks/ ← 生命周期 hook(每个 hook 一个目录)
│ ├── session-now-reporter/
│ │ ├── HOOK.yaml
│ │ └── handler.py
│ └── session-daily-writer/
│ ├── HOOK.yaml
│ └── handler.py
│
├── workspace/
│ ├── AGENTS.md ← 操作入口:session checklist、workflow 路由
│ ├── WORKFLOW.md ← Workflow 索引
│ ├── workflow/ ← Workflow playbooks
│ │ ├── 00-create-workflow.md
│ │ ├── 01-newsletter.md
│ │ └── 02-project-review.md
│ │
│ ├── status/ ← 运行时快照(agent 只读,脚本写)
│ │ ├── MAILLIST.json
│ │ ├── PROJECTS.json
│ │ ├── hermes-heartbeat-state.json
│ │ └── candidates.json
│ │
│ ├── memory/ ← 日记文件(由 hook 自动生成)
│ │ └── daily/
│ │ └── YYYY-MM-DD-shorttitle.md
│ │
│ └── scripts/ ← 数据采集 / 工具脚本
│ ├── email_check.py
│ ├── memlog.py
│ ├── stock_review.py
│ └── trade_cal.py
│
├── memory.db ← 记忆数据库(FTS5 索引,含 now 表)
├── state.db ← 状态数据库(sessions、FSM 历史、cron 记录)
└── sessions/ ← 会话历史原始文件(session_search 的数据源)
与 OpenClaw 时代相比,最大的结构变化:skills/ 从 AGENTS.md 的内嵌描述变成了独立目录,每个 skill 是一个可以单独维护、单独发布的文件;status/ 从分散在 memory/ 里变成了专属目录,运行时状态和记忆系统彻底分开;hooks/ 是全新增加的扩展层,把记忆系统各部分的自动化胶水显式化;memory.db 和 state.db 双库分工,取代了 OpenClaw 时代单一的 NOW.md + MEMORY.md 组合。
7. 一些观察
迁移过程是一次系统重审。 从 OpenClaw 迁到 Hermes 不只是换工具,而是把原来沉淀下来的约定重新过了一遍:哪些是真正有用的,哪些是因为工具限制产生的绕弯路。SOUL.md 的铁律、status/ 的路径约定、cron 的行为原则——这些在新平台上都得到了更清晰的实现形式。
Skill 体系的可维护性提升是真实的。 OpenClaw 时代,一个 skill 出了问题,要去翻 AGENTS.md 找对应的段落,修改,再测试。现在每个 skill 是独立文件,有版本、有描述、有 pitfalls 章节。发现描述过时了,直接 patch;踩了新坑,立刻更新进去。这个反馈循环短了很多。
Cron 的”有意识”是关键升级。 LLM-driven 的定时任务不只是跑脚本,它能读上下文、做判断、根据结果决定下一步。daily-reflection 能选择性地提炼有价值的内容,而不是把今天所有的对话都塞进 memory——这是系统 crontab 调 shell 脚本永远做不到的。
这套系统仍然在演化。 调研 FSM、newsletter 状态机、memory decay 机制——这些不是一开始就设计好的,是在反复使用中长出来的。好的 AI 助手配置,本质是在给一个会学习的系统搭框架:框架要足够稳定,让学习有地方落;又要足够灵活,让实践能反哺设计。
祝你也拥有一个懂你的 AI 助手。