AI Agent记忆系统设计与实践:从上下文窗口到长期记忆

AI Agent记忆系统设计与实践:从上下文窗口到长期记忆 做AI Agent有一段时间了我自己踩过最大的坑不是模型选型不是Prompt调优而是这玩意儿记性太差。你跟它聊完一轮关掉窗口再打开它就像被格式化了硬盘一样完全不认识你。你偏好简洁还是详细的回答你负责哪个项目你上周提到的那个需求已经改了三版——全忘光了。这几乎成了所有Agent落地项目里最让人挠头的问题。前期我带着团队做一个内部知识库助手模型能力完全够用RAG管道也跑通了但用户反馈始终差口气。后来复盘才发现用户真正抱怨的不是“答得不对”而是“它不记得我投过哪些文档、不记得我上次问过什么、不记得我明确说过不要它推送哪类内容”。一句话缺了记忆。这篇是“走进AI Agent”系列的第三篇专门聊怎么让Agent记住你。我会把记忆的类型、架构设计的思路、基于LangGraph的实操做法以及我在生产环境里踩过的坑一次性讲透。适合正在做Agent落地的开发者和技术负责人也适合那些刚把Agent调通、正准备往工程化方向推进的朋友。1. 先搞清楚Agent到底需要哪几种记忆1.1 三层记忆模型别用一套方案解决所有问题很多新手做Agent记忆上来就怼一个向量数据库让模型把所有聊天记录都存进去然后每次对话都检索一遍。这个思路不能说错但粒度太粗效果也差。真正在生产环境里能跑得稳的记忆系统至少要拆成三层。第一层是短期记忆也叫工作记忆指当前会话内需要保持的信息。比如用户前一句话引用了上一句话里的某个对象“它”指代的是什么这就是工作记忆的范畴。传统做法是把最近几轮对话塞进Prompt的context窗口简单直接但受限于模型上下文长度撑死几百轮窗口一满就自然遗忘。第二层是长期记忆指跨会话需要持久化保存的用户画像、偏好、历史决定这类信息。比如用户是后端工程师、习惯Python、喜欢代码里带注释、曾明确说“不要在答案里出现JSON”等等。这些信息需要写进数据库下次新会话开始时再加载到系统提示里。第三层是情景记忆也叫情节记忆指具体发生过的事件和历史交互。比如“上周三用户在订单模块问过权限问题”这属于情景记忆它回答的是“发生了什么”而不是“用户是谁”。情景记忆和长期记忆的区别在于前者侧重事件本身后者侧重从事件里提炼出来的稳定特征。这三层记忆各司其职技术选型也完全不同。短期记忆靠上下文窗口管理长期记忆靠结构化存储加向量检索情景记忆则更依赖时序性的检索和摘要机制。如果你试图用一个缓存池解决所有问题很快就会发现要么检索出来的东西太杂要么关键信息被噪声淹没。1.2 场景驱动没有记忆的Agent根本没资格谈体验记忆不是所有Agent的刚需但只要你做的产品需要“连续性服务”记忆就是生死线。我列举几个最常见的场景你可以对照看自己是不是已经踩在记忆缺失的坑里了。第一类是个人助理型Agent。比如日历管理、邮件代写、日程提醒这类Agent的用户粘性完全建立在记忆之上。用户上周明确说过“每月25号提醒我查绩效”结果这周它连你公司做没做过绩效这件事都忘了体验直接崩塌。第二类是开发辅助型Agent。越来越多的团队在尝试用Agent做代码审查、需求拆解、测试生成这类Agent必须记住项目的架构约定、技术栈选型、代码风格。你让它生成一个接口它连你们团队用的ORM是什么都不知道生成出来的代码根本没法看。第三类是营销和客服型Agent。用户可能跟你聊过“我是会员”“以前投诉过物流问题”如果Agent把这些信息忘了每次都从头问一遍用户会感受到极大的割裂感。这些场景有一个共同的痛点单轮问答本身不难难的是把上下文、偏好、历史事件串联起来形成一个持续演化的用户画像。很多Agent项目Demo阶段看着惊艳一进生产就露馅原因不是模型弱而是记忆这块根本没搭起来。1.3 记忆和上下文窗口是有本质区别的这里必须澄清一个概念误区扩展上下文窗口不等于增强记忆。现在很多模型把上下文窗口越做越长128K、200K甚至百万token的都有。于是有人觉得那我直接把全部历史对话都塞进窗口不就行了理论可行但工程上非常不现实。第一成本。每次请求都携带全部历史token费用指数级上涨。第二延迟。大上下文输入的首字响应时间会明显变慢用户等不起。第三效果。研究早就表明模型对中间位置的上下文注意力会衰减也就是著名的“lost in the middle”现象塞得越多关键信息反而越容易丢。更合理的思路是上下文窗口只保留当前任务相关的信息记忆系统负责筛选和提炼。窗口是工作台记忆是仓库。你需要的是一个机制把仓库里跟当前任务相关的东西搬到工作台上而不是把整个仓库搬到工作台上。2. 记忆架构设计存什么、怎么存、怎么取2.1 给记忆打标签从一团乱麻到结构化设计记忆系统的第一步不是选数据库而是定义记忆的结构。我建议你从三个维度给每条记忆打标签。第一个维度是记忆类型分为user_profile用户画像、conversation_history对话历史、task_progress任务进度、domain_knowledge领域知识。第二个维度是重要程度比如critical、normal、low重要程度直接决定这条记忆是否要永久保存。第三个维度是时间属性包含创建时间、最后访问时间、过期时间时间属性是遗忘机制的基础。举个例子用户说“我是一名全栈工程师后端用Go”这条记忆的类型是user_profile重要程度是critical不需要过期。用户说“今天我把订单模块的权限问题修好了”这条记忆的类型是task_progress重要程度是normal可以设置一个九十天的过期时间。用户说“帮我看看这段代码”这条记忆基本不用存属于一次性交互。有了标签体系你才能在后面对记忆做差异化处理。不分层、不打标一股脑全塞进向量库那是给自己埋雷。2.2 存储选型向量库不是万能药记忆的存储选型要按记忆类型分开看切忌一刀切。用户画像这类结构化很强的记忆我建议用传统关系型数据库或KV存储比如PostgreSQL、Redis。原因很简单这类记忆字段明确、更新频繁、需要精确查询和覆盖写。比如用户偏好你总不能靠向量相似度去检索吧直接根据user_id查出来放在系统Prompt里效率最高。对话历史这类非结构化记忆才适合用向量数据库比如Milvus、Qdrant、Chroma。你需要把文本embedding成向量存进向量库查询时用相似度检索出相关片段。这里要特别注意embedding一定要选好模型不同的模型对长文本、中文、代码等内容的编码能力差异很大。我实测下来OpenAI的text-embedding-3-small处理中文够用但不够精细如果想更稳可以考虑BAAI的bge系列或者国内厂商开源的embedding模型在中文语义上常常表现更好。还有一种混合存储用得越来越多主数据放关系型数据库文本片段放向量库两边通过记忆ID关联。这样你既可以用SQL做精确过滤又可以用向量做语义召回两条路都能走通。我建议任何记忆数据先写主数据库再异步同步到向量库别让向量库当唯一事实来源。一个原因是向量库的写入最终一致性做得不够好另一个原因是向量检索本质是近似匹配不适合做精确校验。2.3 写入策略不是所有对话都值得记住记忆系统设计里最容易被忽略的问题是写入策略。很多人的做法是“先全存起来再说”但工程上的教训是存得越多后面检索的噪声越大。我给团队定过一条规则不重要的对话不写长期记忆。什么算重要两个标准。第一信息具备复用价值比如用户偏好、身份信息、明确的决定、长期有效的要求。第二信息有状态变化比如用户把项目从React迁移到了Vue这时候要更新旧的记忆而不是新增一条。实现上有几种思路。一种是基于规则和关键词命中“我叫”“我喜欢”“我倾向于”“请记住”这类模式时触发记忆写入。另一种是基于大模型判断每次对话结束后让Agent自己判断这轮对话里有没有值得长期保存的信息有的话生成一条结构化记忆。后一种更智能但需要额外一次模型调用成本略高。我见过一个不错的折中方案先按规则粗筛一遍把明显不该存的内容过滤掉剩下模糊的交给模型做二次判断。这样既控制了成本又保证了写入质量。3. 基于LangGraph实现记忆模块一套能直接跑起来的方案3.1 整体流程感知、提炼、存储、检索、注入LangGraph是目前做Agent编排很顺手的一套工具它对状态管理、循环控制、持久化都有原生支持特别适合承载记忆系统。我推荐的记忆模块整体流程分五步。第一步感知。在对话过程中识别哪些信息属于可记忆信息。这一步我放在Node内部用一个轻量级函数做关键词判断命中候选再调用模型提炼。第二步提炼。调用大模型把原始对话内容压缩成一条结构化记忆。比如用户说“忘了说了我们后端统一用Go别再给我写Java示例了”提炼出来的记忆就是一个JSON包含type字段是preferencecontent是“后端使用Go示例代码请用Go”importance是critical。第三步存储。把结构化记忆写入主库和向量库。注意顺序先写主库拿到memory_id再写向量库并回填memory_id。第四步检索。新会话启动时或者每轮对话开始时根据当前上下文检索相关记忆。检索策略我会在后面详细展开。第五步注入。把检索到的记忆按一定格式嵌入到系统Prompt里告诉Agent“这是你之前了解到的用户信息回答时请参考”。3.2 短期记忆用LangGraph的原生Checkpointer搞定LangGraph对短期记忆的支持很成熟核心是Checkpointer机制。它的作用是保存图的运行状态让你可以在任何节点恢复执行。对于对话型Agent来说这就意味着每一轮对话的中间状态都可以持久化下次从断点继续。我用的Checkpointer是SqliteSaver和MemorySaver搭配MemorySaver处理当前会话内的高速存取SqliteSaver负责跨会话的持久化。配置起来很简单你只需要在编译图的时候传入checkpointer参数。from langgraph.graph import StateGraph from langgraph.checkpoint.sqlite import SqliteSaver builder StateGraph(AgentState) # ... 添加各个节点和边 ... checkpointer SqliteSaver.from_conn_string(agent_memory.db) graph builder.compile(checkpointercheckpointer)这样之后每次调用graph的时候带上一个thread_idLangGraph就会自动把该thread的状态存下来。同一个thread_id再进来时短期记忆自动恢复。用这个当短期记忆的底座比自己在Redis里存对话列表要省事得多图的内部状态都能被恢复而不只是对话文本。3.3 长期记忆向量化存储与语义检索实操长期记忆的核心是向量化存储加语义检索。我直接给出一个可用的实现参考。先定义记忆模型。我用Pydantic约束结构保证写入的数据规整。from pydantic import BaseModel from typing import Optional class MemoryItem(BaseModel): memory_id: str user_id: str memory_type: str # user_profile / task_progress / preference / ... content: str importance: str # critical / normal / low created_at: Optional[str] None expires_at: Optional[str] None写入流程分成几步。第一把content字段用embedding模型转成向量。第二向量写入向量库同时JSON写入主库我用PostgreSQL。第三在主库里记录向量ID方便后续删除和更新。import postgresql_client import chroma_client def save_memory(memory: MemoryItem): # 1. 生成embedding embedding embed_text(memory.content) # 2. 存入向量库拿到向量ID vector_id chroma_client.add( collectionuser_memory, idmemory.memory_id, embeddingembedding, metadata{user_id: memory.user_id, memory_type: memory.memory_type} ) # 3. 存入主库 memory.vector_id vector_id postgresql_client.insert_memory(memory)检索的时候把当前用户的那句话embedding之后去向量库做top-k相似度搜索过滤条件加上user_id和memory_type。这里有一个关键技巧检索范围一定要限制在user_id维度内否则会出现A用户的历史记忆被检索给B用户看的情况很尴尬。def retrieve_memory(user_id: str, query: str, top_k: int 5): query_embedding embed_text(query) results chroma_client.query( collectionuser_memory, query_embeddings[query_embedding], n_resultstop_k, where{user_id: user_id} ) return [item for item in results[documents][0]]检索时机上我建议双触发一个是会话启动时把用户画像级别的记忆全部加载进Prompt另一个是每轮用户输入后把当前疑问作为query做一轮语义检索把命中的记忆追加进来。3.4 混合检索策略标签过滤加语义召回的联动实测下来纯靠语义检索效果会时好时坏。原因在于embedding模型对相似语义的捕捉能力虽然强但它不擅长精确匹配。比如用户说过“我不喜欢代码里有TODO注释”你问“代码注释方面有什么要注意的吗”语义上可能检索不到那条记忆因为“TODO”和“注释”的语义距离不是最近。所以我推荐混合检索策略。一路走精确匹配按user_id加memory_type加关键词做SQL过滤另一路走语义检索按embedding相似度召回。两条路的结果合并去重后按importance排序取前N条注入Prompt。def hybrid_retrieve(user_id: str, query: str, top_k: 5): # 1. 关键词精确检索 keyword_hits postgresql_client.query_memory( user_iduser_id, keywordsextract_keywords(query) ) # 2. 向量语义检索 semantic_hits embed_query_retrieve(user_id, query, top_k) # 3. 合并去重按重要程度和时间排序 merged merge_and_rank(keyword_hits, semantic_hits) return merged[:top_k]这一套看起来不复杂但它解决的问题非常实际。有些记忆适合用语义找有些适合用规则找混合起来才能覆盖住真实场景里千奇百怪的查询方式。注入Prompt时也有讲究。我建议用清晰的XML或Markdown标签把记忆区和其他上下文区隔开并明确告诉模型这部分信息的来源。比如memory 以下是你在过去的对话中了解到的用户信息请优先参考 - 用户是后端工程师主要语言为Go - 用户明确表示不需要代码中包含TODO注释 - 用户当前正在推进订单模块的权限重构期望在月底前上线 /memory这样模型就知道这些是历史事实而不只是当前的闲聊内容回答时会主动对齐。4. 记忆的更新、遗忘与隐私比想象中更重要4.1 记忆冲突处理新信息要不要覆盖旧信息记忆系统跑到一定阶段一定会遇到一个问题同一件事用户前后说了矛盾的信息。前几天还说后端统一用Go今天又说新项目改用Rust怎么处理我的方案是分类型处理。用户画像类记忆新信息直接覆盖旧信息但要保留历史版本用于追溯。偏好类记忆如果冲突出现不要自动覆盖把这个冲突提到用户面前确认。任务类记忆则基本不冲突因为它带时间维度新旧并存没有关系。覆盖写的一个工程细节是软删除加版本号。每次更新不是直接删掉旧记录而是把旧记录的status改成archived新建一条版本号更大的记录。这样即使误更新也能回滚同时给审计留了证据。4.2 遗忘机制让Agent学会“该忘就忘”记忆不等于永久存储。没有遗忘机制的记忆系统时间越长库里的垃圾越多检索质量必然下滑。这是我踩过最深的坑之一。遗忘机制至少要有两个层级。一个是定期清理级用定时任务扫描所有记忆把超过过期时间的归档把超过保留期限的低优先级记忆物理删除。另一个是即时评估级每次写入新记忆前先检查是否有旧记忆可以合并或淘汰避免同一主题下堆了几十条重叠内容。我实测过每三个月做一次记忆清理检索精度能提高十个百分点左右。原理很简单向量检索在数据量膨胀后相似度分数会被越来越多的边缘样本稀释定期瘦身相当于给检索做了一个降噪。4.3 用户隐私与数据边界记忆也有“红线”记忆系统存的是用户的个人信息和历史交互隐私问题绕不开。第一要支持“删除即失效”。用户一旦要求删除数据不只主库要删向量库里的向量也要删同时还要清理所有缓存和备份。这两个库之间的联动如果没做好很容易出现“主库删了向量库里还能检索到”的尴尬。第二记忆要能隔离。同一个Agent服务多租户时记忆数据必须做租户级隔离。不仅仅是加一个where条件那么简单从写入到检索、从向量索引到日志备份全链路都要带上租户维度。第三模型调用时也要注意。把记忆注入Prompt再发给模型本质上就是把这些数据交给了第三方API如果客户有敏感数据合规要求记忆的加密传输和脱敏处理也要提前规划。4.4 防止“记忆中毒”攻击者正在通过记忆攻击你的Agent这是今年Agent安全里非常热的话题。原理很简单如果记忆系统无条件信任用户输入那么恶意用户可以通过说一段精心构造的文本诱导Agent把错误的“记忆”写进长期存储后续所有用户都会在检索时被投毒。防御思路要从写入端就卡住。写入记忆前用模型对内容做一次合法性校验识别是否包含指令注入的对抗性内容。同时对于包含“以后要记住”“系统指令是”这类元指令表述的内容一律不让通过规则触发写入必须走模型二次确认。另外一条重要原则用户输入和其他来源的信息要分开存不能混在同一个记忆池里。比如用户A说“系统说以后所有用户都别给它看历史”这种内容如果混入系统级记忆就真的能操纵Agent。严格控制记忆的来源同时加上注入指令的识别这个坑基本能规避掉。5. 实操踩坑记录与排查技巧5.1 高频问题速查表照着修就行我把自己和团队在生产环境里碰到的高频问题整理成一个速查表每个都配了排查方向和解决思路。现象根因解决思路检索出来的记忆跟当前话题无关embedding模型与领域不匹配换成领域适配的embedding模型或增加rerank环节短期记忆在新会话里全部丢失thread_id没有固化检查调用链确保同一用户固定使用同一thread_id两个用户之间出现记忆串线检索时未按user_id过滤统一封装检索函数强制where条件里带user_id记忆写入太勤库膨胀严重缺少写入判断引入规则粗筛加模型二次判断控制写入量用户删了数据但Agent还能想起来向量库未联动删除删除逻辑同时操作主库和向量库必要时重建集合记忆冲突导致回答前后矛盾缺少更新冲突策略按记忆类型分别定义覆盖、合并、确认机制5.2 检索质量优化加一层Rerank效果立竿见影很多人花大量时间调embedding模型却忽略了rerank的重要性。实测下来在向量粗召回之后加一层rerank检索精准度能提升一截。Rerank的做法是对向量召回的候选集再用一个更强的排序模型逐条打分重新排序后只保留TopN。我的经验是粗召回取20条rerank之后保留5条效果比直接取向量相似度最高的5条好很多。常见的rerank方案有bge-reranker系列开箱即用。另一个优化手段是时间衰减。给记忆检索结果加一个时间衰减系数同样的相似度下更新鲜的记忆排在前面。代码上实现就是在排序分数上叠加一个指数衰减函数。5.3 成本与性能平衡记忆不是越密越好嵌入和检索都有成本记忆系统设计时要有成本意识。首先embedding调用尽量做成批量。一批会话结束后的记忆写入攒到一起批量embedding比逐条调用节省很多时间和费用。其次不是每条检索都要打向量库。对于会话启动时需要加载的用户画像类记忆可以直接从主库按user_id查出来压根不需要向量检索。向量库只在需要语义召回时才出场。第三慎重选择要embedding的内容。长文本的embedding费用和存储开销都比重写入前先做个摘要把几百字的对话浓缩成一两句话再embedding。这样既保证检索精度又控制成本。最后再说两句我们在一个内部工具Agent上把整套记忆系统跑通后最大的感受是Agent的体验一下子就“立住”了。用户不再觉得对面是个冷冰冰的问答机器而是一个真的跟你在同一个项目里干活、记得住前因后果的同事。这个感知层面的落差远比模型回答质量的几分别误差值更明显。如果你正在做自己的Agent我建议从最简单的两步开始第一步用LangGraph的Checkpointer把短期记忆接上让同一个用户能连续对话不出戏第二步把用户画像级别的关键信息落到结构化存储里每次新对话加载到上下文。这两步做完体验就已经超过大半没有记忆的Agent了。我个人在实际操作中还有一个习惯给每一条记忆留一个“来源引用”记录它来自哪一轮对话。排查“Agent为什么记住了这个奇怪结论”的时候这个字段能帮你省下大量的排查时间。选型方案可以迭代但记忆审计这件事越早做越值。