1. 从“失忆”说起:Hermes 记忆机制到底在解决什么问题
做过智能体开发的人大概率都经历过这种尴尬:上一轮对话里用户明明说了“我叫老张,做跨境电商的”,下一轮再问“帮我写个选品建议”,模型却像第一次见面一样,回你一句“请问您从事什么行业”。这不是模型不聪明,而是它压根没把上一轮的信息“记住”。Hermes 的记忆机制,本质上就是给智能体装上一套可检索、可分层、可持久化的“外挂大脑”,让它在多轮交互、长任务执行、跨会话协作中不再反复失忆。
我接触 Hermes 这套框架有一段时间了,从最早的 agent 安装部署,到后面啃它的架构详解,再到这次专门拆它的记忆模块源码,踩过的坑不算少。很多人第一次看 Hermes 的记忆机制,会觉得它不过是“把对话存进向量库再检索出来”这么简单,但真去读源码就会发现,它其实做了相当精细的分层设计:短期记忆负责当前会话的上下文窗口管理,长期记忆负责跨会话的知识沉淀,而中间还有一层负责把零散信息压缩成结构化摘要。这三层各司其职,缺一层都会导致智能体要么“记不住”,要么“记太多把上下文撑爆”。
这篇文章适合三类人看:一是正在用 Hermes 搭智能体、想搞清楚记忆为什么时好时坏的一线开发者;二是准备基于 Hermes 源码做二次开发、需要理解记忆模块接口设计的技术人;三是单纯想搞明白“智能体的记忆到底该怎么设计”这个通用问题的架构爱好者。我会尽量把源码里的关键结构、调用链路、参数取舍讲透,同时把我在实际部署中遇到的坑和排查经验一并放出来,让你看完能直接对照自己的项目改。
需要先说明一点:Hermes 的记忆机制并不是一个孤立模块,它和 agent 的调度、工具调用、上下文组装是深度耦合的。所以讲记忆,不能只盯着存储那一块,得把它放回整个 agent 执行流程里去看,才能理解为什么某些设计要这么绕。
2. Hermes 记忆机制的整体设计与分层思路
2.1 为什么不能只用一个向量库搞定所有记忆
刚上手的人最容易犯的错,就是觉得“记忆=向量数据库”,把所有对话一股脑塞进去,检索的时候按相似度捞几条出来拼进 prompt。我早期也这么干过,结果很快就撞墙:一是检索出来的内容经常和当前问题不相关,二是随着对话变长,检索结果越来越杂,三是跨会话的时候根本分不清哪些是用户偏好、哪些是临时信息。
Hermes 的设计者显然想过这个问题。它的记忆机制在源码层面被拆成了几个明确的层次,每一层有不同的生命周期、不同的存储介质、不同的检索策略。这种分层不是为了炫技,而是因为不同种类的信息,其“保质期”和“检索方式”本来就完全不同。
打个生活化的比方:短期记忆就像你手边的一张草稿纸,写着当前这通电话里对方说的关键信息,打完电话可能就撕了;长期记忆像你的通讯录和备忘录,记录的是“这个人是谁、他偏好什么”,会长期保留;而中间那层摘要记忆,像是你打完电话后在备忘录里写的一句“今天和老张聊了选品,他倾向家居类目”,是压缩后的结论。
2.2 三层记忆的职责边界
从源码结构上看,Hermes 的记忆大致可以归为三类职责:
- 会话级短期记忆:绑定当前 session,主要解决上下文窗口有限的问题。它负责维护最近若干轮的原始对话,并在超出窗口时做截断或摘要。
- 持久化长期记忆:跨 session 存在,通常落到外部存储(向量库或结构化数据库),存储用户画像、历史结论、重要事实等。
- 压缩摘要记忆:介于两者之间,把一段较长的交互压缩成简短摘要,既保留信息又节省 token。
这三层的边界不是死的,源码里通过配置项可以调整每层的容量和触发条件。理解这个边界,是理解后续所有细节的前提。
2.3 记忆读写的整体调用链路
一次完整的交互里,记忆的流转大致是这样的:用户输入进来,agent 先触发记忆检索,从长期记忆和摘要记忆里捞出相关内容,和短期记忆里的最近对话一起组装成上下文,送给模型;模型产出结果后,agent 再把这一轮的关键信息写回记忆,该进短期的进短期,该沉淀长期的沉淀,该压缩的压缩。
这个链路里最容易被忽视的是“写回”这一步。很多人只关注检索,觉得检索准就行了,但实际上如果写回策略设计得不好,长期记忆里会堆满垃圾,检索质量会随时间断崖式下降。Hermes 在写回环节做了不少过滤和去重,这部分后面会细讲。
3. 核心数据结构与关键源码解析
3.1 记忆条目的基本结构
Hermes 里一条记忆并不是简单的字符串,而是一个带元信息的结构体。从源码看,一条记忆条目通常包含这些字段:内容本体、时间戳、来源标识(哪个 session、哪一轮)、类型标签(事实、偏好、结论等)、以及一个可选的向量表示。
为什么要带这么多元信息?因为检索的时候,光靠语义相似度是不够的。比如用户问“上次那个方案”,纯语义检索可能捞出一堆无关的“方案”,但如果结合时间戳和来源标识,就能精准定位到最近一次讨论的那个方案。类型标签则用于过滤,比如组装用户画像时只取“偏好”类记忆,不取“临时结论”类。
我在实际项目里就吃过亏:早期没重视类型标签,把所有记忆混在一起检索,结果模型经常把临时性的“这次先按 A 方案来”当成用户的长期偏好,导致后续推荐一直跑偏。加上类型过滤后,这个问题基本消失了。
3.2 短期记忆的窗口管理逻辑
短期记忆的核心矛盾是:上下文窗口有限,但对话可能很长。Hermes 的处理方式是维护一个滑动窗口,窗口内保留原始对话,窗口外的内容根据策略决定是丢弃还是压缩。
源码里这块逻辑通常涉及一个阈值判断:当累计 token 数接近模型上下文上限的某个比例(比如 70%)时,触发压缩。压缩不是简单截断,而是把最早的那部分对话交给模型生成一段摘要,然后用摘要替换原始内容。这样既腾出了空间,又保留了信息。
这里有个参数很关键:触发压缩的阈值比例。设太高,容易在压缩前就超限报错;设太低,压缩太频繁,既费 token 又可能丢信息。我实测下来,70% 到 80% 之间比较稳,具体要看你的模型上下文大小和单轮对话的平均长度。
3.3 长期记忆的写入与去重
长期记忆的写入不是每轮都做,而是有触发条件的。Hermes 通常会在以下几种情况触发写入:用户明确表达了偏好或事实、agent 得出了重要结论、或者一轮任务完成产生了可复用的经验。
写入前会做去重。去重的逻辑一般是先做语义相似度比对,如果新记忆和已有记忆的相似度超过某个阈值,就不重复写入,或者用新内容更新旧内容。这个阈值设得太低,会导致该记的没记住;设得太高,会导致重复记忆堆积。源码里这个阈值通常是可配的,默认值偏保守。
提示:去重阈值不要拍脑袋定,建议先用一批真实对话跑一遍,观察重复率和漏记率,再反过来调阈值。
3.4 摘要记忆的生成时机
摘要记忆的生成时机有两个:一是短期记忆压缩时顺带生成,二是任务阶段性完成时主动生成。前者是被动的,后者是主动的。
主动生成这块值得多说一句。Hermes 允许在 agent 执行流程里插入“记忆固化”的钩子,比如一个多步任务完成后,调用一次摘要生成,把整个任务的输入、过程、结论压缩成一段话存进长期记忆。这样下次遇到类似任务,检索到这段摘要就能直接复用经验,不用从头再来。
我在做自动化流程类 agent 的时候特别依赖这个钩子。没有它的时候,agent 每次执行同类任务都像新手;加上之后,第二次执行明显更顺,因为它“记得”上次哪一步容易出错。
4. 记忆检索的策略与参数调优
4.1 检索不是单纯的向量相似度
很多人以为检索就是“把 query 转向量,然后找最相似的 top-k”。Hermes 的检索要复杂一些,它是多路召回再融合排序。多路包括:向量相似度召回、关键词召回、时间衰减加权、类型过滤。
为什么要多路?因为纯向量召回有它的盲区。比如用户问一个包含具体编号或专有名词的问题,向量召回可能不如关键词召回准;再比如用户问“最近聊的那个事”,时间衰减加权就很重要。多路召回之后再做融合排序,能显著提升命中率。
4.2 时间衰减权重的计算
时间衰减是长期记忆检索里一个很实用的机制。核心思想是:越新的记忆,权重越高。常见的计算方式是指数衰减,公式大致是weight = exp(-λ * Δt),其中 Δt 是记忆距今的时间差,λ 是衰减系数。
λ 的取值直接决定“多快算旧”。λ 大,衰减快,检索偏向近期记忆;λ 小,衰减慢,久远的记忆也有机会被召回。这个参数没有万能值,取决于你的应用场景。做客服类 agent,用户偏好相对稳定,λ 可以小一点;做资讯类 agent,信息时效性强,λ 就得大一点。
我一般会先用一个中间值跑,然后观察检索结果里新旧记忆的比例,再微调。如果发现模型老是引用过时信息,就把 λ 调大;如果发现它忘了用户很早说过的偏好,就把 λ 调小。
4.3 top-k 与上下文预算的平衡
检索返回多少条记忆,也是个需要权衡的事。返回太少,可能漏掉关键信息;返回太多,会挤占上下文预算,还可能引入噪声。
Hermes 里这个数量通常是可配的,而且支持按类型分别设置。比如事实类记忆返回 5 条,偏好类返回 3 条,摘要类返回 2 条。这样比统一返回一个固定数量要合理得多,因为不同类型的记忆,其“有用密度”是不一样的。
我的经验是:先按类型设一个保守的初始值,然后在实际对话里观察模型是否因为缺信息而答偏。如果答偏,优先增加对应类型的召回数量,而不是无脑加总量。
4.4 检索结果的排序与截断
召回之后要排序,排序之后要截断。排序的分数通常是多路分数的加权和,权重也是可配的。截断则是按上下文预算来,把排好序的记忆依次填入,直到预算用完。
这里有个细节容易被忽略:截断的时候要保证记忆的完整性,不能把一条记忆从中间切断。Hermes 在源码里对这点做了处理,按条目为单位截断,而不是按 token 硬切。
5. 实操:从零配置一套可用的记忆机制
5.1 环境准备与依赖确认
在动手配记忆之前,先把基础环境理清楚。Hermes 的记忆模块依赖外部存储,通常是向量库加一个结构化存储。向量库负责语义检索,结构化存储负责元信息过滤和时间排序。
部署的时候,我建议先用容器方式把依赖跑起来,确认各组件能正常通信,再去调记忆参数。很多人一上来就改配置,结果出了问题分不清是环境问题还是参数问题,排查起来很痛苦。
# 以容器方式启动依赖组件(示意) docker run -d --name hermes-memory-store \ -p 6333:6333 \ -v ./memory_data:/data \ memory-store:latest启动后先做一次连通性检查,确认 agent 能正常读写存储,再进入下一步。
5.2 记忆模块的关键配置项
配置记忆模块时,重点盯这几个参数:短期窗口的 token 上限、压缩触发阈值、长期记忆的写入触发条件、去重相似度阈值、检索的 top-k 和类型权重、时间衰减系数。
这些参数不是孤立的,改一个往往要连带调另一个。比如你把短期窗口调大,压缩触发阈值可能就要相应调整,否则压缩时机不对。我一般会把它们整理成一张表,改的时候对照着看。
| 配置项 | 作用 | 建议初始值 | 调整方向 |
|---|---|---|---|
| 短期窗口上限 | 控制原始对话保留量 | 模型上下文的 60% | 对话长则调大 |
| 压缩触发阈值 | 何时开始压缩 | 窗口上限的 80% | 报错则调低 |
| 去重相似度阈值 | 判断是否重复记忆 | 0.85 | 重复多则调低 |
| 检索 top-k | 每类召回条数 | 事实5/偏好3/摘要2 | 答偏则增加 |
| 时间衰减系数 | 新旧记忆权重 | 0.01/天 | 引用过时则调大 |
5.3 写入策略的实操调整
写入策略的调整,核心是搞清楚“什么值得记”。我的做法是先宽后严:初期把所有可能有用的信息都记下来,跑一段时间后分析哪些记忆被检索命中过、哪些从没被用过,然后把从没被用过的类型过滤掉。
Hermes 支持按类型配置写入规则,你可以指定只有“偏好”和“结论”类才写入长期记忆,“过程”类只进短期。这样能有效控制长期记忆的膨胀速度。
5.4 验证记忆是否生效
配完之后一定要验证。验证方法很简单:做一次多轮对话,中间隔几轮再回头问之前的信息,看 agent 能不能答上来。更严格的验证是跨会话:关掉再重开,问同样的问题,看长期记忆有没有生效。
我习惯用一个固定的测试脚本跑回归,每次改完配置都跑一遍,对比命中率。这样能避免“改了一个参数,修好了 A 却弄坏了 B”的情况。
6. 常见问题与排查技巧实录
6.1 记忆检索不准的排查顺序
检索不准是最常见的问题。排查顺序建议是:先看写入是否正常,再看检索是否召回,最后看排序是否合理。很多人一上来就调检索参数,结果发现根本是写入环节就没记进去。
具体排查时,可以先把检索结果打印出来,人工看一眼召回了什么、漏了什么。如果召回的内容明显不相关,多半是向量质量或相似度阈值的问题;如果相关内容召回了但排在后面被截断了,那就是排序权重的问题。
6.2 上下文超限的典型原因
上下文超限通常有三个原因:短期窗口设太大、检索返回太多、摘要没及时生成。排查时先看是哪个环节把上下文撑爆的,再针对性调整。
我遇到过一次很隐蔽的超限:检索返回的条数不多,但每条记忆都很长,加起来就超了。后来在写入环节加了长度限制,超长的记忆先压缩再存,问题就解决了。
6.3 记忆重复堆积的处理
记忆重复堆积会导致检索结果里全是相似内容,浪费上下文。处理办法是调低去重阈值,或者在写入前做一次批量去重。
如果已经堆积了,可以写个脚本做一次离线清理,把相似度高的记忆合并。清理的时候注意保留时间戳最新的那条,因为通常它信息最全。
6.4 跨会话记忆丢失的定位
跨会话记忆丢失,先确认长期记忆的存储是否持久化。如果用的是内存存储,重启当然就没了。确认持久化没问题后,再看 session 标识是否正确传递,很多时候是 session id 没对上导致检索不到。
6.5 常见问题速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 答非所问 | 检索召回不相关 | 检查向量质量和相似度阈值 |
| 忘记前文 | 短期窗口太小 | 调大窗口上限 |
| 上下文报错 | 检索返回过多 | 减少 top-k 或加长度限制 |
| 记忆重复 | 去重阈值太高 | 调低阈值并离线清理 |
| 跨会话失忆 | 存储未持久化 | 检查存储配置和 session 传递 |
| 引用过时信息 | 时间衰减太慢 | 调大衰减系数 |
注意:排查时一次只改一个参数,改完立刻验证。同时改多个参数,出了问题根本不知道是哪个引起的。
7. 我在实际项目里踩过的坑和几点体会
说几个源码文档里不会写、但实际部署一定会遇到的坑。
第一个坑是向量维度和模型不匹配。换了 embedding 模型之后忘了同步改向量库的维度配置,结果写入报错,排查了半天才发现是维度对不上。换模型时一定要同步检查存储侧的维度设置。
第二个坑是时间戳时区问题。跨时区部署时,如果写入和检索用的时区不一致,时间衰减会算错,导致新记忆被当成旧记忆。统一用 UTC 存储时间戳能避免这个问题。
第三个坑是摘要生成的 prompt 没调好。摘要生成质量差,长期记忆里全是没用的废话,检索命中率自然低。摘要 prompt 要明确要求“保留结论和关键事实,去掉过程性描述”,这样生成的摘要才有复用价值。
关于记忆机制的设计,我最大的体会是:不要追求“记住一切”,而要追求“记住该记的”。记忆的价值不在于多,而在于准。一个只记了 100 条但条条有用的长期记忆,远比记了 10000 条但一半是噪声的记忆好用。Hermes 的分层设计和去重机制,本质上都是在帮你做这个取舍,理解了这个取舍逻辑,参数怎么调心里就有数了。
最后分享一个小技巧:定期导出长期记忆做一次人工审阅,看看 agent 到底记住了什么。这个过程经常能发现一些意想不到的问题,比如它把某次测试的临时数据当成了用户偏好。人工审阅一次,往往比调十个参数都管用。