
做了这么多年AI应用我一直觉得“记性”是Agent从玩具变成工具的分水岭。你跟它聊了十几轮它转头就忘你昨天明确告诉过它的偏好今天又问一遍甚至同一个会话里刚说完的话它都能答非所问。这篇是Agent系列第三篇专门聊记忆。坦白说让Agent记住你不是把聊天记录全塞进Prompt里那么简单它背后涉及短期记忆、长期记忆、检索策略、存储选型、遗忘机制一整条链路。这篇文章我会把这套链路拆开讲透从设计思路到选型对比再到能直接抄的落地代码和踩坑记录尽量让不同基础的开发者看完都能动手给自家Agent装上“记忆”。1. 先想清楚Agent为什么需要记忆记忆到底长什么样1.1 上下文窗口不是记忆很多人刚接触Agent时会把上下文窗口和记忆划等号。比如GPT-4级别的模型有几十万token上下文就觉得“这不就是记忆吗多塞点历史记录就行了”。实际一跑就露馅。上下文窗口本质是“可读取的临时工作区”它的容量再大也有三个硬伤。第一是成本每轮请求把全部历史拼进去token消耗随对话轮次线性增长用户聊20轮你就要付20轮的账单第二是干扰历史记录一长模型注意力会被无关信息稀释经常出现“上一句还记得三句话之前的内容开始胡编”第三是失效进程一重启、会话一切换上下文窗口里的东西全部清零它没法跨会话存活。记忆的正确理解应该是能够跨会话、跨场景、在需要的时候被主动唤起的持久化信息。它不要求你记得全部只要求你在对的时间想起对的事情。这个概念搞清楚了后面所有设计才不会跑偏。1.2 把记忆拆成三层工作记忆、情景记忆、语义记忆我在实际项目中习惯把Agent记忆分成三个层级对应的存储和策略完全不同。第一层是工作记忆对应当前会话内的即时信息。比如用户正在跟你讨论一个需求中间提到的临时约束“这个模块不要用第三方库”这类信息只在本轮对话内有效不需要跨会话保留。它的实现最简单直接放在会话状态里配合消息修剪和摘要压缩来控制长度。第二层是情景记忆对应你和某个具体用户之间的历史交互事实。比如用户上次跟你说过“我在用Java 17内部部署、不用上云”或者“周一不方便开会”这些属于个人化事实下一次对话应该能自动回忆起来。情景记忆的典型特征是“跟人绑定、跟时间相关”需要持久化存储并且要支持按用户维度检索。第三层是语义记忆对应不需要绑定具体用户的世界知识、领域规则、工具说明。比如“这个系统的下单接口有过期时间”、“工单状态流转有五个阶段”这些内容可以来自知识库、API文档或者人工配置属于Agent长期要依赖的“背景知识”。理解这个分层很关键因为很多团队做记忆系统失败就是把三层混为一谈用一个向量库全装进去最后检索出来的东西既不是用户事实也不是领域知识四不像。记忆分层之后每一层用什么样的写入逻辑、什么样的召回时机、什么样的更新策略才能逐一理清。2. 记忆系统搭建前的关键选型存储、检索与记忆编排2.1 存储方案选型从内存队列到向量数据库聊完分层就可以讨论每一层用什么存储。很多初学者上来就“上向量数据库”其实没必要。我问你一个问题你的短期工作记忆需要上向量库吗答案大概率是不需要。工作记忆的典型特点是短、快、只在本会话内有效用一个内存中的消息列表或者Redis就足够了。LangGraph的Checkpointer机制默认就把会话状态存在内存或SQLite里每次对话只需要带着最新的状态走不用频繁重建历史。这类存储的核心指标是低延迟和高吞吐而不是语义检索能力强上向量库反而把简单问题复杂化。情景记忆和语义记忆才需要考虑向量化存储。这里我列一个对比表是我在不同项目里实测过的主流方案存储方案适合场景优点明显短板Redis 结构化Key用户事实类、偏好类记忆读写极快、精确匹配、TTL可控无法语义检索“我喜欢轻量级框架”这种模糊匹配做不了SQLite FTS关键词检索单机、中小规模部署简单、零运维语义能力弱跨词表达召回差pgvector已有PostgreSQL的团队事务能力强、能和业务数据JOIN数据量大时索引性能有上限Milvus / Qdrant大规模、高并发语义检索性能强、支持混合检索组件多运维成本明显我的经验是一个相对成熟的Agent记忆系统至少会用Redis加一个向量库组合。Redis存高频精确匹配的用户偏好向量库存语义化的历史事实和知识片段。组合使用的原因很简单向量检索不是银弹用户问“我叫什么名字”你不需要语义召回直接查Redis更快更准。2.2 检索策略给记忆加权重、加时效、加重排存储选型解决的是“记忆放在哪”检索策略解决的是“需要时怎么找回来”。这一块很多人做得很糙就是把用户当前问题做向量化然后TopK召回往Prompt里一塞。实测下来效果非常不稳定。问题出在纯向量检索的局限性上。Embedding擅长捕捉语义相似度但对时间新鲜度、事实优先级、场景相关性完全无感。举个例子用户上周说“我偏好Python”这周又说“这个项目用Go”如果只做语义相似度召回旧记忆可能和新事实同时被检索出来Agent就会混淆说出自相矛盾的答复。我建议在检索链路里加三样东西。第一是时效加权。每条记忆写入时记录created_at和last_accessed_at检索排序时对近期写入或近期被访问过的记忆加权。比如公式可以设计为 score 向量相似度 × 0.6 时间衰减系数 × 0.3 访问频率系数 × 0.1三个维度综合排序。这样既能语义匹配又尊重事实的新鲜度。第二是混合检索。不要只用向量把关键词检索BM25/FTS的结果和向量结果做融合。特别是对于人名、产品名、编号这类专有名词关键词匹配的精确度远高于向量匹配。工程上叫hybrid search实现不复杂但效果提升非常明显。第三是重排Rerank。TopK召回可能召回了20条候选但真正对当前问题有用的只有3条。用一个轻量级交叉编码器模型对候选重新打分把最相关的几条排到最前面。重排会增加几十毫秒延迟但值得。我的原则是宁可多花50毫秒把记忆选准也不能让错误记忆把整轮对话带偏。2.3 记忆编排RAG不是终点MCP才是记忆服务的标准形态聊到记忆系统绕不开RAG检索增强生成。但我得说句实在话RAG解决的是“联网查资料”型的问题不是“记住用户”型的问题。传统RAG把外部文档切片、入库、检索本质上是在给Agent提供静态知识。但记忆系统需要的是动态写入、持续更新、按用户隔离这比RAG复杂得多。我现在的做法是把记忆模块独立成一个服务通过MCP协议暴露给Agent。这样做有几个看得见的好处。一是记忆逻辑和Agent主流程解耦想换存储、换Embedding模型Agent侧不用动二是多个Agent可以共享同一套记忆服务比如一个负责问答的Agent和一个负责执行的Agent能读到同一个用户画像三是MCP协议已经成为AI Agent开发的事实标准之一后续接第三方工具、接知识库管理、接权限控制都更平滑。具体到架构上MCP Server提供几个核心方法remember写入/更新记忆、recall按需召回记忆、forget主动遗忘。Agent在对话流程中通过工具调用这些方法记忆服务内部实现分层存储、检索和更新。这套模型我用了很长时间稳定性和扩展性都很满意。3. 落地实现让Agent真正“记住你”的最小可用方案3.1 短期记忆用LangGraph的Checkpointer实现跨轮对话先从最基础也最急迫的短期记忆说起。没有短期记忆Agent连多轮对话都做不好。LangGraph的Checkpointer机制是我目前用下来最顺手的一套方案。它的核心思路是把图Graph的每个状态的检查点持久化保存下来在下一轮运行时从检查点恢复。这样Agent的执行状态不会因为请求结束而丢失。简单示例from langgraph.graph import StateGraph from langgraph.checkpoint.memory import MemorySaver checkpointer MemorySaver() graph StateGraph(ConversationState) # ... 添加节点和边 ... app graph.compile(checkpointercheckpointer) config {configurable: {thread_id: user-123}} result app.invoke({messages: [{role: user, content: 我姓王}]}, config) # 下一轮继续用同一个 thread_idAgent 就“记得”上一轮说过的内容这里的关键点是thread_id它相当于“会话身份标识”。同一个用户不同会话用不同thread_id记忆互相隔离同一个用户跨会话复用thread_idAgent就能续上之前的状态。但Checkpointer存的还是原始消息轮次多了之后token消耗依然会涨。我一般还会挂一个消息裁剪节点当历史消息超过N轮时把早期消息做一次摘要压缩把摘要作为一条系统消息留在上下文中旧的原始消息移出。这样既保住关键信息又不至于无限膨胀。摘要节点本身用LLM实现成本不高但能显著延续会话寿命。3.2 长期记忆向量库写入、召回、注入一个完整闭环短期记忆只能保证会话内连续真正的“记住你”要靠长期记忆。下面这套写入、召回、注入的闭环流程是我在不断迭代后比较稳定的实践。先说写入阶段。不能所有对话原文都入库那样噪声太大。我的习惯是设计一个“记忆提取节点”每轮对话结束后将最近几轮的消息打包给LLM让它判断是否存在值得长期保存的信息。如果存在就抽取出结构化的记忆条目。比如{ user_id: user-123, type: preference, content: 用户偏好Python明确说不想用Java, importance: 8, created_at: 2025-01-10T12:00:00Z }importance字段很重要它决定这条记忆的保留优先级。我通常让LLM按1-10打分高重要度的记忆后续不会被轻易覆盖。提取完成后把content向量化写入向量库同时把user_id作为过滤条件存进去。再说召回阶段。用户开启新一轮对话时Agent先把用户当前的问题做向量化再联合user_id过滤、时效加权、混合检索召回TopK条相关记忆。召回结果会作为上下文的一部分注入到Prompt里位置放在系统提示之后、最新用户消息之前。这步很关键Agent需要先“看到”关于这个用户的历史事实再回答当前问题才不会答偏。我给一个伪代码流程def recall_memories(user_id: str, query: str, k: int 5): # 1. 向量召回候选 vec_results vector_store.search(query_embedding(query), filter{user_id: user_id}, top_k20) # 2. 关键词召回候选 kw_results bm25_search(query, filter{user_id: user_id}, top_k20) # 3. 融合排序时间衰减 重要度 重排 merged merge_rank(vec_results, kw_results) # 4. 返回 TopK return merged[:k]召回之后注入Prompt才能真正影响Agent的回答。我还会在每个召回条目前加一个简短的来源标记格式类似“[记忆] 用户在2025-01-08提到偏好Python”这样模型看到的是有出处的事实而不是一股脑混进一堆文字准确率会高不少。3.3 记忆更新与遗忘用户改口了怎么办记忆系统最容易忽略的是更新和遗忘。用户上周说了A这周又说B如果系统不知道让旧记忆让位Agent就会一直拿过时信息说事。我建议建立一条“冲突检测与覆盖”流程。当新记忆写入时先在已有记忆中做一次相似度检索如果发现同一用户、同一主题且语义相近的旧记忆就进入合并或覆盖逻辑。怎么判断是“同一主题”我一般用type加语义相似度双条件。比如旧记忆type是preference内容是“偏好Python”新记忆type也是preference内容是“新项目改用Go”相似度超过阈值说明是同一类偏好的更新这时该做的是“更新”而不是再插一条新记录。更新时保留旧记忆的created_at但更新content和updated_at会让事实脉络更清晰。遗忘策略同样不能省。一种做法是定期清理低重要度、且长时间未被访问的记忆另一种做法是给记忆加TTL偏好类记忆有过期时间临时性事实到期自动删除。原因很简单没有人希望AI永远记着一年前随口说的一句话主动遗忘既是用户体验的需要也是隐私合规的底线。4. 工程化中的常见坑检索不准、记忆污染、上下文爆掉4.1 检索结果不相关问题在Embedding和查询改写很多人在检索这块踩坑召回回来的记忆跟当前问题八竿子打不着。我排查过很多次大部分时候不是向量库的问题而是Embedding模型和查询词的问题。Embedding模型选型影响非常大。通用领域上、英文场景选OpenAI的text-embedding-3系列或者开源的bge系列都够用但中文场景、垂直领域比如医疗、法律、代码的表现差异很大建议拿自己的业务语料做一轮简单评测再定。还有一个容易被忽略的点Embedding模型和对话模型是两个模型向量维度、语义空间都不同换对话模型时不会影响已存储的向量但换Embedding模型时所有老向量的语义空间都变了必须重新索引。这个坑我踩过一次重建索引花了半天。查询改写也值得做。用户的提问往往是口语化的短句比如“我上次说过用啥语言来着”。直接拿这句话做向量检索效果不会好。我会在前面加一步让LLM把用户的模糊提问改写成一个适合检索的查询词。比如改写成“用户过去选择的编程语言偏好”。这一小步能让召回相关性提升一大截。4.2 记忆污染与冲突旧记忆怎么让位于新事实做过长期记忆系统的人一定遇到过一个场景提示词里同时出现了冲突的记忆条目Agent开始精神分裂一会儿说用户喜欢A一会儿说用户选了B。这个问题我管它叫“记忆污染”。避免污染要从写入口和读出口两头堵。写入口就是前面说的冲突检测与合并覆盖确保同一主题只有一条最新事实。读出口则是召回后的“记忆去重与仲裁”合并排序后的TopK记忆如果发现两条content语义冲突保留updated_at更新的那条丢弃或者降权旧的那条。还有一个更隐蔽的污染源错误记忆被当成了事实存进长期记忆。用户可能只是开玩笑说“我讨厌写代码”系统就当真的记下来后面每次对话都拿这条“事实”干扰决策。我的建议是在记忆提取节点加一个置信度判断凡是情绪化、不确定、可能只是一时表达的内容要么不写入要么标记为低重要度和低置信度召回时几乎不会被用到。4.3 上下文爆炸记忆注入量的裁剪策略长期记忆召回之后如果一股脑全塞进Prompt上下文还是会爆炸。尤其有些Agent还要同时加载工具定义、系统提示、RAG外部知识上下文预算非常紧张。我的裁剪原则是记忆注入量按“关键任务优先”分配。每一轮对话前先评估当前任务类型如果是闲聊寒暄召回1-2条用户画像类记忆就够如果是技术方案咨询召回的应该是历史技术选型、项目背景类的记忆数量可以放到3-5条如果是执行操作类任务则优先注入用户偏好和约束条件而不是历史聊天记录。实测下来大多数对话场景5条以内的高质量记忆就足够支撑Agent表现出“了解用户”的感觉超过这个量边际收益很低噪声反而变大。控制住注入量token成本能下降一半回答质量反而更稳。此外我还会对每条注入的记忆做字数限制比如每条不超过50个字。太长的记忆在注入前先让LLM压缩一次这样Prompt结构保持一致模型处理起来更稳定。4.4 高频问题速查表最后整理一份我经常在社区和团队内部收到的高频问题速查表方便你排查时直接对照。现象可能原因解决思路换了会话Agent就“失忆”长期记忆未接入只依赖上下文窗口部署向量库跨会话按user_id召回历史记忆召回结果不相关Embedding模型与业务语料不匹配评测并更换Embedding模型或者加查询改写旧记忆覆盖新事实缺少冲突检测与合并逻辑写入口做相似度匹配同主题直接update多轮对话token涨得快未做消息裁剪和摘要增加消息压缩节点旧消息转摘要记忆里混入无效信息提取环节无筛选标准增加LLM置信度判断低置信度不写入多个Agent记忆互串缺少用户级隔离所有记忆操作必须携带user_id并做过滤最后再分享一个我自己的体会。给Agent做记忆最容易犯的错是“贪多”总觉得存得越多越智能。实际上一套好的记忆系统更像一个懂分寸的管家而不是一个什么都往仓库里堆的仓管员。它知道什么事值得记、什么时候该想起来、什么时候该忘掉。施工的时候先跑通“写入—召回—注入”的最小闭环再逐步加上时效加权、冲突合并、遗忘清理这些精细活稳扎稳打你大概率能少走很多弯路。