
1. 为什么 Agent 必须拥有记忆先把话放在这里一个没有记忆的 AI Agent本质上就是一个包了一层函数调用的聊天机器人。你第一次见它它是陌生人第二次见它它还是陌生人。我做客服知识库 Agent 的时候最崩溃的事就是用户每天都得上报一遍我是谁、我上次问了什么。明明昨天刚聊完退货流程今天再问一句那运费呢Agent 一脸茫然仿佛昨晚的记忆被整体格式化。这个体验放在真实业务里是致命的——用户会觉得自己在跟一个永远记不住自己的机器人说话信任感直接归零。所谓让 Agent 记住你指的是让 Agent 在跨会话、跨任务的情况下保留用户画像、历史交互、偏好和事实性知识并在恰当的时机把这些记忆重新取出来参与推理。这件事之所以难根本原因在于当前大模型本质上是无状态的函数输入一段文本输出一段文本。模型内部并不存在上次对话发生了什么这种状态所有记忆都必须外部化——先存到某个地方再在需要时拼回 prompt。所以一个 Agent 记忆系统的本质就三件事写入、存储、读取。听起来简单真正做起来处处是坑。这篇是走进 AI Agent系列的第三篇我打算把记忆这块完整拆开讲从记忆的分类和架构到向量库选型和代码实现再到检索策略和线上问题排查。会尽量写实际能落地的方案而不是停留在概念层面。适合正在做 Agent 应用开发、或者准备把 Agent 产品化的朋友参考。如果你只是对 AI Agent 感兴趣读完也能理解市面上那些会记住你的 AI 助手背后到底发生了什么。有一点需要先说清记忆不是越多越好。很多团队做记忆功能第一版就把所有历史对话全部塞进上下文结果 token 成本暴涨Agent 反而变笨。原因很反直觉——大模型的注意力是稀缺资源塞进去的无关信息越多它对真正关键信息的关注就越弱。记忆系统的核心指标不是记住了多少而是在正确的时机想起了正确的事。2. 记忆架构先分类再设计做记忆系统第一步不是选数据库而是分类。不同性质的记忆存储方式、生命周期和更新策略完全不一样。把记忆按统一格式塞进一个库后期必然出事。我习惯把 Agent 记忆分成三层再加两个语义维度来理解。2.1 三层记忆模型工作记忆Working Memory当前这轮对话的上下文包括最近几轮用户输入、Agent 输出、工具调用结果。它活在模型上下文窗口里对话结束就消失。短期记忆Short-term Memory一次会话 session 内的完整历史通常做滑动窗口裁剪或摘要压缩。长期记忆Long-term Memory跨会话持久化的记忆承载用户画像、长期偏好、历史事实是记住你的关键。这里面最容易犯的错误是试图用一个大而全的存储解决所有问题。我踩过这个坑结果是把对话历史和用户画像混在一个表里查起来要拼各种条件更新逻辑更是剪不断理还乱。现在我对架构的要求很明确工作记忆靠 prompt 管理短期记忆用 Redis 或者内存队列长期记忆用向量库三者各司其职。换成人话说这三层的关系可以类比成一个真人助理的工作方式工作记忆是你正在说的这句话短期记忆是你们这次会议聊过的内容长期记忆是助理对你这个人所有过往的了解。助理不会在每次开会时把你们过去十年的聊天记录全翻出来念一遍他只会在合适的节点调出合适的信息。2.2 情景记忆与语义记忆如果做的是 To C 产品建议在长期记忆里再往下拆一层参考认知科学中的两个概念情景记忆Episodic Memory具体的、带时间地点的事件记录。比如5月10日用户问过电商订单如何退款最后选择了原路退回。语义记忆Semantic Memory抽象出来的通用知识。比如用户偏好当日达快递用户是企业客户月订单量约 200 单。区别在哪情景记忆是原始的、接近事实的记录检索到之后可以直接引用语义记忆是加工过的、可复用的结论适合做用户画像。实际项目里我通常的做法是原始对话片段进情景记忆库定时用 LLM 抽取关键特征写入语义记忆库。两条链路并存查询时按场景选择。举个具体场景用户问我上次那个订单后来怎么样了走情景记忆直接检索历史事件记录用户问你知道我喜欢什么快递吗走语义记忆。如果只做一条链路要么在用户画像里找不到具体事件要么在事件堆里概括不出偏好总有一边是瘸腿的。2.3 为什么记忆模块要独立封装还有一点架构层面的建议把记忆系统设计成独立模块不要和业务逻辑耦合。我见过太多项目把记忆逻辑直接写死在 Agent 主循环里——先查记忆、再拼 prompt、再调模型、再写回记忆一坨代码最后根本没法维护换一个向量库要改一个星期。更好的做法是把记忆抽象成一个接口核心就两个方法read(user_id, query) 和 write(user_id, content)。上层 Agent 只依赖这个接口底层是向量库、Redis 还是混合存储随时可以替换。收益很直接测记忆功能时不用把整个 Agent 跑起来写单元测试只需 mock 记忆模块换存储引擎也不动业务代码。这个封装习惯让我后面迭代省了大量时间。3. 核心实现写入、存储、读取的完整链路下面进入实操部分。我以一个常见的 Python Agent 项目为例完整演示短期记忆和长期记忆的实现。整体流程分四步对话历史管理 → 记忆抽取 → 向量化入库 → 检索注入。每一步都有需要注意的细节。3.1 短期记忆的正确姿势滑动窗口与摘要压缩短期记忆的核心就是管理 conversation history。不要简单地把所有消息堆进 prompt有两个实用技巧滑动窗口和摘要压缩。滑动窗口的意思是只保留最近 N 轮对话超出部分直接截掉。代码很简单MAX_TURNS 10 def build_short_term_memory(messages: list[dict]) - list[dict]: if len(messages) MAX_TURNS * 2: return messages # 保留最近 MAX_TURNS 轮每轮包含 user 和 assistant 两条消息 return messages[-(MAX_TURNS * 2):]这个写的核心是控制上下文长度。为什么是 10 轮而不是 50 轮因为我实测下来多数业务场景最近 8 到 12 轮对话包含了绝大部分有效信息更早的内容通常在逻辑上已经闭环。保留太多token 成本线性上涨模型对早期信息的依赖度却在下降性价比不高。摘要压缩处理的是另一种情况单轮对话特别长比如用户粘贴了一段几十行的报错日志或者上传了一份长文档。这时候可以先把这段内容单独交给 LLM 做摘要再用摘要进入上下文。压缩后的信息密度高得多模型也不容易在超长文本里迷失重点。SUMMARIZE_PROMPT 请用200字以内总结以下内容的关键信息保留所有具体数字、专有名词和结论\n{content} def summarize_content(content: str) - str: resp llm_call(SYSTEM_PROMPT, SUMMARIZE_PROMPT.format(contentcontent)) return resp需要提醒的是摘要必须有一个明确的约束——保留实体和数字。默认的总结一下太泛模型会把退款金额 328 元订单号 A10086这类关键实体丢掉。我踩过这个坑用户问我那个 328 的退款到账没Agent 完全想不起来因为摘要里只剩下了用户办理了退款。所以摘要 prompt 里我都会强调原样保留所有具体数字、专有名词、金额、日期。3.2 长期记忆的存储选型Embedding 加向量库长期记忆的核心组合是 embedding 加向量数据库。这里把选型逻辑讲透。Embedding 模型负责把文本转成向量这一步决定了语义相似到底准不准。选型原则英文为主选 OpenAI 的 text-embedding-3-small兼顾成本和质量都很稳中文为主选开源模型比如 BGE 系列、M3E如果对数据出境有要求就本地部署开源模型。不要一上来就追最大模型我实测 text-embedding-3-small 在很多业务场景的检索效果和 large 差距并不大但成本差了好几倍。向量库负责存储和相似度检索。常见的几个选择FAISS内存库轻量适合单机实验和原型验证。Chroma本地持久化用起来简单适合中小项目快速落地。Qdrant / Milvus分布式部署支持高并发适合生产环境。Redis 新版自带向量检索如果公司已有 Redis 基础设施可以少引入一个组件。我的选型判断标准很简单数据量小于 100 万条Chroma 或者 Qdrant 单机版都够用需要高可用和水平扩展直接上 Milvus不想引入太重的基础设施先用 FAISS 加定期落盘。别为了先进去选一个运维成本极高的方案向量库不是 Agent 的核心竞争力稳定易维护才重要。3.3 记忆写入流程从对话里抽取事实记忆写入是最容易被低估的一步。很多人直接把整段对话存进向量库然后发现检索效果稀烂。因为原生态对话里充斥着寒暄、语气词和无关信息向量化的噪音太大。正确做法是先用 LLM 从对话中抽取结构化记忆条目再向量化入库。import json import uuid from datetime import datetime import chromadb from openai import OpenAI client OpenAI() chroma_client chromadb.PersistentClient(path./memory_db) collection chroma_client.get_or_create_collection( nameagent_memory, metadata{hnsw:space: cosine} ) MEMORY_EXTRACT_PROMPT 请从下面的对话中抽取值得长期记住的事实。 只输出 JSON 数组每个元素包含 type 和 content 两个字段。 type 为 user_profile用户画像或 task_record任务事件。 只抽取明确出现的信息不要推测。 对话 {conversation} def extract_memory(conversation: str) - list[dict]: resp client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: MEMORY_EXTRACT_PROMPT.format(conversationconversation)} ], temperature0, ) return json.loads(resp.choices[0].message.content) def write_memory(user_id: str, conversation: str): memories extract_memory(conversation) for m in memories: text m[content] emb client.embeddings.create( modeltext-embedding-3-small, inputtext, ).data[0].embedding collection.add( documents[text], metadatas[{ user_id: user_id, type: m[type], timestamp: datetime.now().isoformat(), }], embeddings[emb], ids[f{user_id}-{uuid.uuid4()}], )这段代码有几个细节值得划重点。metadata 里必须存 user_id否则多用户系统绝对会互相串记忆。timestamp 字段是给时间衰减用的后面在检索策略里会讲到。type 字段用来在检索时按类型过滤比如用户问我上次那个事优先查 task_record问你还记得我喜欢什么吗优先查 user_profile。temperature 必须调到 0抽取任务要的是确定性不要创造性。还有一个常见问题什么时候触发写入我建议两种策略结合。对话进行中每完成一个明确的用户任务就触发一次抽取比如工具调用结束后对话结束时做一次全量抽取兜底。不要在每轮对话都触发成本高、重复多。3.4 记忆读取与 Prompt 注入写入之后就是读取。读取的关键在于把检索到的记忆以合适的方式注入 prompt并且告诉模型这些记忆仅供参考。def read_memory(user_id: str, query: str, top_k: int 5) - str: query_embedding client.embeddings.create( modeltext-embedding-3-small, inputquery ).data[0].embedding results collection.query( query_embeddings[query_embedding], n_resultstop_k, where{user_id: user_id}, ) docs results[documents][0] return \n.join(f- {d} for d in docs) # 在构建 Prompt 时注入 memory_text read_memory(user_id, user_input) system_prompt f你是用户的专属 AI 助手请根据以下关于用户的记忆回答问题。 --- 用户记忆 --- {memory_text} --- 对话规则 --- 如果记忆与当前问题无关请忽略记忆正常回答不要编造记忆中没有的信息。注意 system prompt 里那句如果记忆与当前问题无关请忽略记忆这是我踩坑踩出来的。没有这句话的时候模型经常被无关记忆带偏强行把旧信息安到新问题上。加了这句话之后模型的判断准确率明显提升。这也是给模型一个拒绝使用记忆的出口反而让它更愿意在合适的时候用。3.5 记忆的更新、合并与遗忘记忆不是只写不删的。长时间运行下来库里的记忆会膨胀、冲突、过期需要一套生命周期管理机制。冲突合并的处理方式新旧记忆冲突时保留新记忆同时把旧记忆标记为 deprecated。比如用户先说我喜欢坐靠窗的位置后来改口靠过道吧直接物理删除旧记录有风险万一用户哪天又问起来历史追溯不了标记弃用则可以在检索时过滤掉又能留底。遗忘机制方面我给每条记忆设置 TTL比如 180 天未命中的记忆定期清理。具体做法是定时任务扫描 metadata 里的 timestamp超过阈值就删除。这样库的规模是收敛的不会无限膨胀导致检索变慢。重要度评分是另一个提升检索质量的维度。用户主动强调的内容比如我很在意记住我讨厌……重要度加一档。实现上可以在抽取时让 LLM 额外输出一个 importance 字段检索重排时乘以权重系数。这块我会在下一节详细展开。4. 检索策略与调优决定记忆质量的胜负手存储做完了真正的难点在检索。同一个用户可能积累了几百上千条记忆每轮对话只该用其中的三五条。把哪几条翻出来、以什么顺序呈现直接决定了 Agent 回答的质量。检索策略是记忆系统的胜负手值得花时间打磨。4.1 Top-K 与相似度阈值召回多少有讲究向量检索不是搜得越多越好。我见过有人直接把 top_k 设成 50上下文塞满了各种相关性很弱的记忆模型反而无所适从。经验值是 top_k 取 5 到 10具体根据记忆质量和任务复杂度调整。光设一个 top_k 不够还要设相似度阈值。向量检索返回的任何结果都有一个相似度分数低于阈值的记忆根本不应该被返回。以余弦相似度为例不同 embedding 模型的分数分布差异很大要先跑一批线上数据看看分布。我的经验是text-embedding-3-small 的阈值设在 0.3 到 0.4 之间比较合理低于这个阈值的记忆宁可不注入。重要经验优先过滤再检索最后才是调参。很多项目检索不准根本不是参数问题而是 where 条件漏掉了 user_id导致把别人的记忆召回了。先把基础条件管好再谈优化。4.2 时间衰减与重要度加权同样是相关的记忆一个月前的和昨天的分量应该不一样。所以在召回之后我会做一个重排把时间新鲜度和重要度都考虑进去。时间衰减的核心是在 metadata 里存了 timestamp重排时计算记忆年龄越接近当前时间得分越高。重要度加权则是对用户主动强调过的内容增加权重。一个我实际在用的简单打分公式score cos_sim * 0.7 recency_score * 0.2 importance_score * 0.1其中 cos_sim 是向量相似度recency_score 按时间衰减importance_score 是抽取时标记的重要度。权重系数 0.7、0.2、0.1 不是拍脑袋定的是我在业务数据上调出来的语义相关性的主导地位必须保证时间次之重要度只是一个微调因子。4.3 多路召回向量加关键词加最近记录单一向量召回有一个天生的盲区向量检索擅长语义相似但对于精确的专有名词、订单号、人名这类实体效果反而不稳定。这时候需要引入多路召回。我的实践中用三路召回第一路向量相似度召回负责语义理解比如用户说上次那个物流问题能匹配到快递迟迟未发货。第二路关键词精确匹配构建倒排索引或者简单做词频匹配负责把订单号 A10086这种精确信息捞出来。第三路最近 N 条记忆直接加载负责兜底。有些场景用户就是随口问最近聊过的内容不需要语义匹配。三路结果合并去重后再做重排。这个方法的效果非常明显——之前单路向量召回时专有名词的准确率在 60% 左右加了关键词路之后直接拉到了 90% 以上。4.4 一次让我印象深刻的线上事故写到这里顺便分享一个真实的翻车案例。有一版我为了调高召回率把 top_k 从 5 改到 20结果线上立刻出事故用户问我上次买的耳机能退吗Agent 突然回复根据您对咖啡机偏好度的了解我建议……——它把用户三个月前查过的咖啡机帖子的记忆当成当前对话的依据了。排查过程大概是这样的先看召回结果日志发现 20 条记忆里有 15 条相似度都在 0.2 以下属于强行捞进来凑数的。再往下追发现 where 条件本身没问题user_id 过滤是生效的纯粹是阈值太低加 top_k 太大。这个事故让我总结出两个教训第一任何检索结果都要附带相似度分数方便线上回溯和分析第二调参之前先做小流量 AB 测试不要直接全量发布。后来我加了日志里记录本次注入了哪些记忆、各自分数多少的功能排查问题从小时级降到分钟级。5. 常见问题与排查技巧实录记忆系统上线之后问题千奇百怪。我整理了一份高频问题速查表全部来自真实线上案例每个都附了排查思路和解决方案。现象可能原因排查方法解决方案检索出明显无关的记忆相似度阈值过低 / top_k 过大查看检索日志中的分数分布提高阈值到合理区间调小 top_k多个用户记忆互相串扰查询时遗漏 user_id 过滤检查 query 的 where 条件所有读写强制带 user_id必要时加单元测试Prompt 超长报错注入的记忆太多统计单次注入的 token 数调小 top_k增加摘要压缩记忆更新不生效新旧记忆冲突未处理查询库中是否存在重复条目合并去重旧记录标记 deprecated模型把旧记忆安到新问题上缺少无关记忆忽略指令查看 prompt 中注入内容在 system prompt 中明确允许忽略无关记忆摘要压缩后关键信息丢失摘要 prompt 未约束保留实体对比原文和摘要文本约束输出结构强调保留数字和专有名词线上响应明显变慢向量库查询慢 / embedding 调用阻塞加耗时日志逐段看瓶颈加缓存记忆检索失败时降级为无记忆模式有几个排查技巧值得展开说。第一一定要给记忆检索加日志。每条注入 prompt 的记忆记录它的内容、相似度分数、来源时间。这个日志是排查一切记忆问题的入口没有它遇到问题只能瞎猜。它也是后续做 AB 测试和效果评估的数据基础。第二记忆模块要做降级策略。万一向量库挂了或者 embedding API 超时Agent 不能直接报错应该降级成无记忆模式继续对话。用户感知到的只是这个 AI 今天不太记得我而不是整个服务不可用。两个错误级别天壤之别。第三做效果评估要用标注数据集。从历史对话里抽样几百条人工标注这条对话应该召回哪些记忆然后拿这个集子反复回归测试。记忆检索和模型的代码一样每次改动都不能靠感觉要有量化指标。我自己的做法是标注了 300 条数据跑一个 recall5 的指标低于某个值就不允许上线。另外一个经常被忽略的点是隐私。当记忆跨越了多个会话系统里其实积累了大量用户敏感信息。我在生产环境里做了三件事个人身份信息脱敏后入库、用户可主动查看和删除记忆、记忆数据单独加密存储。这块不光是产品体验问题也是合规底线越早设计越省事。6. 记忆系统的评测与线上运营实践做完功能只是一个开始。记忆系统的奇特之处在于它不是一个一次性交付的功能而是一个需要持续运营的数据系统。机器人和人一样记忆需要被维护、矫正和清理。评测方面我用两个核心指标看一个两层结构第一层叫召回命中率就是正确的记忆是否在 top_k 结果中第二层叫注入有用率就是模型实际参考注入记忆后生成的回答是否真的变得更好。第一层是自动评估拿标注集跑就行第二层必须做线上 A/B 对比或者至少每周抽一批会话人工评估。两个指标之间常常出现背离召回命中高不代表注入对回答真有帮助——有可能是检索回来的记忆虽然相关但都是些无关紧要的寒暄记录对回答质量问题没有贡献。运营层面的一个重要实践是记忆纠错机制。用户经常会对 Agent 说不对我不是这个意思你记错了。这类反馈是修正记忆的黄金信号。我在系统里加了监听当识别到用户纠正性表达时自动触发一次记忆审核找出当前 prompt 中注入的记忆让 LLM 判断哪一条与用户纠正内容冲突然后把对应记忆标记为需更新在下一轮写入时覆盖。还有一个很实用的小设计给每条记忆加一个最后命中时间字段。这个字段有两个用途一是按时间筛选过期记忆做清理二是统计哪些记忆从来没有被命中过。如果一条记忆存了三个月从来没被用上要么是用户画像变了要么是存储格式有问题。统计出来定期复盘能反推抽取策略的调整方向。记忆数据的备份和恢复也值得提一句。向量数据库里的数据看着不起眼积累几个月就是不可再生资产丢一次损失极大。我现在的方案是向量库每天全量导出到对象存储配合元数据库做双写。恢复流程演练过两次都能在 30 分钟内把记忆完整还原。最后把记忆系统单独做成一个记忆服务而不是 Agent 内部模块是我给团队定的方向。未来如果要做多个 Agent 共享用户记忆或者给记忆系统单独做管理后台独立服务的结构会让这些扩展容易得多。这个决定当时的成本只是多写了一层 API 封装但换来的扩展空间非常大。7. 写在最后一些反复验证过的体会做了一年多 Agent 记忆相关的工作我个人的核心体会可以浓缩成几句话供你参考。别把记忆做成一个大仓库要按生命周期和用途分层工作记忆、短期记忆、长期记忆各管各的。抽取比存储更重要原生态对话直接入库等于存垃圾先用 LLM 提炼成结构化条目才是正路。检索比写入更值得投入向量检索、时间衰减、多路召回、阈值控制每一项都能显著影响体验值得用足够的耐心去调。还有一个被反复验证的直觉用户能原谅一个偶尔记错的助手但很难原谅一个假装记得、却编造记忆的助手。所以 prompt 里那句记不清就直说永远是刚需。一个敢承认自己记不住的 Agent比一个满嘴编造过去、却说得言之凿凿的 Agent靠谱得多用户的信任感也强得多。在我自己的实践里记忆系统的迭代节奏大概是这样先跑通写入和读取的主流程用最简单的 Chroma 加 open-source embedding 上线然后接日志、接降级、接清理任务再往后才是分数调优、多路召回、记忆纠错这类精细化工作。不要一上来就大动干戈上分布式向量库需求还没验证就先把复杂度拉满这几乎是所有 Agent 项目翻车的共同原因。这个系列的下一篇我打算讲 Agent 的自主规划与工具调用那又是一个全新的坑。到时候接着聊。