做了几年大模型应用,我越来越确定一件事: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方案当成最新意图。你哪怕把向量库容量做得再大,也解决不了“旧闻仍有高优先级”的问题。
TTL(Time 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, ttl=ttl_seconds) insert_embedding(user_id, f"{ev.type}:{ev.summary}", ev.id, ttl=ttl_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_items=5): # 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_k=max_items, filter_expired=True, decay_after_ttl=True) 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, action="delete_all", timestamp=now())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记住更多,而是让它在正确的时刻用上恰到好处的旧信息,然后在它该退场的时候安静退场。