阅读契约:跟踪任务收尾时的调度、复盘可写范围与 Skill 生命周期;区分待审批建议、已应用写入和未来可见状态。本文依据所链接的固定公开源码,示例用于解释机制,不代表改写已通过独立评测。
一、为什么把复盘放在前台回答生成之后
前面讲完回合、状态存储和工具权限之后,才适合谈“self-improving”。否则这个词很容易被误解成: 模型一边回答一边反思自己,或者每轮结束都把整段对话总结成长期经验。听起来很聪明,实际会带来两个问题。 第一,当前任务还没交付,模型就开始分心判断“这件事值不值得记住”;第二,中间试错、临时路径和一次性日志可能被写成长期事实。
Nudge 是什么?这个英文词原意是“轻推一下”。在 Hermes 里,它更接近一个到点提醒:
运行时用计数器记录已经经过多少用户回合或工具调用迭代,达到配置间隔后,只把对应的 review 标记设为
true。它不会往用户消息里塞一段提醒,不会立即调用模型,也不等于已经创建了 Memory 或 Skill。
它只是在告诉 finalizer:“这轮未中断且已有最终回答,可以安排一次复盘,看看有没有值得留下的东西。”
开头总图把前台收尾、单轮复盘和周期整理分开。这里的收尾指最终回答已经生成,不保证渠道已把消息送达用户。前台回答完成以后,满足条件的复盘才进入调度;它可能立即运行,也可能等待本地机器空闲。复盘的写入又可能停在“待审批”,只有实际应用的内容才进入后续读取路径。下方的周期维护独立检查 Skill 生命周期,可选的语义合并不是每次复盘的下一步。
可以从一个很普通的场景理解:你让 agent 修一个测试失败。它在过程中发现了项目的固定命令、你的提交偏好, 还试过几条最后没用上的路径。哪些应该留下?测试日志本身应该进 SessionDB;“以后这个项目先跑某个脚本”可能适合变成 skill; “不要自动改生成文件”这类任务规则应进入对应 Skill 或项目 Context;“用户始终偏好简体中文沟通”这类跨任务稳定偏好才适合进 Memory。那些走错的临时路径,则不该变成任何长期状态。
Hermes 的答案不是“每轮自动总结”,而是一条在任务收尾时调度的受控写账流程:当前 turn 先产出最终回答; finalizer 只在满足条件时安排后台 review;review fork 默认通过 Memory、Skills 与只读文件工具检查经验; 写入落到对应 store;之后的任务再通过正常的 memory 快照、skills 索引或检索路径看到这些变化。当前已生成的回答不被改写, 主会话 transcript 也不会被后台 review 当场重写。
| 步骤 | 发生了什么 | 为什么这样安排 |
|---|---|---|
| 1. 先生成回答 | 前台回合完成工具调用和最终回答。 | 用户要的是当前任务结果,不是等待模型自我总结。 |
| 2. 再判断 | finalizer 检查最终回答、中断状态、是否跳过后台复盘,以及 Memory / Skill 到点提醒;后续调度还检查配置与来源。 | 跳过中断或没有最终回答的回合;这些门槛不保证候选经验本身正确。 |
| 3. 开后台分支 | background review 拿本轮 messages snapshot,fork 一个受限 AIAgent 回看刚才发生的事。 |
它能看到本轮用户消息、assistant 回复、工具调用和工具结果,但不抢前台任务的上下文。 |
| 4. 限制能写哪里 | review fork 默认使用 Skills 与只读文件工具;Memory 还需相应触发条件,外部 memory provider 摄取被跳过。 | 默认写入经过 Memory / Skills 管理工具;用户可显式配置额外工具,工具名称仍须存在于父级工具清单中。 |
| 5. 以后再生效 | 写入成功后,后续 turn 通过正常的 Memory / Skills 读取路径使用它。 | 学习结果影响未来任务,而不是反过来篡改已经生成的这一轮回答。 |
所以读 Hermes 的自进化,不要先问“模型有没有反思能力”,而要问四个更具体的问题:谁触发这次学习?它能看什么? 它能写到哪里?写完什么时候才会影响下一轮?源码里的 background review 和 curator,正好分别回答“单轮之后学什么” 和“长期技能库怎么整理”。
二、Finalizer:只在回答完成后排 review
先把 finalizer 的最后几步展开。它已经拿到最终回答,先完成 post hook 和外部 memory 同步,然后才检查后台学习门槛。 删去异常处理后,关键顺序几乎可以直接从源码读出来:
agent._sync_external_memory_for_turn(...)
if final_response and not interrupted and not skip_background_review and (
_should_review_memory or _should_review_skills
):
agent._spawn_background_review(
messages_snapshot=list(messages),
review_memory=_should_review_memory,
review_skills=_should_review_skills,
)
这段删减代码展示的是 finalizer 的入口条件;实际代码还要求 skip_background_review 未开启。Memory 按用户回合计数,Skill 按工具迭代计数,且需要对应工具可用。达到间隔后,计数置零并产生 review 标记。Memory 计数、Skill 计数和finalizer 条件分别说明了这三步。调度入口还会跳过未显式请求的委派子 agent,并检查 auxiliary.background_review.enabled;开启提醒不等于每个来源都允许启动后台任务。
假设 Memory 的间隔是 3 个用户回合:前两轮只是把计数从 0 加到 2,第三轮才产生“该复盘一次”的标记。如果第三轮被中断,
finalizer 仍不会启动 review;如果产生最终回答且未中断,review 也可以判断“没有稳定经验”并什么都不写。反过来,如果前台 agent 已经主动用了
memory 或 skill_manage,对应计数会被重置;见
工具分派前的计数重置。
所以后台 review 不是固定税,而是“这段时间没有主动写入长期信息,且现在已经安全结束,再检查一次”。finalizer 不让 review 参与“当前答案怎么写”,
只让它在答案完成后判断“这轮结果是否暴露了可复用信息”。而且它是 best-effort:启动失败不会推翻已生成结果,启动成功也允许最后什么都不写。
“排入复盘”也不等于“已经开始调用模型”。默认 defer: auto 只对 Hermes 管理的本地 llama-server 延后执行:同一会话只留最新快照,等待前台安静至少 15 秒且服务槽位空闲;等待超过默认 30 分钟则允许调度,不再要求空闲。队列只在内存里,进程退出不会恢复这些待办;出队时还会重查 enabled。显式 /refine 不走延后队列。实现见 空闲复盘队列。
三、Review fork:保留证据,隔离对话,约束写入
finalizer 传入的是已完成对话的结构化副本,嵌套工具调用与内容也复制,防止 fork 清理自己的消息时改到前台历史。继续用修测试的例子,失败与成功路径都在:
user: 修复这个失败测试
assistant -> terminal: 直接运行 pytest
tool -> assistant: 失败,生成文件尚未更新
assistant -> terminal: 先运行生成器,再运行 pytest
tool -> assistant: 测试通过
assistant: 已修复并验证通过
这样才能提炼“先运行生成器”,而不是把失败尝试也当作推荐流程。同模型 fork 继承父级的 system prompt、工具 schema、推理配置和已解析的缓存作用域,尽量复用首请求的暖前缀;如果辅助配置路由到另一个模型,则发送较短的历史 digest。这里的工具 schema 是模型看见的清单,真正能调用什么仍由分派白名单决定。见 fork 构造与 历史选择与运行。
3.1 不写主会话,但允许独立压缩
fork 沿用父级 session id 时,正常持久化会把“请复盘”的 harness 写进真实 transcript。Hermes 因而设置 _persist_disabled=true,清空 _session_db,跳过外部 memory provider 摄取,并阻止 fork 关闭父会话。可是完全关闭压缩又会让长历史在多次工具调用中反复增长;现在的办法是先把压缩器的 SessionDB 和 session id 绑定解除,再只在 fork 的内存历史中压缩:
review_agent._persist_disabled = True
review_agent._session_db = None
compressor.bind_session_state(session_db=None, session_id="")
review_agent.compression_in_place = True
review_agent.compression_enabled = True # 仅当上面的解绑成功
review_agent._review_defer_compaction_before_first_response = True
这段是成功解绑分支的简化形状。首个响应之前延后压缩,保留首请求的缓存复用机会;后续压缩只改变 fork。若插件压缩器不能解绑,源码保持压缩关闭。复盘默认最多 16 次迭代,总输入预算默认取其上下文窗口的 75%,上限 600,000 tokens;窗口未知时用 120,000,配置 max_input_tokens 可覆盖。压缩约束单次上下文,累计预算约束整次复盘成本,两者用途不同。见 预算定义与 压缩隔离。
3.2 能读什么,与能改什么分开检查
默认白名单包含 Skills 工具和 read_file / search_files;只有这次触发了 Memory 复盘且 Memory 或用户画像开启,才加入 memory。用户可用 auxiliary.background_review.extra_tools 放行父级已有的工具,它不会凭空添加 schema。默认终端、普通文件写入和委派工具仍被拒绝。见 分派白名单。
即使 skill_manage 已被放行,它也不能随意改任何 Skill。后台修改只接受交给 Curator 管理的记录,拒绝 pinned、用户自建、Hub、bundled 与外部目录;写入前还必须读过本轮要修改的具体文件。read_file 也会登记这一读取,所以“先读取,再 patch”能通过。见 后台 Skill 写入守卫。
3.3 待审批建议不等于已应用经验
无人值守复盘的 Memory add 可以继续走正常写入规则,但 replace / remove,包括批量操作里的这些动作,会被暂存为提案;暂存失败则拒绝,不能直接删除旧记忆。此外,开启 skills.write_approval 后,Skill 修改也先暂存,用户批准后才重放。下面两个工具结果都可能带 success: true,含义却不同:
[
{"success": true, "staged": true, "pending_id": "proposal-id"},
{"success": true, "message": "Skill 'generated-code-testing' created."}
]
第一条只是受理提案,尚未改变长期 Skill;第二条才表示创建已经执行。后台 action summary 会区分待审批提案与实际修改,并排除继承历史中的旧工具结果;通知本身不证明提案已应用。对应 Memory 提案规则、Skill 审批入口和复盘结果摘要。
如果新用户回合在复盘结束前到来,前台会请求取消 fork,并最多等待 2 秒的请求退出确认;超时记录警告后继续前台。因此不能声称后台一定已停止,也不能保证完全没有资源重叠。针对延后模式的本地复盘,被抢占的任务最多重新入队 3 次。这个流程优先保证用户继续工作,已应用写入不会因取消自动回滚。见 前台入口、取消握手和有限重新入队。
四、Curator:不是每轮反思,而是长期整理
Background review 解决的是“这一轮之后是否有东西值得保存”。但运行几个月后,问题会换一种形态:技能库里可能同时出现 “修改 schema 后先生成代码再测试”“生成文件变化后重跑测试”“测试前刷新 generated files”三条 Skill。它们都可能来自真实成功经验, 却未必应该永远作为三份独立说明存在;与此同时,还有一些 Skill 只是暂时少用,不能因为最近没有碰到对应任务就立刻删除。 Curator 处理的正是这类跨很多轮才显现的维护问题。
4.1 它什么时候醒来:不是每轮结束,而是入口定期检查
Curator 不挂在前面的 finalizer → background review 调用顺序上。CLI 会在新会话启动时检查一次,长期运行的 Gateway 则在 housekeeping
循环里每小时询问一次;真正是否运行,再由 maybe_run_curator 和 should_run_now 共同决定。把这个触发顺序先展开,
后面的“两阶段”就不会误读成每个 turn 都执行两遍:
CLI 新会话启动 / Gateway 每小时轮询
-> curator.enabled 是否开启?
-> 是否被 paused?
-> 距离 last_run_at 是否已过 interval_hours?
-> 若调用方提供空闲时间,是否达到 min_idle_hours?
-> 是:启动一次 Curator run
-> 否:本次检查结束,不读取也不改写 Skill
默认间隔是 7 天,默认空闲门槛是 2 小时;但 CLI 启动和当前 Gateway 轮询都传入无限空闲值,不能把这个配置读成它们实际测量了用户停顿 2 小时。首次看到一个从未运行过 Curator 的安装时,Hermes 只把
last_run_at 记为当前时间,再等待完整的一个间隔,并不会刚升级就立即整理技能库。手动执行
hermes curator run 才会绕过这条定期检查。触发入口见
CLI 启动钩子、
Gateway 轮询
和
should_run_now。
4.2 第一阶段:按时间阈值维护生命周期,不调用模型
Hermes 不把使用统计写进 SKILL.md,而是放在 ~/.hermes/skills/.usage.json 这个旁路记录里。
对每条 Skill,它分别记录“真正加载或引用了多少次”“用 skill_view 看过多少次”“被 patch 过多少次”,以及三种行为最近发生的时间。
last_activity_at 不是第四个独立计时器,而是 last_used_at、last_viewed_at、
last_patched_at 中最新的一个:
{
"generated-code-testing": {
"use_count": 12,
"view_count": 4,
"patch_count": 2,
"last_used_at": "2026-04-10T09:00:00Z",
"last_viewed_at": "2026-04-12T08:00:00Z",
"last_patched_at": "2026-03-28T18:00:00Z",
"state": "active",
"pinned": false
}
}
因此这条记录的最后活动时间是 4 月 12 日。计数更新和时间合并分别在
bump_view、bump_use、bump_patch
与
活动时间计算。
Curator 的生命周期不是四个互斥状态,而是三个状态加一个保护开关:
| 状态或标记 | 仍会被后续任务发现吗 | 怎样进入 | 怎样离开 |
|---|---|---|---|
active |
会 | 新建、恢复,或 stale 后重新出现近期活动 | 默认连续 14 天无活动后变成 stale |
stale |
会。目录仍在正常 Skill 路径中 | 超过 stale_after_days |
再次活动后在下一次 Curator run 回到 active;继续闲置则归档 |
archived |
不会。目录被移到 skills/.archive/ |
默认连续 30 天无活动,或人工归档 | hermes curator restore <name> 恢复为 active |
pinned |
会;pin 不改变 active / stale,它不是第四种状态 | 用户显式 pin | 用户显式 unpin;开启时自动迁移直接跳过它 |
第一阶段逐条读取这份记录,用同一套可配置阈值做迁移。下面是贴近源码、删去异常处理后的判断顺序:
anchor = newest(last_used_at, last_viewed_at, last_patched_at)
anchor = anchor or created_at
if skill.pinned or skill.referenced_by_cron:
continue
if now - anchor >= archive_after:
archive(skill) # 移到 .archive,可恢复
elif now - anchor >= stale_after and skill.state == "active":
skill.state = "stale"
elif now - anchor < stale_after and skill.state == "stale":
skill.state = "active"
继续用 generated-code-testing:第 10 天被使用,最后活动时间更新;第 25 天运行 Curator 时,已经 15 天没有活动,它从 active 变成 stale,但仍可发现。第 28 天再次查看或使用后,下一次 Curator run 会把它恢复为 active;此后一直闲置,则第 43 天再次 stale,第 59 天超过 30 天后归档。这里使用当前默认的 14 / 30 天阈值,实际迁移只在 Curator 运行时发生,不是到点自动搬目录。默认阈值与迁移和宽限逻辑共同决定结果。
候选也不是“磁盘里所有 Skill”。Agent 在后台 review 中自主创建并标记的 Skill 会进入;用户也可显式执行 hermes curator adopt <name> 交出管理权;配置
curator.prune_builtins: true 时,bundled built-ins 也会进入时间维护,但第一次看到只建立基线,不会立刻归档。
Hub 安装、外部目录和受保护的内置 Skill 不进入候选;被 cron job 引用的 Skill 与 pinned Skill 一样跳过自动迁移。
候选范围见
候选枚举。
4.3 所以它不是 LRU,也不是 LFU
“最近使用”“使用次数”“归档”这些词很像缓存淘汰,但 Curator 没有“容量满了必须赶走一个条目”的前提,也不会把全部 Skill 排名后淘汰最后一名。 更准确地说,第一阶段是每条 Skill 独立计时的生命周期规则,接近带恢复通道的时间阈值管理:
| 问题 | LRU / LFU 缓存 | Hermes Curator 第一阶段 |
|---|---|---|
| 什么时候淘汰 | 通常在容量或内存压力出现时 | 定期检查到单条 Skill 超过时间阈值时 |
| 主要决策信号 | LRU 看全局最近次序,LFU 看全局频率 | 看该 Skill 最近一次 use / view / patch 的时间 |
| 是否需要全局排名 | 需要从候选中选牺牲者 | 不需要;多条 Skill 可以同时不变、stale 或 archived |
| 移除是否不可逆 | 缓存条目通常直接丢弃并可从源重建 | 只移动到 .archive,可以 restore 或整体 rollback |
use_count、view_count 和 patch_count 会出现在 status 与候选清单里,便于人和第二阶段理解活跃程度;
但自动 stale / archive 并不按频率排序。use_count == 0 只触发一个保护:刚创建、还没等到合适任务的 Skill,
至少先活过 stale 窗口,不能因为“零次使用”立即归档。这就是它与 LFU 最容易混淆、也最需要分开的地方。
4.4 第二阶段:可选的语义合并,不是默认必跑
时间戳能判断“多久没碰”,却不能判断三条测试 Skill 是否应该合成一条。因此 Curator 才保留第二阶段:把
state、pinned、cron 引用、use / view / patch 计数和最后活动时间渲染成候选清单,
再让一个受限的辅助 agent 阅读内容关系。但当前源码里 consolidate 默认是 false:
普通自动运行只做第一阶段;只有配置 curator.consolidate: true,或手动运行
hermes curator run --consolidate,才会付出模型调用成本。
counts = apply_automatic_transitions(now=start) # Phase 1 总会执行
if not consolidate:
write_report("llm: skipped (consolidation off)")
return
candidate_list = _render_candidate_list()
if candidate_list:
llm_meta = _run_llm_review(candidate_list) # Phase 2 可选
仍以三条测试 Skill 为例。辅助 agent 不是看到名字相似就删除,而是先读取完整 Skill package,判断它们是否属于同一类可复用流程。
如果确实重叠,它可以选择或创建一条 generated-code-testing umbrella Skill,把各自独有的步骤写进正文、
references/、templates/ 或 scripts/,再把已吸收内容的旧目录归档;如果只是某条说明漂移,
就 patch 那条 Skill。第二阶段不能仅凭“内容无效”裸删:后台 delete 必须带 absorbed_into,且目标 umbrella 已存在;没有承接目标会被拒绝,过期清理由第一阶段负责。换句话说,第二阶段可以 keep、patch、consolidate,并归档已合并旧包。
合并前
test-after-codegen
refresh-generated-files
pytest-schema-change
语义判断
三者共享“生成代码变化后的验证流程”
但各自保留不同工具、失败信号和命令
合并后
generated-code-testing/
SKILL.md
references/schema-change.md
scripts/verify-generated-files.sh
三个旧目录 -> skills/.archive/(记录 absorbed_into,可恢复)
pinned 不是模型可随意授予的“优秀等级”,而是用户设置的保护栏;第二阶段必须跳过 pinned Skill。
bundled built-ins 在允许清理时最多被归档,不参与内容合并;Hub 与外部目录仍然完全越界。模型阶段的开关和执行顺序见
run_curator_review,
删除限制见 合并归档守卫;候选字段见
_render_candidate_list。
4.5 整理完成后,谁会看见什么
只有启用语义合并时,Curator 才会在修改前 best-effort 尝试为 ~/.hermes/skills/ 做整树快照;只做时间清理时不复制整棵树,归档目录本身提供单条恢复路径,并清理过期快照。运行后,状态、迁移、合并去向、工具调用和恢复信息进入 ~/.hermes/logs/curator/<timestamp>/。active 与 stale 仍在正常发现树中,archived 退出发现;合并后的 umbrella 从后续扫描开始可用。快照条件和报告写入说明了两种运行方式的成本区别。
不放心时可以先运行 hermes curator run --dry-run:它跳过自动状态写入,并要求模型只报告、不调用任何修改工具;
dry-run 也不会推进正式运行的 last_run_at。如果正式整理结果不对,既可以 restore 单条 archived Skill,
也可以在 pre-run snapshot 确实创建成功时回滚合并前的技能树。于是“两阶段”的分工就很清楚了:第一阶段只回答“这条 Skill 在生命周期的哪一段”,
第二阶段只回答“这些仍值得保留的知识应该怎样组织”。
五、结论:三只时钟共同改进,但不能混成同一个过程
到这里,自进化的流程就完整了:短期任务由前台 turn 解决;单轮之后的偏好和流程是否写入 Memory 或 Skills,由 background review 判断; 多轮累积后的技能库膨胀、过期和合并问题由 curator 解决。三者都叫“改进”,但触发时机、输入快照、决策方式和可写范围都不同。
这里完成的是运行时学习,还不是离线优化。
Background review 回答“这一轮有什么值得留下”,curator 回答“长期技能库怎样整理”;它们都没有证明某次 Skill 改写在一组独立任务上表现更好。
要回答后一个问题,需要离线评测与优化;在开始这组独立任务对比以前,下一篇先补上用户主动学习的入口。
下一篇会沿一条 /learn 请求,看 agent 怎样读取指定资料、创建 Skill,
以及 Learning Graph 为什么只是关系视图而不是另一套神秘学习引擎。