1. 从“金鱼记忆”到“长期记忆”:为什么我们需要一个会长记性的 Agent
做 AI 应用开发的人都有一个共同的痛点:你花了一下午调教好的对话助手,关掉窗口再打开,它就像被敲了一记闷棍,完全不记得你是谁、之前聊过什么、上次那个任务做到哪一步了。这种“金鱼记忆”在单轮问答场景下还能忍,一旦涉及多步骤任务、跨会话协作、个性化服务,就彻底没法用了。
Hermes Agent 这个项目,核心要解决的就是这个问题。它不是又一个套壳对话机器人,而是一个带持久记忆层的智能体框架。简单说,它让 AI Agent 拥有了“记性”——能记住用户偏好、历史交互、任务上下文,甚至能从过往的成功和失败中提炼经验,在后续任务中主动调用。
这个项目适合谁?如果你正在做以下事情,它值得你花时间研究:需要跨会话保持用户画像的客服系统、需要多轮迭代才能完成复杂任务的工作流引擎、需要积累领域知识越用越聪明的垂直助手。哪怕你只是想给自己的个人助理加一个“别每次都问我同样问题”的能力,Hermes Agent 的记忆架构设计也能给你不少启发。
我花了大概三周时间把这个项目的核心逻辑跑通,又花了两周做定制化改造,踩了不少坑,也总结了一些文档里不会写的经验。下面我把整个拆解过程按模块讲清楚,从设计思路到实操细节,再到问题排查,尽量让不同基础的朋友都能拿走能用的东西。
2. 记忆架构的整体设计:为什么是“分层存储+主动召回”
2.1 核心设计思路:把记忆当成数据库来管,而不是塞进 Prompt
很多开发者第一反应是:要记忆?简单,把历史对话拼接到 Prompt 里不就行了。我一开始也这么干过,结果就是 Token 消耗爆炸,而且模型在超长上下文里会“迷失”,该记住的没记住,不该关注的细节反而干扰了判断。
Hermes Agent 的思路完全不同。它把记忆拆成了三个独立的层,每层有不同的存储介质、生命周期和召回策略:
| 记忆层级 | 存储内容 | 存储介质 | 生命周期 | 召回方式 |
|---|---|---|---|---|
| 工作记忆 | 当前会话的即时上下文 | 内存/Redis | 单次会话 | 全量注入 |
| 情景记忆 | 历史交互事件、任务执行记录 | 向量数据库 | 长期持久 | 语义检索 |
| 语义记忆 | 提炼后的用户偏好、领域知识 | 结构化数据库+向量库 | 长期持久 | 规则+语义混合 |
这个分层设计的精妙之处在于:它模拟了人类记忆的工作方式。你和一个老朋友聊天,不需要回忆你们认识以来所有的对话,只需要记住“他不吃辣”“他最近在学吉他”“上次答应帮他找的资料还没发”——这些就是语义记忆。而具体某次聊天的细节,只有在相关话题触发时才会被回忆起来,这就是情景记忆。
提示:不要试图用一个向量库解决所有记忆问题。结构化信息(如用户ID、偏好标签)用关系型数据库查得更快更准,非结构化信息(如对话摘要、任务描述)才适合向量检索。混在一起用,查询效率和准确率都会打折扣。
2.2 为什么选择“主动召回”而不是“全量注入”
全量注入的问题前面说了,Token 成本高、干扰大。但主动召回也有个坑:召回什么、召回多少、什么时候召回,这三个问题如果处理不好,Agent 要么“失忆”,要么“过度联想”。
Hermes Agent 的做法是引入一个记忆管理器模块,它负责在每次用户输入后、模型推理前,执行以下判断流程:
- 当前输入是否包含需要历史信息的意图(如“上次那个方案”“我之前说过”)
- 如果包含,从情景记忆中检索 Top-K 相关事件
- 从语义记忆中提取与当前任务相关的用户偏好和约束
- 将召回结果压缩成结构化摘要,注入到 Prompt 的指定区域
这个流程的关键在于压缩。召回的记忆不能原封不动塞进去,必须经过摘要和格式化。我实测下来,把 10 条相关记忆压缩成 200 字以内的结构化摘要,效果比直接塞 2000 字原始记录好得多。
2.3 记忆的写入策略:什么时候该记,什么时候该忘
记忆不是越多越好。我见过一个反面案例:某开发者把所有对话都存进向量库,结果 Agent 变得“神经质”,经常引用几个月前无关的对话内容,用户体验极差。
Hermes Agent 的写入策略有几个触发条件:
- 显式触发:用户说“记住这个”“以后都这样”时,强制写入语义记忆
- 任务完成触发:一个多步骤任务成功完成后,将任务摘要和关键决策点写入情景记忆
- 偏好推断触发:当用户连续多次做出相同选择时(如每次都选“简洁模式”),自动提炼为偏好写入语义记忆
- 定时衰减:情景记忆有 TTL(生存时间),超过一定时间未被召回的记忆会被降权或归档
这个策略的核心逻辑是:记忆的价值在于被使用,而不是被存储。一条从未被召回的记忆,占着存储空间还增加检索噪声,不如让它自然衰减。
3. 核心模块拆解:记忆管理器、召回引擎与反思机制
3.1 记忆管理器:Agent 的“海马体”
记忆管理器是整个系统的大脑,它决定了什么信息值得记、以什么形式记、放在哪里。我在实现时把它拆成了三个子模块:
写入判断模块负责给每条交互打标签。不是所有对话都值得写入长期记忆,比如“你好”“谢谢”这类寒暄,写进去就是噪声。我的做法是给每条消息计算一个“记忆价值分”,综合考虑信息密度、情感强度、任务相关性三个维度。信息密度看是否包含具体事实或决策,情感强度看用户是否表达了明确喜好或不满,任务相关性看是否与当前进行中的任务有关。分数超过阈值的才进入写入队列。
摘要生成模块负责把原始对话压缩成可存储的记忆单元。这里有个经验:摘要不能只靠模型生成,还要保留原始消息的指针。因为模型摘要可能丢失关键细节,当后续需要精确回忆时,可以通过指针回溯原始记录。我的做法是摘要存向量库,原始记录存对象存储,两者用同一个 ID 关联。
冲突消解模块负责处理记忆矛盾。用户上周说“我喜欢详细解释”,这周说“以后简洁点”,两条记忆冲突了怎么办?Hermes Agent 的策略是时间优先+置信度加权。新记忆默认置信度更高,但如果旧记忆被多次召回且用户没有纠正,置信度会累积。冲突时取置信度高的,同时把旧记忆标记为“待验证”,下次相关场景时主动询问用户确认。
3.2 召回引擎:怎么在正确的时间想起正确的事
召回引擎的挑战在于平衡召回率和精确率。召回太少,Agent 显得健忘;召回太多,Agent 显得啰嗦且容易跑偏。
我的实现方案是两阶段召回:
第一阶段用规则过滤快速缩小范围。比如当前对话涉及“代码”,就只召回与编程相关的记忆;涉及“日程”,就只召回时间管理相关的记忆。规则可以基于关键词、意图分类或实体识别来触发。
第二阶段用语义检索在缩小后的范围内找最相关的 Top-K。这里有个细节:查询向量不能只用当前用户输入,还要拼接最近几轮对话的摘要。因为用户说“那个方案再改改”时,单看这句话根本不知道“那个方案”是什么,必须结合上下文。
注意:Top-K 的 K 值不要设太大。我实测下来,K=3 到 K=5 效果最好。超过 5 条,模型注意力会被分散,反而容易忽略最关键的那条记忆。如果确实需要更多信息,宁可分多次召回,也不要一次性塞太多。
3.3 反思机制:让 Agent 从经验中学习
这是 Hermes Agent 最有意思的部分。它不只是被动地存储和召回,还会主动反思。
具体来说,每次任务完成后,Agent 会触发一个反思流程:回顾整个任务执行过程,分析哪些步骤顺利、哪些步骤卡壳、用户的反馈是正面还是负面。然后生成一条“经验总结”写入语义记忆,格式类似:“当用户要求做 X 类任务时,采用 Y 方法效果更好,注意避免 Z 问题。”
这个反思机制让 Agent 有了“越用越聪明”的可能性。我拿一个代码生成任务测试过:第一周 Agent 生成的代码经常缺少错误处理,用户反馈了几次后,反思机制生成了“生成代码时必须包含异常捕获”的经验。第二周再测试,Agent 主动加上了 try-catch 块,用户满意度明显提升。
反思的触发频率可以配置。太频繁会浪费计算资源,太稀疏又学不到东西。我的建议是:任务成功时每 5 次反思一次,任务失败时立即反思。
4. 实操落地:从零搭建一个带记忆的 Agent
4.1 环境准备与依赖选型
先说我用的技术栈,这套组合是我试过几套方案后觉得最稳的:
- 向量数据库:选了一个轻量级的开源方案,支持本地部署,单机就能跑,不用额外运维
- 关系型数据库:用 SQLite 起步,数据量大了再迁移到 PostgreSQL
- 缓存层:Redis,存工作记忆和热点召回结果
- 模型层:支持多模型切换,我主要用两个,一个负责推理,一个负责摘要和反思
安装过程不复杂,核心依赖就几个包。但有个坑要注意:向量数据库的索引类型选择会影响召回速度。数据量小于 10 万条时用扁平索引就行,超过后再考虑 IVF 或 HNSW。我一开始就上了 HNSW,结果小数据量下反而比扁平索引慢,因为图构建有开销。
# 核心依赖安装(示意) pip install vector-db-client pip install redis pip install sqlalchemy pip install openai # 或其他模型 SDK4.2 记忆写入的完整流程与参数配置
写入流程我拆成了五步,每步都有可调参数:
第一步:消息预处理。把用户输入和 Agent 回复拼接成标准格式,去除无关字符,提取实体和意图标签。
第二步:记忆价值评分。我用的评分公式是:
score = 0.4 * info_density + 0.3 * sentiment_intensity + 0.3 * task_relevanceinfo_density 用命名实体数量除以总词数,sentiment_intensity 用情感分类模型的置信度,task_relevance 用当前消息与进行中任务的语义相似度。阈值我设的是 0.6,低于这个值不写入长期记忆。
第三步:摘要生成。调用模型生成 50 字以内的摘要,同时保留原始消息 ID。摘要的 Prompt 我调了好几版,最后定下来的模板是:
请将以下对话压缩成一条记忆摘要,要求: 1. 保留关键事实和决策 2. 保留用户偏好和约束 3. 不超过50字 4. 以“用户”或“Agent”开头标明主体第四步:冲突检测。用新摘要的向量去语义记忆里检索相似度超过 0.85 的旧记忆,如果有,触发冲突消解流程。
第五步:写入存储。摘要和向量写入向量库,结构化标签写入关系库,原始消息写入对象存储。
4.3 召回流程的实现细节与调优
召回流程我调了大概两周才稳定下来。核心代码如下(伪代码示意):
def recall_memory(user_input, session_context): # 第一阶段:规则过滤 intent = classify_intent(user_input) candidate_tags = map_intent_to_tags(intent) # 第二阶段:语义检索 query_vector = embed(user_input + session_context[-3:]) candidates = vector_db.search( query_vector, filter={"tags": {"$in": candidate_tags}}, top_k=10 ) # 第三阶段:重排序 reranked = rerank_by_recency_and_confidence(candidates) # 第四阶段:压缩注入 memory_summary = compress_to_summary(reranked[:5]) return memory_summary调优的关键在第三阶段的重排序。我用的排序公式是:
final_score = 0.5 * semantic_similarity + 0.3 * recency_score + 0.2 * confidence_scorerecency_score 用指数衰减,半衰期设的是 7 天。confidence_score 根据记忆被召回后用户是否纠正来动态调整,用户没纠正就加分,纠正了就减分。
实操心得:召回结果一定要做去重。我遇到过同一个事实被多条记忆重复召回的情况,因为用户在不同对话里说了类似的话。去重逻辑用语义相似度判断,超过 0.9 的只保留置信度最高的那条。
4.4 反思机制的触发与经验沉淀
反思机制的实现相对独立,它是一个定时任务,也可以手动触发。核心逻辑是:
def reflect_on_task(task_record): # 提取任务执行轨迹 trajectory = extract_trajectory(task_record) # 分析成功与失败点 analysis = analyze_trajectory(trajectory) # 生成经验总结 lesson = generate_lesson(analysis) # 写入语义记忆,标记为“经验”类型 semantic_memory.write( content=lesson, type="lesson", confidence=0.8, source_task=task_record.id )经验总结的 Prompt 我改了很多版,关键是让模型输出可操作的规则,而不是泛泛而谈。比如“要注意错误处理”这种没用,“生成 Python 代码时,如果涉及文件操作,必须包含 try-except 块”这种才有指导意义。
5. 常见问题与排查技巧实录
5.1 记忆召回不准确:从三个维度排查
召回不准确是最常见的问题,我总结了一个排查清单:
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 召回无关记忆 | 查询向量质量差 | 检查 embedding 模型是否适合当前领域 | 换领域适配的 embedding 模型 |
| 该召回没召回 | 规则过滤太严 | 打印过滤前后的候选数量 | 放宽标签匹配逻辑 |
| 召回重复内容 | 去重阈值太高 | 统计召回结果的相似度分布 | 降低去重阈值到 0.85 |
| 召回旧记忆 | 时间衰减太慢 | 检查 recency_score 计算 | 缩短半衰期或提高衰减系数 |
我踩过最坑的一次是 embedding 模型选错了。用了一个通用模型,结果在代码领域的语义相似度计算完全不准,“函数”和“方法”的向量距离很远。换成代码领域微调过的模型后,召回准确率从 60% 提升到 85%。
5.2 记忆冲突处理:当用户改变主意时
用户改变偏好是常态,但处理不好会让 Agent 显得“精神分裂”。我的处理流程是:
- 检测到冲突后,不立即删除旧记忆,而是把旧记忆标记为“历史偏好”
- 新记忆写入时,在元数据里记录“替代了哪条旧记忆”
- 下次召回时,如果新旧记忆都被召回,优先返回新的,但把旧的作为“历史参考”附在后面
- 如果用户再次确认旧偏好,则恢复旧记忆的置信度
这个流程的好处是保留了偏好演变的历史轨迹,对于需要理解用户长期行为的场景很有价值。
5.3 性能优化:当记忆量增长到十万条以上
记忆量大了之后,召回延迟会明显上升。我做了几个优化:
索引优化:向量库的索引从扁平换成 IVF,查询速度提升 3 倍。但要注意 IVF 需要训练,训练数据要覆盖所有聚类中心。
缓存热点:把最近 7 天被召回过的记忆缓存在 Redis 里,命中率大概 40%,平均延迟降低 50ms。
异步写入:记忆写入改成异步队列,不阻塞主流程。用了一个简单的生产者-消费者模式,写入延迟从 200ms 降到 20ms 以内。
分级存储:超过 90 天未被召回的记忆迁移到冷存储,召回时先查热存储,没有再查冷存储。
注意:异步写入有个坑,如果 Agent 在写入完成前就崩溃了,记忆会丢失。我的做法是写入队列持久化到磁盘,重启后自动恢复未完成的任务。
5.4 隐私与安全:记忆系统的红线
记忆系统存储了大量用户交互数据,隐私保护是必须考虑的问题。我做了几件事:
- 所有记忆内容加密存储,密钥不落盘
- 支持用户一键清除所有记忆,清除操作不可逆
- 敏感信息(如密码、身份证号)在写入前自动脱敏
- 记忆召回时做权限校验,确保只能召回当前用户的数据
脱敏这块我用的正则+模型双重检测,正则覆盖常见格式,模型检测变体。实测下来,正则能拦住 90% 的敏感信息,剩下的靠模型兜底。
6. 记忆系统的扩展方向与个人体会
这个项目的基础架构跑通后,我尝试了几个扩展方向,有些效果不错,有些还在摸索。
多 Agent 共享记忆是个有意思的方向。让多个 Agent 共享一个语义记忆库,每个 Agent 有自己的情景记忆。这样团队协作时,一个 Agent 学到的经验其他 Agent 也能用。我测试了一个客服场景,三个 Agent 共享记忆后,整体解决率提升了 15%。但挑战在于记忆冲突会更复杂,需要引入“记忆所有权”和“记忆投票”机制。
记忆可视化对调试帮助很大。我写了一个简单的 Web 界面,把记忆的写入时间、召回次数、置信度变化画成图表。有一次发现某条记忆的置信度在持续下降,追查后发现是用户每次都被问烦了直接跳过,说明这条记忆的召回时机不对。可视化让这种隐性问题变得可见。
跨模态记忆是下一步想做的。目前只处理文本,但实际场景中图片、语音、文件都是重要的记忆载体。比如用户发了一张设计图说“按这个风格来”,这张图就应该被存入记忆,下次生成时作为参考。技术上的难点在于跨模态检索,文本查询怎么准确召回图片记忆,我还在试不同的方案。
我个人在实际操作中的体会是:记忆系统的核心不是技术,而是对用户意图的理解。同样的对话,理解不同,该记的东西就不同。我见过一个案例,用户说“帮我订张票”,Agent 记住了“订票”这个动作,但没记住用户之前说过的“要靠窗”。后来我把意图理解模块和记忆写入模块做了更紧密的耦合,让意图识别结果直接指导记忆提取,效果好了很多。
最后分享一个小技巧:定期做记忆审计。我每个月会抽样 100 条记忆,人工检查摘要质量和召回准确性。有一次发现模型把用户的玩笑话当成了真实偏好记了下来,导致后续推荐一直跑偏。这种问题不审计根本发现不了。记忆系统就像花园,不修剪就会杂草丛生。