1. Hermes Memory 这套记忆方案的出发点
做 Agent 久了,大家基本都会撞上同一个尴尬场景:用户早上说完“我喜欢极简风格的装修”,下午换了个频道聊起家常,晚上再回来问“刚才说的那个方案呢”,系统一脸茫然,仿佛这段对话从来没发生过。这就是典型的跨会话记忆缺失。
我这次整理的项目,是某开发者为解决这个问题设计的记忆层,项目代号Hermes Memory。它的定位很明确:在会话引擎和用户画像之间,加一层可控、可追溯、可持久化的记忆通道,核心是两个闸门——有界闸与冻结闸。
先说清楚这个方案解决的三类问题:
- 会话重启后,关键上下文不丢失,用户不用重复交代背景和偏好;
- 对用户的描述从“一次性聊天记录”升级为“可积累的画像资产”,后续对话能够基于历史偏好做个性化回复;
- 同时避免无限堆积的信息导致检索失控、画像漂移、旧错新错互相污染。
这套设计适合谁参考?一句话:任何正在做个人助理、陪伴型Agent、垂类客服机器人、或者大模型对话系统的人。如果你已经过了“单轮问答”阶段,开始思考“怎么让系统越来越懂用户”,那 Hermes Memory 的“有界+冻结”思路会给你不少启发。
我个人为什么觉得“双闸”值得单独拿出来写?因为市面上大部分记忆方案只解决了“存储”和“召回”,没人认真回答“哪些记忆该留、哪些该锁、哪些该修剪”。Hermes 有意思的地方,在于它把记忆当成了一套需要经营的数据资产,而不是一条流水账。
2. 有界闸:给记忆划定一个物理天花板
2.1 为什么记忆必须“有界”
很多做记忆系统的第一反应是:多存点,总没错。实际跑起来会发现,记忆无限膨胀的后果非常现实:
- 检索结果严重稀释。向量召回 TOP-K 时,相关的记忆片段被大量“寂寞”的聊天记录挤掉,真正有用的画像信息反而排不到前面;
- 上下文窗口撑爆。每次组装 Prompt 时,如果历史记忆无边界地塞进去,token 成本飞快上涨,响应延迟也在涨;
- 模型注意力涣散。几千字的无关历史混在里面,LLM 很容易被带偏,答非所问的比例直线上升。
所以 Hermes 的有界闸,本质上做的就一件事:给每条记忆、每一层存储都划定明确边界,超出边界的记忆要么被压缩、要么被淘汰、要么被归档。
这个设计理念和生活里“断舍离”是一个道理。房间只有这么大,硬塞进去库房里的所有旧报纸,真正常用的东西反而找不到了。
2.2 有界闸的三级容量控制
Hermes Memory 把记忆分成三层,每一层都有独立的容量上限,互不侵占:
- 工作记忆(短期层):容纳最近 N 轮对话的原始要点,容量阈值为 200 条记忆项,一旦超出,最早的非核心项触发压缩流程;
- 长期记忆(画像层):从工作记忆中提炼出来的用户偏好、事实描述、行为模式,容量阈值为 1000 条,超出时按重要性和置信度分档淘汰低价值项;
- 核心档案(固化层):用户主动声明或系统多次确认的信息,容量只有 50 条,超出时进入人工/复审流程,而不是自动删除。
我听到有人会说:“50条太少了吧?”这里解释一下:核心档案装的是“用户真实姓名、不可变更的偏好、禁忌事项”这类高权重信息,刻意把容量做小,是为了强制系统持续做减法,保证每一条都是精兵,而不是什么都往里面塞。
注意:边界的意义不只是控制体积,更在于约束“召回路径”。有界之后,检索的时候可以直接按层过滤,不需要每次在全量记忆海里捞针。
2.3 压缩与淘汰的策略选择
有界闸不只是一个“满了就删”的简单动作。Hermes 里用了三档处理手段,按优先级从低到高:
第一档:摘要化压缩。多条同主题、同场景的记忆被合并成一条浓缩记录。比如 10 条“中午吃了麻辣烫”“下午想喝冰美式”细节会被压缩成“用户偏好川辣口味,日常饮食重油重辣,忌口香菜”。这个操作我建议放在每晚的闲时任务里跑,避免影响在线响应。
第二档:时效性衰减。每条记忆带一个last_accessed_at时间戳,长期没有被调用的记忆,分数逐渐下降。这里我用的衰减公式是:
score = base_score * pow(decay_factor, days_since_last_access)衰减因子设 0.95,也就是说大约 14 天没被激活的记忆,分数会降到原来的一半左右。这个参数不是拍脑袋定的,我实测过几组对比:0.90 太激进,容易误杀周期性偏好(比如“每周末去爬山”);0.98 又太保守,失效记忆占着坑位不吐出来。
第三档:负面淘汰。如果一条长期记忆在后续对话中反复被新事实推翻,系统直接标记为“过期”,并移出活跃区。例如用户搬家后新地址确认过三次,旧地址记忆就进入淘汰候选区。
这套三级处理合起来的逻辑是:能压缩的不删除,能衰减的不硬删,真正被证伪的才移出。
2.4 有界闸的参数配置与调优
从工程落地角度,我最想提醒的是:有界闸的三级阈值应该独立可配,而不是一套参数全局生效。
- 工作记忆容量:取决于单次会话平均轮数 × 每轮需要保留的关键信息数。比如你的 Agent 平均每次聊 20 轮、每轮产生 8 个信息点,那工作记忆 200 条就是为 10 次会话余量设计的;
- 长期记忆容量:结合实际召回精度的衰减拐点来定。我在实验里看到,超过 800 条之后,向量召回 TOP-10 时相关项占比从 82% 下降到 64%,收益递减明显,所以 1000 上限算是留了缓冲击穿的余量;
- 核心档案容量:这个保守一点,宁缺毋滥,50 条足够覆盖一个自然人长期稳定的关键画像维度。
实操心得:参数不要一配置就上线,先跑一周模拟数据,重点观察“淘汰命中率”——有多少次淘汰操作是真正把低价值记忆移出去的。如果命中率低于 60%,说明阈值定得不对,边界内装了大量本该压缩的冗余项。
3. 冻结闸:让高价值记忆不可被篡改
3.1 为什么要设置“冻结”机制
记忆系统有个隐蔽的坑:画像漂移。
举个例子。用户第一次说“我喜欢深色系风格”,系统记住了。第二次用户随手发了一句“黑色看着有点闷”,系统如果把这个理解为“用户不喜欢深色”,就会把画像从“偏好深色”悄悄改成“排斥深色”。第三次用户再回来聊装修,系统给的建议就变得非常拧巴。
这不是模型理解能力的问题,而是记忆系统缺少“哪些信息可以更新、哪些必须锁定”的权限边界。Hermes 的冻结闸,就是针对这个痛点设计的:一旦某条记忆经过充分确认,进入核心档案并冻结,普通对话内容没有权限修改它,必须走专门的解冻流程才能变更。
生活化类比:书架上用玻璃罩罩住的那本旧相册,你可以看、可以引用,但随手拿记号笔往上写字是不行的。真要改里面的照片,得打开罩子,确认是“本人要换照片”,再操作。
3.2 冻结对象的三个层级
我落地时把“可以冻结的东西”分了三种,它们的作用和操作权限完全不同:
- 用户声明事实:用户主动说出的、明确性的个人信息,例如“我住在杭州”“我家里三只猫”“我对花生过敏”。这类信息冻结优先级最高,不可覆盖;
- 跨轮重复确认的偏好:同样或高度相似的偏好出现在 3 次以上不同会话中,系统自动提升为候选冻结项,等待二次确认后才锁定;
- 系统推导的高置信结论:比如通过用户行为数据推断出的“阅读习惯更偏好短文”,如果置信度超过 0.9 且连续多轮验证无冲突,也可以进入冻结区。这类冻结可以解冻,但必须经过完整复核流程。
我在设计时的原则是:用户直接说的 > 行为推导的 > 单次对话里的临时印象。前两者可以冻结,第三个永远不进冻结区。
3.3 冻结管理的“锁状态机”
冻结闸的真功夫在状态管理上。Hermes 给每条记忆设计了简单的状态机,四个状态:
pending -> confirmed -> frozen ↑ | └-- unfrozen -┘pending:新提取的记忆,未经确认,可修改可删;confirmed:经过一次以上确认,权重较高,修改需要触发逻辑校验;frozen:核心档案内锁定,普通写入操作直接拒绝;unfrozen:临时解冻状态,只在显式变更流程内存在,变更完成后重新进入frozen。
这里最容易踩的坑是:设计成“一旦冻结就永不可改”。但真实场景里用户一定会搬家、换工作、改口味,所以必须留有受控的解冻通道。Hermes 的做法是,解冻操作必须记录unfreeze_reason和来源对话 ID,所有变更留痕,方便追责和回溯。
3.4 冻结与有界的联动规则
两个闸门不是孤立的,它们在运行时协同工作,这也是 Hermes 记忆设计比较扎实的地方:
- 有界闸的淘汰流程不能越过冻结闸:处于
frozen状态的记忆不参与衰减评分,不会因为长期未访问而被自动清理。如果容量压力迫近上限,系统优先压缩和淘汰未冻结的低分项; - 冻结闸的新增来源必须经过有界闸:一条记忆不能直接写入冻结区,它得先出现在工作记忆里,经过提炼、确认、再确认,才可能升格为冻结项;
- 容量冲突时走人工降级:当核心档案满 50 条且需要新冻结某条信息时,Hermes 不自动替换,而是生成“冻结冲突告警”,交由运营人员判断是否解冻旧项。
我最想提醒的是:双闸设计最难的不是实现状态机,而是想清楚“谁有权限打破规则”。我见过不少系统把冻结设计成硬编码,最后用户改个手机号都改不动,体验非常糟糕。正确的做法是保留受控的变更通道,而不是把所有入口都封死。
4. 跨会话记忆与 USER 建模的工程联动
4.1 从记忆到用户画像的数据流水线
记忆系统的价值,最终要体现在“系统更懂用户”上。Hermes 里的 USER 建模,不是单独建一个表存用户属性,而是从记忆数据里实时汇总画像。
我给的流水线结构是:
对话原始记录 -> 信息抽取 -> 记忆条目写入(工作记忆) ↓ 主题分类 + 实体链接 ↓ 画像槽位更新(长期记忆/核心档案)关键点在于:画像槽位不是直接由对话内容覆盖,而是由记忆条目累加而成。每一个槽位(比如“饮食偏好-辣度”)背后挂着来源记忆列表。查询画像时,系统给出的是综合多条记忆的结论,而不是孤零零一条记录。
这样的好处是:一条与旧画像冲突的新记忆进来时,不会瞬间推翻旧结论,而是先引起“置信度波动”,等多次冲突确认后才触发画像槽位的重写。这个机制天然缓解了前面说的画像漂移问题。
4.2 画像槽位与记忆条目的双向映射
USER 建模需要定义好“画像维度”,我用三四层标签体系来组织:
- 基础属性层:年龄区间、地域、职业类型(这些来自用户声明,冻结度高);
- 行为偏好层:内容偏好、消费习惯、活跃时段(这些来自行为观察,更新频率高);
- 交互风格层:回复长度偏好、语气偏好、敏感话题禁忌(这些来自多轮对话统计,中等冻结度);
- 临时状态层:最近情绪、当前关注热点(这些存在工作记忆即可,不进入长期层)。
每一层与记忆条目的映射关系维护在字典表里,例如content_preference.reading_length这个槽位,可能由十几条pending或confirmed状态的记忆共同支撑。
4.3 跨会话召回时的组装策略
用户发起新会话时,Hermes 会按以下顺序组装跨会话上下文:
- 拉取核心档案中所有冻结条目,直接填入系统提示词;
- 从长期记忆中按画像槽位召回 Top-K 条相关记忆,附加时间戳;
- 从工作记忆中提取近期 N 轮摘要,作为对话背景;
- 临时状态层的信息走轻量注入,不占用主要 token 预算。
这套顺序排下来,理论上token 分配比例大致为 冻结画像 35%、长期偏好 40%、近期摘要 20%、临时状态 5%。
我踩过的坑:早期把召回权重全部押在“相关度”上,结果一些冷门但高价值的画像(比如过敏信息)反而排不进去。后来调整为先按画像槽位过滤,再在槽位内排序,相关度和高价值信息都能照顾到,这个改动对体验提升非常明显。
4.4 用户建模中最容易被忽略的“负向记忆”
大部分建模系统只记录“用户喜欢什么”,却很少系统性地记录“用户讨厌什么”。Hermes 里专门留了一类负向记忆,独立存放,独立加权。
负向记忆包括:用户明确反感的表达、拍板否决的方案、连续纠正过两次以上的内容。我建议把负向记忆的权重乘一个 1.2 的惩罚系数,宁可用错也不用触雷。比如用户说过一次“别再给我推荐短视频了”,那这条负向记忆在后续两周内都保持高召回优先级,直到用户主动点开三次短视频后才降权。
5. 实操落地:我实现 Hermes Memory 的完整过程
5.1 存储选型与表结构设计
这块我认为是最值得抄作业的环节。实际实现时,我用的是组合存储,而不是单一数据库:
- 元数据与状态:用关系型库存储,字段包括
memory_id、user_id、layer、state、content_type、decay_score、created_at、last_accessed_at; - 向量化内容:用向量库存储,保存内容的 embedding,用于语义检索;
- 画像槽位表:单独的宽表,一行一个用户,每列对应一个画像维度,值里存的是记忆 ID 列表。
关键表的大致结构:
CREATE TABLE memory_items ( memory_id VARCHAR(64) PRIMARY KEY, user_id VARCHAR(64), layer ENUM('working','longterm','core'), state ENUM('pending','confirmed','frozen','unfrozen'), content TEXT, content_hash VARCHAR(64), importance FLOAT, decay_score FLOAT, source_dialog VARCHAR(64), created_at TIMESTAMP, last_accessed_at TIMESTAMP, INDEX idx_user_layer (user_id, layer), INDEX idx_user_state (user_id, state) );字段content_hash是我后来加的。因为同一个事实可能在多轮对话里被重复抽取,System Prompt 里会塞进重复信息。加了这个字段之后,写入前先查哈希,命中就直接更新权重,不再新增记录,干净很多。
5.2 记忆写入主链路实现
写入流程的核心代码思路如下,我贴的是简化版伪代码:
def write_memory(user_id, dialog_turn): extracted_items = extract_memories(dialog_turn) for item in extracted_items: # 1. 哈希去重 existing = find_by_hash(item.content_hash) if existing: existing.decay_score = min(existing.decay_score + 0.1, 1.0) existing.last_accessed_at = now() continue # 2. 候选冻结判定 if is_declared_fact(item) or repeat_count(item, threshold=3): state = 'pending' else: state = 'pending' # 3. 写入工作记忆 insert_memory(user_id, 'working', state, item) # 4. 有界闸容量检查 if working_memory_count(user_id) > 200: compress_or_evict(user_id, 'working')值得注意的是,写入主链路里没有“直接写冻结区”的路径。所有冻结项都要从pending经过确认升级,再进入冻结。这个设计是我故意做严的,防止模型对话中出现“用户随口开玩笑说要住在月球”这种信息被误冻结。
5.3 冻结闸的确认升级与解冻流程
升级逻辑上,我设定了一条记忆从pending到frozen最少需要经历 2 次独立确认。所谓“独立确认”指的是不同会话轮次中对同一事实的复述或强相关表达。用伪代码表示:
def confirm_and_maybe_freeze(user_id, memory_item): if memory_item.state == 'pending': memory_item.confirm_count += 1 if memory_item.confirm_count >= 2 \ and memory_item.layer == 'longterm': memory_item.state = 'confirmed' if memory_item.state == 'confirmed': if is_core_attribute(memory_item) \ and core_frozen_count(user_id) < 50: memory_item.state = 'frozen' memory_item.layer = 'core'解冻入口我做了两个:一是用户主动发出修改指令,二是系统在对话中发现矛盾次数超过阈值时自动发起复核。从我跑过的测试数据看,99% 的场景走的是第一个入口,用户直接说“其实我搬家了,现在住苏州”,系统定位到旧地址记忆,确认后执行frozen -> unfrozen -> 修改 -> frozen的完整链路。
5.4 召回阶段双闸的配合
召回阶段需要同时读取冻结画像和非冻结偏好,但两条路径的加权方式不一样。我把代码里的关键逻辑列出来:
def assemble_context(user_id, query): # 冻结区:全量加载,不参与打分 core_items = load_frozen(user_id) # 非冻结区:按画像槽位召回并对候选打分 longterm_items = recall_by_slot(user_id, query, top_k=10) # 分数衰减与重要性加权 for item in longterm_items: score = item.importance * item.decay_score item.rerank_score = score # 组装 system prompt prompt = format_core(core_items) + format_preferences(longterm_items) return prompt这里最容易出错的是:冻结区虽然不参与打分,但它也会占 token。我在一次压测中发现,冻结区写得太啰嗦会把非冻结召回的 40% 预算挤掉,导致个性化回复明显变差。后来强制对冻结区内容做摘要模板化,比如“居住地: 苏州”而不是原句“反正我现在一直住在苏州那边,园区那一块”,token 开销直接降了一半。
5.5 数据回流与画像更新闭环
记忆系统的闭环不能缺“回流”环节。每晚我处理三类数据:
- 对当天新增的
confirmed状态记忆做批量确认,观察是否满足升级条件; - 对长期未被访问的
pending记忆做清理,避免工作记忆层囤积垃圾; - 对画像槽位和真实用户行为做离线比对,计算画像准确率和漂移率。
有一次比对发现“内容偏好-视频时长”槽位的准确率只有 51%,排查下来是因为某几场对话里系统把用户分享给别人的视频链接误解为用户的个人偏好。后来我给画像更新加了“行为主体识别”,要求区分“用户自己观看”和“用户转发给他人”,准确率拉回了 80% 以上。
6. 常见问题与避坑实录
6.1 记忆污染:如何避免旧信息干扰新对话
现象:用户上个月说喜欢某品牌家居,这个月已经明确改口,但系统还在拿旧偏好做推荐。根因是画像更新太慢或者旧结论权重太高。
我的解决方案是双轨并进:既给每条长期记忆设周期衰减,又在用户明确表达冲突时执行“冲突优先覆盖”规则,让新结论在 48 小时内生效,而不是等衰减慢慢磨。
特别注意:千万不要因为用户一次情绪化发言就推翻长期冻结偏好。比如用户吐槽“做饭好烦”,并不代表他不喜欢烹饪,这种临时状态只进入工作记忆层,不触碰画像槽位。
6.2 并发写入冲突:多轮对话同时改写同一记忆
实际运营中一定会遇到:用户在 App 和 Web 两个端同时聊,两条线程同时对同一条记忆做确认。我处理方式是在关系库里加了乐观锁,用version字段做控制:
UPDATE memory_items SET state = 'confirmed', version = version + 1 WHERE memory_id = ? AND version = ?;如果更新影响行数为 0,说明版本冲突,重新读最新数据再做合并判断。实测下来这个策略能解决 99.7% 的丢失更新问题。
6.3 跨会话记忆的“超长尾”遗忘
有人问:有界闸把长期记忆上限设在 1000 条,但用户用了半年之后,一些很早期的偏好会不会被误杀?
答案是会,但我刻意留了一个历史归档区。被淘汰的记忆不是物理删除,而是打成压缩包存入冷存储。当用户新会话中重新提到相关主题时,系统会先查活跃区再查归档区,如果归档区有高相关且时效内(一年以内)的记录,会重新激活回长期记忆层。
这个“冷热分层”的代价是多了一次冷存储查询,但对于长期用户的价值覆盖完全值得。
6.4 大模型伪造记忆:如何防止幻觉写入
这是我最想强调的问题。模型在对话中可能凭空捏造用户偏好,比如用户根本没说过的话被抽取进记忆。Hermes 的防线是:所有抽取结果必须挂载到原始对话 ID 上,并且内容必须来自用户发言。系统侧生成的内容不参与记忆抽取。
同时,对于pending状态的高价值断言,我会引入“延迟确认”机制,等下一次自然对话中出现用户真实表达的佐证,才升格。这一条防线极大降低了幻觉写入率,从测试数据看,未加这层验证时错误记忆占总量约 4.7%,加上之后压到 1.1% 左右。
7. 这套方案的边界和可扩展方向
7.1 什么场景不适合用双闸设计
说实话,双闸设计不是银弹。如果你的 Agent 只做一次性问答,不需要跨会话记忆,这套东西就是重负;如果你的系统用户量极小且画像极其简单,直接用一张配置表就够了,没必要引入状态机和冷热分层。
另外,如果模型接的是高度敏感的内容场景(医疗、金融),记忆冻结的权限控制还需要更严格的合规审计,这套方案里的“人工降级”通道就要升级成“必须人工确认”的强约束,不能靠自动流程。
7.2 可扩展方向:群体画像与多用户记忆
Hermes 目前是单用户的记忆系统。后续可以扩展的方向有两个:
一是群体画像聚合:在用户授权范围内,把同类型用户的行为偏好聚合成群体特征,用来冷启动新用户或改进推荐策略,而不需要每个新用户从头积累记忆。
二是记忆共享与权限管理:家庭成员共用同一套 Agent 时,不同成员的记忆需要隔离和共享并存。冻结闸在这个场景下可以演变为“谁有权限修改哪条记忆”的权限模型,有界闸则演变为“每个成员的记忆配额”。
我实测的感受是:把双闸做成权限模型后,多用户场景的适应性会大幅提升,值得做成独立的组件服务,而不是内嵌在单机代码里。
8. 从零到一落地 Hermes Memory 的一些实在建议
如果看完上面这些内容,你决定在项目里也做一套类似的东西,我给几条顺序上的建议:
第一,先定你的记忆边界。写代码之前,先把用户画像里最核心的 20 个槽位列出来,再倒推工作记忆和长期记忆各需要多少条。参数先保守再放开,不要一上来就追求“存得多”。
第二,冻结闸的优先级高于有界闸。很多团队做反了,先把淘汰逻辑写好,再想哪些该锁,结果锁的规则处处受制于淘汰逻辑。先定义不可变资产,再考虑可变资产怎么流动。
第三,量化反馈很重要。我没有给你一套现成的指标,但我强烈建议你在上线前就制定好三个指标:召回相关率、画像准确率、用户重复表达率(用户不需要重复描述同一件事的比例)。这三个指标可以完整评价一套记忆系统是否真的在干活。
第四,不要害怕做减法。记忆系统设计不是加法题,边界、淘汰、锁定,每一个机制都是为了在某些时刻主动“忘记”。记住该记住的,锁住该锁住的,忘掉该忘掉的,这才是跨会话记忆的核心能力。
我在这套方案的多次迭代里,最深的一个体会是:用户并不会因为系统“记得多”而觉得智能,而是会因为系统“记得准、不啰嗦、不会翻旧账”而觉得贴心。Hermes 的双闸设计,本质上就是在帮系统划清“什么事值得记忆”和“什么记忆值得坚持”这两条界线。希望这个方案的思路,能给你正在折腾的 Agent 项目带来一点启发。