搭建类似 Hermes Agent 的自进化长期记忆:四层记忆、心跳整理与行为验收
摘要
本文实现一套类似 Hermes Agent 的自进化长期记忆:从 RAG 与记忆系统的边界出发,用文件系统构建 working、episodic、semantic 四层记忆,说明每层的抽取、压缩与输出,再以心跳定时触发整理管线,最后接入 Workspace,用同一任务的带记忆与不带记忆对照验证行为差异。
搭建类似 Hermes Agent 的自进化长期记忆:四层记忆、心跳整理与行为验收
写在前面
对话型 Agent 的上下文只在会话窗口内存活:任务结束、窗口关闭,这段经历就被丢弃。真正难解决的不是会话内的状态维持,而是任务结束之后的三件事——哪些经历值得留下、留下之后怎样被修正和遗忘、下一次任务开始时怎样把该带的带回来。本文实现一套类似 Hermes Agent 的自进化长期记忆闭环,回答这三个问题。
全文沿一条主线推进:先看到缺记忆的 Agent 长什么样,再建立四层记忆理论,接着讲每一层从哪里抽取、怎样压缩、输出成什么,然后把这套抽取管线放到心跳里定时触发,最后用新任务唤起和 Workspace 综合案例验证效果。
部分 | 核心问题 | 结论 |
|---|---|---|
问题背景 | 没有记忆的 Agent 为什么重复踩坑 | 上下文随会话销毁,失败证据与用户交代无人继承;RAG 只覆盖召回一格 |
四层记忆 | 长期记忆应该按什么粒度分层 | working / raw / consolidated / semantic 四层治理方式不同,文件产物各司其职 |
抽取与晋升 | 哪些经历值得变成规则 | 模型判断自然语言,代码按结构化证据决定晋升,晋升门槛宁紧勿松 |
心跳整理 | 后台如何定时驱动整条管线 | 一次心跳做四件事,checkpoint 记录进度,间隔自适应 |
新任务唤起 | 如何只带该带的记忆进上下文 | semantic 优先、episode 按相关性挑选、raw 只在需要证据时回读 |
综合案例 | 文件记忆如何接入工程后端并验收 | Workspace 提供 scope 隔离与上下文打包,行为差异来自记忆而非通识 |
有一条分工原则贯穿全程:理解自然语言的判断交给模型,可核对的结构化判断留在代码。模型负责哪句话值得记、两条候选是不是同一件事;代码负责工具返回的 status、跨会话出现次数、证据链接和 checkpoint。前半段只用文件系统保存中间产物,Workspace 只在最后的综合案例中承担工程集成。
一、缺少记忆的 Agent:同一个坑摔两次
一个旅行规划 Agent 的案例足以说明问题。6 月中旬,用户让它排北京三日亲子游,初版把故宫和环球影城排进同一天,路线检查工具报失败;修正成同区域聚合、远距离大项单独占一整天之后检查通过。用户临走前交代了一句:以后给这种结果,把检查结论一起附上。
一周后,同一个用户来排西安两日游。Agent 把兵马俑和回民街塞进同一天——兵马俑在临潼区,距市区约 40 公里,和上次北京那次是同一类跨区错误;用户的附检查结论要求,它也像没听过一样。
这不是模型能力问题。会话结束时上下文被丢弃,上一次的失败证据、修正方案和用户交代,全都留在那段再也不会被读到的聊天记录里。要解决它,第一反应往往是引入 RAG(Retrieval-Augmented Generation,检索增强生成),但先要看清 RAG 和记忆系统各自覆盖什么。
记忆系统有四个动作,RAG 只覆盖其中一个
RAG 回答的问题是,从一批资料里找相关片段。资料本身是静态的,不管任务成败都不会变。记忆系统面对的是不断产生的经历,至少要回答四个问题:
动作 | 要回答的问题 | 常见工程实现 |
|---|---|---|
ingestion 写入 | 什么经历进入记忆系统 | 事件流、工具结果、用户偏好、决策记录 |
revision 修正 | 旧记忆怎样被改写 | 摘要重写、同义合并、证据重链 |
forgetting 遗忘 | 什么不再进入候选上下文 | 过期、降权、归档、删除 |
retrieval 召回 | 新任务取回哪些记忆 | 文本检索、规则过滤、上下文打包 |
图 1 中,RAG 只对应召回一格。数据库负责存进去还能读出来;记忆系统还要负责这条记录在未来该不该影响行为,后者是治理问题,不是存储问题。写入、修正、遗忘这三个动作决定了一条记录在未来值不值得信,这正是本文要实现的。
二、四层记忆:先分清放在哪里
直接写代码之前,先把长期记忆分层。CoALA(Cognitive Architectures for Language Agents,语言智能体认知架构)把语言 Agent 的记忆分成四类:Working Memory(工作记忆)、Episodic Memory(情景记忆)、Semantic Memory(语义记忆)、Procedural Memory(程序记忆)。本文把它落到工程上,其中情景记忆按治理需要拆成原始和整理后两层,程序记忆收窄为可召回的规则:
图 2 左侧是生产链:原始事件经整理晋升为稳定规则;右侧是使用方:任务按需召回。落到文件产物上,四层记忆的职责如下。
层 | 放什么 | 默认是否进 prompt | 文件产物 |
|---|---|---|---|
Working Memory | 当前任务必要信息 | 是 |
|
Raw Episodic | 原始事件、工具输出、对话 | 否 |
|
Consolidated Episodic | 任务摘要、决策、失败原因 | 候选 |
|
Semantic / Policy | 稳定规则、偏好、长期事实 | 高优先候选 |
|
分层不是为了名称好看,而是为了三件事:只把少量稳定信息放进上下文;需要证据时能回读原始记录;记忆变旧或冲突时能定位并修正。每一层的治理属性都不一样——raw 层只追加、不修饰,semantic 层少而稳定、每条都要有晋升理由。
分层治理也不等于堆数据库。Hermes Agent(Nous Research 的个人 Agent)的实际结构值得参考,因为它把长期记忆拆得很克制:小而常驻的稳定记忆、可检索的完整会话历史、以及可选的外部 Memory Provider。本文不是照搬它的文件格式,而是复现它背后的系统分工。
Hermes Agent 实际结构 | 本文四层里的位置 | 本文的落地方式 |
|---|---|---|
| Semantic / Policy 的稳定规则与用户偏好 |
|
SQLite + | Raw Episodic 的完整事件流 |
|
会话结束抽取 / 后台 review | Raw 到 Consolidated 到 Semantic 的整理动作 | 心跳定时触发文件整理步骤和 checkpoint |
Memory Provider | 语义检索与外部记忆扩展 | 可选的向量库扩展(ChromaDB / LanceDB),不作为主存储 |
Skills 自改进 | Procedural Memory | 收窄为 Semantic / Policy 层的可召回规则 |
三、四层记忆的抽取:来源、压缩与输出
四层记忆不是同一个时间点生成的。raw、consolidated、semantic 主要由整理管线生成和治理;working memory 是新任务进来时,按任务目标和项目范围动态构建的。
记忆层 | 来源 | 抽取方式 | 压缩方式 | 输出例子 |
|---|---|---|---|---|
Raw Episodic | 对话事件、工具调用、工具结果 | 代码原样写入,不做语义判断 | 不压缩,只保留原始事件 |
|
Consolidated Episodic | 同一会话内的 raw 事件 | agent direct request 读取事件流,抽取摘要和候选记忆 | 多轮对话压成会话摘要、失败原因、候选经验 |
|
Semantic / Policy | 多个 episode 的候选记忆 | agent 判断同义候选,代码按证据决定晋升 | 候选句压成稳定规则,保留晋升理由和证据链接 |
|
Working Memory | 当前任务 + 可召回的长期记忆 | 读取 semantic 和 consolidated 文件 | 规则、经验、证据按相关性打包成任务简报 |
|
这个表是后面所有代码的地图。素材是一段仿真长对话:三个会话、36 个事件,覆盖旅行规划 Agent 的完整流水。每个事件带四类字段:身份(event_id / project_id / session_id / turn)、角色(user / assistant / tool)、内容(text),以及工具事件特有的结构化结果(tool / status)。其中 status 字段值得留意:路线检查的 failed / passed 是工具返回的结构化事实,后面它会成为代码可以直接核对的晋升证据。
会话 | 内容 | 埋了什么 |
|---|---|---|
| 北京三日亲子游 | 路线检查失败后修正、用户偏好(午睡、别塞满)、显式交代以后附检查结论、天气闲聊 |
| 西安两日游 | 同一类跨区失败第二次出现、重点排上午偏好、口误噪声 |
| 出租车发票报销 | 另一个项目的显式交代,用于验证记忆的项目隔离 |
3.1 raw 层只保存,看没有整理的召回长什么样
这个实验故意只做一半:把 36 个事件原样写进 raw_events.jsonl,不做任何整理,然后用最朴素的文本重叠召回,观察新任务能拿回什么。新任务是给一家三口规划上海两天家庭游。召回结果全是流水片段:两条失败报错、两条修正后的检查通过、一句以后会附上结论的应答。看起来条条沾边,但没有一条能直接遵守——最值钱的教训,远距离大项单独占一整天,分散在失败报错、修正方案、用户确认三四个事件里,任何逐条检索都拼不回完整的它。
问题不在检索算法,在于 raw 层里根本不存在这条整理后的记录。值得留下的信息需要先被抽取和整理成独立的记录,才谈得上被召回。这就是分层要解决的具体问题。
3.2 consolidated 层:把一次会话压成摘要和候选记忆
抽取由模型完成,通过 agent direct request(Agently 的链式请求方式,用 info 注入事件流、instruct 声明约束、output 声明结构化 schema,一次调用完成抽取)把一组会话事件压成 episode_summary 和 memory_items:
抽取约束里有两个字段值得停一下。第一,statement 要求脱离本次对话也能读懂——记忆是给未来的任务读的,用户说下午要午睡,到了下个月没人知道指的是谁、哪次行程。第二,supported_event_ids 强制每条候选挂上支撑它的事件——没有证据的记忆后面既不能核对,也不能晋升。跑完三个会话,得到 3 条会话摘要、16 条候选记忆;天气闲聊和口误都不会被抽进来。
3.3 semantic 层:模型合并同义,代码按证据晋升
candidate_memories.jsonl 还不是长期规则。晋升分两步:先让模型合并同义候选,再由代码按证据决定是否写入 semantic_rules.jsonl。同义判断是自然语言任务,交给模型;证据判断是可核对的结构化事实,留在代码:
一条候选满足三个通道中的任意一个才晋升:跨会话重复、工具失败证据、用户显式长期指令。16 条候选经模型合并同义后,代码按证据最终晋升 8 条语义规则,另有 6 条候选组因证据不足保留在候选层。
规则 | 晋升通道 | 内容 |
|---|---|---|
rule_001 | 用户显式指令 | 5 岁女儿下午两点到四点需要回酒店午睡,行程不能太满 |
rule_002 | 用户显式指令 | 不喜欢网红打卡式安排,一天不要塞太满 |
rule_003 | 用户显式指令 | 输出行程时要附上路线检查结论和依据 |
rule_004 | 工具失败证据 | 下午从故宫跨区到环球影城单程约 1.5 小时,对 5 岁儿童不可行 |
rule_005 | 用户显式指令 | 希望行程不要太累,适合带孩子 |
rule_006 | 用户显式指令 | 重要景点尽量安排在上午,孩子上午精神最好 |
rule_007 | 工具失败证据 | 兵马俑距市区约 40 公里,不能与下午的市区景点排在同一天 |
rule_008 | 用户显式指令 | 每次行程规划后需要附上路线检查结论 |
没晋升的那批同样值得注意:住王府井附近这类信息只出现过一次、没有失败证据、用户也没说以后都这样,它留在 episode 层等下次证据,而不是急着变成规则。晋升门槛宁紧勿松——semantic 层进 prompt 的优先级最高,进去一条错的,以后每个任务都要为它买单。两条跨区失败教训之所以只被允许进入语义层,正是因为 route_checker 的 failed 状态是工具返回的硬证据,代码可以直接核对。
3.4 working memory:新任务进来时按需打包
四层记忆里只有 working memory 是动态生成的。新任务到来时,系统读取 semantic 规则和 consolidated 摘要,按项目隔离,再按任务相关性挑选 episode,压成一份 task_brief.json,而不是把历史全部塞回上下文。
四、心跳整理:把抽取管线定时化
到这一步,四层记忆的来源、压缩和输出已经清楚。心跳机制做的事简单很多:它不是新的记忆理论,而是一个后台定时任务,在系统空闲时扫描新 raw 事件,触发第三节那套抽取、合并、晋升和 checkpoint 流程。
整理不适合放在对话进行中做——用户还在等回复,没人愿意每轮多花几秒等 Agent 记笔记。更合理的位置是后台:对话结束后、系统空闲时,由一个定时唤醒的整理器扫描新事件。沿用 OpenClaw 一类个人 Agent 的叫法,这个后台触发器称为 heartbeat(心跳)。
一次心跳做四件事
图 3 是一条四步管线:扫描新记录、模型抽取记忆点、代码分流、写 checkpoint。分工原则在这里落得很具体。模型负责的判断包括哪几句值得长期记住、两条措辞不同的候选是不是同一件事、用户是不是显式说了以后都这样——这些都是理解自然语言,代码里写 if "失败" in text 这类关键词匹配靠不住,换个说法就漏。代码负责的判断包括工具返回的 status 是不是 failed、一条候选被几个会话独立支持、处理到哪条了——这些是可核对的结构化事实,交给模型反而引入不确定性。
心跳本体:扫描 checkpoint,触发既有整理步骤
心跳本体不负责抽取逻辑,只负责醒来、检查有没有新 raw 事件、有新事件时触发同一套整理函数、最后写回 heartbeat_state.json。用 TriggerFlow(Agently 的流程编排组件,把多步操作串成可复用的流水线)实现:
scan_checkpoint 把 raw 事件 id 与 checkpoint 里已处理的 processed_event_ids 做差集,得到新事件;write_checkpoint 把新事件并入已处理集合,并依据新事件数量决定下一次心跳间隔。运行两次心跳验证幂等性:第一次心跳发现 36 个新事件,跑完 raw 到 consolidated、candidate 到 semantic、semantic 到 working memory 三步,间隔设为 60 秒;第二次心跳新事件为 0,跳过整理步骤,间隔拉长到 900 秒。checkpoint 的价值就在于此:第二次心跳不重复处理同一批记录。
心跳的安全边界:自动整理不能变成自动污染
心跳是后台进程,它读到的内容不只有用户对话——邮件、消息、网页、仓库都可能流进来。后台写记忆这条通道如果没有治理,会出安全问题。相关论文对 Claw 系个人 Agent 的测量显示:后台接触的不可信内容进入会话上下文、被例行的随手存记忆写成长期记忆,再在之后的前台任务里改变行为,这条 E 到 M 到 B 的路径在缺治理时是常态;带社交可信度包装的误导内容行为误导率最高 61%,例行记忆保存把短期污染固化进长期记忆的比例最高 91%,跨会话行为影响最高 76%。
关键的一点是,不需要提示注入,普通的社交谣言就够了——问题出在架构,不在某条恶意 prompt。因此后台写入必须有独立治理:来源标记、证据链接、置信度、可回滚。本文管线里的 source、supported_event_ids、promotion_reason 这些字段,就是在给每条记忆留它凭什么在这里的答案。心跳不是越主动越好:没有来源、没有证据、没有回滚路径的后台记忆写入,会把自动整理变成自动污染。
五、新任务唤起:从文件记忆构建 working memory
长期记忆整理成几个可追溯的文件产物之后,新任务唤起要做的事,是读取 semantic 规则和 consolidated 摘要,压成当前任务可用的 task_brief.json,而不是把历史全部塞回上下文。
召回的四步
图 4 展示召回的完整链路:新任务进来,先推导 scope(project / user / 任务域 / 风险等级),再分优先级取候选,打包为任务上下文,最后带着记忆执行。候选优先级从高到低是:semantic 规则先进,少而稳定,直接变成任务约束;consolidated 经验按相关性挑选,解释之前为什么失败;raw 证据只在需要时回读,默认不进上下文。每一条都带来源,可以追问这条记忆凭什么在这里。
召回的产出是一个可审计的 ContextPackage,不是一段越拼越长的历史记录。文件版实现不需要复杂 API:先按 project_id 做项目隔离,再按任务目标挑选相关 episode,最后把稳定规则和候选经验打包成一个任务简报。重点在路由和边界,而不是某个存储后端。
文件产物
文件版的记忆库一共就这几个产物,每层的产出边界清晰:
raw_events.jsonl保存完整事件流,只追加不修饰;consolidated_sessions.jsonl保存会话摘要和候选数量,每条能追到 raw 证据;candidate_memories.jsonl保存模型抽取出的候选记忆;semantic_rules.jsonl保存被代码按证据晋升的稳定规则,带晋升理由和证据链接;task_brief.json保存当前新任务要带入上下文的 working memory。
新任务拿到的就是这些整理后的规则和摘要,不再是 raw 对话流水。向量库解决的问题是自然语言近义匹配这一层增强——文本重叠检索抓不住换了说法的同一个意思——但它只保存可重建的检索副本,主事实仍然留在 JSON / JSONL 文件里。
六、综合案例:文件记忆接入 Workspace
到目前,整套记忆都只是本地文件。综合案例把这些文件产物导入 Workspace(Agently 的持久化工作区,提供记录入库、scope 隔离、检索和上下文打包),让文件产物负责记忆治理,Workspace 负责持久化记录和工程集成。
6.1 文件记忆导入 Workspace
ingest 时每条记录携带四类元数据:collection 区分语义规则与会话摘要,kind 标注记录类型,scope 记录归属边界,source 和 meta 保留来源与晋升理由。build_context 再按任务目标、scope 过滤和字符预算打包,产出带来源的候选列表。导入后,语义规则 8 条、会话摘要 3 条进入 Workspace,candidate_count 为 10。
6.2 同一个任务的 A/B 对照
验收的关键是同一个任务在带记忆和不带记忆两种上下文下各出一版草案。任务不变:带 5 岁的女儿去上海玩两天,孩子一直想去迪士尼乐园,最终路线要可执行。两版输出都用同一个 schema——行程骨架、输出附带、规则遵守。
版本 | 第一天 | 第二天 |
|---|---|---|
不带记忆 | 下午逛迪士尼小镇、体验儿童游乐区 | 全天游玩迪士尼乐园,按低龄儿童友好路线 |
带记忆 | 下午去迪士尼小镇免费游玩、熟悉氛围 | 一早入园,重点体验适合 5 岁女儿的项目,中午在园内用餐并观看花车巡游 |
两版都能把迪士尼排成独立一天,但这属于通识——迪士尼是远距离大项、适合整日游玩,模型不需要记忆也能给出。稳定的差异来自通识给不出的那部分:带记忆版围绕 5 岁儿童的节奏编排了具体项目顺序和园内午间安排,而不带记忆版没有午休窗口意识,更不会知道这个用户要看路线检查结论。这组对照是整条管线的验收:行为差异来自记忆,而不是来自模型通识。
6.3 把验收从人眼扫一遍变成逐条核对
在第一个演示版本里,规则遵守字段在两侧输出中都为空——这本身是模型输出的正常波动,也正是它要解决的问题:肉眼看行程容易漏掉规则是否落实。工程化的验收在完整脚本版本里做了三件事。
第一,规则遵守字段是结构化输出而非散文,两侧共用同一 schema;第二,代码兜底 apply_memory_guards:如果稳定规则要求附路线检查结论但 输出附带里没有以路线检查结论开头的说明,强制补一条模拟检查说明;如果规则要求午睡窗口但行程里没有 14:00 到 16:00 的休息安排,补一条午休安排。第三,独立核验 check_rule_compliance:用第二个 agent 请求逐条对照稳定规则原文输出已满足、部分满足、未满足以及证据或待办。验收就从人眼扫一遍变成逐条核对。
6.4 项目隔离生效
综合案例还有一个观察点:expense-agent 的小额报销规则不会被召回。Workspace 的 scope.project_id 卡住了项目边界,同一份记忆库里不同项目的规则互不串扰。Workspace 不是记忆理论本身,而是把文件记忆接入工程系统的后端:文件产物负责记忆治理,Workspace 负责记录、隔离、召回和上下文打包。
七、工程边界:谁负责什么
整套系统由一组职责互不重叠的模块组成。边界分开之后,每个模块都可独立替换:将来增加 Workspace 或向量库不影响文件产物格式;换抽取模型不影响 checkpoint 和证据链;调整晋升门槛不影响任务接口;加安全策略不用重写业务 Agent。
角色 | 负责什么 | 不负责什么 |
|---|---|---|
Session | 当前会话历史、窗口裁剪 | 跨任务的长期记忆治理 |
文件记忆库 | raw、consolidated、semantic、task_brief 等中间产物 | 工程级权限、索引、并发和查询优化 |
Workspace | 综合案例里的工程化持久化、scope 隔离、ContextPackage | 四层记忆理论本身、判定某条记忆一定为真 |
向量库扩展 | 语义近义召回加速 | 主事实存储、证据链、权限 |
心跳整理器 | 定时触发抽取、合并、晋升、checkpoint | 自己决定记忆理论和业务规则 |
模型 | 抽取、同义判断、摘要、表达 | 独立完成记忆治理 |
总结
这套实现把长期记忆从抽象概念落成了可运行的闭环,四条结论值得记住。
第一,记忆是治理问题。RAG 只覆盖召回一格,写入、修正、遗忘这三个动作决定一条记录未来该不该影响行为,这是存储之外的治理问题。
第二,Hermes Agent 给了可落地的参照。稳定记忆小而常驻,历史会话完整可查,外部 Memory Provider 只做附加增强;本文用文件产物把这套分工拆成四层,每层的治理方式不同——raw 只追加、consolidated 带证据、semantic 少而稳定。
第三,先用文件系统跑通记忆闭环,再谈工程化。心跳定时驱动抽取管线,checkpoint 保证幂等;Workspace 是工程化后端,向量库只是可重建的召回副本。分工上,抽取靠模型,治理靠代码,验收看行为——理解自然语言的判断交给模型,可核对的结构化判断留在代码,最终看同一个任务在带记忆和不带记忆时有没有可观察的行为差异。
