记得刚把带记忆的Agent跑通时,最明显的感觉不是“它聪明了”,而是“它终于像一个长期共事的人,而不是一个每次见面都重新自我介绍的路人”。这个项目代号叫Hermes Agent,核心就一句话:让AI Agent拥有跨会话的长期记忆。它能在上一轮对话结束时记住关键信息,在下一轮对话开始时自动想起,并且根据用户偏好调整回答方式。很多Agent Demo看起来聪明,实际上只是“短时上下文”玩得好,一旦窗口清空、会话重建,模型对用户一无所知。Hermes Agent就是专门解决这个断层问题的实践产物。这篇文章会从记忆架构设计、向量记忆仓实现、检索与遗忘策略、实际跑通流程,以及我在调试和压测中踩过的坑,完整拆开讲一遍。适合自己动手做过Agent、想给项目加“记性”的开发者,或者正在纠结“怎么让Agent记住用户偏好”的人。
1. 项目整体设计与记忆机制拆解
1.1 为什么Agent最缺的是“记性”
大模型本身的上下文窗口再大,本质上还是一次性的。你开一个新会话,模型不会记得你昨天让它帮你跟踪过某个项目的进度,也不会记得你明确说过“不要用术语,用大白话解释”。这就导致很多Agent应用看起来很惊艳,用起来却总差一口气:每次都要重复交代背景、重复设定口径、甚至重复贴一遍资料。它们不是能力不够,是“记性”不够。
“会长记性”的核心价值,就是让Agent能够把历史对话、用户偏好、任务状态、领域知识持久化,并且在下一次交互时主动使用这些信息。这跟人的记忆非常像:我们和同一个人聊天,不需要每次重报家门,因为大脑会从长期记忆里调出“这个人是谁、上次聊到哪、他偏好什么、他不喜欢什么”。Agent要做的,就是把这个机制用工程手段复刻出来。
Hermes Agent在设计时定下几个原则:记忆必须结构化、检索必须高效、存储必须可扩展、遗忘必须有策略。四个原则听起来简单,真正实现时会牵扯出不少细节,后面会逐个展开。
1.2 记忆模型:短期、长期、工作记忆如何分工
直接上完整的大模型记忆系统容易失控,我一开始的版本把所有对话历史一股脑塞进向量库,结果检索命中率惨不忍睹。后来把记忆模型拆成了三层,才真正稳定下来。
- 短期记忆:对应当前会话的上下文,直接靠大模型的上下文窗口承载。这部分不需要持久化,会话结束即可丢弃。
- 工作记忆:当前会话中需要持续关注的动态状态,比如用户正在操作的任务进度、临时设定的条件、暂时性的偏好。这一层会以结构化摘要的形式写入长期存储,但更新频率高、生命周期短。
- 长期记忆:跨越会话的稳定信息,比如用户的职业背景、表达偏好、明确表达过的喜好与禁忌、反复出现的主题。这部分是Hermes Agent的“镇店之宝”,也是检索增强的主要来源。
三层记忆的流转关系很直接:短期记忆随着对话推进,有沉淀价值的信息由工作记忆层抽取并归纳,再写入长期记忆;长期记忆在下一轮会话开始时被检索出来,回填到上下文里,变成新的短期记忆。我后来把整条链路叫作“记忆生命周期”,因为信息确实是从短期状态流向长期存储,又被读回短期上下文的循环过程。
这个设计的好处是:检索规模可控、存储成本可控、召回质量高。如果所有历史对话全部扁平化丢进向量库,检索时会被大量低价值信息干扰,噪声一大,Agent就分不清“用户上次随口提的一句”和“用户长期稳定的核心偏好”哪个更重要。
2. 核心实现:向量记忆仓与检索增强
2.1 记忆的写入:从对话到向量化
要记住东西,首先得把“值得记的信息”从对话里抽出来。原始对话文本是不能直接进向量库的,会越存越乱。我采用的方式是“先摘要、再结构化、后向量化”三步走。
第一步,把当前窗口的对话交给模型做一次轻量摘要,提取出:用户陈述的事实、用户的偏好或禁忌、任务进展状态、当前未完成事项。第二步,把摘要解析成JSON结构,每个记忆条目包含内容、类型、时间戳、重要度评分。第三步,把JSON序列化成文本片段,交给嵌入模型转成向量,写入向量数据库。
这里有个关键细节:记忆条目要分块。比如用户在一段对话里同时说了“我是做数据分析的”和“我讨厌每周汇总邮件”,这两条信息不能揉进同一个向量块里。原因很简单,检索的时候往往只需要其中某一条,混合块会在相似度计算上稀释掉关键信号。我早期在本上吃过亏,把两条信息放一块,结果检索“用户职业背景”时混进了邮件偏好,十分恼火。
向量化的具体操作可以用现成的嵌入模型。为了让记忆检索更贴合领域语义,我对中文场景做过分词预处理,实际测试下来对小规模记忆库没有明显收益,反而多了一步故障点,后期直接去掉,用原句嵌入。
| 写入阶段 | 输入 | 输出 | 说明 |
|---|---|---|---|
| 摘要抽取 | 对话原文 | 自然语言摘要 | 过滤寒暄、废话、临时性内容 |
| 结构化拆分 | 摘要 | JSON记忆条目 | 类型分类、重要度打分 |
| 向量化 | JSON文本片段 | 浮点向量 | 分块后逐条嵌入 |
2.2 记忆的召回:相关性检索与时间衰减
写入只是存,召回才是真正决定“记性好不好”的环节。Hermes Agent在召回上做了两层过滤。
第一层是向量相似度检索。用户开启新会话,Agent先从长期记忆库里按当前输入语义检索Top-K条最相似的历史记忆。这一步是基础筛选。第二层是时间衰减与重要度加权。纯向量相似度有个毛病:很久以前的一条闲聊记录,和“用户量化偏好”这类稳定特征向量撞车概率不高,但如果是同类话题,旧记录往往不如新记录有参考价值。所以检索出来的条目要重新算一遍融合分:
融合分 = 向量相似度 × 0.6 + 重要度 × 0.3 + 时间衰减系数 × 0.1
时间衰减系数不是简单线性衰减,而是用半衰期函数。我设置了7天半衰期,7天前的记忆权重减半,14天前再减半,但不会归零。这个设计的直觉是:长期稳定的偏好不应该被彻底遗忘,但近期信息更有参考价值。用户上周说你“喜欢简洁回答”,比两个月前说过的同类型话更值得被遵守。
召回结果最后会被组装成“记忆上下文块”,塞进系统提示词里。这个块有固定格式,包含记忆原文、发生时间、重要度,让模型能分清“这是历史事实”和“这是当前对话”。
2.3 记忆管理与遗忘策略
“会记住”和“会忘掉”是一体两面。很多Agent只做加法不做减法,记忆库里堆了几万条陈年旧事,检索速度下滑是一方面,更麻烦的是过期信息会污染判断。
Hermes Agent的遗忘策略分主动和被动两种。
主动遗忘,靠时间衰减和重要度双重判断。每条记忆都带时间戳和重要度,当系统发现某个条目的融合分低于阈值,就会把它标记为“归档”,不再参与召回,但保留在冷存储里。归档不等于删除,万一用户突然问起半年前的事,还可以从冷存储里捞出来。
被动遗忘,靠用户反馈。如果用户明确纠正过Agent某次记忆错误的使用方式,系统会把对应记忆条目直接标记为无效。比如我告诉Agent“以后不要主动推荐食物”,系统记录之后,Agent还继续推荐,我一句“说了多少遍不要推荐吃的”,这条记忆会被降权并最终退出召回范围。
遗忘策略落地时最需要注意的是一点:不要因为单次纠正就物理删除记忆。用户可能只是当时心情不好,或者语境变化。物理删除不可恢复,情绪化删除容易误伤。降权、归档、冷处理,是更稳妥的方案。
3. 实操过程:跑通一个会长记性的Agent
3.1 环境准备与依赖清单
我在实操中用的是Python环境加一个轻量级向量数据库。具体依赖如下:
python>=3.10 openai>=1.12.0 numpy>=1.24.0 jieba>=0.42.1向量库选了嵌入式模式,零运维,适合本地开发。模型接口走的标准Chat Completion API。整个项目的核心逻辑就三个模块:记忆抽取器、向量存储层、检索融合模块。
依赖安装有个小坑:openai库升级到1.x之后,接口风格变化比较大,如果你参考的老代码是在0.x版本写的,大概率会碰到openai.ChatCompletion.create报错,需要改成client.chat.completions.create方式。我第一次跑的时候被这个点卡了半小时,后来翻更新日志才反应过来。
3.2 核心代码:记忆层与调用流程
记忆层最简单的实现,是把对话历史转成摘要后写入向量库。逻辑不复杂,关键是抽象好接口。下面这段是我调试通过的简版实现,去掉了鉴权和异常处理的外壳,只留核心链路。
# -*- coding: utf-8 -*- import json, time import numpy as np class MemoryUnit: def __init__(self, content, mem_type, importance, timestamp=None): self.content = content self.mem_type = mem_type self.importance = importance self.timestamp = timestamp or int(time.time()) def to_document(self): return json.dumps({ "content": self.content, "type": self.mem_type, "importance": self.importance, "ts": self.timestamp }, ensure_ascii=False) # 模拟召回与融合打分 def recall_memory(query_vector, memory_vectors, memories, top_k=5): scores = [] for i, mv in enumerate(memory_vectors): sim = cosine_similarity(query_vector, mv) mem = memories[i] time_decay = 0.5 ** ((time.time() - mem.timestamp) / 604800) fused = sim * 0.6 + mem.importance * 0.3 + time_decay * 0.1 scores.append((fused, i)) scores.sort(reverse=True) return [memories[i] for _, i in scores[:top_k]]上面这段代码只是示意,向量库的实际读写由嵌入式库负责,不需要自己维护向量列表。我保留这段是为了展示融合打分的形式,它的核心在于time_decay的计算:604800秒正好是7天,半衰期逻辑直接用了指数衰减。
调用流程我封装成了一个agent_with_memory函数,每次用户发消息,先走记忆召回,把历史记忆注入Prompt,再调模型。这样上层业务逻辑完全感知不到记忆的存在,对已有的Agent代码侵入性很小。
3.3 实测场景:跨会话记住用户偏好
跑通一个功能最直接的验证方法,就是用真实对话场景来测。我设计了一个模拟场景,连续两天以不同会话分别说要考数据分析师认证,以及不喜欢长篇大论的回复风格。
第一轮会话结束时,记忆抽取器应该沉淀两条记忆条目:一条是“用户在准备数据分析师认证”,类型为任务状态;另一条是“用户不喜欢长篇大论”,类型为表达偏好。第二条我会给更高的重要度,因为表达偏好比临时任务更有长期参考价值。
第二轮会话我直接问“我上次说要准备什么来着”,没有提供任何背景。Agent从向量库里检索到“数据分析师认证”这条记忆,并回填到上下文,直接给出准确回答。这就验证了记忆的跨会话能力。如果这时候再问“你能帮我参考一下备考计划吗”,Agent应该会配合“不喜欢长篇大论”的记忆,输出精炼的规划建议,而不是再一次输出几千字庞大文章。
这个测试有两个关键指标:一是记忆召回准不准,二是被召回的记忆有没有真正影响模型输出风格。很多实现只做到“存了但没用”或者“存了但召不回”,这两点必须同时打通,才叫真正的“会长记性”。
4. 踩坑记录与性能调优
4.1 常见问题速查表
做这个项目的过程中,我踩过不少坑,专门整理成了一张表,方便后来人直接对照排查。
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 记忆被插入但不生效 | 检索融合分太低,召回没覆盖到目标记忆 | 调低top_k拿更多候选,或提高重要度占比 |
| 检索结果全是近期闲聊 | 时间衰减权重过高,把长期稳定偏好挤掉了 | 降低衰减系数,或对稳定偏好单独加权 |
| 向量库里垃圾太多 | 抽取器没有过滤寒暄和临时无意义内容 | 在抽取阶段增加“是否有保留价值”的判断 |
| 越跑越慢 | 无限制累积记忆,检索规模膨胀 | 启用归档策略,把低融合分条目冷却 |
| 用户纠正一次但系统反复犯 | 记忆更新没有同步到抽取器 | 纠正信息也要写入记忆,且标记高重要度 |
| 上下文窗口溢出 | 回填记忆块过大 | 限制回填条数和每条长度,按top_k截断 |
这六个问题里面,最隐蔽的是第一个。Memorize是物理过程,Recall是逻辑过程,你存了不代表能想起来。项目跑了一段时间后,我再三强调检索融合分的设计,就是这个原因:记忆系统真正的智障,往往发生在召回阶段。
4.2 调参经验与内存占用控制
调参这件事没有银弹,但有几组数值是我经过多轮尝试后觉得比较稳的初始值。
重要度评分我用1到10分。用户直接表达的偏好给8到10分,任务进展给6到7分,闲聊型的事实给3到4分。这个打分会直接影响融合分里的0.3权重部分,所以评分的稳定性很重要。如果模型打分忽高忽低,检索结果就会随机漂移。
top_k值我建议从5开始。记忆库规模小的时候5够用,超过5000条之后可以升到8到10。但注意top_k不是越大越好,回填的记忆块太大会挤占上下文空间,反而稀释模型对当前输入的注意力。
内存占用方面,嵌入式向量库在10000条记忆以内,开发环境实测占用在300MB左右。如果你要跑大规模并发,建议把向量库独立出去,做成服务化部署。我在单机Demo阶段是直接进程内嵌的,方便调试,但不适合生产。
性能优化的一个关键技巧是:记忆抽取不要每次对话都全量跑。我设置了触发条件——当对话长度超过10轮,或检测到明确偏好表达时,才触发抽取器。这样可以大幅减少不必要的嵌入计算,也让记忆库的增量更有价值。
5. 经验总结与扩展方向
5.1 从Demo到生产环境,还缺什么
Hermes Agent目前的形态,本质上是一个完整的记忆框架Demo。从它能跑,到它能真正扛住生产环境流量,中间还差几步硬功夫。
第一是多租户的记忆隔离。现在的设计里记忆属于单一用户,一旦要接多人使用,就必须在每一个记忆条目上挂身份ID,所有读写操作前先做身份过滤。不然就会出现“用户A说讨厌什么,用户B的回答被莫名其妙地纠正”这种事故。
第二是记忆的一致性。多个会话同时写入同一条记忆时可能冲突,比如一个会话里用户说喜欢快节奏回复,另一个会话里却说希望慢节奏细致分析。这种矛盾需要合并策略,最稳妥的是以最新一次明确表达为准,而不是通过投票。
第三是记忆的可解释性。用户如果问“你为什么这么回复我”,系统最好能明确指出“因为你上次说过不要长篇大论”。这一步我还没完全实现,但准备把记忆条目的ID直接拼进Prompt的引用信息里,让模型在回答时可以引用来源。
第四是记忆的安全与隐私。长期记忆里存了很多用户个人偏好,生产环境里必须加密存储、权限分级、支持用户主动清空记忆。记忆不能变成黑盒,用户应该有权把Agent“忘掉”。
5.2 记忆能力还能怎么玩
这个框架在基础上还能扩展出不少有意思的方向。
多模态记忆是顺理成章的下一步。现在只记文本,用户上传图片、语音,经过解析后同样可以沉淀成记忆条目,比如“用户在讨论UI设计时偏好用深色调截图举例”。
记忆共享可以做Agent协作。多个Agent各自维护不同领域的记忆,通过一个共享的记忆网关互相调用。比如负责日程的Agent记住了“用户周五有会”,负责内容生产的Agent在安排写作计划时可以主动避开这个时间。
反而我觉得最值得期待的是“记忆复盘”。系统定期把记忆库里的高频主题和长期模式整理成一份“用户画像报告”,让用户直接看到Agent眼中的自己是什么样。这个过程既能增强信任感,也是一种很直观的可用性展示。
我个人在实际操作中的体会是,记忆系统最怕的不是笨,而是乱。笨可以通过调参优化,一旦存取逻辑混乱、遗忘策略失效,整条链路都会失控。所以宁可先小规模跑通,把召回质量打磨稳定,再逐步扩大记忆规模。这个项目做到现在,“会长记性”已经从功能扩展成了一个思维习惯,设计任何Agent能力时都会下意识问一句:这个信息,值得被记住多久?