ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AI Agent记忆系统实战:短期、长期与工作记忆的架构设计

AI Agent记忆系统实战:短期、长期与工作记忆的架构设计 你有没有遇到过这种情况跟一个AI Agent聊了半天第二天重新打开它完全不记得你是谁、聊过什么、你关注什么你只能耐着性子再从零开始交代一遍。这几乎是今天大多数Agent产品的通病——模型能力很强但Agent没有自我因为它没有记忆。做过一段时间AI Agent开发的人应该都有共识决定一个Agent是演示品还是真助手的分水岭往往不是提示词写得多漂亮而是它有没有能力记住你。在这个系列里前面两篇拆过Agent的基本运行逻辑和工具调用这一篇专门解决记忆这个老大难。我会先把Agent记忆到底该记什么聊清楚再把短期记忆、长期记忆、工作记忆三种形态的架构设计掰开揉碎讲一遍最后给出一套可以直接上手的工程实现方案以及我在真实项目里踩过的坑。内容适合正在做Agent产品的开发者也适合刚看完LangGraph教程、想把自己Demo做出人味的同学。1. 为什么这个系列要专门把记忆拎出来讲1.1 没有记忆的Agent体验天花板是固定的市面上很多Agent Demo跑起来很惊艳你让它查天气、写周报、调API它都能干。但一旦进入真实连续交互问题就来了你跟它说帮我把刚才的方案改一版语气更正式一点它一脸茫然因为它根本不记得刚才的方案是谁。你让它帮你找资料它反问你要找什么你明明十分钟前说过。这种体验用一次就劝退用户嘴上不说心里已经把它归类为玩具。记忆缺失影响的不是某一个功能而是整个交互模式的底层假设。人和人协作之所以高效是因为我们默认对方记得上下文记得你的偏好记得你们上次聊到哪了。Agent如果做不到这一点它和用户之间就永远隔着一层每次都要重新自我介绍的玻璃墙。客服场景更是如此用户报完账号Agent转头就忘那这个客服系统连最基础的可用性都达不到。从技术角度看模型本身是无状态的每次调用就像一次失忆的会谈。你给它多少上下文它就只知道多少关掉请求一切都归零。所以让Agent记住你本质上不是模型能力问题而是工程问题我们需要在模型之外再搭一套记忆系统把散落在会话里的信息捞出来、存起来、在合适的时机再塞回去。1.2 你可能已经踩过的伪记忆坑很多人一开始会觉得记忆还不简单把聊天记录全存下来每次请求全部塞进Prompt里不就行了。我见过不少团队第一版就这么干的包括我自己早期也这么做过。短期跑通没问题一旦对话超过二十轮你会发现三个问题接踵而至。第一是Token成本飞涨长对话的请求体越来越大费用肉眼可见地飙升。第二是响应延迟变长Prompt越长模型处理越慢用户的耐心是有限的。第三最隐蔽——模型其实处理不好超长上下文。业界有个著名的Lost in the Middle现象模型对大段文本中间位置的信息记忆最差。你把50轮聊天全塞进去它真正能用上的可能只有最开头和最末尾的几轮中间的大量信息要么被忽略要么被错误理解。还有一种伪记忆是把数据存进Session或Redis但只存不取。系统重启后Session丢失或者存了一堆JSON但从来没设计过怎么把它拿出来用。我接过好几个外包项目代码里明明有历史记录表Agent却从来不去查。这种属于典型的光存储、无召回本质上还是没记忆。真正有效的记忆系统必须形成写入—索引—召回—注入的闭环缺一环都是白搭。1.3 记忆的三层划分短期、长期、工作做记忆系统之前先把概念理清楚。我习惯把Agent记忆拆成三层跟人脑的结构勉强能对应上短期记忆、长期记忆、工作记忆。这三层各管各的事存储介质和技术方案也完全不同。记忆类型生命周期典型内容核心载体参考实现短期记忆当前会话内最近几轮对话、当前话题、临时指令内存、Redis、滑动窗口消息列表 摘要长期记忆跨会话用户偏好、历史事实、项目背景、知识沉淀向量数据库、关系库Embedding 结构化存储工作记忆任务执行期间任务状态、中间结果、多Agent间的传递数据状态对象、检查点LangGraph State、Checkpointer短期记忆解决的是当前这轮对话别断片长期记忆解决的是下次来我还认识你工作记忆解决的是一个复杂任务在执行途中不要丢状态。三者不是互斥的而是一个完整Agent运行时需要同时具备的底层能力。下面我会按这三层逐个拆解把每层的设计和实现讲透。2. 架构设计三种记忆各管什么、怎么存怎么取2.1 短期记忆Context的滑动窗口与摘要压缩短期记忆最直观的形态就是对话上下文。问题是上下文窗口再怎么大也扛不住无限对话。我的经验是无论模型支不支持128K上下文实际工程中都不要放任上下文无限增长成本、延迟、准确率三座大山压着哪怕到了2026年这条约束依然成立。所以短期记忆的核心策略是滑动窗口加摘要压缩。滑动窗口的意思是只保留最近N轮对话更早的全部摈弃。N怎么定我一般结合场景来普通闲聊型Agent取6到10轮就够复杂任务型Agent可以放宽到15轮超过这个阈值信息增益就开始递减。摘要压缩则是让模型定期对早期对话生成一条结构化摘要把用户说他下周去上海出差想订一家离陆家嘴近的酒店压缩成一句用户下周出差上海偏好陆家嘴附近住宿这样即使滑出窗口关键信息还在。这里有个我踩过的坑摘要不是每次对话都重新生成那样太费Token。合理的做法是每累计10轮或每次滚动清理时把即将被移除的旧消息喂给模型让它生成或更新摘要然后再把摘要放回上下文的顶部。这样既保留了对话脉络又控制了上下文体积。代码实现不复杂后面第3章我会给出完整结构。2.2 长期记忆向量库加结构化双轨长期记忆是整个记忆系统里价值最高、也最容易做砸的部分。它的目标不是记住每句话而是从历史交互中提炼出关于用户的稳定事实和偏好。我倾向于把长期记忆拆成两条轨道并行。一条是向量轨道用来存非结构化的信息比如用户对某个话题的态度、你从对话里抽取出的隐式偏好。这类信息天然是语义型的用户提问往往也不是精确匹配而是你之前是不是聊过某某事这种场景必须靠向量检索才能召回。另一条是结构化轨道用来存高置信、强约束的信息比如用户姓名、年龄、公司、VIP等级、不允许做什么、必须怎么做。这些用JSON或者数据库字段存查询时直接精确命中根本不需要跑向量相似度。为什么要双轨因为纯向量检索有它的脆弱性。第一相似度高的结果不一定是对的第二涉及用户ID、时间、状态这类强约束条件向量库的元数据过滤能解决一部分但复杂逻辑拼接起来很难受第三向量召回结果没法保证100%命中而用户名这种信息一旦召回错了整个交互就彻底翻车。所以我的原则很明确偏好类信息走向量身份和硬约束走结构化两条轨道各自为政在最终注入Prompt时再合并。2.3 工作记忆任务状态与多Agent传递工作记忆这个名字听起来学术其实对应到工程就是任务做到哪一步了。特别是当你用LangGraph这类编排框架做Agent时一个复杂任务会被拆成多个节点从规划、调用工具、到生成结果中间任何一个环节出错或者需要用户确认任务状态都必须被持久化否则用户说一句继续Agent根本不知道从哪儿继续。在LangGraph里工作记忆的载体是Graph的State对象。你定义一个全局状态各个节点往里面读写数据框架负责把状态在不同节点间传递。更关键的是Checkpointer机制它能把State持久化到存储层内存、SQLite、Redis等配合thread_id参数就让一个Graph可以暂停、恢复、继续执行。我经常把thread_id设成用户ID加会话ID的组合这样每个用户的任务状态天然隔离互不干扰。到了多Agent场景工作记忆的含义会再扩大一层。两个Agent协作时Agent A的输出要被Agent B看到除了在编排层手动传递消息更稳妥的方式是维护一个共享的黑板或者消息总线所有Agent都往上面读写。这块如果做得不好就会出现A做完事、B完全不知道的尴尬局面多Agent不但没有提效反而制造了更多沟通成本。3. 实操从零给Agent装上可用的记忆系统3.1 技术栈选型不要一上来就上分布式向量库很多同学一听到长期记忆就着急上Milvus或者自家运维的向量集群我觉得大可不必。选型的核心依据是你的数据量、并发量和对运维复杂度的容忍度。我见过不少项目日活不到一千却搭了三节点Milvus最后运维成本比业务成本还高。按我的实践不同阶段有不同选择。原型验证阶段直接用Chroma或者FAISS就够了Chroma是嵌入式向量库pip装完就能跑数据落到本地文件重启不丢非常适合快速验证思路。到了需要稳定上线的阶段如果团队已经有PostgreSQL优先考虑pgvector直接复用现有数据库少维护一套服务而且事务、权限、备份都现成。只有真正到了日均百万级向量查询、需要水平扩展的时候才值得上Milvus这类分布式方案。方案部署成本适合规模典型场景Chroma极低个人项目、原型本地开发、知识库DemoFAISS低单机内存级离线批量检索、服务内嵌pgvector中中小团队生产已有PostgreSQL的团队Milvus高大规模生产海量向量、高并发查询Embedding模型的选择也很有讲究。英文场景直接OpenAI的text-embedding-3-small便宜且效果好。中文场景我实测推荐开源BGE-M3系列对中文语义的理解比通用英文Embedding好不少而且支持本地部署不需要把数据送到外部接口。多模态场景再考虑CLIP这类专用模型。记住一个经验Embedding模型没有绝对好坏跟你业务的语料越匹配越好所以上线前一定要拿自己的测试集跑一遍对比。3.2 核心代码落地把三种记忆串成一个闭环先看短期记忆的实现。我用一个带摘要的滑动窗口类来管理会话上下文核心思路就是开头讲的保留最近N轮加滚动摘要。from collections import deque class WindowedMemory: 短期记忆滑动窗口 滚动摘要 def __init__(self, max_rounds6, summary_modelNone): self.rounds deque(maxlenmax_rounds) # 只保留最近 max_rounds 轮 self.summary self.summary_model summary_model # 用于生成摘要的 LLM def add(self, user_msg: str, assistant_msg: str): # 当窗口满了最旧的一轮会被自动挤出 self.rounds.append({user: user_msg, assistant: assistant_msg}) def _update_summary(self): # 把即将被挤掉的旧消息和已有摘要合并让模型生成新摘要 old list(self.rounds)[0] # 实际应从被挤出的消息里取 merged f旧摘要{self.summary}\n新消息{old[user]}{old[assistant]} self.summary self.summary_model.summarize(merged) def build_context(self) - str: parts [] if self.summary: parts.append(f[历史摘要] {self.summary}) for r in self.rounds: parts.append(f用户: {r[user]}) parts.append(f助手: {r[assistant]}) return \n.join(parts)注意上面只是个示意结构生产环境里被挤掉的旧消息不会等到build的时候才处理而是在add触发队列淘汰时就立刻去更新摘要避免把已经丢掉的信息再翻回来。再来看长期记忆的向量轨道。我用Chroma做存储核心操作就两个写入记忆、按查询召回记忆。关键是写入时把用户ID写进元数据召回时用where条件做硬隔离这一步极其重要能避免用户A的记忆被用户B检索到。import uuid import chromadb from chromadb.utils import embedding_functions class LongTermMemory: 长期记忆向量化存储 元数据隔离 def __init__(self, path./memory_db, collectionuser_memory, emb_fnNone): self.client chromadb.PersistentClient(pathpath) # 中文场景强烈建议传入 BGE 等中文友好的 embedding 函数 self.emb_fn emb_fn or embedding_functions.DefaultEmbeddingFunction() self.collection self.client.get_or_create_collection( namecollection, embedding_functionself.emb_fn, ) def save(self, user_id: str, content: str, memory_type: str preference): 写入一条记忆。content 是提炼后的句子不要存原始聊天记录。 self.collection.add( ids[f{user_id}-{uuid.uuid4()}], documents[content], metadatas[{user_id: user_id, type: memory_type}] ) def recall(self, user_id: str, query: str, k: int 5, threshold: float 0.6): 召回用户记忆。threshold 是相似度下限低于它的一律丢弃。 result self.collection.query( query_texts[query], n_resultsk, where{user_id: user_id} # 关键按用户做隔离 ) docs, metas, dists result[documents][0], result[metadatas][0], result[distances][0] memories [] for doc, meta, dist in zip(docs, metas, dists): if dist threshold: # 注意Chroma 返回的是距离距离越大越不相似 continue memories.append({content: doc, type: meta[type], score: 1 - dist}) return memories这段代码里最容易被忽略的是非结构化偏好与结构化约束的分流我的做法是LongTermMemory只存偏好、习惯、经历这类模糊信息而用户姓名、邮箱、禁用词、强制规则绝不进向量库单独存到一个JSON或者关系表里。我见过有人把所有记忆一股脑灌进向量库结果问用户名字时召回了三条互相矛盾的结果场面十分尴尬。所以结构化轨道虽然代码只有几十行但它兜住了Agent的底线。3.3 记忆召回与Prompt注入的策略细节记忆存进去只是第一步真正决定体验的是什么时候召回、召回之后怎么用。以我自己的实践执行策略可以总结成三条提前召回、按需选择、显式注入。提前召回是指每次收到用户新消息时先拿这条消息去检索长期记忆而不是等模型自己想起什么。按需选择是指召回回来的TopK条记忆不能全部塞进Prompt要先做一步过滤去掉相似度太低的、过于陈旧的、跟当前话题无关的。显式注入则是指记忆在Prompt里要有明确标记比如用用户档案或已知偏好这样的标签让模型明确知道这些是可信背景而不是聊天过程中的临时发言。Thats still not enough — 你还要考虑记忆的生命周期。不是所有记忆都值得永远保留。我通常会为每条记忆附带时间戳召回时做时间衰减三个月前的偏好权重明显低于本周的行为。另一个细节是更新闭环用户说我最近开始吃素了那么之前的用户喜欢吃牛肉火锅这条旧记忆应该被标记为过期或者直接删除。我的实现是在保存新记忆时先用同一query去检索旧记忆如果命中且内容冲突就删旧迎新。这个逻辑虽然简单却能让长期记忆保持干净。最后Prompt注入的模板也建议固定下来。下面是我常用的一个格式你是用户的人工智能助手。 [用户档案] - 偏好简洁回答必要时给要点列表 - 关注AI工程化最近在调研记忆系统 [最近对话摘要] 用户正在写一篇关于Agent记忆的技术文章希望获得工程实践层面的建议。 [当前对话] 用户: ... 助手: ...模板里用户档案和最近对话摘要就是记忆系统的输出其他部分是固定系统提示词。这样一层一层区分清楚模型不会把记忆和当前指令搞混。我在最初版本里把记忆和指令混在一起结果模型把某条历史偏好当成用户当下的指令执行了闹出过笑话。分层写好这个问题基本就绝迹了。4. 上线之前踩坑经验与调优清单4.1 上下文爆掉与成本飙升记忆系统上线后第一个常见的坑就是上下文被撑爆。症状很典型先是请求报Token超限然后是账单环比上涨再是用户反馈越聊越笨。根因基本都是短期记忆没有做窗口约束或者做了但是窗口开得太大。解决办法除了前面说的滑动窗口加摘要我还要补一个心得给组合Prompt设定一个总预算。比如如果模型单次上下文上限是32K我会把预算拆成三块——系统提示词占2K记忆注入占10K当前对话占20K。任何一部分超了都要做截断或压缩。这个预算要在代码里做成配置项而不是写死在逻辑里。我踩过的坑是自认为反正模型支持200K结果成本翻了五倍准确率反而下降了后来老老实实做了Token预算管理。另一个容易被忽视的成本点是每次请求都会携带长期记忆的召回结果。要控制召回条数TopK别超过5条而且每条记忆最好限制在几十个字以内。我在一次优化中把召回条数从10减到4效果几乎没有变化成本和延迟直接砍了一半。搜索是个高频动作任何多余Token都会被放大。4.2 召回不准chunk与embedding的双重影响长期记忆召回不准是吐槽重灾区。用户明明聊过某件事Agent却像失忆一样想不起来。我排查这种问题通常按两步走。第一步看embedding模型和语言的匹配度。我早期用OpenAI的embedding处理大量中文记忆效果一直不理想后来换成BGE-M3之后同类查询的召回准确率立竿见影地提升。这不是说OpenAI不行而是中文语义和英文语义空间差异很大用英文为主训练的模型去编码中文很容易把细微语义差别抹平。第二步看chunk方式。很多教程会让你按固定长度切文本比如512个字符一段。这个做法在文档知识库场景里勉强能用但用在用户记忆场景就很别扭。一条记忆应该是一句完整的话或者一个完整的事实而不是被拦腰截断的字符串。我的做法是让模型先做一次记忆提取把用户说他对某某话题很感兴趣提炼成一句几十字的短句再去做向量化。这样做的好处是召回时命中率极高因为语义粒度精准匹配了查询粒度。还有一个我最近实践的技巧给记忆做多标签。除了存文本本身我再让模型打两到三个标签比如偏好-编程语言-Golang、事件-出差-2026-03。标签既作为元数据存起来可以用于过滤也可以拼到向量文本里去增强语义。实测下来带标签的记忆召回效果比纯原始文本平均提升一截。4.3 记忆串台隔离不是可选项记忆串台是记忆系统里最严重的安全事故比召回不准可怕得多。想象一下用户A问我的银行卡要到期了帮我换绑结果Agent把用户B的银行卡信息答出来了。这种事故一次就能让产品彻底失去信任。串台的原因几乎都是同一个信息服务层没有做用户维度隔离。不管是Chroma的where条件、关系库的查询SQL还是Redis的key设计都必须在最底层强制拼上用户ID。我见过最隐蔽的串台是发生在Embedding层面查询向量时没有过滤条件整个集合一起算相似度结果把别的用户内容也召回了。这个bug在开发环境很难发现因为测试数据少、感觉不出来一上生产、数据量一上来就爆雷。我现在的做法是双保险。第一道保险是存储层隔离所有记忆写操作都带user_id所有读操作都强制where user_idxxx。第二道保险是应用层校验召回结果回来后再在代码里做一次user_id比对不一致的直接丢弃并记录告警日志。两道保险一起上基本就堵死了串台的可能。除此之外涉及明文隐私的数据手机号、身份证、银行卡我建议根本不要进记忆系统即使用户提过也只在当次会话里使用不落库。4.4 量化评估怎么才算真正记住了记忆系统没有评估指标的话就永远在感觉还行和感觉不对之间徘徊。我给自己的项目搭了一套比较轻量的评估方法不需要太高深的框架但能真实反映记忆效果。离线评估阶段我会准备一组测试样本每条样本包含三件事一个用户查询、一份正确的记忆集、一份干扰记忆集。然后跑召回看正确记忆有多少出现在TopK里。指标我一般看两个RecallK正确记忆被召回的比例和记忆列表的准确率召回来的结果里正确的占比。这两个指标能帮我判断该调embedding模型还是该调K值。在线评估阶段我更关注两类硬指标。第一类是任务完成率比如客服Agent的场景里用户是否在一次会话内解决了问题第二类是再次提及率也就是用户下一次会话里有没有引用了之前聊过的内容。如果一个记忆系统上线之后用户开始说按我之前说的那种风格来写而不是又重新描述一遍需求那就说明记忆真正起作用了。这类行为变化虽然没法自动化得很精准但做一轮用户回访或者日志抽read就能判断方向对不对。5. 进阶用MCP协议把记忆能力服务化5.1 MCP在记忆场景里到底解决什么问题如果你的Agent只有一个前面那套方案完全够用。但在实际业务里一个完整的Agent产品往往有多个入口网页对话、客服机器人、文档助手、甚至内部的编程辅助Agent。每个入口各带一套记忆用户就会陷入在网页端告诉过它的事到文档助手那儿它又不知道的割裂状态。MCPModel Context Protocol就是来解这个问题的。它本质上是把模型与外部工具、资源、上下文能力之间的交互标准化。放到记忆场景里我们可以把记忆能力封装成一个MCP Server对外暴露统一的读记忆、写记忆、删记忆接口任何支持MCP的客户端和Agent都能通过同一套协议接入。这样记忆服务就成了一个独立的基础设施各个Agent共享同一个记忆大脑。这套设计还有一个额外的好处记忆服务和业务逻辑完全解耦。对话Agent、客服Agent、文档Agent各自升级都不影响记忆层的稳定。我现在的项目就是这么做的记忆模块单独部署其他Agent通过MCP工具调用它职责边界非常干净。5.2 一个能跑起来的Memory MCP Server骨架使用MCP生态的SDK我们可以快速封装一个记忆服务。下面是一个简化的骨架核心是暴露两个Toolmemory_get和memory_put。# memory_mcp_server.py import json from mcp.server import Server from mcp.types import Tool, TextContent server Server(memory-server) server.list_tools() async def list_tools(): return [ Tool( namememory_get, description根据用户ID和查询词召回用户的长期记忆, inputSchema{ type: object, properties: { user_id: {type: string}, query: {type: string}, k: {type: integer, default: 5} }, required: [user_id, query] } ), Tool( namememory_put, description写入一条用户长期记忆, inputSchema{ type: object, properties: { user_id: {type: string}, content: {type: string}, memory_type: {type: string, default: preference} }, required: [user_id, content] } ) ] server.call_tool() async def call_tool(name: str, arguments: dict): if name memory_get: memories long_term_memory.recall( user_idarguments[user_id], queryarguments[query], karguments.get(k, 5) ) return [TextContent(typetext, textjson.dumps(memories, ensure_asciiFalse))] elif name memory_put: long_term_memory.save( user_idarguments[user_id], contentarguments[content], memory_typearguments.get(memory_type, preference) ) return [TextContent(typetext, textok)]这个骨架往深了做还可以把记忆更新和记忆过期也暴露成工具让Agent具备遗忘能力。遗忘听上去反直觉但一个只进不出的记忆系统时间长了一定被垃圾信息淹没。能删、能改、能遗忘才是健壮的记忆系统。热词里频繁提到的mcp协议与ai agent开发正是这个方向编排层走MCP记忆层也走MCP整个Agent生态才能像乐高一样自由拼接。5.3 场景延伸把知识库变成Agent的长久记忆最后一层扩展把个人知识库接入记忆系统。很多人问Obsidian加AI Agent知识库怎么玩思路其实很清晰把笔记库当作Agent的长期记忆源文档就是结构化的长期事实对话历史则是动态记忆两者在召回阶段合并。你可以用MCP把笔记检索封装成工具写作助手在回答时既参考你历史对话的偏好又检索你的笔记内容产出的建议会明显更贴合你的思考方式。前端AI辅助编程也是同样的逻辑。好的编码Agent不能只懂通用代码规范它还应该记住你项目的架构约定、你偏好的代码风格、你踩过哪些坑。把这些信息沉淀成项目级记忆再通过MCP暴露给Agent它给出的代码建议会从通用正确进化成更懂这个项目的正确。这也是为什么现在Skill和Agent的组合越来越流行——Skill负责封装具体能力记忆负责让能力真正长在上下文上。再多说一句实操建议知识库进记忆系统之前先做章节化和清洗把临时笔记、日志、无关摘录过滤掉否则检索噪声会非常高。我自己用BGEM3加Chroma跑了几万条笔记的检索效果基本能满足日常写作查找需求。这块没有太多玄学核心就是做好文本质量再配一套稳定的召回管道。我个人在实际项目里最大的体会是记忆系统是一个越用越值钱的慢变量。刚开始做的时候你可能感觉不到它有多大作用觉得把Prompt写漂亮点效果也差不多。但只要积累足够多的真实交互数据记忆带来的体验差异会越来越大。用户会明显感觉到这个Agent懂我这种信任感是任何花哨功能都换不来的。给想入手的同学一个建议从短期记忆开始把会话体验做顺再逐步叠加长期记忆和工作记忆不要第一步就追求大而全。先让Agent记住你再让它越来越懂你。
返回列表