Hermes Agent 记忆治理:从 84% 爆满到自动化备份 + 审计

前几天在 Hermes 里输了 /config 看一眼,记忆使用率 84%,37 条,快摸到 8192 字符的默认上限了。

Hermes 的内置记忆会在每轮对话开头注进 system prompt。满了不会停,会被截断或压缩,然后你就发现它开始忘事。或者更糟,记岔了。

翻开 MEMORY.md,里面塞满了各种东西:博客 frontmatter 格式、MarxChou 生图 prompt 模板、知识库分层体系、RustDesk 部署拓扑、辅助模型配置。很多内容对应的 skill 里早就有了,同一份知识存了两份,纯冗余。

其实这不是第一次爆满。上个月就发现 76% 了,当时还走了点弯路。

弯路:holographic 插件

最初的想法是借助外部工具。holographic 是一个号称能「扩展 AI 上下文记忆」的 Python 包:

Terminal window
pip install holographic

结果很惨烈。holographic 版本太老(0.0.1),安装时把 Hermes 的核心依赖全部降级 ——pydantic 2.13.4 → 1.10.26、requests 2.33.0 → 2.28.2、rich 14.3.3 → 13.0.1。Hermes 本身和 mcp 等工具全部罢工,立刻 pip uninstall 回滚。

事后在 Hermes 源码里 grep memory_char_limit 才发现,扩容本身就是 config.yaml 的可配置项,根本不需要任何插件。

Terminal window
hermes config set memory.memory_char_limit 12000

教训:不要往 Hermes 的 venv 里装第三方包。依赖冲突的风险远大于收益。

清理:从 37 条砍到 16 条

清理逻辑很简单,三条:

  • 已进 skill 的,删。xiaohei-illustrations、zcblog-article-writing、knowledge-base、hermes-auxiliary-models、bitwarden-cli 这些 skill 里已经覆盖的内容,memory 里就是冗余。
  • 历史记录的,删。电商文件删除记录、Obsidian 迁移记录、cc-switch 移植记录 —— 一次性的操作,事后没必要留在记忆里。
  • 重复的,合并。RustDesk 两条合并一条。

清理完:16 条,2300 字符,28%。顺便把 memory_char_limit 从 8192 拉到 12000,够用很久。

核心原则:skill 管” 怎么做”,memory 只管” 你是谁 + 在哪 + 哪些坑”。下次再记东西之前,先过一遍这个筛子。

具体来说:

应该存不应该存
用户行为偏好(「本地执行,别 SSH」)API Key、密码、端口号
项目约定(「文档放 /root/archives/」)服务安装 / 配置参数
环境事实(「两台 NUC9 的 IP」).env / config.yaml 里能查到的任何值
工具技巧 / 经验教训版本号、commit SHA
跨会话有用的稳定事实临时任务状态

如果把大量配置类数据塞进 memory,不仅浪费空间,更重要的是 —— 配置改了还得记得同步改 memory,迟早不一致。从根源上分开,才是最省心的做法。

问题:清理完了还是会再膨胀

手动清一次很简单。但问题是 —— 用着用着就又攒起来了。

我就想搞个 cron 定时任务,每周跑一次,自动审计。

然后踩了一个坑:cron 会话默认 skip_memory=True,agent 看不到 memory 内容,没法清理。

这个设计其实合理 ——cron 是独立会话,不应该带着你的私人记忆跑。但对记忆审计来说,这就堵死了。

解法:no_agent 脚本 + 定时 cron

既然 agent 看不到 memory,那就绕过 agent,直接写个 Python 脚本读 MEMORY.md 和 USER.md 文件。

脚本做三件事:

  1. 备份。每次运行前,把 MEMORY.mdUSER.md 复制到 memories/_backups/<timestamp>/,保留最近 30 份。
  2. 审计。逐条扫描,检查是否被已有 skill 覆盖、是否为历史过期信息、是否重复。
  3. 报告。发现问题时输出报告;没变化就静默。

脚本路径:~/.hermes/scripts/memory-audit.py

cron 配置也很简单 ——no_agent=true,纯脚本运行,零 token 消耗:

cronjob: Memory Audit
schedule: 0 9 * * 1 # 每周一 9:00
script: memory-audit.py
no_agent: true

容灾设计

光审计不够,万一哪天手动清理手滑删错了呢?

脚本每次运行第一步就是备份。备份目录结构:

memories/_backups/
├── 20260616-170557/
│ ├── MEMORY.md
│ └── USER.md
├── 20260616-170724/
│ └── ...
└── .last_state.json # 上次审计指纹

回滚只需要一条命令:

Terminal window
cp memories/_backups/20260616-170557/MEMORY.md memories/MEMORY.md

最多保留 30 份,超出自动清理。30 周 ≈ 7 个月的回滚窗口,够了。

静默逻辑:不打扰才是最好的自动化

如果每周围着你唧唧歪歪” 一切正常”,那这个 cron 就是在制造噪音。

脚本用 .last_state.json 存了上次审计的问题指纹(md5)。新跑一次,指纹没变 → 直接 exit 0,什么也不输出。

只有两种情况会推送报告:

  • 首次运行:列出所有问题
  • 后续运行出现新问题:只报新的

干净的时候你不知道它跑过。出问题的时候它才说话。

审计脚本怎么写的

核心逻辑不到 200 行。几个要点:

