资讯详情

资讯详情

Agent长期记忆系统设计:分层、TTL与数据治理实战

做了几年大模型应用我越来越确定一件事Agent落地最难的部分往往不是模型能力不够而是记忆系统没设计好。用户跟Agent聊到第十分钟Agent突然忘了五分钟前刚说过的关键条件这种体验基本等于劝退。但反过来说如果你的Agent什么“屁大点事”都记还会把三个月前的旧习惯当成今天的用户意图来执行那就从“工具”变成了“困扰”。“长期记忆”这个词听起来很性感实际上工程里全是坑。我拆开讲的话核心就三件事Memory 分层、TTL、数据治理。分层解决“记什么、放哪层”的问题TTL解决“多久算过期、过期怎么清”的问题数据治理解决“能不能记、能不能删、怎么审计”的问题。这三件事做扎实Agent才能做到该记的记牢、该忘的忘掉、该避嫌的避嫌。这篇文章不聊那些虚的框架直接从真实落地场景出发把我自己踩过的坑、验证过的参数和还在用的方案全部摊开来讲。1. Agent记忆问题的真实场景为什么“记不住”和“乱记住”一样致命1.1 没有长期记忆的Agent每次对话都在“重新认识用户”先看一个最朴素的需求一个客服Agent用户第一天来报修说“我家路由器经常断网”。Agent给了一套排查步骤。第二天用户回来说“昨天你让我重启路由器我试了还是不行”。如果这个Agent没有长期记忆它根本不知道“昨天”发生了什么只能再次问“您的网络是怎么个不稳定法”——“崩溃”两个字就写在用户脸上。我见过很多团队在这个阶段的做法是把整个对话历史直接塞进Prompt里。这种方法在上下文窗口内的确有效但一超过窗口长度基本就只能“截断旧消息”。截断后的结果五花八门有的把关键信息丢了有的把过期的状态当成新信息最离谱的是截断后的半句话和当前问题组合在一起直接导向幻觉。所以“长期记忆”不是一种可有可无的“加分项”而是Agent能否连续服务、能否积累用户信任的关键机制。它的作用不是“记住更多的字”而是“在需要的时候把对的旧信息取出来用”。1.2 记忆越界比记忆缺失更麻烦三大典型翻车现场记忆系统设计不好最常见的翻车长这样第一个是“记忆串味”。我把A用户的需求存进了B用户的记忆库或者在多轮对话中把“甲项目的预算”和“乙项目的预算”混在一起Agent在关键时刻张冠李戴。这种错误在人工客服里几乎不会犯但在Agent里只要embedding阈值设得松一点就能出现。第二个是“旧闻误国”。用户两个月前说“明年我要去欧洲玩”Agent在那之后每次都默认“用户在准备欧洲旅行”。可是人早就改计划了。没有TTL或者TTL设得过于长的记忆会变成一种“温柔的偏见”不断干扰新的判断。第三个是“隐私裸奔”。Agent记住了用户在某句话里提到的身份证号、家庭住址、公司薪酬然后在另一段完全无关的对话中把这些信息“顺手”带了出来。即使模型没有恶意用户也会觉得“它怎么知道这些它是不是在监视我”越界不是“记多了”这么简单而是记忆在错误的时间、错误的对象、错误的场景下被错误地调用了。解决思路就是给记忆分级、给它设寿命、再把使用权关进笼子里——也就是分层、TTL、数据治理这套组合拳。1.3 从“窗口补丁”到“记忆系统”我们到底在设计什么很多团队说的“长期记忆”直到今天还停留在“把历史消息存进向量库”这个层面。这不算设计记忆系统这只能算“把窗口换了个地方”。真正的记忆系统至少要回答四个问题哪些信息值得进入长期记忆记忆该以什么粒度保存、存多久召回时该按什么策略挑选和组合用户提出“删除”时系统能不能干脆利落地执行这四个问题分别对应本文章节里的分层、TTL、召回机制和数据治理。没有这套设计任何“记忆增强”都只是在给模型喂更多上下文而模型的判断力并不会因此变好。2. Memory分层设计把记忆拆成五层各管各的2.1 工作记忆Prompt窗口是“草稿纸”不是“档案库”最先要分清的是“工作记忆”和“长期记忆”。工作记忆就是当前这一次对话的上下文窗口它的特点是“速度快、容量小、随会话结束而清空”。之前很多团队把这些上下文一股脑全存起来以为这就叫“记忆”这其实等于把草稿纸当成遗嘱归档。我推荐的边界是工作记忆里只放“当前任务相关的临时信息”比如用户正在填的表单、正在看的那一页商品、刚才对话中刚提到但还没确认的事实。一旦任务完成这些临时信息就该进入短期缓存或者直接丢弃而不是转入长期记忆。实操上我给每个会话初始化一个“工作记忆区”里面是一个结构化的JSON对象记录当前状态、最近一轮的核心结论、尚未解决的问题。每一轮Agent处理完都由一个单独的模块不是模型自己来更新这个JSON。这个模块的作用很像操作系统的内存管理负责“下一轮还要用到的变量”的存续和释放。2.2 情景记忆向量库里的“历史剧情回放”情景记忆是指“发生过什么事情”的记录比如“2025年3月12日用户投诉了配送延迟”“2025年3月20日用户完成了KYC认证”。这类记忆天生适合向量化召回因为它们有实体、有时间、有事件。存储时我建议不要只存对话原文而是用“事件抽取”的方式把每条对话提炼成结构化事件谁、什么时间、在哪、做了什么、结果如何。原文可以留一份缩略版本放在事件附件里但召回时主索引应该是事件本身而不是整段对话。设计的关键点是“剧情回放”要有时间轴。我通常给每条情景记忆打上start_time和end_time两个时间戳这样在召回时既能做时间窗过滤又能判断事件之间的先后顺序。Agent在回答“为什么我上周的订单没到”这类问题时就能顺着时间轴把链路串起来而不是只靠embedding相似度梗着脖子撞。2.3 语义记忆“用户画像”的精炼与更新而不是原样堆积语义记忆是“关于这个世界和关于用户的抽象知识”比如“用户偏好晚上下单”“用户住在南方空调滤网有异味问题常见”等等。这类记忆和具体事件的差别在于它不是一条记录而是一个不断被验证和更新的结论。我踩过的一个坑是一开始我把语义记忆也做成“向量记录”用户每说一句“我讨厌客服转接”就插入一条embedding。结果投喂给Agent时它读到的全是互相矛盾和重复的陈述反而增加了幻觉概率。后来我改成“画像档位”模式每个用户维护一组定义好的画像字段比如沟通风格、价格敏感度、常用产品线、禁忌话题。模型或规则从对话中抽取更新量由更新模块合并到对应字段里并保留一个“最近一次更新时间”。这样语义记忆的量非常小但每一条都高度可用。2.4 程序性记忆把“用户偏好”变成“调用规则”程序性记忆比较特殊它记录的不是事实而是“在什么情况下该怎么做”的经验规则。比如“该用户对保险条款特别敏感解释时要先给免责声明”“该用户是技术型客户跳过基础问答直接给API文档”。这类记忆最适合放在配置层而不是数据层。我通常把它们做成“规则列表”每条规则包含触发条件如用户画像匹配、优先级、生效时间。召回时机是“Agent生成初步方案之后执行之前”让规则层来做动作调整。分层的价值在这里体现得最明显程序性记忆负责“行为校准”语义记忆负责“事实背景”情景记忆负责“历史参照”工作记忆负责“眼下这件事”。模型不会被塞入大段历史噪音而是拿到一份精心组合过的“快照”。2.5 分层之间的读写与优先级策略分层不是把数据放四个地方就行真正的难点是“跨层读写规则”。我给出一个比较省心的配置原则工作记忆的写入优先级最高语气词和临时状态只进这里。情景记忆的写入必须经过“事件抽取器”并且做去重。语义记忆的写入要过“画像合并器”而不是直接追加。程序性记忆的写入是人工或高置信度规则模型不直接改。读取的时候正好反过来。Agent先看工作记忆里的“当前状态”再看程序性记忆的“规则约束”然后按语义记忆找“用户画像”最后按需要调取情景记忆的具体事件。这个顺序能大幅降低无关记忆被召回的次数。在看到这个效果之前我也试过先把所有相关度高的记忆统统塞进Prompt结果就是上下文爆炸、模型开始顾头不顾尾。3. TTL机制给每条记忆一个“失效时间”让Agent保持清醒3.1 为什么必须引入TTL而不是只靠容量限制最先让记忆系统出问题的往往不是容量而是“过期信息仍然可用”。用户上个月热衷于讨论A方案这周已经明确转向B方案可是旧记忆的embedding相似度依然很高Agent一召回还是一股脑地把A方案当成最新意图。你哪怕把向量库容量做得再大也解决不了“旧闻仍有高优先级”的问题。TTLTime To Live才是真正的解药。它的本质是给每条记忆打一个expires_at时间戳超时后系统要么直接删除要么降权。降权有时候比删除更实用因为某些记忆可能还有参考价值但不能让它再影响主要判断。比如三个月前的信用卡偏好不允许再作为默认项被注入了但用户主动询问“我上次用哪个银行”时仍然可以以“历史记录”的方式呈现。3.2 各层记忆的默认TTL建议我调试过几套业务之后目前的默认TTL参考值如下可以按业务调整工作记忆会话结束即失效或30分钟无操作即清理。它的定位是临时状态不需要长期保留。情景记忆默认30天到90天。超过90天的事件级记录除非用户主动pin为“重要”否则进入归档区不再参与日常召回。语义记忆默认180天。画像字段每次更新会刷新该字段的TTL所以“活跃用户画像”实际上会一直保持新鲜而长期不活跃的用户画像会自然衰减。程序性记忆默认永久但需要人工复核。规则类记忆的变更影响面太广不能让TTL悄悄把规则干掉适合用人工审批流。需要提醒的是不同业务的数据敏感度差异很大。金融或医疗场景的记忆保留期要短得多可能情景记忆只有7天而兴趣爱好类场景可以拉长到一年。关键是每条记忆都带独立的TTL而不是“全库一把尺”。3.3 TTL的两种触发方式惰性删除与定时清理工程实现上TTL不是“到点就准时删”那么简单还要考虑数据库负担。我实践下来最稳妥的是两种机制组合一种是惰性删除。每当要从记忆库取数据时先检查目标记忆的expires_at如果过期则在这一访问路径上直接拒绝召回并顺手标记为“待删除”。好处是不用额外建立定时任务坏处是最终删除可能要拖很久导致存储成本降不下来。另一种是定时清理。我一般把任务设在每天凌晨跑一遍DELETE FROM memory WHERE expires_at now()或者批量更新降权状态。这个做法成本低也很容易纳入运维告警体系。有些团队用消息队列触发逐条过期对超大系统来说是必要的但中小型场景我建议别过度设计。提示使用PostgreSQL这类关系库时记得给expires_at建立索引否则定时清理会在百万级记录上把库拖垮。这个坑我踩过一次扫描全表用了好几分钟直接把业务读请求拖到超时。3.4 我踩过的三个TTL坑时间源、宽限期、级联失效第一个坑时间源不统一。有的记忆用业务服务器时间写入有的用回调网关时间本机时钟一漂移就会出现“未来时间戳”导致记忆永远不过期。后来我统一规定所有记忆写入的时间戳一律以应用层的授权时间服务为准不允许上游设备自报时间。第二个坑TTL边界太生硬。用户今天还在用某条记忆明天刚好过期Agent直接“断片”了。我加了一个宽限期策略过期记忆不是直接不可见而是进入“降权区”只有在用户明确问到时才允许展示为历史信息不能主动参与推理。这个宽限期我设了24小时。第三个坑级联失效。用户删除了一个画像字段但它关联的情景记忆和程序性规则还在工作。比如用户表明“以后再也不想收到营销信息”语义画像里的preferred_channel更新了但情景记忆里还存着几条参加促销活动的事件程序性规则还写着“该用户高频率参加活动”。这个问题必须靠删除时的级联策略解决——凡是引用被删字段的规则、事件都要同时降权或标记失效否则你会看到Agent一边说“好的不会再推送营销”一边又把促销情报翻出来给用户看。4. 数据治理记忆可以长但不能“忘本”4.1 最小化收集别把对话里所有话都塞进长期记忆一个很容易犯的错误是“能存就存”。对话里充满了语气词、重复抱怨、临时情绪、无效试探。全部入库的结果一是让存储成本毫无意义地上涨二是让召回时无差别带出一堆噪声。我在事件抽取模块里加了一条硬规则只有包含“事实描述”“需求变更”“情感倾向显著”“明确偏好”这四类特征的片段才有资格被选中进入长期记忆。其余内容最多留在工作记忆里做本会话补充绝不跨会话持久化。这条“最小化收集”原则同时也降低了数据合规的风险。你可以跟用户解释“我们只保存与您服务相关的关键信息”这句话在很多合规场景下比“我们会保存全部对话记录”要好得多。4.2 用户“遗忘权”删除接口与级联清理的设计要点默默记了一堆用户信息却不给用户一个“忘记我”的按钮这在产品上很容易触发反感在合规上也可能踩线。我建议至少提供两种删除能力第一种是“清空该用户的全部记忆”。这个操作必然级联删除三块情景记忆、语义画像、程序性规则。实操里最怕的是只删了主表向量索引里的embedding没删结果下次主动召回还能“隐约看到影子”。所以每个embedding记录都要带user_id和source_id删除时同步调向量库的delete接口而不是只删关系表。第二种是“定向遗忘”用户说“我不想让你记住我的家庭地址”。系统要能识别到具体实体并定向抹除对应字段和关联事件。因为大模型提取实体不总是准确我建议在保存时就把实体和记忆记录建立关系表删除时按实体ID反向清理。4.3 审计与可解释性怎么知道Agent“依据什么记忆”回答记忆用得越多就越难回答“刚才那句话是哪条记忆触发的”。这个问题直接影响故障排查和用户信任。我的做法是为每次召回都生成“记忆引用清单”哪几条记忆被输入了Promopt、各自打了多少分、是否带TTL降权标记。然后把这份清单写入审计日志保留至少90天。工具层面很简单就是在召回函数里把候选记忆的id数组一起返回再跟着整个会话日志落盘。需要排查“为什么Agent知道用户上个月投诉过”时直接查会话日志里的召回清单即可。这事别指望事后靠prompt分析因为模型输出里根本不会透露它internal context从哪里来。4.4 安全存储与权限隔离如果用户的记忆里包含姓名、手机号、地址等敏感数据存储层至少要满足两点一是敏感字段做加密比如用字段级AES加密避免一旦数据库泄露就“全包圆”二是权限隔离运维账号和业务账号要分离不能让一个能看全量日志的人同时也能导出全量用户记忆。我见过一种很吓人的配置工程师为了调代码方便把包含用户记忆的生产库账号权限开成了只读root结果一次误操作就把全量用户画像导出到了临时文件里。虽然最后没出事但这种事故一旦发生业务基本就凉了。所以记忆库的访问权限最好单独拉一个只读专用账号所有导出操作都走审计平台。5. 实操案例给一个客服Agent装上“分层记忆系统”5.1 场景假设与需求拆解假设我们要做一个简单但真实的客服Agent用户可以通过它查询订单、报修、预约上门。需求拆解后如下用户每次来Agent应当知道ta的名字、历史订单、上回是否报修过、偏好沟通时间。用户明确提到的地址和电话应该能复用但不能在其他无害对话里被提起。用户说“以后不要联系我”之后系统必须快速失效所有营销类和主动触达类记忆。如果用户要删除历史记忆可以一键执行并在24小时内全链路失效。基于这些需求我会在系统里配置四层记忆并挂上TTL和删除接口。具体配置用表格梳理一下更清楚记忆层 | 存储示例 | 概括字段 | TTL默认值工作记忆 | Redis | 当前会话状态、临时表单 | 会话结束或30分钟情景记忆 | PostgreSQL向量 | 事件id、时间、事件类型、行为摘要 | 90天语义记忆 | PostgreSQL | 用户画像字段、更新次数 | 180天程序性记忆 | 配置中心 | 规则条件、动作、优先级 | 人工复核永久有效5.2 存储方案选择关系型向量库还是纯内存小规模部署完全可以用PostgreSQL加pgvector搞定关系部分保存用户画像和事件元信息向量部分保存事件摘要和对话片段的embedding。这样做的好处是不引入太多中间件备份和一致性都好做。规模真的上来之后我会再把“事件检索”切到独立向量库比如Milvus这类服务原因是pgvector在千万级向量上的并发召回延迟很难扛住高QPS。但如果你刚起步别急着上分布式否则光运维开销就够你喝一壶。选择的关键依据不是“新潮”而是“你目前有没有专职的向量库运维能力”。5.3 核心流程代码写入、召回、过期、删除下面给几段可以直接改用的核心伪代码。写入部分我习惯用一个“记忆治理过滤器”包在外层先判断这段文本是否真的需要进长期记忆。def write_memory(user_id, source_text, memory_layer, ttl_seconds): # 1. 判断是否有资格写入 if memory_layer episodic: events extract_event(source_text) # 调用事件抽取器 if not events: return for ev in events: insert_event(user_id, ev, ttlttl_seconds) insert_embedding(user_id, f{ev.type}:{ev.summary}, ev.id, ttlttl_seconds) elif memory_layer semantic: profile_updates extract_profile_changes(source_text) merge_into_user_profile(user_id, profile_updates) # 合并不是追加 refresh_profile_ttl(user_id, ttl_seconds) def recall_memory(user_id, query, max_items5): # 1. 先取工作记忆当前状态 working get_working_memory(user_id) # 2. 取程序性规则 rules get_active_rules(user_id) # 3. 语义画像 profile get_user_profile(user_id) # 4. 再查情景记忆带TTL过滤和降权 events vector_search(user_id, query, top_kmax_items, filter_expiredTrue, decay_after_ttlTrue) return build_memory_prompt(working, rules, profile, events)删除部分最关键的是“级联删除”。以下是一个简化实现def delete_user_memory(user_id): # 1. 删除情景记忆和向量索引 event_ids select_event_ids(user_id) delete_embeddings_by_source_ids(user_id, event_ids) delete_events(user_id) # 2. 删除语义画像 delete_profile(user_id) # 3. 删除程序性规则或置为inactive deactivate_rules(user_id) # 4. 清空工作记忆 delete_working_memory(user_id) # 5. 记录审计 write_audit_log(user_id, actiondelete_all, timestampnow())5.4 提示词中如何注入“记忆摘要”而不是整段历史最后一个步骤是把整理好的记忆喂给模型。我的习惯是这样做顺序先注入工作记忆的当前状态比如“用户已选择售后工单类型网络报修”。再注入程序性规则比如“该用户属于工单高频用户回复时提供优先通道链接”。接着注入语义画像的一句话摘要比如“用户偏好晚间联系关注路由器信号质量”。最后注入召回的情景事件只给最近两三条关键事件每条控制在50字以内。这样的记忆Prompt总长通常不超过600字节比把整个历史消息硬塞进去要短一个数量级。而且因为信息经过了筛选和合并模型反而更容易抓到重点。我测试过在相同业务上这种“结构化记忆摘要”比“全文向量召回”把关键信息准确率提升了30%左右幻觉率也明显下降。6. 常见问题与排查技巧实录6.1 问题速查表整理了一份清单都是真实项目里容易出现的状况现象可能原因处理方式用户说“你记错了”Agent却坚持旧信息语义画像更新滞后给画像字段加“更新时间”定期重算置信度召回结果里混入大量无关旧事件向量检索门槛太低调高相似度阈值加时间窗过滤用户删除记忆后仍被提及向量索引没有同步删除check删除流程保证source_id级联TTL设了但不过期时间源不统一或字段未建索引统一切到应用层时钟为expires_at建索引Agent回答开始变得啰嗦重复注入记忆过多限制记忆摘要总量增加去重合并跨用户记忆串味用户隔离的key设计不当所有查询/写入都强制带user_id索引联合筛选6.2 热门案例记忆串味、幻觉变多、TTL过期不生效记忆串味是最常见也是最严重的bug。有一次我在联调环境里发现A用户的订单状态莫名其妙出现在B用户的会话上下文里查了半天发现是embedding查询时忘了加user_id过滤条件结果向量库把全库近似样本都拉出来了。这种问题压力测试很难发现必须在召回层与缓存层双向检查隔离条件。幻觉变多经常是因为记忆摘要里混进去了矛盾信息。用户三个月前说“我不吃辣”但昨天刚说“聚餐可以试试火锅”。这时候系统如果同时把两条记忆都注入Agent就会左右为难然后编出一个并不存在的“微辣偏好”。我的对策是给矛盾记忆打“覆盖标记”后者覆盖前者时前者的权重降到极低除非用户主动追问历史。TTL过期不生效多数是“懒删除”导致数据表面上还在。如果查询路径里没走“先判断expires_at再召回”的逻辑等于把TTL写成了摆设。我写了个单元测试专门构造一条半小时后过期的记忆然后模拟“过期后的召回必须返回零结果”这道测试所有改动都会跑。6.3 如何用日志和测试用例验证记忆系统没有“越界”越界是下意识的靠人肉复盘很难发现。我建议至少做三件事一是为每个召回请求打日志记录“注入了几条记忆、哪些记忆、分数如何”。二是做敏感信息走查把名字、地址、卡号等实体标记出来在沉淀记忆时自动打上high_sensitivity标签并在Prompt注入时做脱敏替换。三是建模拟用户集把一些明显不该跨会话提起的信息比如“用户上个月摔伤了腿”放进历史对话然后测试Agent在新会话里会不会主动问候“腿好了没”。如果它会就说明记忆越界了。6.4 我建议的“分层记忆三原则”总结成三句话就是分层要严TTL要狠治理要细。“严”是指每一类信息必须走对层级不能什么都往向量库里塞“狠”是指该过期就过期、该降权就降权不要对旧记忆心慈手软模型的判断力就是被这些陈旧记忆拖垮的“细”是指删除、审计、隔离这些不直接创造体验但决定存亡的机制一定要做到位。最后再分享一个我自己摸索出来的小技巧别看“召回率”和“记忆命中率”不如直接盯“上下文无用信息占比”。我每次调记忆系统只看一个指标——Prompt里有多少比例的token是当前任务真正需要的。这个比例上去了Agent的稳定性和用户观感都会同步变好。记忆系统的本质不是让Agent记住更多而是让它在正确的时刻用上恰到好处的旧信息然后在它该退场的时候安静退场。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →