如果只让我用一个词总结做 AI Agent 的踩坑体验那就是“失忆”。模型能力再强prompt 设计得再花哨只要 Agent 每次开新对话都把你当陌生人这产品就永远差口气。这篇是“走进 AI Agent”系列的第三篇主题很聚焦让 Agent 记住你。我会从记忆的类型、存储选型、代码实现到生产级避坑把“Agent 记忆”这件事完整讲清楚。适合正在做 Agent 原型或准备上生产的开发者参考看完能直接动手改造自己的项目。1. 为什么Agent需要“记忆”从“失忆”到“记住你”1.1 没有记忆的Agent是什么体验很多初学者刚开始搭 Agent觉得只要接上大模型 API、写好 system prompt、配上几个工具调用一个“智能体”就算完成了。但真拿去用两天就会发现问题用户第一天告诉 Agent“我平时喜欢简洁的回复风格”“我是做 Java 开发的经常需要写 Spring Boot 接口”第二天再打开对话Agent 全忘了。你问它“帮我看看昨天说的那个项目的接口设计”它一脸茫然因为你昨天说的话只存在于上一个会话的上下文窗口里会话一关什么痕迹都没有。这种体验有多糟糕就好比你每次去同一家理发店托尼老师都问“今天想剪什么样的”。一次两次没问题次次都这样你就会觉得这店不专业。AI Agent 也是一样。“记忆”不是锦上添花的功能而是决定 Agent 能否从“工具”进化为“协作者”的分水岭。没有记忆的 Agent本质上还是一个带工具调用能力的聊天机器人有了记忆它才真正开始“懂你”。1.2 记忆不只是“历史聊天记录”我见过不少团队在实现“记忆”时第一反应是“把聊天记录存数据库不就行了”。这个思路方向对但颗粒度太粗。聊天记录属于原始日志里面大量内容是无效的寒暄、临时指令、重复提问、错误信息。如果直接把整段聊天记录塞进上下文token 消耗先不说模型反而会被无关信息干扰回答质量直线下降。真正要记的是从对话中提炼出来的结构化信息。比如“用户偏好简短回复”是一个稳定的偏好“用户正在开发 XX 项目技术栈是 Java Spring Boot”是一个长期事实“用户上周说过想学 LangGraph”是一个目标。这些东西才是“记住你”的核心资产。所以我更愿意把“记忆”理解成对交互历史的提炼、存储与重用而不是简单的日志回放。1.3 记忆机制对架构的影响记忆不是孤立的功能模块它会影响 Agent 的整体架构设计。我在改造项目时是把记忆层单独抽出来的与模型调用层、工具执行层平级对待。加了记忆层之后一个完整的 Agent 请求链路会变成接收用户输入。先从记忆层召回与当前问题相关的长期记忆。把短期上下文当前会话最近几轮和长期记忆一起组装进 prompt。模型推理决定是否调用工具。把回应写入短期上下文。根据策略判断哪些信息值得写入长期记忆。这一步调整直接让项目的代码结构从“单一 prompt 文件”扩展为“记忆管理 检索 写入”的多模块结构。刚开始会觉得麻烦但这是生产级 Agent 必须付出的代价。如果你用 LangGraph 这类框架它的 State、Checkpointer 本身就和记忆机制天然契合后面我会讲到。2. 先把“记什么”想清楚记忆的四种类型2.1 短期记忆上下文窗口里的即时信息短期记忆最简单也最容易被误解。它指的是当前会话内、模型上下文窗口里能直接访问的信息比如用户刚才说的话、上一步工具返回的结果、最近几轮的对话状态。这一块通常不需要额外开发交给模型本身的上下文机制就行但要注意两个问题。第一上下文窗口是有限的。GPT-4o 有 128K 上下文Claude 也有 200K听起来很大但你在里面对话轮次多了token 依然会爆。我做过一个测试一个普通客服场景平均每轮往返大约 1500 token128K 上下文大概能撑 80 多轮。听起来够用但当你的 Agent 还要注入大量背景知识、工具定义、历史记忆时可用空间就被压缩得很厉害。第二短期记忆需要“压缩”策略。常见的做法是滑动窗口只保留最近 N 轮对话。摘要压缩当对话超过阈值时让模型把前面的对话总结成要点替代原始文本。混合策略最近几轮保留原文更早的只保留摘要。我在生产项目里用的是第三种效果最稳。摘要压缩虽然会丢细节但能把“用户刚刚说过要什么”这个核心状态保留下来。2.2 长期记忆跨会话的持久信息长期记忆是“让 Agent 记住你”的核心。它指的是跨会话、跨时间维度的稳定信息包括用户的基本事实姓名、职业、技术栈、明确的偏好风格、语气、格式要求、长期目标正在做的项目、计划学习的方向等。长期记忆的难点不在“怎么存”而在“存什么”和“怎么读”。存什么决定了记忆的价值密度。我早期踩过坑把用户每一句话都存进去结果向量数据库里塞满垃圾检索时全是噪音。后来引入了一套“记忆价值判断”机制用另一个 prompt 让模型判断这条信息是否满足以下条件之一——稳定的长期事实、明确的偏好、影响后续交互的目标或计划。只有满足条件才写入长期记忆。怎么读决定了记忆的利用率。长期记忆不能每次全量注入必须按需检索。当用户问“帮我看看那个 Spring Boot 项目怎么优化”时你希望召回的是“用户做 Java 开发”“用户在开发 XX 项目”这些相关记忆而不是“用户昨天点了一份外卖”。这里就要用到向量检索后面详细讲。2.3 情境记忆与程序性记忆容易被忽略的两类除了上面两种还有两类记忆在实践中非常重要但容易被忽略。情境记忆指的是“当前所处场景”的感知信息比如时间、地点、天气、用户当前浏览的页面、正在进行的任务状态。情境记忆能显著提升回复的贴心程度。比如同样是问“有什么安排”早上问和晚上问、工作日问和周末问应该给出不同的回应。这类记忆通常不需要长期存储而是在请求时通过工具或 API 动态获取临时注入 prompt。程序性记忆指的是 Agent 对“怎么做某件事”的固定流程的掌握。比如你教会 Agent 如何做代码审查先拉取代码、再检查安全漏洞、然后评估性能、最后输出报告。这套流程在每次执行代码审查任务时都应该被复用。程序性记忆在工程上通常体现为工作流模板或可复用的 Sub-agent 编排在 LangGraph 里就是一个预先定义好的子图。我建议你在设计记忆系统时把这四类分开建模不要全部塞进同一个库。短期和情境走内存/缓存长期走向量库程序性走代码模板或配置。混在一起后期维护成本会非常高。3. 落地选型向量数据库与记忆管理框架3.1 向量数据库怎么选长期记忆的核心存储方案业界基本都用向量数据库。原因是“怎么读”这个问题本质上是一个语义相似度检索用户当前的问题和历史上哪些记忆片段最相关。传统关系型数据库用 SQL 的 LIKE 匹配做不到语义检索所以向量检索成了标配。我尝试过几种主流方案直接说结论数据库部署方式适合场景记忆检索相关的优势坑点Chroma嵌入式/本地原型开发、个人项目零配置Python 自带pip 装上就能用数据量大时并发能力弱FAISS嵌入式库对性能敏感的单机场景检索极快Meta 出品灵活性高只提供检索能力需要自己管持久化QdrantDocker/自托管/云中小规模生产内置丰富的 metadata 过滤REST API 好用需要独立部署运维MilvusDocker/K8s大规模生产吞吐高支持分布式部署和运维成本高Pinecone全托管云不想碰运维的团队省心稳定性好数据量上来后成本高如果你只是做原型验证我强烈建议选Chroma一个pip install chromadb就搞定本地文件持久化不用起服务。如果你直接准备上生产我更推荐Qdrant它的 metadata 过滤能力在做“按用户隔离记忆”时非常好用REST API 也很清晰。3.2 嵌入模型与检索策略选完向量库下一步是选嵌入模型embedding model。这一步很关键因为检索效果的上限由嵌入质量决定向量数据库只是不让效果打折。我自己的建议如果是纯中文场景BGE-large-zh、text-embedding-v3都没问题我实际对比过两者中文语义检索精度差距不大。如果中英混合且追求极简维护OpenAI 的text-embedding-3-small够用成本也低。如果对数据安全有要求必须本地部署那就用BGE-m3或GTE-Qwen2这类的开源模型本地跑也能有不错的效果。嵌入模型选好后还要解决一个核心问题嵌入模型有输入长度上限记忆片段不能太长。我一般把每条记忆控制在 100~200 字左右超出就拆分或压缩。太长的记忆既浪费 embedding 的输入空间检索时召回的也是大段噪声。检索策略上别只依赖纯向量检索。我现在的做法是双通道召回同时做向量语义检索和关键词过滤然后合并去重。比如用户问“Spring Boot 性能优化”向量通道能召回“用户在开发 Spring Boot 项目”关键词通道能锁定“性能优化”相关的历史讨论。两个结果做加权效果远好于单一通道。3.3 写入、召回与遗忘记忆的完整生命周期很多文章只讲“存储”但生产级记忆系统一定要考虑完整生命周期我按自己的实现总结为四个阶段写入。从对话中提取候选记忆判断价值过滤垃圾信息做去重再生成向量存入库中。去重很重要用户可能多次表达同一个偏好如果不做去重库里的记忆会快速膨胀。我的做法是用记忆内容做语义相似度比对如果新记忆和库里某条相似度超过 0.9就认为重复直接丢弃。召回。根据用户当前请求检索相关记忆并按相关度排序。召回还有个进阶操作时效性加权。比如用户三个月前说“我打算学 Python”上周说“我已经转去做 Java 后端了”那“学 Python”这条记忆的权重应该降低。我实现了一个简单的时间衰减因子score similarity * exp(-age / half_life)效果还可以。更新。用户偏好是会变的记忆不能只增不改。当模型从新对话中提取出与旧记忆冲突的信息时应该用新信息覆盖旧记忆或者把旧记忆标记为“过时”。遗忘。这步经常被忽略。没有遗忘机制的记忆库时间长了会积累大量“僵尸记忆”检索噪音越来越大。我建议定期清理对超过有效期的临时记忆直接删除对长期未被召回的冷记忆做归档。简单点可以设置 TTL复杂点可以定期用模型做一次记忆审计。4. 实操搭建一个会记住用户的 Agent4.1 整体流程设计下面我用一个具体例子把整个流程串起来。场景是一个“开发助手 Agent”用户会对它聊自己的技术栈、项目、偏好之后 Agent 要根据这些记忆给出个性化回答。整体流程分四步用户输入进入 Agent。先做记忆召回把相关长期记忆注入 prompt。模型生成回复后做记忆提取判断哪些值得写入长期记忆。把提取结果写入向量库供后续会话使用。这套流程里的“记忆提取”环节我用的是 LLM 自己来完成而不是规则匹配。规则匹配只能处理“我叫 XX”这种模板化句子遇到“平时写代码我更喜欢函数式风格”这种自然表达就失效了。用 LLM 提取泛化能力强得多。4.2 核心代码MemoryStore 与记忆写入存储层我以 Chroma 为例实现一个MemoryStore实际迁移到 Qdrant 时只需要替换对应方法即可# memory_store.py import time import uuid from typing import List, Dict, Optional import chromadb class MemoryStore: def __init__(self, persist_directory: str ./memory_db, collection_name: str agent_memory): self.client chromadb.PersistentClient(pathpersist_directory) self.collection self.client.get_or_create_collection( namecollection_name, metadata{hnsw:space: cosine}, ) def add_memory(self, content: str, user_id: str, memory_type: str fact, importance: float 0.5, metadata: Optional[Dict] None) - str: memory_id str(uuid.uuid4()) self.collection.add( documents[content], ids[memory_id], metadatas[{ user_id: user_id, memory_type: memory_type, importance: importance, created_at: time.time(), **(metadata or {}), }], ) return memory_id def retrieve_memory(self, query: str, user_id: str, top_k: int 5) - List[Dict]: results self.collection.query( query_texts[query], n_resultstop_k, where{user_id: user_id}, ) memories [] for idx, doc in enumerate(results[documents][0]): memories.append({ content: doc, metadata: results[metadatas][0][idx], distance: results[distances][0][idx], }) return memories这段代码有两个细节值得注意。第一where{user_id: user_id}是记忆隔离的关键不同用户的记忆必须完全隔离不能让 A 用户的偏好泄露给 B 用户。第二我用cosine距离而不是默认的 L2因为在文本语义检索场景余弦相似度对向量长度不敏感效果更稳定。接下来是对话中记忆提取的部分。我单独写了一个函数专门用来从对话里抽取需要长期保存的信息# memory_extraction.py import json from typing import List, Dict EXTRACTION_PROMPT 你是记忆提取引擎。请从以下对话中提取值得长期记住的信息。 值得记的信息包括用户的身份事实、明确偏好、长期目标、重要事件。 不值得记的信息包括临时指令、重复寒暄、一次性的查询。 对话内容 {conversation} 请输出JSON数组每个元素包含 - content: 记忆内容使用第三人称陈述 - memory_type: fact | preference | goal | event - importance: 0到1之间的浮点数 - keywords: [相关关键词] 只输出JSON不要输出任何其他内容。 def extract_memories(conversation: str, llm) - List[Dict]: prompt EXTRACTION_PROMPT.format(conversationconversation) response llm.invoke(prompt) # 防止模型输出多余的前导或尾随文字这里做一次严格的JSON提取 start response.content.find([) end response.content.rfind(]) if start -1 or end -1: return [] try: return json.loads(response.content[start:end 1]) except json.JSONDecodeError: return []注意这里的 JSON 解析做了截取因为我在实际使用中经常遇到模型在 JSON 前后加“以下是提取结果”之类的废话直接json.loads会报错。这个小处理省了很多调试时间。4.3 记忆召回与 Prompt 注入用户每次发消息时先把相关记忆查出来塞到 system prompt 里。我封装了一个build_system_prompt函数# agent.py from memory_store import MemoryStore from memory_extraction import extract_memories class MemoryAgent: def __init__(self, llm, store: MemoryStore): self.llm llm self.store store def build_system_prompt(self, user_id: str, user_input: str) - str: memories self.store.retrieve_memory(user_input, user_id, top_k5) if not memories: return 你是开发助手。当前用户还没有足够的长期记忆请正常回答问题。 memory_lines \n.join( [f- {m[content]} for m in memories] ) return f你是开发助手。以下是与该用户相关的长期记忆请优先参考这些信息来回答问题 {memory_lines} 召回的核心逻辑就一行retrieve_memory(user_input, user_id, top_k5)。为什么把user_input作为查询词而不是用整个历史因为记忆召回只需要理解“当前用户关心什么”历史会由短期上下文处理。每次召回 5 条是我在 token 消耗和覆盖率之间折中的结果你可以根据自己的场景调整。整体对话流程串起来就是def chat(self, user_id: str, user_input: str): # 1. 召回长期记忆构建 system prompt system_prompt self.build_system_prompt(user_id, user_input) # 2. 调用模型生成回复 response self.llm.invoke( [{role: system, content: system_prompt}, {role: user, content: user_input}] ) # 3. 对话结束后异步提取并写入记忆 conversation f用户{user_input}\n助手{response.content} extracted extract_memories(conversation, self.llm) for mem in extracted: if mem[importance] 0.4: # 只看重要度较高的 self.store.add_memory( contentmem[content], user_iduser_id, memory_typemem[memory_type], importancemem[importance], ) return response.content这里我设置了importance 0.4才写入等于一个简单过滤阀。你可能会问 0.4 这个值怎么来的老实说没有标准答案我在不同项目里试过 0.3 到 0.6太低会写入大量弱相关信息太高会漏掉重要信息。建议你先设 0.4跑几天看写入日志再微调。这套最小实现大概 200 行代码已经能让 Agent 跨会话记住用户的技术栈、偏好和项目信息了。我用它做了一个内部知识库助手效果立竿见影用户第二次问“我之前提到要重构的模块在哪”Agent 能准确告诉他是哪个项目、哪个模块。5. 常见问题与排查技巧实录5.1 检索不到记忆先别急着换数据库这是群里问得最多的问题明明已经写进去了为什么用户提问时检索不到我排查这类问题有一套固定的顺序。先检查 metadata 过滤条件。最常见的情况是user_id对不上写入和检索用了不同的用户标识结果被where{user_id: user_id}过滤掉了。这种问题最坑因为代码逻辑没错纯粹是数据标识没对齐。再查阈值和 top_k。我给自己的项目加过相似度阈值过滤结果发现阈值设太高很多弱相关但有用的记忆被滤掉了。后来我把阈值从 0.3 降到 0.15召回率明显提升。top_k 我目前固定为 5 到 8太小会漏太大会注入太多噪声。最后才怀疑 embedding 模型。如果你检索词和记忆内容描述的是同一件事但用词差异很大比如记忆写的是“前端”相关用户问的是“React 页面”在好的 embedding 模型下应该能召回。如果召回不了大概率是你选的中文 embedding 太弱或者记忆片段被压得太碎、语义丢失了。前者换模型后者调整记忆写入策略。5.2 记忆冲突与更新用户改主意了怎么办记忆系统和真实世界一样用户是会变的。我遇到过的最典型情况用户周一告诉 Agent“我用 Python 做数据分析”周五说“我最近转 Go 做后端了”。如果 Agent 还拿着周一的记忆回答就会闹笑话。处理这个问题的关键是“更新优先于新增”。我在写入新记忆之前会先做一次冲突检测拿新记忆内容去检索同用户的旧记忆如果旧记忆memory_type相同、内容语义高度相似距离小于 0.1就用update操作覆盖旧记忆而不是新增一条。Chroma 里由于文档不可变我的做法是删除旧文档再写入新文档这会多一次写操作但是一次性的量不大不存在性能问题。生产环境用 Qdrant 的话它有原生的 point 更新接口处理更优雅。另外一个容易踩的坑是同一事实的不同表述会变成两条重复记忆。比如“用户喜欢简洁回复”和“用户偏好简短回答”大概率同时入库。仅靠向量去重不够我建议每次写入前跑一次相似度检索相似度大于 0.9 的直接跳过。这会把记忆库的体积增速降下来不少。5.3 记忆膨胀、隐私与安全生产级必须面对的坎记忆系统上线跑一阵子之后你会遇到三个绕不开的问题。第一个是记忆膨胀。用户活跃度高的话长期记忆库会持续增长。检索时如果只按相似度排序很可能把几条“看起来相关但早已过时”的记忆召回来。我的方案是三层时间衰减权重、定期归档冷记忆、设置用户记忆数量上限。超过上限后优先淘汰重要性最低且最久未被召回的记录。第二个是隐私与合规。记忆系统本质上是在“窥探”用户的私人信息。如果 Agent 会被多人使用尤其是企业场景隐私问题非常敏感。我的原则很简单敏感信息密码、身份证、银行卡等一律不写入长期记忆。可以在记忆提取 prompt 里明确加一条规则也可以在写入前用正则或模型检测一次。泄漏一次信任就没了。第三个是记忆的“误导”风险。记忆被错误抽取后会持续污染后续所有对话。比如模型误把“用户吐槽某框架难用”理解成“用户正在使用该框架”之后的所有推荐都会跑偏。我在反馈循环里加了一招记忆召回时同时返回记忆的写入时间让模型判断是否采信。这样即时某条记忆错了也只影响一次回答不会在老路上越走越远。如果你用的是 LangGraph还可以利用它的 Checkpointer 特性做会话状态持久化配合外部向量库做长期记忆。它自带的MemorySaver只适合本地调试生产环境记得换成能持久化的存储后端。我在多个项目里试过短期会话状态交给 Checkpointer长期用户记忆交给独立向量库职责划分清晰代码也好维护。这阵子做下来我最大的感受是记忆系统不是一个“加了就完事”的功能而是一个需要持续调优的模块。从写入过滤、去重策略、检索召回、冲突更新到遗忘机制每一环都有值得抠的细节。好在这套东西的投入产出比非常明确——当你看到 Agent 第二次对话就能准确说出“你之前说过的那个项目”时你会觉得前面那些调参、踩坑都值了。最后再分享一个小技巧记忆写入和召回最好做成异步的把耗时控制在用户无感知的范围。写入异步化尤其重要不能让用户等记忆写完才收到回复。我用的是简单的消息队列把提取和写入丢到后台跑实测体验好了很多。你可以根据自己项目的基建情况用线程池、Celery 或者消息队列都行核心思路是一样的。