技能关键词映射 —— 如果 memory 条目的内容匹配到某个 skill 的关键词,就标记为可移除:

SKILL_KEYWORDS = {
"xiaohei-illustrations": ["生图", "Seedream", "MarxChou", "配图"],
"zcblog-article-writing": ["frontmatter", "slug", "博客", "pubDate"],
"knowledge-base": ["知识库", "L0", "L1", "L2", "概念页"],
# ...
}

过期模式检测 —— 正则匹配历史 / 一次性操作记录:

STALE_PATTERNS = [
(r"已删除.*\d+ 个文件", "deletion record"),
(r"移植了 \d+ 个 skill", "one-time migration"),
(r"session \d{4}-\d{2}-\d{2}", "session-specific"),
# ...
]

增量指纹 —— 用 md5 做问题指纹,跨进程稳定。别用 Python 内置 hash(),那个受 PYTHONHASHSEED 影响:

fingerprint = hashlib.md5(issue_text.encode()).hexdigest()

完整脚本见 GitHub 知识库 scripts/memory-audit.py

首轮效果

跑了几天,试下来效果还行 —— 新加了条目忘了进 skill,脚本会指出来。看一眼报告,回一句” 清理记忆” 就搞定。

三周后发现的两个坑

脚本跑了三周,每周一早上跑一次。直到今天我才发现 —— 它一直在白跑。

坑一:deliver 设错了,报告全被吞

cron job 创建时,deliver 参数设成了 "local"。这个值的意思是” 把输出存下来但不推送”。对 no_agent 脚本来说,脚本的 stdout 就是报告正文 ——"local" 等于把报告吞了。

三周,三次审计,22 条问题 —— 全被吞了。我完全不知道。

修复只需改一个参数:

Terminal window
cronjob(action='update', deliver='origin', job_id='0e9ba8c6a8f9')

"local" 改成 "origin",报告就会推送到创建 cron 的对话。下次再有新问题,我会直接收到。

教训:配完 cron 一定要手动跑一次确认送达。silent failure 比报错更可怕 —— 报错你知道它坏了,silent failure 让你以为一切正常。

坑二:关键词太宽,过半是误报

第一份报告出了 22 条问题。仔细一看,差不多一半是误报。

问题出在关键词匹配。MarxChou 是我的品牌名,几乎每条 memory 都有 ——git 仓库叫 MarxChou/blog-article-knowledge-base,AFFiNE 域名是 affine.marxchou.com。但脚本把它关联到了 xiaohei-illustrations(生图 skill),因为当初顺手把 “MarxChou” 当成角色形象的代名词塞进了关键词。

同理,Bitwarden 出现在 Cloudflare DNS 条目里(API Token 存在 Bitwarden),被关联到了 bitwarden-cli skill。

解法不是删关键词,而是分级:

# 主关键词 — 单独命中即判定被 skill 覆盖
SKILL_KEYWORDS = {
"xiaohei-illustrations": ["生图", "Seedream", "配图", "插图", "角色形象"],
"zcblog-article-writing": ["frontmatter", "slug", "文风特征", "写作流程"],
"knowledge-base": ["知识库", "L0 原始", "L1 整理", "概念页", "Wikilm"],
"bitwarden-cli": ["BW_CLIENTSECRET", "bw-session", "bw get", "vaultwarden"],
}
# 次级关键词 — 不单独触发,只做辅助
SECONDARY_KEYWORDS = {
"xiaohei-illustrations": ["MarxChou", "封面", "prompt"],
"zcblog-article-writing": ["博客", "文章", "写作"],
"bitwarden-cli": ["Bitwarden"],
}

find_skill() 只检查主关键词。MarxChouBitwarden博客文章 这些宽泛词降为次级,不单独触发判定。

效果立竿见影:22 条报警 → 2 条。剩下的一个是知识库 git 仓库名里的 “blog-article” 误触,换了个更精准的词也修掉了;另一个是 workspace UUID 的裸值被包含在 AFFiNE 条目里,36 个字符的重复,删不掉但无害。

顺便:配置迁出

修完脚本,顺手把 memory 里的配置信息也迁走了。

memory 的预算是 12000 字符,每轮对话都注入。服务器 IP、RustDesk Key、AFFiNE 工作区 ID、Cloudflare DNS 脚本路径 —— 这些东西只在特定场景用到,没必要霸着每轮的 token 预算。

迁到独立文件 ~/.hermes/config/context.md,按域名分组。需要时 read_file 读取:

~/.hermes/
├── memories/MEMORY.md ← 行为规则(7 条,715 chars)
├── memories/USER.md ← 用户偏好(3 条,314 chars)
└── config/context.md ← 纯配置(需时读取,不注入上下文)

判断标准很简单:一条信息影响大多数交互 → 留 memory;只在特定场景用到 → 进 config。

整轮清理下来:

清理前清理后
MEMORY33 条 / 6016 chars7 条 / 715 chars
USER8 条 / 941 chars3 条 / 314 chars
审计误报22 个0 个

memory 从快满变成了几乎空着,审计脚本也终于不再说谎。


从手动救火到自动化治理,再到现在把自动化的坑也填上 —— 说到底就是把” 人的判断” 固化成” 机器的规则”,然后盯着规则跑一段时间,把规则里的 bug 也修掉。skill 管方法论,script 管健康检查,cron 管执行节奏。memory 只需要记住那些没法被规则化的东西 —— 你是谁、在哪、踩过什么坑。