ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

AI Agent记忆系统设计:从短期上下文到长期记忆的落地实践

AI Agent记忆系统设计:从短期上下文到长期记忆的落地实践 AI Agent 聊到第三篇我想说点扎心的。大部分 Agent 做出来之后你会发现它强是真强蠢也是真蠢。强在知识面广、能调工具、能写代码蠢在它永远记不住你——同一件事你昨天刚交代过今天再问它依然像第一次见面一样客客气气地让你把背景重新说一遍。这种体验放在工具上还能忍放在需要长期协作的助手类产品上基本就是灾难。这个系列前面两篇分别聊过 Agent 的运行逻辑和技能扩展今天专门讲记忆。记忆这件事在 AI Agent 的技术体系里长期被低估但我觉得它是 Agent 从“玩具”走向“伙伴”的关键分水岭。这篇不是泛泛科普记忆的概念而是从设计到落地把“让 Agent 记住你”这件事完整拆一遍记忆怎么分类、系统怎么设计、代码怎么落地以及我实际项目里踩过的坑。适合已经在做 Agent 开发、或者正准备把自己那套 RAG 应用升级成带记忆形态的读者看看。1. 为什么“记住你”是Agent的分水岭在没有记忆的 Agent 面前用户只是一个个“匿名提问者”。它知道你是谁不是因为真的认识你而是因为你刚刚在对话里交代过背景。你可能上午告诉它“我在杭州通勤靠地铁”下午让它推荐晚餐它宁可去猜也想不起来上午说过什么。这种体验说白了就是一个随时失忆的新员工每次见面都要重新自我介绍。1.1 从“工具”到“伙伴”只差一个记忆层我们的出发点是Agent 首先是个工具工具解决单次请求但如果目标是让它成为数字助手就必须有在多次请求之间保留信息的能力。前端开发里有个词叫“状态管理”无状态服务容易横向扩展但产品体验取决于状态怎么维护。Agent 也一样无状态跑起来简单但所有个性化、连续性、信任感都靠记忆层来实现。举个例子。一个咨询天气的 Agent如果记不住用户所在城市它每次都要反问“您在哪座城市”这个过程重复三回用户就会烦。而一旦它能记住位置下次直接说“明天杭州适合跑步吗”它就能结合天气和用户习惯直接回答。后者带来的体验跃升不是模型能力提升带来的而单纯是记忆带来的。1.2 短期记忆、长期记忆到底在说什么我经常被问到一个问题大模型的上下文窗口已经很大了是不是不需要额外做记忆了这里有个常见的混淆。上下文窗口是“工作台”不是“档案库”。工作台上堆的东西再多下班收工就清空了档案库里的东西才能跨天、跨月、跨年反复调用。在 Agent 系统里我会把记忆分成三个层次来看记忆类型载体生命周期典型内容短期记忆上下文窗口、会话状态单次会话内当前任务、用户刚说的指令、工具返回的中间结果长期记忆外部数据库、向量库跨会话、可持续数月到数年用户画像、偏好、历史决策、明确约束工作记忆Agent 内部的状态机/目标栈当前任务的执行过程当前子目标、已完成步骤、待办项三者的关系可以类比成你和一个老朋友交谈。短期记忆是你的聊天内容说完可能就忘长期记忆是你对他的了解多年不变工作记忆是你正在处理的事情比如“正在帮他订机票已经选好航班就差付款确认了”。Agent 想要真正“记住你”核心是把短期对话里值得沉淀的信息转换成长期记忆再在合适的场景下重新拿回工作台。这里想多说一句。很多人以为长上下文就能当长期记忆这是个误区。上下文窗口再大也只是给当前这次对话用的临时空间会话结束就释放了。你换个时间、换个会话进来它照样两眼一抹黑。所以长期记忆必须外置落到数据库、向量库或者文件系统里这件事没有捷径。2. 先想清楚再动手记忆系统的设计思路好多第一次做 Agent 记忆的人上来就搭向量库把用户所有聊天记录往库里灌然后在 prompt 里塞一堆检索结果。结果是什么记忆是有了但质量差得离谱相关的内容找不到不相关的内容全来了token 早就爆了。所以做记忆我建议先停下来问一个问题到底需要记住什么2.1 不是所有信息都值得记先给记忆分个类以我自己的项目经验值得进长期记忆的信息差不多是这几类用户画像所在城市、职业、作息、家庭情况、长期偏好。特点是变化慢、复用价值高。明确约束用户主动提出的要求比如“以后不要给我推荐含糖高的东西”这类信息优先级最高必须持久化。关键事件用户提到的重要事项比如“下周要去上海出差”“准备换工作”。这类信息有强时效过期之后要归档。任务结果用户让 Agent 做过的重要产出比如“帮我写了一份简历模板”保留产出摘要能避免重复工作。互动风格用户喜欢简洁回答还是详细展开说话正式还是随意。这个在体验层面影响很大技术人容易忽略。有一个我总结的粗粒度判断公式记忆价值 ≈ 复用概率 × 影响程度 ÷ 存储成本。高复用、高影响、低成本的记忆优先存临时性、一次性、靠上下文就能搞定的不要浪费存储。很多人踩的第一坑就是把所有原始聊天记录一股脑塞进去美其名曰“全量记忆”结果检索噪声大得没法用。2.2 分层存储与召回是记忆系统的两条腿存储层设计我会按“热、温、冷”三层去拆。热的走缓存比如用户当前会话、最近的交互状态用 Redis 这类高速存储温的走检索也就是需要语义匹配的长期记忆放向量库按用户维度做隔离冷的是归档数据比如超过三个月的原始对话丢对象存储或者数仓主要给数据分析和模型训练用平时不参与召回。召回层的核心是“怎么从记忆里找对东西”。我早期犯的错误是只做向量 TopK查什么就捞相关的后来发现完全不够。真正跑得通的做法是融合多种召回信号向量相似度语义上最接近的记忆。适合偏好类、画像类信息。时间衰减越近的记忆权重越高。适合事件类、动态信息。访问频次用户反复提到或 Agent 反复用到的记忆可信度更高。类型过滤根据当前场景过滤记忆类型。比如用户在问天气就优先召回“城市”类记忆而不是“美食偏好”。这几路召回各自打分最后加权融合。为什么要这么做因为单一路召回在真实场景里都不够稳。纯向量会把相似但无关的历史对话捞进来纯时间衰减会漏掉一年前说过的关键约束纯频次会被高频废话带偏。组合起来才能在候选集里挑出真正有用的记忆。召回结果最终要注入到 prompt。这里有个经验值记忆区块占用的 token 尽量控制在总上下文的 15% 以内超出会让模型分不清主次。可以把最相关的几条放前面再按重要程度排序而不是把所有召回结果都塞进去。3. 三步搭建一套可用的Agent记忆层聊完设计落代码。我没有用什么特殊框架就用 Python 写一套最基础的记忆层让没接触过的读者也能照着自己撸一遍。核心就三步记忆入库、记忆召回、记忆更新。3.1 记忆入库把对话变成可检索的记录先定义记忆单元。一个记忆单元是一段最小的有语义的信息比如“用户住在杭州”“用户喜欢耶加雪菲”。设计上我习惯保留这几类字段内容、向量、类型、重要度、时间戳、访问次数。from dataclasses import dataclass from datetime import datetime from uuid import uuid4 dataclass class MemoryUnit: memory_id: str user_id: str content: str embedding: list memory_type: str # profile / constraint / event / task_result importance: float # 0~1 created_at: str last_accessed_at: str access_count: int def create_memory(user_id, content, memory_type, importance): vec get_embedding(content) # 调用 embedding 模型 now datetime.utcnow().isoformat() return MemoryUnit( memory_idstr(uuid4()), user_iduser_id, contentcontent, embeddingvec, memory_typememory_type, importanceimportance, created_atnow, last_accessed_atnow, access_count0 )embedding 模型这块很多开源模型比如 BGE、M3E 的中文效果都够用不一定非得花钱调大厂的接口。向量库如果只是项目原型用 Chroma 或者 FAISS 就够了用户量大一点再上 Qdrant、Milvus 这类。别一开始就把架构搞重能被业务验证是第一位的。存储接口我给一个极简实现class MemoryStore: def __init__(self): self.data {} # user_id - list[MemoryUnit] def _get_list(self, user_id): if user_id not in self.data: self.data[user_id] [] return self.data[user_id] def save(self, mem: MemoryUnit): self._get_list(mem.user_id).append(mem) def search(self, user_id, query_vec, top_k10): items self._get_list(user_id) scored [] for mem in items: score cosine_similarity(query_vec, mem.embedding) scored.append((score, mem)) scored.sort(keylambda x: x[0], reverseTrue) return [mem for _, mem in scored[:top_k]]这里有个容易被忽略的细节检索一定要按用户隔离。很多人做 Demo 时把所有人的记忆放在一个库里结果用户 A 的问法把用户 B 的记忆召回进来了这在产品里属于严重事故。按 user_id 做集合隔离是最基本的底线。上面的示例代码虽然简陋但隔离这个思想是完整的。那对话里怎么提取记忆不能把对话原文直接写入我通常在会话结束后用 LLM 做一次结构化抽取提炼出画像、约束、事件。给一个 prompt 示例你是一个记忆提取器。下面是一段用户与助手的对话请提取值得长期保存的用户信息。 要求 1. 只提取用户明确表达或可可靠推断的信息 2. 每条输出为内容 | 类型 | 重要度(0~1) 3. 忽略临时性、一次性内容 对话 {conversation}这种抽取逻辑适合放在一轮对话自然结束或达到一定回合后异步执行避免阻塞主流程。3.2 记忆召回把“有用的旧信息”送进prompt召回的职责是从记忆库里选出对当前问题最有帮助的条目。我常用的召回函数会叠加时间衰减和访问频次两个信号def recall(user_id, query, top_k5): qvec get_embedding(query) candidates store.search(user_id, qvec, top_ktop_k * 3) now datetime.utcnow() scored [] for mem in candidates: last datetime.fromisoformat(mem.last_accessed_at) age_days (now - last).days age_factor 1.0 if age_days 7 else max(0.4, 1.0 - age_days / 365) freq_bonus min(1.5, 1.0 mem.access_count * 0.1) score mem.importance * age_factor * freq_bonus scored.append((score, mem)) scored.sort(keylambda x: x[0], reverseTrue) return [mem for _, mem in scored[:top_k]]这里三个系数解释一下为什么这么设计。importance 是记忆本身的价值这个在写入时就定好了age_factor 让近期记忆有优势但老记忆不会完全消失freq_bonus 表示被反复用到的记忆值得被优先看到。这三项乘积是我试下来比较可靠的一个组合实际项目里可以继续调参数。召回结果如何注入 prompt我一般用固定的 Memory 区块以下是关于用户的记忆可能有参考价值 - 用户所在城市杭州 - 用户偏好手冲咖啡尤其喜欢耶加雪菲 - 用户最近在准备技术分享会 如果这些记忆与当前问题无关请忽略如果相关请在回答时自然使用。注意措辞要让模型把记忆当“背景信息”而不是“绝对指令”。用户的要求可能已经改变所以 prompt 里要留出口子。另外注入顺序也有讲究记忆放最前面当前对话放最后面模型对最近内容的注意力通常更强这样当前指令优先级更高。3.3 记忆的写入、更新与遗忘机制很多记忆系统做出来是“只增不删”时间一长库越来越臃肿召回结果也随之漂移。我的经验是记忆必须有一整套生命周期管理。写入时做去重。新记忆入库前先拿它和已有记忆做一次相似度检索如果跟某条相似度高得离谱说明已经有内容了这时候不是新增而是考虑要不要更新新的信息更完整就覆盖旧条目新信息只是重复就跳过。这个逻辑能省掉大量垃圾数据。更新时给属性做版本化。拿用户偏好举例用户以前说不吃辣最近说开始吃微辣了。这两条记忆如果同时存在召回时模型会懵。我引入一个“当前有效”标记存历史版本但召回时只把最新版本交给模型。遗忘不是删除而是降级。连续 90 天没被访问、重要度又低于阈值的记忆从向量库挪到冷存储不再参与常规召回用户明确要求删掉的才是真正物理删除。这样既控制检索范围又保留可追溯能力。4. 实战中踩过的记忆坑与排查技巧记忆系统跑起来容易跑得好很难。下面这四类问题是我在真实项目里反复遇到的每个都配了排查思路希望能帮读者跳过这些坑。4.1 召回不准相似度阈值不是拍脑袋定的我最早做记忆召回把向量检索的相似度阈值定在 0.7也不管有没有依据结果召回结果偶尔离谱。后来有一次排查召回错误我打印了一批检索结果发现大量记忆和查询的相似度集中在 0.6 到 0.7 之间0.7 的阈值直接把这些全挡掉了调到 0.6 又有一堆不相关的内容混进来。阈值真的不能拍脑袋定至少要用自己的数据分布去校准。做法很简单收集几百条真实查询和对应的关联记忆算一遍相似度分布然后找一个能把“相关”和“不相关”拉开差距的分数区间作为阈值。如果分布重叠严重不要强调阈值而是先优化 embedding 模型或改写查询语句。另外TopK 不是一个固定值召回条数和阈值可以联动先过滤阈值再按 TopK 截断。4.2 记忆污染别让不相关的旧记忆干扰新对话记忆污染是比召回不准更隐蔽的问题。检索到的记忆可能在语义上相关但已经过期或者不符合当前场景。最典型的例子用户几个月前说“我在减肥不要推荐高热量食物”最近开始增肌饮食需求已经变了。旧的“减肥”记忆还在库里每次推荐食谱都被它带偏。排查方法在本地测试时把每次 prompt 中注入的记忆和模型回答一起 dump 出来逐条检查是哪条记忆影响了回答。修复手段三个第一写入时给记忆打上有效期第二召回时对“明确约束”类记忆做冲突检测发现新旧描述矛盾就覆盖旧的第三重要的偏好变更要在对话中主动向用户确认。前两条技术上都不难第三条属于体验设计但效果最好。4.3 重复记忆与信息膨胀如何做去重和压缩对话一多重复记忆几乎是必然的。用户可能每周都会说一次“我喜欢喝浅烘焙的咖啡”没有去重逻辑的话库里会堆出几十条高度相似的记录。这个问题的直接后果是召回时相同内容的记忆占据多个位置反而把真正新的信息挤掉。我的做法是双重去重内容级的写入前做一次相似度比对相似度超过 0.92 就认为重复走更新而非新增语义级的定期让 LLM 对同类型、同属性的记忆做一次聚类和合并把“喜欢浅烘焙”“偏好水洗豆”“常去某家咖啡店”合并成一条更完整的画像。另外对话历史超过一定长度时我会把早期对话交给 LLM 做摘要然后删掉原始详情只留结构化记忆。存储不是无限的token 更不是压缩是刚需。4.4 隐私与数据边界用户记忆不是用来当谈资的记忆系统存储的都是用户真实信息隐私这条线做产品的人必须提前想清楚。我的原则是三条最小化收集、用户可管理、数据严格隔离。最小化收集是只存对完成任务有帮助的信息不为了“以后可能有用”去偷录用户对话用户可管理是每个用户可以查看、修改、删除自己的记忆这个接口必须存在数据严格隔离指的是不同用户的数据不能互相访问团队内部也一样日志脱敏记忆数据不做无授权分析。技术上记忆库里不要存明文敏感信息比如身份证号、银行卡这类能脱敏就脱敏能哈希就哈希。很多团队直到产品出事才想起隐私问题等到那一步修复代价就是指数级上升。我见过不止一个项目因为这点翻车还是提前设计比较现实。5. 最后说点掏心窝的话把 Agent 的记忆做顺之后我真的觉得记忆不是一个技术问题更多是一个产品问题。前面聊的工具、模型、框架市面上都有成熟方案难的是你想清楚要记住什么、忘掉什么、以什么样的方式让用户感觉到“你懂我”。给记忆留出优先级给用户留下控制权这两件事做好Agent 的自然度会明显上一个台阶。我个人的建议是不管你现在做的是个人助手、客服机器人还是知识库问答尽早把记忆层纳入架构图。一个好的记忆系统不是上线以后打补丁能补出来的它在设计期就决定了这个 Agent 是工具还是伙伴。如果这系列还有下一篇我想聊聊 Agent 的技能规划也就是怎么把记忆和工具调用结合起来。先到这里有新的坑我会继续回来记录。
RELATED READING

延伸阅读

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