ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent记忆系统设计:从写入到遗忘的完整工程实践

Agent记忆系统设计:从写入到遗忘的完整工程实践

1. 面试官到底在问什么

聊 Agent 记忆系统之前,先把一个常见的误区掰开说清楚。很多人一听到“记忆”两个字,脑子里第一反应就是“存聊天记录”,于是上来就答“用向量数据库把历史对话存起来,每次检索 top-k 塞进 prompt”。这个答案不能说错,但它只覆盖了记忆系统里最表层的一小块,面试官听完大概率会追问一句“然后呢”,你就卡住了。

面试官真正想听的,是你有没有把记忆当成一个完整的系统来思考。什么叫系统?就是它得有写入、有存储、有检索、有更新、有遗忘、有冲突处理,还得考虑成本、延迟、一致性。这跟设计一个后端服务没有本质区别,只不过存储的对象从订单、用户变成了“Agent 在交互过程中产生的、对未来决策有用的信息”。

我面过也带过人,总结下来,一个 Agent 记忆系统的设计题,面试官心里其实有一张评分表,大致是这么几档:

档位表现面试官内心OS
青铜只会说“存向量库,检索拼接”背过八股,没真做过
白银能区分短期/长期记忆有点概念,但不成体系
黄金能讲清写入、检索、更新、遗忘的完整链路真做过,能落地
钻石能讲清各环节的取舍、成本、失败模式踩过坑,能带队
王者能结合具体业务场景给出分层方案并解释为什么这人我要了

所以这篇文章,我就按“怎么从白银打到钻石”这个思路来拆。不管你是准备面试,还是真的要在项目里落地一套记忆系统,下面这些内容都能直接拿去用。

先给一个总纲,记忆系统的核心就四件事:写什么、存哪里、怎么取、怎么维护。听起来简单,但每一件往下挖都有大量细节。我见过太多项目,前面检索做得花里胡哨,结果写入策略一塌糊涂,存进去的全是噪音,检索再准也是垃圾进垃圾出。

提示:面试时如果对方只给了一句“设计一个 Agent 记忆系统”,别急着报技术栈。先反问一句“这个 Agent 主要面向什么场景,是长周期任务助手还是单轮问答”,把需求边界问清楚再答,这一问本身就是加分项。

2. 记忆到底分几层,为什么要分层

2.1 短期记忆与长期记忆的本质区别

短期记忆和长期记忆的区别,不是“存的时间长短”,而是生命周期和访问模式完全不同。

短期记忆是当前这轮任务/会话的工作内存,它的特点是:容量有限、访问极频繁、任务结束基本就没用了。典型实现就是对话上下文窗口,或者一个会话级的 buffer。它的核心矛盾是上下文窗口有限,你不可能把几十轮对话全塞进去,所以要考虑截断、摘要、滑动窗口这些策略。

长期记忆是跨会话、跨任务沉淀下来的信息,特点是:容量大、访问相对稀疏、需要持久化。它存的不是“用户说了什么”,而是“从用户说的话里提炼出的、以后还用得上的事实和偏好”。比如用户说“我下周三要去杭州出差”,短期记忆记的是这句话,长期记忆应该提炼成“用户下周三有杭州行程”这样一条结构化事实。

我习惯用一个类比:短期记忆是电脑的内存,长期记忆是硬盘。内存快但小,断电就没;硬盘大但慢,要持久。你不会把所有东西都往内存里塞,也不会每次读个变量都去硬盘捞。Agent 记忆系统也是这个道理。

2.2 三层记忆模型的落地拆解

实际项目里,我一般会拆成三层,比单纯的两层更好落地:

第一层:工作记忆(Working Memory)

就是当前推理步骤直接要用的信息。比如 Agent 正在执行一个多步任务,当前这一步需要知道“上一步的中间结果是什么”。这层通常就放在 prompt 里,或者用一个结构化的 scratchpad 维护。它的生命周期是“当前任务”,任务结束就清掉。

第二层:会话记忆(Session Memory)

当前这次会话(可能包含多个任务)的上下文。这层要解决的是“用户刚才说的那句话,跟现在这句有什么关系”。实现上通常是对话历史 + 会话级摘要。会话结束时,把值得长期保留的部分提炼出来,写入第三层。

第三层:长期记忆(Long-term Memory)

跨会话的持久化知识。这层又可以细分:

  • 事实记忆:用户是谁、有什么偏好、做过什么。比如“用户偏好用 Python,不喜欢 Java”。
  • 经验记忆:Agent 自己总结出来的、可复用的策略。比如“处理这类表格任务时,先做列名归一化成功率更高”。
  • 语义记忆:领域知识、文档片段,通常来自外部知识库的检索。

