Agent记忆系统数据分支设计:从写入到遗忘的完整拆解

Agent记忆系统数据分支设计:从写入到遗忘的完整拆解
最近在梳理 Agent 记忆系统的时候又把 oGMemory 的记忆方案翻出来做了一次拆解。这套系统给我的直观感受是它把最容易被忽略的“数据分支”当成了记忆质量的主干。很多 Agent 项目跑着跑着就变傻不是模型不够好而是记忆链路里写入和读取走了同一条路数据互相污染。oGMemory 的做法是先把数据拆成独立分支再让不同场景按分支访问。这篇分集 1我就把记忆系统里最基础的数据分支设计拆开讲讲。适合看这篇的人有三类。一类是正在做 Agent 记忆模块想搞清楚记忆中数据怎么分类、怎么存储、怎么隔离。另一类是遇到多轮对话漂移、记忆不生效、检索结果混乱的开发者和产品经理。第三类是纯好奇记忆系统如何设计想建立整体框架的同学。不需要把 oGMemory 理解成一个多复杂的框架。它的核心判断很简单记忆不能是一个大池子必须按照数据的来源、用途和生命周期分成多个分支。下面我从为什么要做数据分支开始拆。1. 为什么说数据分支是记忆系统的命门1.1 记忆系统真正要处理的四类数据如果我们把一个 Agent 的对话过程拆开看需要沉淀的数据其实有好几类。第一类是事实型数据。比如用户“我叫李然工作在杭州”或者“项目上线时间是下周五”。这类数据有明显的答案适合直接查和直接改。第二类是偏好型数据。比如“用户喜欢简洁回答”“不喜欢 emoji”“更倾向于阅读代码而不是解释”。这类数据没有固定答案需要在多次交互里提炼而且会随场景变化。第三类是过程型数据。比如上一轮说过什么、正在执行哪一步、已经调用了哪些工具。这类数据只在当前任务或当前会话内有意义跨会话之后价值会快速下降。第四类是关系型数据。比如用户与项目、角色与权限、问题与历史答案之间的关联。这类数据通常用于检索和推荐而不是直接回答。oGMemory 的数据分支设计本质上就是为这几类数据分别安排路径。不用一个逻辑存储解决所有问题而是让事实进事实分支偏好进偏好分支临时上下文进短期分支关系索引进关系分支。1.2 如果不做数据分支会发生什么很多人在早期会把所有记忆统一塞进一个向量库对话时用 embedding 相似度检索。这样做的优点是接入快但问题很快就会出现。第一个问题是检索互相干扰。用户问“杭州天气怎么样”结果向量检索把“用户喜欢杭州菜”也捞出来了。看起来是相似度误差其实是不同类型的数据在同一个空间里互相拉扯。第二个问题是更新困难。如果事实分支和偏好分支混在一起用户说“我不再喜欢喝茶了”系统不知道该改成偏好还是误改到事实分支里。旧记忆删不掉新记忆写不进去最后只能靠模型强行理解。第三个问题是无法精确控制生命周期。临时对话内容应该迅速过期长期偏好应该保留很久。如果所有数据在一个池子里只能统一设置 TTL做不到分层治理。第四个问题是调试困难。一旦回答质量下降很难判断是记忆写错了、检索错了、还是更新错了。没有分支就没有清晰的链路。所以我在复盘记忆系统时一直坚持一个判断数据分支不是加分项而是记忆系统的命门。oGMemory 把这条主线拎出来方向是对的。2. oGMemory 记忆系统的总体分层与数据流向2.1 从输入到记忆先做一次数据分流再看 oGMemory 的设计。虽然我拿不到整套源码但从它的记忆分层和数据流向上看可以还原出一条非常清晰的链路输入 - 识别 - 分流 - 加工 - 写入分支 - 按需查询。这条链路里最容易被忽略的是“分流”这一步。很多系统不是没做加工而是没在入口判断“这条信息到底属于哪个分支”。比如用户说“以后回复的时候不要用太多客套话”这句话如果被当作事实存储记录成“回复不能客套”也能用但类型错了。后续如果用户说“我对客套很反感”系统就无法关联到同一条偏好的变化。oGMemory 的做法是先给数据打一个分支标签再进入下一个环节。标签决定存储位置、检索范围、过期策略和更新规则。2.2 各层之间不用一个“大池子”从分层上看oGMemory 大致可以分为四层。记忆采集层负责接收用户输入、系统事件、工具调用结果。这个层只做收集不做判断。记忆加工层负责识别实体、抽取摘要、判断情感和意图然后打分、排序、去重。这里的核心是给每条候选记忆分配分支标签。记忆存储层根据分支标签把数据落到对应的存储中。事实型走结构化存储或向量索引偏好型走摘要存储过程型走短期缓存。记忆应用层是给上层 Agent 提供查询接口。查询时必须带上业务场景比如当前是闲聊、任务执行还是个性化推荐不同场景走不同分支。这四层之间没有一个大池子而是通过分支标签串联。这样做的好处是每一层都可以独立扩展。加工层觉得某种类型识别不准可以只改该分支的规则存储层觉得某种类型性能不够可以单独换数据库。2.3 数据分支的命名与映射实际落地时分支命名要尽量稳定。我见过太多项目把分支名写得像日志标签比如 mem_a、mem_b时间一长自己都分不清。可以按数据生命周期和用途来命名例如fact_memory事实记忆pref_memory偏好记忆task_context任务上下文session_tmp会话临时数据relation_index关系索引每条记忆条目除了 ID、内容、时间还要带上 branch 字段。后续查询时查询器先根据 branch 过滤再进入相似度或规则匹配。这个字段就是数据分支的入口。我建议把分支映射做成一个独立的配置而不是散落在代码里。后续要调整分支策略时只需要改映射表不需要上一轮代码。3. 分支设计拆解写入、检索、更新、遗忘3.1 写入分支先决定“这条记忆归谁管”在 oGMemory 的分支设计里写入是第一步也是最关键的一步。写入分支要回答的问题不是“这条信息记不记”而是“这条信息应该归谁管”。我常用的判断顺序是这样如果内容有明确的时间、地点、人物、数字并且是客观描述进入事实分支。如果内容来自用户的表达习惯、喜好、态度并且带有主观色彩进入偏好分支。如果是当前任务里的中间步骤、工具调用结果、已经答过的问题进入临时上下文分支。如果内容描述的是多个实体之间的关联比如“李然负责 A 项目”除了进入事实分支还要在关系索引里建一条边。这个顺序不只是分类也是在防止后期更新时找不到入口。比如今天把“李然负责 A 项目”写入事实分支明天他说“我已经不负责 A 项目了”更新时可以明确只改事实分支不会碰偏好分支。写入分支时还有一个常见动作去重。同一用户说“我喜欢简洁的回答”过两天又说了类似的话应该合并成一条偏好记录而不是生成两条相似向量。去重的判断标准不是完全相同而是语义相似度是否超过阈值。阈值太低会误并太高会重复。一般可以先从 0.85 开始测具体要看 embedding 模型的表现。3.2 检索分支不同场景查不同类型数据写入时按分支隔离检索时也要按分支隔离。查询方必须先声明场景再决定检索策略。比如当前在做任务执行Agent 需要知道项目信息、用户角色、上一步状态。这时检索范围应该是事实分支加任务上下文而不是把偏好分支也查一遍。如果当前是在闲聊或做推荐才需要重点查偏好分支。oGMemory 让我比较喜欢的一点是它对检索分支做了轻量化设计。每个查询进来后先做一次场景分类命中哪个场景就把相关分支加入候选集。然后每个分支内部可以独立设置 topK、相似度阈值和排序规则。这样不会因为某个分支数据质量差把其他分支的检索也带崩。检索时还有一个容易被忽略的点分支内部也要区分“强规则”和“弱语义”。事实分支可以同时维护结构化字段和向量索引查询时可以先用字段过滤再用向量排序。偏好分支更依赖语义相似度可以直接走向量检索。不要把所有分支都用同一种查询方式。3.3 更新分支解决旧记忆和新事实打架记忆系统一定会遇到旧记忆和新事实冲突。比如用户原来在“北京”后来搬到“上海”旧记录如果不更新 Agent 就会反复给用户推北京天气。更新分支要解决的是一致性问题。这里不能简单“覆盖写”。如果旧记录还有一部分正确直接覆盖会把有效信息丢掉。比如“用户在北京工作住在海淀”后来搬到上海工作正确的更新是“用户在上海工作住在浦东”而“北京”的相关记录可以保留为历史但不是首选。oGMemory 的数据分支在更新时会有版本概念。同一分支的同一记忆可以有多条历史版本查询时默认取最新版本。这样既保证一致性又保留回溯能力。更新策略通常分为两种覆盖式更新和追加式更新。覆盖式适用于事实变化比如城市、职位、联系方式。追加式适用于偏好和过程比如“用户现在更倾向看到结论而不只是解释”可以在原偏好记录上叠加新的偏好。实际配置时我建议给每条记忆加 updated_at 和 source 字段。updated_at 用于排序source 用于告诉系统这条记忆来自哪次对话。这样出了问题可以快速定位是哪一轮对话写坏了。3.4 遗忘分支不是删数据是降级访问权遗忘是记忆系统里最少被讨论、但最容易出问题的部分。遗忘不等于删除更合理的做法是“降级访问权”。临时任务上下文在处理结束后可以立刻降级为低优先级或者直接被清理。过期的会话数据如果仍然占着索引就会干扰后续检索。偏好记忆不是永久不变的。一个人去年喜欢的内容今年不一定还喜欢。遗忘分支可以按照最后访问时间和更新频率来调整权重。比如超过 90 天没有被检索过的偏好记录在查询时降低排序权重超过 180 天可以考虑归档。oGMemory 在这块的思路我理解下来是把遗忘也做成一个分支动作而不是一个后台定时任务。每个分支有独立的过期规则和降级规则。事实分支可以长期保留但需要定期校验过程分支必须短生命周期偏好分支需要动态计算活跃度。这里有一个判断标准如果你发现检索结果很干净但整体响应速度变慢了先怀疑是不是没有做遗忘分支。数据量膨胀后向量检索的延迟会明显上升归档旧数据比调索引参数更有效。4. 实际落地时的配置与参数建议4.1 一份可以照着改的配置模板就算没有现成源码我们也可以把 oGMemory 的这套思路迁移到自己的记忆模块里。下面我给出一个常用配置模板字段和含义都标好你可以根据自己的业务简化或扩展。memory: branches: fact_memory: storage: sqlitevector ttl_days: 365 top_k: 5 similarity_threshold: 0.82 update_strategy: override pref_memory: storage: vector_only ttl_days: 180 top_k: 3 similarity_threshold: 0.78 update_strategy: append task_context: storage: cache ttl_minutes: 30 top_k: 10 similarity_threshold: 0.90 update_strategy: override session_tmp: storage: cache ttl_minutes: 5 top_k: 5 similarity_threshold: 0.85 update_strategy: replace global: dedup_threshold: 0.86 max_return_entries: 20 enable_audit_log: true这份模板不是 oGMemory 官方参数只是按我在类似记忆系统里的落地习惯整理出来的。你可以把它当作讨论基线重点看下面几个参数。4.2 相似度阈值不是越大越好很多人在设置 similarity_threshold 时有个误解阈值越大结果越准。其实不一定。阈值过高会导致大量本来有效的记忆被过滤掉Agent 看起来像失忆阈值过低会把无关内容捞上来回答变得混乱。更合理的做法是分分支设置。事实分支一般希望精确阈值可以设高一点偏好分支本来就是模糊的阈值设低一点更容易覆盖到用户潜在的偏好。我上面写 fact_memory 0.82、pref_memory 0.78就是这个思路。阈值调优不要拍脑袋。要拿一批真实对话记录分别用不同的阈值跑一遍统计“检索结果有效比例”。能保持 70% 以上有效性的阈值通常是一个不错的起点。4.3 更新策略要区分覆盖和追加之前讲过覆盖和追加的区别。配置上也要体现出来。事实型数据覆盖时要注意先锁定同一条记录的 ID而不是全分支删除再插入。全量删除再插入有一个风险如果新的写入失败旧数据已经被清掉了 Agent 就没有可用记忆。更稳妥的是在一条事务里同时完成更新和版本切换。偏好型数据追加时要控制追加次数。如果同一偏好一直追加记录会越来越长。建议给偏好分支设置最大摘要长度超过之后把相似偏好合并成一条更高层级的偏好而不是继续堆。任务上下文和临时会话则建议覆盖或替换不要追加。否则上一轮脏数据会长期存在影响当前任务的判断。4.4 批量写入时控制并发记忆系统通常不是只服务一个用户。批量写入时如果并发过高容易出现两个问题。第一个是重复写入。两个请求同时发现用户没有历史记录于是各插入一条相似记忆。解决方式是在写入前做一次基于相同 ID 或相同内容哈希的占用检查。第二个是资源争抢。向量化需要调用 embedding 模型如果一次性并发几百个请求模型服务会被打满整体延迟升高。我一般会把单批请求数控制在 8 到 16 之间再根据模型服务端口的平均响应时间调整。批量写入还有一个容易被忽视的问题输出命名和分支标签要一起写。如果只记录内容没有记录 branch后续所有分支策略都会失效。5. 怎么验证记忆系统真的变好了5.1 用三个典型场景做冒烟测试验证记忆系统不能只看一句“效果不错”要回到具体场景。我会先做三组测试。第一组事实记忆测试。对话中告诉 Agent“我住在上海工作地在杭州”下次提问“我住在哪里”。如果回答正确说明事实分支的写入、更新和检索链路都通了。第二组偏好记忆测试。连续几轮让 Agent 感受用户喜欢简短的回复然后换一个话题再问“你希望怎么回复我”。正确结果是 Agent 能把偏好从历史里捞出来而不是只依赖当前轮次。第三组任务连续性测试。在一个多步骤任务中用户完成第二步后再追问“刚才的第一步结果是什么”。如果 Agent 能准确回答说明任务上下文的临时分支是正常的。这三组测试通过后再进入更大规模的评测。5.2 记录成功率、冲突次数和检索延迟正式验证时我会记录四个关键指标。记忆命中率。在一组 50 轮以上的对话中需要依赖记忆才能回答的问题占比以及正确回答的比例。冲突次数。用户明确给出新事实后 Agent 是否还会按旧事实回答。冲突次数越少越好。检索延迟。一次查询从发起检索到拿到记忆结果的耗时。如果超过 300 毫秒就需要关注存储层和向量化服务。存储增量。每轮对话结束后的新增记忆条数和存储字节数。如果增长太快说明去重和遗忘策略没生效。不建议只拿人工观察结论来评估因为记忆系统的很多问题要连续多轮才暴露。把指标和日志记录下来才能定位具体是写入、检索还是更新环节出了问题。5.3 一个简单的测试样例表下面这张表是我做记忆模块回归测试时常用的结构字段可以根据业务调整。场景输入动作预期记忆结果检查点事实写入用户说“我在上海工作”fact_memory 中生成工作地点记录branch 为 fact_memory值正确事实更新用户说“我换到北京工作了”工作地点覆盖为北京旧版本保留最新版本正确偏好写入用户说“解释太多我只想要结论”pref_memory 中生成表达偏好类型为偏好不进入事实分支偏好追加用户说“最好别用 emoji”表达偏好叠加不产生新 ID内容合并临时上下文用户说“先查询订单再计算总价”task_context 保存两阶段信息30 分钟后自动降级冲突处理用户新地点与旧地点冲突检索结果优先最新版本冲突位置有日志6. 自己排查时优先看的几个位置6.1 记忆“像没记住一样”先查写入分支如果 Agent 在一个会话里明明知道用户的信息换一个会话就忘了问题大概率出在写入分支。先看这条记忆是否真的写进存储了。不要只看应用日志有没有“save success”要看存储里的实际记录。有时提示成功但分支标签写错了导致后续检索的时候查不到。再看写入时是否做了长度限制或数据清洗。如果原始输入被截断摘要丢了关键信息入口数据的质量就不够。最后看是否被去重误删。有些去重逻辑会把内容相似但实际不同的记录合并成一条导致信息丢失。可以查去重日志确认。6.2 检索出来一堆不相关内容先查查询范围和阈值如果检索结果很多但大部分不相关先不要调模型先看查询范围。查询时是否声明了 branch。如果没声明系统很可能把所有分支都查了一遍。事实和偏好混在一起必然出现无关结果。先确认查询侧有没有强制带 branch 参数。然后看阈值。如果阈值设置在 0.7 以下基本什么都会捞上来。可以把阈值调高到 0.8 以上看结果是否变干净。还要注意向量化方式。同一个问题用不同的 embedding 方法会得到不同向量。如果写入和查询用的不是同一套 transformer 或 API相似度会失真。排查时先确认两侧使用的向量模型一致。6.3 旧记忆覆盖不了新事实先查更新策略用户明明说了新信息 Agent 还是按旧信息回答先查更新策略。看这条记忆是不是被设置为 append 模式。如果偏好型分支没有更新时间判断新内容可能只是追加到原条目后面而查询时取的还是旧摘要。这时需要确认查询逻辑是否优先取最新版本或者最新片段。再看是否存在多副本。如果同一事实被写进了事实分支又因为历史版本被写进偏好分支查询时两份都命中排序靠前的可能是旧内容。解决方式是先按分支过滤再按 updated_at 排序。还有一点容易忽略 Agent 的上下文窗口会缓存旧记忆。已经完成更新但当前对话上下文中还带着旧摘要模型就会用旧内容。此时需要在上下文压缩阶段重新生成摘要而不是只更新存储层。6.4 性能变差先查存储膨胀和全量检索记忆系统用了一段时间后变卡先看存量数据是否有失控增长。查询存储里的记录总数和日均新增。如果单用户记忆条目已经达到几千条且没有归档策略每次检索都要扫大量向量耗时自然上升。再看是否存在全量检索。有的查询没有指定分支也没有加时间过滤系统会遍历所有记忆。解决方法是把同时查找的分支限制在 2 到 3 个以内并加上时间范围。最后看 embedding 服务是否成为瓶颈。批量任务一旦并发上去模型推理耗时会影响整体响应。可以观察 QPS 和排队时间必要时做批量合并或异步写入。7. 边界、场景和后续还能怎么做7.1 哪些项目值得上记忆分支不是所有 Agent 都需要一套完整的记忆分支系统。如果只是单轮问答每次对话没有连续的上下文上记忆分支反而增加复杂度。但如果你的 Agent 需要做长对话、个性化推荐、多轮任务、跨会话身份保留那么数据分支几乎是必需品。我通常这样判断如果 Agent 在对话里需要记住用户 3 条以上有效信息并且这些信息会跨轮次影响回答就值得做。如果信息只是当前会话内部使用用一个缓存就能解决不需要复杂分支。oGMemory 这套思路适合作为中型或大型 Agent 项目的基础设计。早期数据量小的时候可能感受不到分支带来的优势但当记忆条数超过几百条分支的价值会越来越明显。7.2 资源开销怎么估算记忆分支并不能让存储变成零成本。每条记忆除了内容本身还有向量、时间戳、分支标签、版本信息。按我的经验一份完整记忆的存储开销大约是原始文本的 5 到 10 倍所以要控制写入量。向量化是最明显的资源消耗点。每次写入都要调一次 embedding。如果日均新增记忆超过几万条需要评估 embedding 服务的吞吐和成本。更经济的做法是只对需要检索的文本做向量化纯记录型数据可以只存文本。查询侧的开销主要看 branch 数量和 topK 配置。分支压得越少查询越快。我一般会让单次查询最多覆盖 3 个分支超过 3 个就拆成两个阶段先确定主分支再决定是否要补充检索。7.3 如果 oGMemory 没有现成代码怎么复用这套思路有时候我们看一个系统的解读并不是为了照抄代码而是为了修正自己的设计。即使你手头没有 oGMemory 的完整实现也可以把数据分支思想迁移过来。第一步先画一张自己系统的记忆数据流图。把输入、分流、加工、存储、查询、更新、遗忘七个环节画出来标清楚每个环节的数据格式和分支属性。第二步选择一个最小的业务域做验证。比如先只做事实记忆和偏好记忆两个分支不要一上来就把所有分支铺开。第三步设置日志和审计。每条记忆的写入和查询都记录 branch、source、updated_at。没有这些信息出了问题很难复盘。踩过几次之后我发现很多记忆系统的问题不是模型能力不够而是前置分支设计没有做干净。oGMemory 给我的最大启发就在这里先把数据分支理清楚再谈模型效果。后续的分集里我会继续拆记忆系统里的查询编排、上下文压缩和跨会话迁移。这一集先把数据分支主线补上。如果你正在做 Agent 记忆模块我的建议是先把单任务跑稳再考虑大规模并发和复杂分支。分支宁肯少也不要乱。

最新新闻

日新闻

周新闻

月新闻