Hermes Agent 记忆治理:从 84% 爆满到自动化备份 + 审计
前几天在 Hermes 里输了 /config 看一眼,记忆使用率 84%,37 条,快摸到 8192 字符的默认上限了。
Hermes 的内置记忆会在每轮对话开头注进 system prompt。满了不会停,会被截断或压缩,然后你就发现它开始忘事。或者更糟,记岔了。
翻开 MEMORY.md,里面塞满了各种东西:博客 frontmatter 格式、MarxChou 生图 prompt 模板、知识库分层体系、RustDesk 部署拓扑、辅助模型配置。很多内容对应的 skill 里早就有了,同一份知识存了两份,纯冗余。
其实这不是第一次爆满。上个月就发现 76% 了,当时还走了点弯路。
弯路:holographic 插件
最初的想法是借助外部工具。holographic 是一个号称能「扩展 AI 上下文记忆」的 Python 包:
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 的可配置项,根本不需要任何插件。
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 文件。
脚本做三件事:
- 备份。每次运行前,把
MEMORY.md和USER.md复制到memories/_backups/<timestamp>/,保留最近 30 份。 - 审计。逐条扫描,检查是否被已有 skill 覆盖、是否为历史过期信息、是否重复。
- 报告。发现问题时输出报告;没变化就静默。
脚本路径:~/.hermes/scripts/memory-audit.py。
cron 配置也很简单 ——no_agent=true,纯脚本运行,零 token 消耗:
cronjob: Memory Auditschedule: 0 9 * * 1 # 每周一 9:00script: memory-audit.pyno_agent: true容灾设计
光审计不够,万一哪天手动清理手滑删错了呢?
脚本每次运行第一步就是备份。备份目录结构:
memories/_backups/├── 20260616-170557/│ ├── MEMORY.md│ └── USER.md├── 20260616-170724/│ └── ...└── .last_state.json # 上次审计指纹回滚只需要一条命令:
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 条问题 —— 全被吞了。我完全不知道。
修复只需改一个参数:
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() 只检查主关键词。MarxChou、Bitwarden、博客、文章 这些宽泛词降为次级,不单独触发判定。
效果立竿见影: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。
整轮清理下来:
| 清理前 | 清理后 | |
|---|---|---|
| MEMORY | 33 条 / 6016 chars | 7 条 / 715 chars |
| USER | 8 条 / 941 chars | 3 条 / 314 chars |
| 审计误报 | 22 个 | 0 个 |
memory 从快满变成了几乎空着,审计脚本也终于不再说谎。
从手动救火到自动化治理,再到现在把自动化的坑也填上 —— 说到底就是把” 人的判断” 固化成” 机器的规则”,然后盯着规则跑一段时间,把规则里的 bug 也修掉。skill 管方法论,script 管健康检查,cron 管执行节奏。memory 只需要记住那些没法被规则化的东西 —— 你是谁、在哪、踩过什么坑。