为什么要分这么细?因为不同层的写入时机、检索方式、淘汰策略都不一样。事实记忆要防冲突(用户改主意了怎么办),经验记忆要防过时(策略失效了怎么办),语义记忆要防污染(检索到不相关的文档怎么办)。混在一起做,最后一定是一锅粥。

2.3 分层带来的一个关键收益:成本可控

分层最实际的好处是成本可控。如果你不分层,每次请求都去向量库捞一大堆历史,token 成本会爆炸,延迟也扛不住。分层之后,工作记忆走内存,会话记忆走缓存,只有真正需要跨会话信息时才去查长期记忆,而且可以加缓存、加过滤。

我实测过一个客服 Agent 的场景,不分层的时候单次请求平均要拼 6000+ token 的历史,分层 + 摘要之后降到 1500 左右,响应延迟从 2.3 秒降到 0.9 秒,成本直接砍掉一大半。这个数字在面试里报出来,比你说十句“分层很重要”都有说服力。

3. 写入策略:什么该记,什么不该记

3.1 写入是记忆系统最容易翻车的地方

我敢说,80% 的记忆系统效果差,问题都出在写入环节。检索做得再好,存进去的是垃圾,捞出来的也是垃圾。写入的核心问题是:从海量的交互流里,判断哪些信息值得长期保留。

这里有个反直觉的点:不是记得越多越好。记忆系统的价值在于“精准”,而不是“全量”。你存了一万条记忆,检索时召回一堆不相关的,反而干扰模型判断。所以写入的第一原则是宁缺毋滥。

那怎么判断一条信息值不值得记?我一般用三个筛子:

  • 持久性:这条信息过一段时间还有用吗?“今天天气不错”没用,“用户对花生过敏”有用。
  • 复用性:这条信息以后还会被用到吗?“用户这次问的是退款流程”可能只用一次,“用户是某平台的会员”会反复用到。
  • 独特性:这条信息是不是已经知道了?重复写入只会增加噪音。

3.2 用 LLM 做写入决策的实操方案

判断“值不值得记”这件事,规则很难覆盖全,最靠谱的还是让 LLM 来做。但直接让 LLM 判断“这句话要不要记”效果一般,因为它缺乏上下文。我的做法是给它一个结构化的抽取任务,而不是一个二分类任务。

具体来说,我会在每轮对话结束后,跑一个抽取 prompt,让它输出结构化的记忆条目。prompt 大致长这样(这是简化版,实际会更细):

你是一个记忆抽取器。请从下面这轮对话中,抽取值得长期记住的信息。 只抽取满足以下条件的信息: 1. 关于用户的稳定事实(身份、偏好、习惯、约束) 2. 用户明确表达的长期目标或计划 3. 对后续任务有复用价值的结论 不要抽取:临时性信息、闲聊、已经存在于已知记忆中的信息。 已知记忆: {existing_memories} 本轮对话: {conversation} 请以 JSON 数组输出,每条包含: - content: 记忆内容(一句话) - type: fact / preference / goal / experience - confidence: 0-1 的置信度 - source: 来源对话轮次

这个方案有几个关键设计点,我逐个解释为什么:

第一,给它看已知记忆。这是为了防止重复写入。如果不给,它会把“用户叫张三”这种已经记过的信息反复抽出来。给了之后,它就能判断“这条已经有了,跳过”。

第二,要求输出置信度。因为 LLM 抽取会有误判,置信度低的条目可以先存着但不参与检索,或者进一个待确认队列。这比一刀切地全存或全不存要灵活。

第三,区分类型。不同类型走不同的存储和检索策略。事实类走精确匹配 + 向量,偏好类走向量,经验类可能要带条件(什么场景下适用)。

第四,保留来源。出问题的时候能追溯,也方便做冲突检测。

3.3 写入的时机选择

写入时机也有讲究,常见的有三种:

  • 每轮写入:每轮对话结束就跑一次抽取。优点是及时,缺点是成本高、噪音多。
  • 会话结束写入:整段会话结束后统一抽取。成本低,但会话中途如果断了就丢了。
  • 异步批量写入:攒一批再处理。适合高并发场景,但实时性差。

我一般用混合策略:高价值信号(比如用户明确说“记住我……”)实时写入,普通信息会话结束批量写入。这样既保证了关键信息不丢,又控制了成本。

注意:写入一定要做幂等。同一轮对话因为重试被处理两次,不能产生两条重复记忆。我一般用“会话ID + 轮次ID + 内容hash”做去重键。

4. 存储选型:向量库不是万能药

4.1 不同记忆类型对应不同存储

一提到记忆存储,很多人条件反射就是向量数据库。但向量库只擅长一件事:语义相似度检索。而记忆系统里有很多查询根本不是语义相似度能解决的。

