1. 为什么 Agent 需要一套像人脑一样的记忆系统1.1 从“金鱼脑”到“有记性”的转折点我最早做 Agent 项目的时候踩过一个特别典型的坑用户上一轮刚说了“我住在杭州帮我查下明天适合跑步吗”下一轮问“那后天呢”Agent 直接反问“请问您在哪个城市”。那一刻我意识到一个没有记忆的 Agent本质上就是个高级一点的命令行工具根本谈不上“智能体”。后来我花了大概两个月时间把记忆和知识库这两块从零搭起来中间试过纯向量库、试过图数据库、也试过混合方案最后沉淀出一套相对稳定的架构。这一章就是把这套东西完整拆开讲包括短期记忆、长期记忆、永久记忆三层怎么划分RAG 知识库怎么和记忆体系配合以及实际落地时那些文档里不会写的坑。先说清楚这套东西适合谁看如果你已经在用 LangChain、AgentScope、Dify 这类框架做过 Agent但发现它“聊三句就失忆”“知识库召回全是废话”那这篇就是给你写的。如果你还没入门建议先把 Agent 的基础循环感知-决策-执行跑通再来看不然会有点吃力。核心要解决的问题就三个记住当前对话、记住跨会话的用户偏好、记住外部专业知识。这三件事对应三种不同的存储和检索策略混在一起做必然翻车。1.2 三层记忆模型的设计动机为什么一定要分三层我一开始也想偷懒全部塞进一个向量库结果发现两个致命问题一是短期对话上下文被长期记忆污染检索出来的全是几个月前的闲聊二是永久知识比如产品手册和用户个性化记忆混在一起权重根本没法调。所以后来我参考了认知科学里人类记忆的分类方式做了这样的划分记忆层级对应人类记忆存储介质生命周期典型内容短期记忆工作记忆内存/Redis单次会话当前对话轮次、临时变量长期记忆情景记忆向量库关系库跨会话持久用户偏好、历史任务永久记忆语义记忆向量库/图库永久产品知识、领域文档这个划分不是拍脑袋来的。短期记忆用 Redis 是因为它读写快、支持 TTL 自动过期一个会话结束就清掉不占资源。长期记忆用向量库是因为它需要语义检索用户问“上次那个方案”你得能通过语义找到。永久记忆则更偏向静态知识更新频率低但要求召回准确率高。提示三层不是必须的小项目可以只做短期永久两层。但只要你涉及“用户个性化”长期记忆这层就绕不开。1.3 记忆与知识库的边界在哪里很多人会把记忆和知识库混为一谈觉得都是“存东西然后查出来”。实际上它们的写入方、读取方、更新频率完全不同。知识库的写入方是你开发者或运营内容是相对静态的领域文档更新周期可能是天级甚至周级。记忆的写入方是Agent 自己在对话过程中自动抽取内容是动态的、个性化的更新周期是秒级。这个区别决定了它们的架构选择知识库可以用离线流水线比如 Dify 的知识库流水线做分块、嵌入、索引追求召回质量记忆必须在线实时写入追求低延迟所以通常用轻量级的抽取异步落库。我在实际项目里是把两者放在同一个向量库实例里但用不同的 collection 或 namespace 隔离检索时分别召回再融合。这样既省资源又不会互相污染。2. 短期记忆的实现让 Agent 记住当前这轮对话2.1 滑动窗口不是万能药短期记忆最朴素的做法就是把最近 N 轮对话拼进 prompt。我一开始用 N10后来发现两个问题一是 token 消耗爆炸二是早期的重要信息被挤出去了。比如用户第一轮说“我对花生过敏”聊到第 15 轮问“推荐个零食”如果窗口只有 10 轮这条关键信息早就没了。所以我后来改成滑动窗口 关键信息摘要的混合模式。具体做法是保留最近 5 轮完整对话同时对更早的对话做一次摘要压缩把摘要作为 system 的一部分注入。摘要的触发条件是 token 超过阈值我设的是 2000 token用一个小模型比如本地部署的 7B 模型异步生成不阻塞主流程。def build_short_term_context(session_id, max_recent5): recent redis.lrange(fsession:{session_id}:turns, -max_recent, -1) summary redis.get(fsession:{session_id}:summary) or context [] if summary: context.append({role: system, content: f早期对话摘要{summary}}) context.extend([json.loads(t) for t in recent]) return context这个方案实测下来token 消耗比纯窗口降低了约 40%关键信息保留率明显提升。2.2 会话状态的持久化与恢复短期记忆虽然叫“短期”但不代表可以只放内存。用户刷新页面、切换设备会话不能丢。我的做法是 Redis 做主存储同时异步落一份到关系库MySQL 或 PostgreSQL用于故障恢复和数据分析。Redis 的 key 设计我踩过坑一开始用session:{id}存整个列表结果并发写入时经常覆盖。后来改成用 List 结构每次RPUSH追加读取时LRANGE天然支持并发。TTL 设 24 小时超过就自动清理。恢复逻辑是这样的如果 Redis 里没有就从关系库按 session_id 查最近的记录重新灌回 Redis。这个降级路径一定要有不然 Redis 一重启所有进行中的会话全断。注意会话 ID 的生成要用 UUID 而不是自增 ID避免被猜测导致会话劫持。这个安全细节很多人会忽略。2.3 上下文压缩的几种实用策略除了摘要还有几种压缩策略我试过各有适用场景实体抽取式把对话里的关键实体人名、地点、时间、偏好抽出来存成结构化字段注入时只放这些字段。适合任务型 Agent比如订票、点餐。向量召回式把历史对话全部嵌入每轮根据当前 query 召回最相关的几轮。适合长对话但延迟会高一些。分层摘要式每 5 轮做一次小摘要每 25 轮做一次大摘要形成树状结构。适合超长会话实现复杂度最高。我现在的默认方案是“实体抽取 滑动窗口”因为大部分业务场景下用户关心的就是那几个关键信息没必要把整段对话都带上。3. 长期记忆跨会话记住用户是谁3.1 记忆抽取的时机与粒度长期记忆的核心问题是什么时候写、写什么、怎么写。我的经验是不要每轮都写那样噪音太大。触发写入的时机有三个会话结束时做一次全量抽取检测到用户表达了明确偏好“我喜欢”“我不要”“记住”时实时抽取每隔 N 轮我设的是 10 轮做一次增量抽取抽取的粒度要控制好。太细会碎片化太粗会丢失信息。我一般抽成这样的结构{ user_id: u_123, memory_type: preference, content: 用户对花生过敏, confidence: 0.95, source_session: s_456, created_at: 2025-01-15T10:30:00Z }confidence这个字段很关键它让后续检索可以做加权。比如用户随口说的一句“可能吧”confidence 就低检索时权重也低。3.2 向量库选型我为什么最后选了混合方案长期记忆的存储选型我折腾了很久试过纯 FAISS、纯 Milvus、也试过 pgvector最后落地的是pgvector 关系表的混合方案。原因很实际长期记忆不只是语义检索还需要按 user_id、时间范围、memory_type 做过滤。纯向量库做这些过滤要么不支持要么性能差。pgvector 直接在 PostgreSQL 里SQL 和向量检索可以一起写运维也简单不用额外维护一套向量数据库。SELECT content, 1 - (embedding %s) AS score FROM user_memories WHERE user_id %s AND memory_type preference AND created_at NOW() - INTERVAL 90 days ORDER BY embedding %s LIMIT 5;这条 SQL 一次搞定过滤排序实测在百万级数据下延迟在 50ms 以内完全够用。如果你的数据量到千万级再考虑上 Milvus 或 Qdrant。3.3 记忆冲突与更新策略长期记忆最麻烦的是冲突。用户三个月前说“我喜欢咖啡”现在说“我戒咖啡了”两条记忆都在库里检索时召回哪条我的处理策略是时间衰减 显式覆盖。时间衰减就是给每条记忆算一个时效分越新的分越高。显式覆盖是当检测到用户明确否定某条旧记忆时把旧记忆标记为deprecated检索时过滤掉。def compute_memory_score(memory, now): age_days (now - memory.created_at).days time_decay math.exp(-age_days / 180) # 半年衰减到 0.37 return memory.confidence * time_decay * memory.recency_boostrecency_boost是当这条记忆在最近被召回并确认有用时给它加的分。这样就形成了一个正向反馈常用的记忆越来越容易被召回不用的自然沉底。实操心得不要物理删除旧记忆标记为 deprecated 就好。用户有时候会反悔物理删了就找不回来了。4. 永久记忆与知识库RAG 的正确打开方式4.1 RAG 和 MCP 的区别别再搞混了这两个概念经常被放在一起问我简单说下我的理解。RAG 是一种架构模式解决的是“如何让模型用上外部知识”的问题核心是检索生成。MCP 是一种协议标准解决的是“模型如何标准化地调用外部工具和数据源”的问题。打个比方RAG 像是你给模型配了个图书馆管理员你问问题管理员去书架找资料给你MCP 像是给模型配了个万能遥控器它能按标准协议去操作各种设备。两者不冲突可以一起用——用 MCP 去连知识库服务知识库内部用 RAG 做检索。我现在的架构就是知识库服务对外暴露 MCP 接口Agent 通过 MCP 调用服务内部走 RAG 流水线。这样 Agent 侧不用关心知识库怎么实现换知识库也不用改 Agent 代码。4.2 知识库分块90% 的召回问题出在这里知识库效果差十有八九是分块没做好。我见过太多人直接把整篇文档丢进去嵌入然后抱怨召回不准。分块的核心原则是每一块要能独立表达一个完整语义。我的分块策略是这样的优先按文档结构分标题、段落、列表项单块 token 控制在 300-500 之间块之间保留 10-15% 的重叠避免语义被切断表格和代码块单独成块不拆分def smart_chunk(text, max_tokens400, overlap50): # 先按标题切 sections split_by_headers(text) chunks [] for sec in sections: if count_tokens(sec) max_tokens: chunks.append(sec) else: # 超长段落按句子边界切 chunks.extend(split_by_sentence(sec, max_tokens, overlap)) return chunks这个策略比固定长度切分召回率提升了大概 25%尤其是在技术文档上效果明显。4.3 混合检索向量 关键词才是王道纯向量检索有个天然缺陷对精确匹配不敏感。用户问“ERR_4032 怎么解决”向量检索可能召回一堆“错误处理”的泛泛内容就是找不到那个具体错误码。所以我现在一律用混合检索向量检索召回语义相关的BM25 召回关键词匹配的然后用 RRFReciprocal Rank Fusion融合排序。def hybrid_search(query, top_k5): vec_results vector_search(query, top_k20) bm25_results bm25_search(query, top_k20) # RRF 融合 scores {} for rank, doc in enumerate(vec_results): scores[doc.id] scores.get(doc.id, 0) 1 / (60 rank) for rank, doc in enumerate(bm25_results): scores[doc.id] scores.get(doc.id, 0) 1 / (60 rank) return sorted(scores.items(), keylambda x: -x[1])[:top_k]那个 60 是 RRF 的标准常数不用改。实测下来混合检索在技术问答场景的召回准确率比纯向量高了 30% 以上。4.4 重排序最后一道质量关卡召回之后一定要做重排序Rerank。召回阶段追求的是“不漏”重排序追求的是“精准”。我用的是 BGE-reranker本地部署延迟大概 100ms但效果提升非常明显。流程是混合检索召回 20 条 → reranker 打分 → 取 top 5 注入 prompt。这 5 条的质量比直接取向量 top 5 高出一大截。注意reranker 模型要和嵌入模型匹配。用 BGE 的嵌入就用 BGE 的 reranker混用效果会打折。5. 记忆与知识库的融合Agentic RAG 的落地5.1 什么时候该查记忆什么时候该查知识库Agent 每收到一个 query都要决定去哪个库查。我的路由逻辑是这样的涉及“我”“我的”“上次”等个性化词 → 优先查长期记忆涉及产品、文档、专业术语 → 优先查知识库两者都涉及 → 并行查融合结果这个路由可以用规则做也可以用小模型做意图分类。我一开始用规则后来发现边界情况太多就训了个小分类器准确率能到 92%。5.2 上下文组装别把 prompt 塞爆召回了记忆和知识怎么组装进 prompt 也有讲究。我的原则是记忆在前知识在后都带来源标注。[用户记忆] - 用户对花生过敏来源2025-01-10 会话 - 用户偏好简洁回答来源2025-01-12 会话 [知识库] - 花生过敏的常见替代零食包括...来源产品手册 v2.3带来源标注有两个好处一是模型知道哪些是用户个性化信息哪些是通用知识二是出问题时你能追溯是哪条记忆或文档导致的。5.3 记忆的自我进化让 Agent 越用越聪明最后说个进阶玩法让 Agent 根据对话反馈自动更新记忆。比如用户说“这个回答不对”Agent 就把这次召回的记忆标记为低质量下次降低权重。这个机制我用了一个简单的反馈循环每次会话结束用一个小模型评估“本次召回的记忆对回答是否有帮助”有帮助的加分没帮助的减分。跑一段时间后高质量记忆自然浮上来噪音沉下去。def update_memory_feedback(session_id, memory_ids, helpful): delta 0.1 if helpful else -0.15 for mid in memory_ids: db.execute( UPDATE user_memories SET recency_boost recency_boost %s WHERE id %s, (delta, mid) )减分比加分幅度大是为了让噪音更快沉底。这个参数可以根据业务调我实测 0.1/-0.15 比较平衡。6. 实战中踩过的坑与排查清单6.1 记忆召回不准的五个常见原因现象可能原因排查方法召回无关记忆嵌入模型不匹配领域换领域微调的嵌入模型该召回的没召回分块太大或太小检查块 token 分布新旧记忆冲突缺少时间衰减加时间衰减因子召回延迟高向量库索引没建好检查 HNSW 参数记忆重复写入抽取触发太频繁加去重逻辑6.2 知识库更新的坑知识库更新最怕的是“更新了文档但索引没更新”。我的做法是给每个文档算一个内容哈希更新时对比哈希变了才重新嵌入。这样既省算力又不会漏更新。另外删除文档时一定要同步删向量。我见过有人只删了原文向量还在库里结果召回出一堆“幽灵内容”排查了半天。6.3 性能优化的几个实用技巧嵌入计算用批处理一次算 32 条比一条条算快 5 倍以上向量检索的 HNSW 参数ef_search调到 64-128 之间再高收益递减记忆写入用异步队列不要阻塞主对话流程热点用户的记忆可以缓存在 Redis减少向量库压力7. 我个人的一些经验体会这套记忆知识库的架构我从第一版到现在大概迭代了七八次。最大的体会是不要追求一步到位先跑通再优化。我第一版就是简单的滑动窗口一个向量库能跑起来之后才逐步加长期记忆、加混合检索、加重排序。另一个体会是评估比实现更重要。没有评估集你根本不知道改动是变好还是变坏。我后来专门建了一个 200 条的测试集每次改动都跑一遍召回率、准确率、延迟三个指标一起看。这个习惯帮我避免了好几次“以为优化了实际退步”的情况。最后分享一个小技巧记忆和知识库的日志一定要打全包括召回了什么、分数多少、最终用了哪几条。出问题时这些日志就是你的救命稻草没有日志的排查基本靠猜。