Agent终身记忆框架SimpleMem:语义无损压缩与三阶段流水线
写Agent的朋友应该都有过类似的体验对话刚结束下次再问模型已经把上回聊的结论忘得干干净净。尤其是那种需要持续跟踪多轮需求的任务比如帮用户做月度复盘、维护项目背景信息、跨会话执行偏好设置上下文一丢整个系统就等于失忆。围绕这个痛点我们做了一个叫SimpleMem的Agent终身记忆框架核心思路是“语义无损压缩”加“三阶段流水线”让Agent不只记住聊天记录而是真正沉淀出可复用、可检索、可更新的长期记忆。这篇文章就把这个框架拆开讲透包括设计取舍、三阶段的具体实现、一个可运行的简化版本以及我在实际调优中踩过的坑。1. 先把问题说清楚Agent的“记性差”到底差在哪1.1 上下文窗口是工作台不是仓库很多人最初面对Agent记忆问题时第一反应是“把上下文窗口调大”。窗口确实能装更多token但它解决的是“当前这一次对话能参考多少材料”而不是“跨对话还能记住多少”。窗口的本质是工作台——所有信息堆在工作台上随着对话推进前面的内容会被后面的话挤掉。一旦会话结束工作台清空一切归零。这带来一个很直接的矛盾Agent需要长期稳定的用户偏好、项目背景、历史决策但这些恰恰不在当前窗口里。每次新会话都从零开始用户必须重复说明一遍“我上次提过那个需求”体验极差。而我实测下来单纯把窗口从4k拉到32k成本增加4到8倍但跨会话记忆问题几乎没有任何改善。因为问题的核心不在“装得多不多”而在“存不留得住”。1.2 会话隔离带来的知识碎片化另一个容易被忽略的问题是会话隔离导致的碎片化。一个Agent可能同时处理用户的多条任务线今天聊A项目的方案明天聊A项目的排期后天又跳到B项目的预算。如果每个会话各自为政A项目的方案和排期永远碰不到一起模型也就没法把“方案定了”和“排期还没排”这两件事关联起来。我接触过不少团队早期方案就是简单地把所有会话日志灌进向量库检索时按相似度挑一批片段填进上下文。这种方式看起来解决了“跨会话”实际上只是把碎片倒进了同一个桶并没有真正建立关联。检索出来的片段互相矛盾、时序颠倒、重复冗余的情况非常多。深层原因是日志是流水账不是知识。知识需要提炼、合并、去重、归档流水账只是原始素材。1.3 好用的终身记忆需要满足四件事结合上面的痛点我在设计SimpleMem之初就给“终身记忆”定了几条硬指标跨会话记忆要在会话结束后保留并能被后续会话按需召回。冲突可解同一件事的多次描述如果有出入系统要有办法合并或标记冲突而不是叠加覆盖。有损可控存储不可能无限膨胀必须压缩但压缩掉的是冗余和噪声关键事实和数值不能丢。注入可感知记忆回填到上下文时Agent要能区分“这是历史记忆”和“这是当前对话”避免混淆。这四条看着朴素真正实现起来就会发现每一条都逼着你做结构化设计。SimpleMem的三阶段流水线正是围绕这四条逐层展开的。2. 核心解题思路为什么是“语义无损压缩”2.1 传统压缩解决字节冗余语义压缩解决信息冗余提到“无损压缩”做技术的人第一反应是zip、gzip那套逐字节还原一个标点都不能差。但对话记忆的场景完全不同。原始对话里充斥着“嗯嗯”“好的”“那我再说一遍”“其实上次的意思是”这类过程性表达把它们逐字保留没有意义。真正需要留的是结论、涉及的对象名、数值、时间点、因果关系、用户的明确偏好。SimpleMem说的“语义无损”不是字节级的无损而是信息要素级的无损。我把它类比成读研报摘要摘要不会把原文每个字抄下来但会把关键结论、核心数据、推导逻辑按原意保留。单看摘要读者能复原出报告最重要的部分这就够用了。对应到记忆系统语义无损就是保证压缩后的文本能被模型准确还原出原始交互中所有关键信息要素。这里有个重要的判断标准压缩后如果让一个对这些事完全不知情的模型去读压缩结果它能否准确回答出“原始对话里到底决定了什么”。如果答案是可以就是语义无损如果关键数字丢了、因果倒置了那叫语义缺损。2.2 不该丢的三类关键信息我在实际压缩文本时会明确划分“保留区”和“丢弃区”。保守起见下面几类内容必须保留事实数值金额、日期、数量、ID、版本号、人名、项目名。哪怕是“预算大约5万”里的“5万”也不能被模糊成“预算充足”。决策与结论用户拍板的事、拒绝的事、改过的口径。比如“最终采用方案B”这种句子是记忆里最值钱的部分。明确偏好与约束用户说“不喜欢邮件沟通”“代码风格用4空格缩进”这类长期有效的指令型内容。对应的丢弃区包括寒暄、口头禅、重复表达、与主任务无关的细节、过时的中间结论。压缩策略允许丢弃中间推理但要保留最终结果和被丢弃推理的关键依据。2.3 为什么不能直接无脑进向量库很多人会问既然要检索为什么不把所有原始对话直接embedding存向量库检索的时候用相似度topk这样不就没压缩了吗这个方案我最早也试过代价非常直接第一全量embedding的存储成本和检索延迟随会话量线性增长跑一段时间就重得扛不住第二原始文本语义噪音太大相似度高的片段经常是两段都在说“好的”“明白了”这种废话真正关键的结论反而排在后面第三原始片段之间没有关联结构同一件事的三个表述会被当成三条独立记忆命中哪条纯看运气。SimpleMem选择“先压缩、后入库、再检索”的顺序本质上是把语义去重和知识提炼前置到存储之前。压缩后的记忆条目每条都是独立、完整、低噪声的信息单元检索质量和存储成本同时得到优化。这一步想通之后整个框架的技术路线就定了。3. 三阶段记忆流水线从原始交互到长期记忆3.1 阶段一感知与触发——不是什么都值得记记忆系统最容易犯的错是把Agent经历的每一句话都记录下来。这会导致存储爆炸也会让后续压缩模块疲于处理大量无意义内容。SimpleMem的阶段一目标是把好钢用在刀刃上。我在实现里定义了五类记忆触发条件用户明确指令记忆“记住我不吃辣”“以后都用表格汇报”。决策拍板对话中出现了“就按这个来”“改吧”“确认”这类带有最终结论性质的表达。关键信息变更某个项目金额、日期、参与人发生变化且前后不一致。任务里程碑阶段性目标完成、交付物产生、验收通过。长期偏好隐式暴露比如用户连续三次提到“不要周末发消息”这种模式本身就值得沉淀。触发判断可以由规则承担也可以让模型判断。我自己的做法是做“规则粗筛模型精挑”的嵌套模式先按关键词和句式快速过滤出候选交互段再让模型输出这些交互段是否值得记忆、属于哪一类。这样既保证效率又保留了理解力。阶段一的产出是带时间戳和来源会话ID的记忆触发事件列表。3.2 阶段二语义压缩与结构化编码——把对话变成条目这是SimpleMem最关键的一环也是“语义无损压缩”真正落地的位置。我常用的做法是让模型执行一次结构化改写输出固定格式的记忆条目。核心的压缩提示词要点包括提取所有事实性对象人物、项目、时间点、数值、约束。用一句话概括事件或决策必要时拆成“原因动作结果”。合并同一主题的重复信息保留最新状态。标注信息置信度明确说过的算“确定”推测到的算“待确认”。压缩后的记忆单元我统一存成如下结构{ id: mem_20250111_001, entity: project_a, type: decision, summary: 项目A正式采用方案B替代原方案A预算调整为5万元截止日期延至3月15日, facts: { budget: 50000, deadline: 2025-03-15, decision: 方案B }, source_session: session_123, timestamp: 2025-01-11T14:30:00, confidence: confirmed }结构化字段的意义在于检索时可以直接按“entitytype”过滤不必依赖整篇语义匹配。summary字段则保留了可读性方便直接注入上下文供模型理解。压缩模块还有一个合并步骤如果新条目与某条旧记忆指向同一个实体和事件更新旧条目并把旧版本标记为“superseded”而不是无脑新增。3.3 阶段三分层存储与按需检索——让记忆在需要时出现压缩后的条目进入存储层我这里设计了“热记忆—温记忆—冷记忆”三层热记忆本会话内刚产生的记忆直接保存在会话上下文中不落盘响应最快。温记忆近7天内活跃使用的记忆条目以结构化文本形式缓存检索时优先命中。冷记忆更早的、低频访问的记忆全量保留但默认不加载只在明确检索时才启用。检索不是简单地按相似度取topk而是先做两步过滤。第一步用规则过滤根据当前对话的实体名、任务类型筛出候选记忆条目集合第二步用语义排序把候选条目按和当前问题的相关度排前几名。最终注入上下文时我还会做一步“记忆压缩打包”把多条记忆条目合并成一段紧凑的“历史记忆摘要块”放在system prompt区域而不是用户消息区避免污染当前对话流程。这套流程跑通后跨会话记忆不再是“把历史聊天记录翻出来”而是“按需取用经过提炼的知识”。从工程角度看每个会话只新增几十到几百token的记忆开销而不是几千上万token的原始日志回放。4. 实操过程搭建一个可运行的SimpleMem最小实现4.1 整体模块与数据流设计本节给出一套可直接复现的最小实现不依赖重型存储适合先跑通再扩展。整个系统由五个模块组成对话采集器捕获模型与用户的交互流输出会话文本片段。触发过滤器规则模型判断筛选出候选记忆。语义压缩器使用LLM调用把候选交互段压成结构化记忆条目。记忆仓库一个基于JSON文件的轻量存储实现读取、写入、合并、过期标记。检索注入器根据当前对话上下文召回相关记忆并格式化为提示词片段。数据流方向对话采集器 - 触发过滤器 - 语义压缩器 - 记忆仓库 - 检索注入器。整个最小实现用Python写核心依赖只有一个带JSON输出的模型接口存储层先用文件代替数据库。4.2 记忆写入管线实现我提供一个关键片段的伪代码说明写入管线如何串联压缩与存储。import json from datetime import datetime def trigger_filter(segment: dict) - bool: 规则粗筛命中关键词则进入精排。 decision_keywords [确认, 定下来, 就按, 不要, 改] for kw in decision_keywords: if kw in segment.get(content, ): return True return False def semantic_compress(segment: dict) - dict: 调用LLM做语义无损压缩输出结构化条目。 prompt f请把以下对话压缩为记忆条目保留所有事实数值、决策结论、明确偏好删除寒暄和重复内容。 输出JSON格式 {{entity: 对象名, type: decision|preference|milestone|fact|change, summary: 一句话概述, facts: {{键:值}}, confidence: confirmed|to_check}} 对话 {segment[content]} llm_response call_llm_json(prompt) # 替换为实际模型调用 llm_response[source_session] segment[session_id] llm_response[timestamp] datetime.now().isoformat() return llm_response def merge_or_append(memory_db: dict, new_item: dict) - None: 同实体、同类型记忆合并保留最新事实。 for i, item in enumerate(memory_db[items]): if item.get(entity) new_item.get(entity) and item.get(type) new_item.get(type): # 保留旧ID标记为被覆盖 item[superseded_by] new_item[id] # 新条目继承旧条目的历史来源 new_item[history] item.get(history, []) [item[id]] memory_db[items].append(new_item) def write_memory_pipeline(segment: dict, memory_db: dict) - None: if not trigger_filter(segment): return compressed_item semantic_compress(segment) if compressed_item.get(confidence) ! to_check: merge_or_append(memory_db, compressed_item) with open(memory.json, w, encodingutf-8) as f: json.dump(memory_db, f, ensure_asciiFalse, indent2)这个实现里trigger_filter用的规则只是示意实际可以扩展到正则、短语匹配也可以让LLM做二次精排。我在早期版本没有merge逻辑导致同一项目在记忆仓库里出现五六条互相矛盾的条目后面补上merge_or_append之后仓库里的条目数直接下降了60%检索精度也上来了。这是整个写入管线里性价比最高的一个函数。4.3 记忆检索与上下文注入实现写入解决了“积累”检索解决“用起来”。我的检索注入器逻辑分成三步候选召回、相关性排序、格式化为历史记忆块。def retrieve_memory(question: str, entity_hint: str, memory_db: dict, top_k3): 召回并注入相关记忆。 cands [] for item in memory_db[items]: if item.get(superseded_by): continue # 跳过已被覆盖的旧记忆 score 0 # 实体命中优先 if entity_hint and entity_hint in item.get(entity, ): score 10 # summary中的关键词命中 if any(word in item.get(summary, ) for word in question.split()): score 3 # 新鲜度加权近期记忆更容易被召回 item_time datetime.fromisoformat(item[timestamp]) freshness max(0.0, 1.0 - (datetime.now() - item_time).days / 30) score freshness * 2 if score 0: cands.append((score, item)) cands.sort(keylambda x: x[0], reverseTrue) return [c[1] for c in cands[:top_k]] def build_memory_prompt(memories: list) - str: 把记忆条目拼接成system区域的历史记忆块。 lines [回顾以下历史记忆据此回答当前问题] for i, m in enumerate(memories): facts_text json.dumps(m.get(facts, {}), ensure_asciiFalse) lines.append(f{i1}. [{m[type]}] {m[summary]}事实:{facts_text}时间:{m[timestamp]}) return \n.join(lines)实际使用中我建议把entity_hint抽出来在对话开始时先让模型判断“当前对话涉及哪些实体”再带着这些实体名去检索记忆。如果不做实体提取直接拿整段用户问题去做关键词匹配召回结果会非常散相关性高的历史记忆经常排不到前面。注入方式也要注意。我测试过把记忆直接拼在用户消息前面效果不稳定因为模型容易把记忆内容当成用户当前说的话来理解。更稳的是放在system prompt里并加一句“历史记忆仅供参考以当前对话内容为准”这样模型既能利用历史信息又不会产生角色混淆。4.4 关键参数选型与调优经验几个运行时会直接影响效果的关键参数值得单独拿出来说触发阈值规则命中后的精排概率。如果阈值过高记忆条目太少Agent跨会话还是“半失忆”阈值过低仓库里全是碎屑。我调过一段时间后固定为“规则命中且模型判定值得记忆的概率0.7”才写入平衡度最好。top_k记忆条数。不是越大越好。我试过top_k设成8上下文直接被历史记忆占满当前任务的执行准确率反而下降。后来的经验是普通任务top_k3复杂决策任务top_k5上限不要超过5。合并窗口。同实体记忆多久合并一次。太短会频繁更新太长会漏掉状态变更。我的默认值是同实体7天内的更新合并为一组超过7天强制拆开避免旧记忆被长时间带偏。新鲜度衰减系数。上面代码里用了一个线性衰减30天归零。对于偏好类记忆衰减太快会忘掉长期习惯对于项目类记忆衰减太快会漏掉重要背景。实际我做了区分偏好类衰减慢项目决策类衰减快。这几个参数没有绝对固定的答案但我的体会是它们对效果的影响比模型本身更大。因为记忆系统的瓶颈很少是“理解力不够”更多是“存了什么、什么时候拿出来的策略不对”。5. 常见问题与排查技巧实录5.1 问题一记忆回填后Agent反而答非所问这是我调试过程中遇到最多的现象明明把正确记忆注入进上下文了模型回答却比不注入还差。排查下来主要两个原因。第一是记忆块位置不对——放在用户消息区被模型当成了用户的新输入第二是记忆内容和当前问题相关性太弱强行注入反而引入歧义。解决办法很简单第一记忆块统一放system区域第二增加“与当前问题弱相关则不注入”的判断宁可漏掉也不能乱塞。我在检索模块里加了一个相关性得分下限低于下限直接返回空这个改动把任务成功率提升了近15个百分点。5.2 问题二关键数值被压缩过程吞掉压缩模型偶尔会把“5万”概括成“预算充足”或者把“3月15日之前”变成“预计春季前”。这类数值丢失属于典型的语义缺损一旦出现对后续决策的影响是灾难级的。我用了两层防守第一层是靠提示词强约束明确要求所有数字、单位、日期、人名必须原样输出到facts字段第二层是在压缩后用规则校验正则扫描原始对话中的数字和日期如果facts里没有出现就自动重写压缩提示词再跑一次。这个“数值回填校验”虽然增加了一点调用成本但换来了记忆内容最基本的可信度。5.3 问题三旧记忆与当前事实冲突当同一个项目经历多次状态变更仓库里很容易同时存在“预算5万”和“预算8万”两条记忆。如果不做冲突处理检索时两条都会命中模型就懵了。我最终的方案是merge阶段强制保留“最新状态”作为主条目同时把旧版本作为历史记录挂载在items的history链下。检索时默认只返回主条目除非当前对话明确问到“历史变化过程”才把整条history展开。这样既保证了记忆一致性又保留了追溯能力。5.4 排查速查表平时出问题时我习惯按下面这个表快速定位现象可能原因处理建议跨会话不答疑触发阈值过高记忆没写入确认触发后是否真正落库手动走一次写入管线注入后回答变差记忆块放错位置或相关性太弱移到system区域增加弱相关过滤记忆条目过多过杂触发阈值过低或未做合并调高触发概率要求检查merge是否生效关键数值错乱压缩提示词未强调数值保留增加facts字段约束和数值回填校验旧记忆带偏当前判断冲突记忆未标记superseded检查合并逻辑确保稳定覆盖旧版本检索耗时过高全量扫描仓库items按entity建索引过滤后再排序这套三阶段框架的核心并不是某一个算法多高级而是把“存储什么、怎么提炼、何时注入”这三件事真正做到位。我个人在反复打磨中最深的体会是语义无损压缩真正难的不是压缩动作本身而是确定什么是“不能丢”的取舍标准。这个标准要在具体业务里反复用真实案例去校准跑一两个月之后才能稳定下来。如果你也在做Agent的记忆系统不妨先从最简单的一版跑通把记忆条目格式固定住再逐步加合并、衰减、分层这些策略。数据积累起来之后你会看到Agent从一个“每次重来”的新手慢慢变成一个“带着经验干活”的老手。