举个例子,用户问“我上次说的那个截止日期是哪天”。这是一个精确的事实查询,你要的是“截止日期”这个字段的值,向量检索很可能给你召回一堆语义相近但日期不对的条目。这种场景,结构化存储(比如关系库或 KV)反而更靠谱。

所以我的选型原则是按查询模式选存储,而不是按“时髦”选:

记忆类型典型查询推荐存储
用户事实/偏好精确字段查询 + 语义召回关系库 + 向量库混合
对话历史时间范围 + 语义召回文档库 + 向量索引
经验策略条件匹配 + 语义召回结构化 + 向量
语义知识纯语义召回向量库

4.2 混合检索的工程实现

实际项目里,我基本都用混合检索:先用结构化条件过滤,再做向量召回,最后做重排。这个链路听起来复杂,但每一步都有明确的必要性。

第一步,结构化过滤。比如当前用户是 A,那就先过滤出 user_id = A 的记忆。这一步能把候选集从百万级降到千级,向量检索的压力瞬间小一个数量级。

第二步,向量召回。在过滤后的候选集里做语义检索,取 top-k(一般 k=10~20)。这里要注意,向量检索的 embedding 模型要和写入时保持一致,否则相似度计算会失真。

第三步,重排。用一个更精细的模型(可以是 cross-encoder,也可以是 LLM)对召回的候选做相关性打分,取最终 top-n(n=3~5)。这一步是提升精度的关键,因为向量召回是“粗筛”,重排是“精筛”。

我实测过,加了重排之后,记忆召回的准确率能从 60% 左右提到 85% 以上。代价是每次检索多几十毫秒,但换来的是模型回答质量的明显提升,非常值。

4.3 一个容易被忽略的点:记忆的时效性

存储的时候一定要带时间戳,而且检索时要考虑时效性。一条三年前的偏好,可能早就变了。我的做法是给记忆加一个“衰减因子”,检索打分时把时间因素算进去:

final_score = relevance_score * decay_factor decay_factor = exp(-λ * age_in_days)

λ 是个超参,根据业务定。快消类业务 λ 大一点(记忆衰减快),长期助手类 λ 小一点。这个公式不复杂,但面试时能说出来,说明你考虑过记忆的“新鲜度”问题,这是很多人想不到的点。

5. 检索策略:怎么把对的记忆捞出来

5.1 检索的核心矛盾:召回率与精度的平衡

检索这件事,永远在召回率和精度之间权衡。召回率高,捞得多,但噪音大;精度高,捞得准,但可能漏。记忆系统的特殊之处在于:漏掉一条关键记忆的代价,往往比多带几条噪音更大。因为模型如果不知道用户的关键约束,可能会给出完全错误的回答。

所以我的默认策略是偏召回:宁可多捞几条,让模型自己判断哪些有用。但“偏召回”不等于“无脑捞”,还是要做基本的过滤和排序。

5.2 查询改写:让检索更准的关键一步

用户当前的问题,往往和记忆里的表述不一样。比如用户问“帮我订个安静点的餐厅”,记忆里存的是“用户不喜欢嘈杂环境”。这两句话语义相关,但直接拿“订个安静点的餐厅”去检索,未必能召回“不喜欢嘈杂环境”。

解决办法是查询改写:在检索前,先用 LLM 把当前 query 改写成几个不同角度的检索 query。比如上面这个例子,可以改写成:

  • “用户的用餐环境偏好”
  • “用户对噪音的态度”
  • “用户的餐厅选择习惯”

然后用这几个 query 分别检索,合并结果。这个技巧叫多查询检索,实测能明显提升召回率。代价是多几次检索调用,但记忆检索本来就不是高频操作,这点成本可以接受。

5.3 检索结果的组织方式

捞出来的记忆怎么塞进 prompt,也有讲究。我见过有人直接把一堆记忆条目拼成一坨,模型看了也懵。我的做法是按类型分组 + 按相关性排序 + 加简短说明:

[用户事实] - 用户对花生过敏(高相关) - 用户是某平台会员(中相关) [用户偏好] - 用户偏好安静环境(高相关) [相关经验] - 处理类似任务时,先确认时间约束(中相关)

这样组织,模型能快速定位到关键信息,而不是在一堆文本里自己找。别小看这个格式,它直接影响模型对记忆的利用率。

6. 记忆的更新、冲突与遗忘

6.1 冲突检测:用户改主意了怎么办

记忆系统最棘手的问题之一就是冲突。用户上周说“我喜欢喝咖啡”,这周说“我戒咖啡了”。两条记忆都在库里,检索时都捞出来,模型该信哪个?

