这两年做智能体类应用,我感触最深的一点是:模型能力决定一个 Agent 的“起跑速度”,记忆体系才决定它能“跑多远”。很多团队把上下文窗口塞满、把知识库挂上,就觉得“有记忆了”,但真正用起来会发现,昨天用户刚纠正过的偏好,今天对话里又被模型“忘记”了;知识库里明明有最新文档,Agent 却老引用旧版本。问题的根源,不是模型记性差,而是记忆体系没有做分层,写入和更新完全靠运气。
我花了大半个周期在模拟项目X上重构智能体的记忆模块,核心思路就一句话:把“对话现场”“外部资料”“个人经验”这三类东西分清楚,分别用短期记忆、知识库、长期记忆三种机制来管,再设计一套“什么值得写、怎么写、怎么更新”的规则。这篇文章把我拆这个体系的过程、踩过的坑、以及最终落地的参数和代码流程都整理出来,给同样在做 Agent 记忆层的朋友做一个可以直接参考的底稿。
1. 为什么要把记忆拆成三层
1.1 短期记忆:只存对话现场
短期记忆在工程上最直观的形态,就是会话窗口里的消息列表。它对应一次完整对话过程中的“上下文现场”,包括用户最近说了什么、你上一步回复了什么、目前进行到哪个环节。
这个层级的核心特征是时间短、信息密、不持久。它存在的意义是让模型在一个连贯的“工作台”上完成当前的推理和回复,而不是把所有历史都翻出来重放。很多刚开始做 Agent 的人最容易犯的错,就是把所有历史消息一股脑塞给模型,美其名曰“全量上下文”。结果 token 消耗暴涨,响应延迟拉高,而且模型在长上下文里对早期信息的注意力权重反而会下降——你给了它一千条历史,它记住的还是最后那五十条。
我建议的短期记忆容量控制方式是:为当前会话定义一个“上下文预算”,一般给到最近 10-20 轮对话,超出部分要么滚动丢弃,要么压缩成摘要存到更低的层级。这里的核心原则是,短期记忆只负责“当下”,不负责“过去”。
1.2 知识库:可更新的外部资料,不需要“记住”
知识库解决的是另一类问题:Agent 需要回答的事实型问题,来自产品手册、制度文档、行业规范、FAQ 等静态或半静态的参考资料。这类内容和用户的对话无关,和用户是谁也无关,它属于“组织资产”而不是“个人记忆”。
知识库的本质是外部存储,Agent 对它做的是“检索”而不是“记忆”。它不参与模型对用户历史的分析,也不会因为某次对话而改变。正因如此,知识库非常适合用向量检索 + 关键词检索的方式来组织:文档切片、向量化、建立索引,用户提问时先检索再交给模型生成答案。
这套做法成熟、稳定,业界已经验证了很久。它的问题是,知识库本身是“冷冰冰的资料”,它不知道用户上次问过什么、用户在某个问题上的真实偏好是什么。如果产品只靠知识库回答,体验就会像一台“查资料的搜索引擎”,能答对,但显得没有温度。这正好说明:知识库之上,还需要一层真正个性化的东西。
1.3 长期记忆:带积累效应的个人化经验
长期记忆才是我说的“智能体记性变好”的关键。它存的是从一段段对话里沉淀出来的、关于用户的重要信息:用户的工作背景、兴趣偏好、明确的目标、纠正过你的话、反复出现的需求。
和知识库最大的区别在于,长期记忆是“长”出来的,不是“放”进去的。知识库由管理员主动整理,长期记忆则是 Agent 在一次次的对话中自己判断、自己提炼、自己更新的产物。它是一条一条的“用户画像片段”,比如:
- 用户偏好 Python 技术栈,且在数据可视化方向有较深经验
- 用户每周五下午会需要一份当周项目进度汇总
- 用户明确表示“以后推荐餐厅避开辣味菜品”
这类信息有一个共性:它们在单次对话里可能只占很小篇幅,但跨会话看非常有价值。如果每次对话都靠模型临时从历史里翻,不仅慢,而且容易漏。更致命的是,如果没有一个专门的存储和管理机制,这些信息会随着会话过期而彻底丢失。
1.4 三层分工的边界:各管各的,别串门
把三层放在一起看,它们的边界非常清晰:
| 层级 | 存储形态 | 更新频率 | 查询方式 | 信任级别 |
|---|---|---|---|---|
| 短期记忆 | 会话消息窗口 | 实时追加与滚动 | 直接拼接进上下文 | 当前会话内可信 |
| 知识库 | 切片向量+全文索引 | 低频,人工或定时 | 检索匹配 | 组织事实,可信但不含用户个性 |
| 长期记忆 | 结构化记忆条目(支持向量与属性筛选) | 每次对话后增量更新 | 语义检索+条件查询 | 个性化偏好,需版本管理与冲突处理 |
串门的意思就是混淆这三者的职责。比如,有人把用户对话里的信息直接写进知识库文档,结果知识库被用户个人偏好污染,其他用户提问时也检索到这些内容,回答就“张冠李戴”了。又比如,把知识库里的最新制度直接当作长期记忆写入,结果每个用户都背上了一大堆不该有的“个人画像”,检索噪声极大。
所以,分工的第一要务不是“存什么”,而是“在哪个库里存、用什么机制管、谁有权限改”。
2. 长期记忆的写入策略
2.1 写入时机与触发条件
确定了长期记忆“值得单独建一层”之后,下一个问题就是:什么东西值得从一次对话里被提炼并写进长期记忆?
我的经验是,用“行为触发”代替“内容全量抓取”。不是把每轮对话都生成摘要存下来,那样成本高、噪声大,而是设置具体的触发条件,只有命中条件的内容才进入长期记忆写入流程。我做模拟项目X时用了下面这组触发规则:
- 用户主动表露偏好或禁忌(如“我不吃辣”“我喜欢简洁的回答”)
- 用户明确纠正了 Agent 的错误(如“我说的是 A 项目,不是 B 项目”)
- 用户重复提到某个目标或计划(如第二次说“我下个月要发布一个竞品分析报告”)
- 用户在某个阶段任务结束后确认了成果(如“就用这个方案,按这个方向推进”)
- 关键决策节点(如“我们技术选型定了,长期用 Java”)
为什么必须设触发条件而不是全量记录?因为长期记忆的写入是有成本的——存储成本、检索噪声、还有更新冲突的概率。一次轻松闲聊里的“我最近在看露营装备”未必值得长期记住,但如果用户第三次提到露营,那这就是一个值得沉淀的长期兴趣。触发条件本质上是给记忆写入加了一道“价值判断闸门”。
2.2 从对话到结构化记忆的加工流
触发之后,原始对话片段不能直接入库,需要经过一次加工。我在实践中使用的是“抽取-规范化-校验”三步流程。
第一步抽取:把命中的原始段落交给一个小模型或一条结构化提示词,让它抽出关键字段。我的抽取模板核心字段包括:记忆主体(user)、记忆类型(preference / plan / correction / decision / fact)、记忆内容(一句话描述)、实体对象(如“餐厅”“技术栈”“项目X”)、上下文(原始对话片段)。
第二步规范化:把同一类的记忆统一成一致的表达格式。比如“用户不喜欢吃辣”“别给我推荐辣的”“上次那个辣椒炒肉太辣了”这三句话,在抽取后会统一规范为“用户不接受辣味食物”这种标准语义表达。这一步非常重要,它决定了后续检索和更新的颗粒度。
第三步校验:用一个评分函数判断这条记忆的“确定性”和“重要性”。我给它们各打 0-1 分。确定性低于 0.6 的,标记为“待观察”,不进入活跃记忆库,只进入低置信度暂存区;重要性和确定性都达标的,才正式写入。没做这步的时候,经常出现用户随口一句“可能吧”被当成事实记住的情况,教训很深。
2.3 溯源与去重:保留原始上下文
很多团队做记忆沉淀时只存“提炼后的摘要”,丢了原始上下文。这是个大坑。摘要是被模型二次加工过的,天然存在概括偏差;如果后续发现摘要和用户真实意图不符,没有原始句子做证据,连排查都无从下手。
我的做法是,每条长期记忆都带一个source字段,指向产生这句记忆的原始对话消息 ID,并保留原始句子片段。这样有两个好处:一是当用户说“我没说过这句话”时,能迅速追溯并给出证据;二是后续做记忆更新或冲突处理时,可以重新审视原始语境,而不是对着干巴巴的摘要猜。
去重要做在规范化之后。判断两条记忆是否重复,不能只靠字符串匹配,要结合语义和实体。我的去重规则是:记忆类型相同、主体实体相同、语义相似度超过阈值(我用的是向量相似度 0.85 以上)时,视为重复,走合并流程而不是新增。
2.4 优先级标记与重要性打分
长期记忆虽然叫“长期”,但也不能所有信息平起平坐。我给每条记忆打了一个importance_score(0-1),综合以下维度:
- 用户是否明确强调(用户用了“千万”“一定”“记住了吗”这类词会加分)
- 是否与用户的核心目标相关(目标型记忆分更高)
- 是否会影响后续任务的执行方向(决策类记忆分更高)
- 是否只是临时情境信息(天气、时间等情境类记忆分更低)
重要性分数的真实用途有两个:一是检索时做重排加权的参考,高分记忆更容易被召回;二是更新与清理时的“淘汰候选”,低分记忆在容量不足时优先被归档或删除。我最初没引入这个分数,后来记忆积累到一定规模,发现检索结果里经常冒出“用户曾经在某次聊天中提到某部电影”这类毫无价值的片段,开始打重要性子分数之后,这个问题基本可控。
3. 更新机制与一致性维护
3.1 三种更新方式:新增、合并、失效
长期记忆不是“写一次就永远不变”。同一件事,用户的想法会变,偏好会变,计划会变。如果不处理更新,Agent 就会拿陈旧信息做判断,典型场景是:用户上周说“周末去爬山”,这周改成“周末改成加班了”,如果记忆库里还是“周末爬山”,整个推荐和安排都会跑偏。
我把更新操作分成三种原子动作:
- 新增:完全没见过的新信息,直接插入
- 合并:发现新信息与已有记忆表达同一事实但更具体,则把旧条目的有效信息合并进新内容,保留一条
- 失效:旧信息已被明确推翻,或已被执行完毕、不再具备参考价值,则将旧条目状态标记为
expired,不从物理上删除,但检索时默认不召回
三者必须在写入流程中显式区分,而不是由大模型自由发挥“更新”。早期我用模型端对端做记忆更新,它经常把“用户说过 A 后来又说了 B”这种场景处理成简单覆盖,原始信息丢了,等用户说“我早就说过啊”的时候根本找不到历史。
3.2 冲突处理:版本、置信度、时间戳
新增和合并的最大难点是冲突处理。用户昨天说“我倾向于用 Python”,今天说“我决定改用 Go”,这两条信息语义上矛盾,谁覆盖谁?
我采用了一套轻量级的版本控制方案。每条记忆带这几个元信息字段:
version:递增的版本号,每次更新加 1conflict_with:指向与之冲突的旧条目 IDconfidence:这条记忆的置信度(0-1)created_at和updated_at:时间戳
当一条新记忆与旧记忆冲突时,不直接删除旧的,而是先做一次对比打分:如果新记忆的置信度更高且时间更新,则旧记忆标记为被替换(superseded),新记忆写入;如果两条置信度都比较低,则保留旧的、新的进入“观察区”,等后续对话交叉验证后再决定。这套办法让我在“用户随口一说”和“用户下定决心”之间做了一个缓冲,误覆盖率明显下降。
3.3 遗忘与衰减策略
长期记忆如果只增不减,最后会退化成“另一个知识库”,检索噪声和存储成本都会失控。遗忘机制不是什么高深的东西,本质上是给记忆加“保质期”。
我给每条记忆设置了decay_factor和hit_count两个字段。hit_count表示这条记忆被有效检索并成功辅助回答的次数;decay_factor表示遗忘曲线斜率,不同类型记忆的衰减速度不同:偏好类的衰减慢,决策类的衰减更慢,临时计划类的衰减快。
后台定时任务按以下逻辑扫描:
- 如果
hit_count近 30 天为 0 且importance_score低于 0.3:归档到冷存储 - 如果
hit_count近 90 天为 0 且importance_score低于 0.5:降级为不活跃状态 - 任何超过 180 天未命中且置信度原本就低于 0.7 的记忆:标记待清理
这套衰减策略的核心不是“删数据”,而是“调整数据的活跃等级”,避免把可能还有用的信息一棍子打死。用户三个月没提某件事,不等于以后永远不提,只是检索时不应该排在前面而已。
3.4 知识库与长期记忆的联动
很多体系把知识库和长期记忆完全隔离开,这是我后来发现不够用的地方。典型场景:知识库里更新了产品价格文档,而长期记忆里还存着用户上次问到的“旧价格 + 用户对价格的反馈”。用户再次提到价格时,长期记忆里的旧价格就会干扰新信息的呈现。
我的做法是引入“引用机制”:长期记忆中凡是与知识库内容相关联的条目,都会记录一个linked_doc_id。当知识库文档更新时,触发一条联动事件:将长期记忆里关联条目的status标记为needs_review,下次对话中如果这个条目被召回,系统会提示模型先重新检索知识库最新内容,再做综合回答。机制不复杂,但确实解决了“旧记忆污染新事实”的类问题。
4. 检索路由与上下文组装
4.1 路由规则:什么时候用哪层
记忆体系建好了,Agent 生成回复时怎么决定调用哪层?我对模拟项目X里的路由策略做了一个“先判断意图、再选择来源”的规则:
- 如果用户问的是事实型问题(产品参数、文档规定、流程要求),优先走知识库检索
- 如果用户表达的是个性化偏好或规划类需求(“帮我推荐”“我打算”“我更喜欢”),优先走长期记忆检索
- 如果当前问题涉及多轮协作和上一轮的对话状态(“然后呢”“刚才那个方案继续说”),直接走短期记忆上下文
- 混合型问题(“按我的习惯帮我安排下周计划”)先并行检索长期记忆和知识库,再做结果合并
这里的关键是路由必须是一个显式决策,而不是模型临场猜。我在工程上把路由做成一个轻量分类,命中规则后直接决定检索策略,避免每次请求都让大模型去“想一想应该调什么”。实测这样做响应更稳定,也更容易排查问题。
4.2 混合检索与重排策略
长时记忆检索光靠向量相似度不够。我的做法是“向量检索 + 属性过滤 + 重排”三步。
第一步向量检索找到 top 50 候选。第二步用结构化条件过滤:只保留status=active且与当前用户user_id匹配的记忆。如果用户明确提到某个实体(比如“上次说的那个餐厅”),再叠加实体过滤。第三步重排,把importance_score、recency_bonus(近 30 天有命中的条目加分)和语义相似度按权重综合评分,最终取 top 5 进入上下文。
这个三步流程里,过滤和重排其实决定了记忆质量的 60%。单纯提高向量检索的 topK 不能解决“低质记忆混进来”的问题,必须靠结构化条件和重要性分数去压噪声。我调了大概两周,最后把旧记忆的混入率从 30% 压到了 8% 以内。
4.3 记忆片段的上下文组装方式
检索到的记忆不是直接往 prompt 里一塞就完事。我最终采用的组装格式是把“知识库内容”和“长期记忆内容”分区块展示,并在记忆前标注来源和置信度:
{ "role": "system", "content": "以下是与该用户相关的长期记忆(按重要性降序):", "memory_items": [ { "content": "用户不接受辣味食物", "type": "preference", "importance": 0.92 }, { "content": "用户下月需交付竞品分析报告", "type": "plan", "importance": 0.85 } ] }长期记忆的置信度信息不直接暴漏给用户去看,但会作为 temperature 和回答风格的隐式参考:对于高置信度的决策类记忆,回答语气更笃定;对于低置信度的观察类记忆,回答更倾向于“我理解您可能……,对吗?”的确认式表达。
4.4 Token 预算与去噪
记忆检索最容易被忽略的是 token 预算。长期记忆检索出来的片段如果太多,会挤压短期记忆上下文的空间。我给模拟项目X定的硬性规则是:长期记忆 token 占比不超过总上下文的 20%,知识库片段占比不超过 30%,短期记忆最多占 50%。
超预算时的处理不是硬截断,而是按重要性和相关性分数进行“层次裁剪”:优先丢弃低于重要性阈值的记忆条目;若还不够,则丢弃置信度低的“观察类”信息。这套规则保证了上下文结构稳定,模型每次都能在“最新对话 + 关键记忆 + 相关资料”的完整输入下工作,而不是被一堆过期信息灌晕。
5. 实操案例:用户偏好记忆模块的完整落地
5.1 场景与数据模型设计
拿一个我做过的实际模块举例:给一个内部助手加“用户偏好记忆”。要求是从多轮对话里自动记住用户的偏好(工作习惯、内容风格、常见任务),并在后续对话里复用。
表结构设计如下:
CREATE TABLE memory_items ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, type TEXT NOT NULL, -- preference / plan / correction / decision / fact content TEXT NOT NULL, entities JSONB, source_message TEXT, importance_score REAL DEFAULT 0.5, confidence REAL DEFAULT 0.7, status TEXT DEFAULT 'active', -- active / observed / superseded / expired version INTEGER DEFAULT 1, conflict_with TEXT, hit_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now() );向量索引单独存在向量库的 collection 里,memory_items.id与向量库的 payloadid一一对应,每次更新记忆内容时同时更新向量。之所以用两张“存储”而不是全部塞进向量库,是因为结构化筛选(按 type、user_id、status 过滤)在关系型数据库里更可靠,且在故障排查时可以快速用 SQL 看数据。
5.2 核心写入与更新代码流程
写入流程的核心代码如下,逻辑是:先尝试找冲突条目,再做新增或更新决策:
def write_memory(user_id, candidate, vector_store, db): # 1. 合法性校验 if candidate.confidence < 0.6: candidate.status = "observed" db.insert(candidate) return # 2. 去重与冲突发现:找同一用户、同类型、同实体的活跃记忆 duplicate = find_duplicate(db, user_id, candidate) if duplicate: merge_memory(db, vector_store, duplicate, candidate) return conflict = find_conflict(db, user_id, candidate) if conflict: resolve_conflict(db, vector_store, conflict, candidate) return # 3. 纯新增 candidate.version = 1 db.insert(candidate) vector_store.upsert(candidate.id, embed(candidate.content))resolve_conflict的决策逻辑:新记忆置信度高出 0.15 以上且时间更新,旧记忆标记为 superseded,新记忆写入;否则新记忆进入 observed 状态等人工确认。这套代码上线后,冲突误覆盖的问题从每周十几起降到了几乎没有。
5.3 检索链路实现
检索时会同时请求向量库和 SQL 结构化查询,然后合并排序:
def recall_memories(user_id, query, top_k=5): # 1. 向量候选 vec_candidates = vector_store.search(query, top_k=50) # 2. 结构化过滤:只留 active 且属于当前用户 filtered = db.filter(vec_candidates.ids, user_id=user_id, status="active") # 3. 重排打分 scored = [ { **item, "final_score": 0.6 * item["vector_score"] + 0.3 * item["importance_score"] + 0.1 * recency_bonus(item) } for item in filtered ] return sorted(scored, key=lambda x: -x["final_score"])[:top_k]这个实现里我犯过的一个低级错误是忘了过滤user_id,结果测试时用户 A 的助手把用户 B 的“偏好”用在了回答里,出现了严重的串台事故。自那以后,我强制要求:任何检索入口都必须先带用户维度过滤,再走语义检索。
5.4 效果评估与调优方向
模块上线后,我重点盯三个指标:
- 记忆写入准确率:抽检 200 条新写入的记忆,人工判断是否准确表达了用户真实意图。达标线 90%,我最终跑在 93% 左右
- 记忆命中率:用户发出偏好相关需求时,系统是否召回正确的记忆条目。达标线 80%,线上约 85%
- 误覆盖率:旧记忆被新记忆错误覆盖的比例。要求低于 1%,最终在 0.4%
另外我留了一个“记忆审计模式”,内部人员可以在后台按用户查看所有长期记忆条目,并手动修正或删除错误记忆。这个兜底手段在早期特别有用,用户不会直接感知到“记忆坏了”,但内部排查快了非常多。
6. 常见问题与排查技巧实录
6.1 记忆串台:把A用户的信息错记到B用户
最典型的症状:切换用户后,Agent 回答里出现上一个用户的偏好。排查时先查两件事。第一,SQL 是否在检索时带上了user_id条件;第二,向量库 collection 是否按用户做了隔离。我的经验是,小型场景直接用“统一 collection + payload 里带 user_id 过滤”,数据量大后再拆按租户分 collection。千万不要在业务代码里依赖“调用前清空上下文”这种方式来防串台,那不是防串台,是赌运气。
6.2 更新丢失:写入成功但检索不到
有一种很难查的 bug:记忆条目明明在库里,但检索时就是不来。我排查后定位到两个常见原因:
- 向量库的 embedding 没有和 SQL 条目同步更新,导致旧向量还在索引里,但新文本已经改了内容
status字段被默认初始值污染,比如某条记忆的 ranking 分数严重偏低,被 topN 截断了
解决办法很直接:写入和更新必须走同一个“写穿”函数,无论 SQL 与向量库哪个失败都要回滚或重试;检索 SQL 时排除attracted的过旧条目,并定期校准重排权重。我还加了一条审计日志,每次写入都会记录“实际入库的向量维度”和“SQL 条目数”,两边的数量对不上,日志里一眼就能看到。
6.3 检索结果过载或缺失
过载一般是阈值或者裁剪没做;缺失则可能是查询语句和记忆表达方式差异太大。我用的方式是“双路召回”:一路向量,一路关键词/属性过滤,两边结果合并后统一重排。缺失时看是不是知识库和记忆 schema 里用了不同的实体叫法,导致同实体在两个库里的 embedding 差别大。这种情况只调向量模型意义不大,要做实体归一化,比如建立“用户说法的别名映射表”,“餐厅”映射restaurant和“馆子”。
6.4 成本与延迟:记忆变大之后
记忆从几千条涨到几十万条后,你会发现每次请求都做 top50 的向量召回,延迟和成本都不可忽略。优化顺序建议是:
- 先做用户级分片,让每次检索只在当前用户的记忆集合里跑,而不是全部向量池
- 再做条目级降维,把
importance_score长期垫底且status=observed的条目移到冷存储 - 最后才是换更强的向量检索策略
成本控制上不要过度依赖“减少召回”,更好的方式是把低置信度的记忆写入频率降下来。写得多不如写得准,每次入库哪怕多花一次模型调用做校验,整体成本也是省的——因为错误的记忆会让后续每轮对话都出现偏差点和纠错开销。
最后分享一点个人体会
做了这么多轮迭代之后,我最大的感受是:记忆体系的设计本质上是在做“取舍”——写什么,不写什么;什么时候信,什么时候怀疑;哪些记忆要长期留着,哪些该让它慢慢退场。这三个问题想清楚了,Agent 的表现会有质的提升。还有很多团队总想着“把记忆功能做全”,我反而建议先做“少而准”的写入规则,配上完整的更新与过期机制,跑通一条从写入到检索、再到遗忘的完整链路,再去考虑增加更复杂的记忆类型。
一个小技巧收尾:长期记忆模块上线后,记得给每条记忆条目的来源和生命状态加完完整整的日志字段。最初多花半天,后期排查问题能省下好几天。我吃过没留追溯信息的亏,后来几乎所有记忆模块都强制要求来源可查、状态可追溯。这些看起来不性感的工程细节,才是记忆体系真正能长期稳定跑下去的基础。