我的处理方案是新记忆覆盖旧记忆 + 保留历史。具体做法:

  • 写入新记忆时,先检索是否有语义冲突的旧记忆。
  • 如果有,把旧记忆标记为“已失效”,新记忆标记为“当前有效”。
  • 检索时默认只召回“当前有效”的记忆,但保留历史用于追溯。

判断“是否冲突”这一步,还是交给 LLM。给它新旧两条记忆,问“这两条是否矛盾”,矛盾就触发覆盖逻辑。这个判断不需要很精确,宁可多判几次冲突,也不要让矛盾记忆同时生效。

6.2 遗忘机制:不是所有记忆都值得留

遗忘不是 bug,是 feature。一个不会遗忘的系统,最后一定会被噪音淹没。遗忘策略我一般分三种:

  • 时间衰减:超过一定时间没被访问的记忆,降低权重或归档。
  • 容量淘汰:记忆总量超过阈值时,淘汰最久未访问或置信度最低的。
  • 主动清理:定期跑一个任务,让 LLM 审查记忆库,合并重复、删除过时。

这里有个经验:遗忘要可逆。别直接物理删除,先归档。万一删错了还能捞回来。我踩过一次坑,一个清理任务把用户的关键约束删了,导致后面一连串回答都跑偏,排查了半天才发现是记忆被误删。

6.3 记忆的自我反思

高级一点的玩法是让 Agent定期反思自己的记忆。比如每天结束时,让它回顾当天的交互,总结出“哪些做法有效、哪些无效”,形成经验记忆。这个机制能让 Agent 越用越聪明,而不是每次都从零开始。

反思的 prompt 大致是让它回答几个问题:今天哪些任务成功了,成功的关键是什么;哪些失败了,失败的原因是什么;有没有可以沉淀成通用策略的结论。产出的经验记忆带上适用条件,检索时按条件匹配。

7. 常见问题与排查实录

7.1 记忆系统典型故障速查

现象可能原因排查方向
模型答非所问检索召回不相关记忆检查 embedding 一致性、重排是否生效
重复问同样的问题记忆没写入或写入失败检查写入链路日志、幂等键
回答前后矛盾冲突记忆同时生效检查冲突检测逻辑、失效标记
响应越来越慢记忆库膨胀、检索无过滤检查是否有结构化过滤、是否做归档
成本飙升每次拼太多记忆检查 top-k 设置、是否做摘要

7.2 几个我踩过的坑

坑一:embedding 模型换了没重刷历史。有次升级 embedding 模型,新写入的用新模型,历史的是旧模型,检索时相似度完全乱套。教训是:换 embedding 模型必须全量重刷,或者做双写过渡。

坑二:把整段对话当一条记忆存。早期图省事,直接把对话原文存进去。结果检索时召回一大段无关内容,token 浪费严重。后来改成抽取结构化条目,效果好太多。

坑三:忽略并发写入的竞态。高并发下,同一个用户的记忆可能被同时写入和读取,出现读到旧值的情况。后来加了版本号,读取时校验版本,才解决。

坑四:检索没做权限隔离。多用户场景下,A 的记忆被 B 检索到了,这是严重事故。一定要在存储层就做好 user_id 隔离,别指望检索层过滤。

7.3 面试时怎么把这些问题变成加分项

面试官问记忆系统,你如果能主动提到“冲突处理”“遗忘机制”“成本控制”这些点,基本就稳了。因为这些是真正做过的人才会遇到的问题。你可以这样组织回答:

先讲整体分层,再讲写入策略(重点讲怎么判断该不该记),然后讲检索(重点讲混合检索和查询改写),最后讲维护(冲突、遗忘、反思)。每一层都带一句“这里容易出的问题是……我的处理是……”。这样答下来,面试官会觉得你不只是懂概念,是真的落地过。

8. 一套可直接参考的最小实现

如果你现在就要动手搭一个,我给一个最小可用的方案,不追求完美,但能跑起来。

存储层:用关系库存结构化记忆(user_id, content, type, confidence, created_at, status),用向量库存 embedding。两者用记忆 ID 关联。

写入层:每轮对话后异步跑抽取,产出结构化条目,去重后写入。高价值信号实时写。

检索层:结构化过滤 → 向量召回 top-20 → LLM 重排 top-5 → 按类型分组拼进 prompt。

维护层:每天跑一次归档任务,处理冲突、衰减权重、清理低置信度条目。

这套方案我用了大半年,在中等规模的场景下足够稳。等业务量上来了,再考虑加缓存、分片、专门的记忆服务。

最后分享一个我个人的体会:记忆系统的难点从来不在技术选型,而在判断什么值得记。这个判断没有标准答案,只能靠不断观察模型的实际表现来调。我一般会定期抽样看检索出来的记忆,人工判断哪些是噪音,然后反推写入策略哪里需要改。这个迭代过程,比任何架构设计都重要。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进