ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

让Agent记住你:AI Agent记忆分层与持久化实践

让Agent记住你:AI Agent记忆分层与持久化实践 你有没有遇到过这种情况跟一个 AI Agent 聊得正起劲它前一秒还言之凿凿地分析你项目里的问题后一秒换个话题就把你十分钟前交代的背景信息忘得干干净净。你只好耐着性子把“我叫什么”“我在做什么项目”“我偏好什么风格”从头再说一遍甚至说完了它还能再犯同样的错。这不是模型智商的问题而是 Agent 的“记忆”没做对。今天这篇《让 Agent 记住你》核心关键词就两个AI Agent和Agent Memory。我会从记忆分层开始到手把手教你用 Redis 和向量数据库存记忆再把多智能体框架的记忆设计捋一遍最后附上我自己踩过的一堆坑。这篇偏向实操适合已经在做 Agent 开发、或者准备从 Demo 往正式系统迁移的朋友没有基础的读者也能跟着逐步复现。1. 为什么总说“Agent 需要记忆”先看一个真实对话1.1 无状态 Agent 的“社交尴尬”我拿一个实际发生过的场景来说。你让 Agent 当你的“项目助理”第一轮你说“我叫王强数据部门负责人正在推进一个用户增长项目核心指标是次月留存率目标从 12% 提到 18%。”Agent 回复得非常好给了几个增长实验建议。第二轮你问“那我把预算从 20 万加到 50 万你帮我重新算一下优先级。”Agent 也能答——它当前这一轮的上下文里还带着你的话。但第三轮你突然切换话题问“我上次说的项目你觉得第一个实验应该怎么设计”如果这个 Agent 没有记忆机制它只能回复一句“抱歉我没有找到关于您项目的更多信息”。你第一轮说的名字、部门、指标、预算全部丢失了。这就是典型的无状态 Agent 现象。你可能会说LLM 不是有上下文窗口吗几十万 Token 还不够记吗这就要说到本质了。1.2 大模型的“天生失忆”大模型本质上是一个无状态函数。每一次调用模型都不记得上一次调用发生过什么。它给你的回答只依赖这一次传入的 messages 参数——系统提示词、历史对话、用户问题仅此而已。所谓“上下文窗口”更像是办公桌上的一张白纸。窗口大白纸就大你可以在上面写更多信息但这张白纸仍然是一次性的对话结束、进程重启、会话切换白纸就被扔掉了。所以你会看到三个在实际项目中非常头痛的问题Token 成本失控每次把完整历史全部塞进 messages调用越长价格越贵甚至超过预算。响应延迟上升上下文越长模型处理时间越长用户等得越不耐烦。关键信息稀释一万条琐碎对话里混着一条关键需求模型可能根本抓不住重点。我见过不少团队一开始图省事把所有对话记录都拼接进提示词结果上线两周账单爆炸用户还是抱怨“它记不住我说过的话”。原因就在于他们混淆了“临时上下文”和“真正记忆”的区别。所以接下来我们要把记忆这件事做成分层设计。2. 给 Agent 的记忆分个层Context、工作记忆与长期记忆2.1 记忆四层模型早期讨论 Agent 架构的文章里大家普遍把记忆拆成 4 层这个分法到今天依然很实用记忆层次生命周期典型实现类比上下文记忆Context单次请求内messages 数组、系统提示词临时便利贴工作记忆Working Memory单次会话内Session 缓存、Redis办公桌上的草稿纸长期记忆Long-term Memory跨会话、跨天向量数据库、SQLite笔记本外置知识库External Memory长期且结构化RAG 文档库、企业 Wiki图书馆书架这四个层次之间的关系像极了人脑的记忆机制你处理眼前任务时用的是工作记忆但那些重要的、需要长期记住的事情会经过编码固化成长期记忆。Agent 也一样。2.2 每一层到底存什么先说上下文记忆。这一层是模型自带的能力你把对话历史塞进 messages 就行适合存当前这轮对话中必须用到的“即时状态”。它的问题是脆弱一旦请求结束什么都没了。再说是工作记忆。它的作用是跨越用户同一次会话里的多次请求。用户在界面上跟你连续聊天每句话都是一次 API 调用但会话 ID 可以帮你把这些调用串起来。工作记忆一般放在 Redis 里key 是 session_idvalue 是对话消息列表或状态对象。它的特点是读写快但容量有限而且宕机就丢。然后是长期记忆。这里才是“让 Agent 记住你”的关键。用户第二次进系统、明天再回来、下个月再回来Agent 还能记得他的名字、偏好、历史结论和项目背景。长期记忆的核心诉求是跨会话持久化而且要在海量信息里快速找到最相关的一部分所以通常使用向量数据库。它保存的不是原始对话流水账而是从对话中提炼出来的“事实条目”比如“王强数据部门负责人关注次月留存率”。最后是外置知识库。很多团队把它和长期记忆混在一起其实有区别。外置知识库通常是文档类的确定性知识比如企业制度、产品手册、行业报告而长期记忆是用户的个性化信息。两者都可以用向量检索但来源、更新机制、权限控制都不一样架构上应该拆开。2.3 各层怎么协作我设计记忆系统时会给它定一条清晰的“数据流向”用户每次发起请求Agent 先从长期记忆里把与该用户相关的记忆片段捞出来再结合工作记忆中的会话上下文组装成当前请求的 messages交给大模型。对话结束后由一条异步管线决定哪些新信息值得写入长期记忆。打个比方长期记忆是“你记得的关于这个用户的一切”工作记忆是“这次聊天聊到哪了”上下文记忆则是“模型眼前能看到的那几段话”。三层各管一段缺一不可。这也是很多开源框架底层强化 Agent Memory 能力的通用逻辑。3. 落地第一步把 Context 窗口管理好3.1 滑动窗口代价最小的方案如果你的系统还处在原型阶段技术栈不复杂最直接的记忆方案是滑动窗口——只保留最近 N 轮对话。这个方案看起来简单但做的过程中有两个细节非常容易被忽略按 Token 截断而不是按“轮数”截断保留系统提示词和最近用户意图而不只是无脑保留最后几轮。按轮数截断有个典型问题某轮用户一次性贴了一大段日志看起来是“一轮”实际上吃掉了大量 Token。我建议用 tiktokenOpenAI 开源的 Token 估算库来做精确计算。示例代码如下import tiktoken def count_tokens(messages, modelgpt-4o): enc tiktoken.encoding_for_model(model) # 不同模型的计数规则略有差异这里是通用估算 return sum(len(enc.encode(msg.get(content, ))) for msg in messages) def trim_messages(messages, max_tokens8000): 将消息列表裁剪到 max_tokens 以内但永远保留系统提示词。 system_parts [m for m in messages if m[role] system] history_parts [m for m in messages if m[role] ! system] while count_tokens(history_parts) max_tokens: # 从头丢弃最早的对话保留最近的 history_parts.pop(0) return system_parts history_parts这段代码逻辑很直白从最老的对话开始丢丢到 Token 数低于阈值为止。这样做虽然粗暴但在原型期特别稳不会出现“对话太长导致请求直接报错”的问题。3.2 轮转策略摘要压缩代替简单丢滑动窗口最大的弊端是信息无差别丢失。用户聊天时随口说的一句话可能正是项目最关键的需求但窗口一滑就被清掉了。所以稍微进阶一点的做法是摘要压缩。具体做法是当历史消息超过阈值时不直接丢而是调用一次大模型把早期对话压缩成一段摘要。例如生成“用户目前项目背景、核心指标、已确认的决策”等结构化总结。压缩后的摘要放在消息列表最前面代替那批原始对话。这个方案成本比纯滑动窗口高一点但效果提升非常明显。我在实际项目中通常设置两层如果总 Token 超过阈值的 50%触发摘要压缩如果压缩后仍超限再启用滑动窗口丢弃最老对话。两者搭配既有记忆的连续性又控制了请求体大小。注意摘要压缩最好用单独的一次模型调用生成不要和主对话混在一起。否则摘要生成失败会直接影响用户请求的响应。3.3 落库别让对话“流走”无论你用滑动窗口还是摘要压缩都建议把用户的原始对话在进入消息列表之前先落库。我常用的方案是存两份一份原始 JSON 存 MySQL 或 MongoDB用于审计和回溯一份精简后的消息列表存 Redis用于快速读取。Redis 侧的存储结构我习惯这样设计# 会话历史右侧追加新消息 RPUSH session:{session_id}:messages {message_json} # 记录最后访问时间同时设置过期时间避免无限堆积 EXPIRE session:{session_id}:messages 86400这个设计的优点是读取顺序天然对应对话顺序配合 EXPIRE 可以自动清理一天前的临时会话数据不会把 Redis 撑爆。如果你的对话交互很频繁我会建议再加一个异步任务每分钟把 Redis 里的增量消息同步到 MongoDB防止 Redis 重启导致数据丢失。4. 长期记忆怎么存Redis 与向量数据库的选型与接入4.1 Redis 适合存“工作记忆”别让它硬扛语义检索很多朋友一说 Agent 记忆就上 Redis这没错但得先分清 Redis 在记忆系统里扮演什么角色。Redis 本质是 KV 内存数据库读写极快适合存“结构化、精准查询”的信息比如会话状态、用户 ID、最近对话轮次、任务进度。我见过有人把几万条用户语义记忆直接塞进 Redis然后靠模糊匹配去查效果奇差。因为 Redis 的字符串匹配对语义无能为力“留存率”和“用户回访比例”在 Redis 里就是两个完全不同的 key根本匹配不上。跨句子找相关性这件事必须交给向量检索。所以 Redis 的定位应该是工作记忆层典型使用方式import redis r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def save_working_memory(session_id, user_id, state): key fagent:session:{session_id} r.hset(key, mapping{ user_id: user_id, state: state, # 比如 current_task, last_question updated_at: time.time(), }) r.expire(key, 3600) # 工作记忆只保留 1 小时这满足工作记忆“读写快、临时性、高频访问”的全部要求。你想存更久、更结构化的数据比如用户实名、岗位、项目偏好放在这里也确实不合适。4.2 向量数据库长期记忆的“笔记本”真正承担长期记忆的是向量数据库。它的核心思路是把每个记忆条目用大模型转换成向量一串浮点数存进库中。等新问题来的时候也把问题转成向量然后通过余弦相似度或内积找到历史记忆里最相关的条目。目前常用的向量库有 Chroma轻量、Qdrant功能全、Milvus分布式、Pinecone云服务。我自己的经验是中小型项目和原型阶段优先用 Chroma因为零部署、API 友好等数据量到了几百万条以上再迁移到 Milvus 或 Qdrant。下面是一个用 Chroma 建设长期记忆的示例import chromadb from openai import OpenAI client OpenAI() chroma_client chromadb.PersistentClient(path./agent_memory) collection chroma_client.get_or_create_collection(memory_store) def add_long_term_memory(user_id, content, metadataNone): # 向量化记忆内容 embedded client.embeddings.create( modeltext-embedding-3-small, inputcontent ).data[0].embedding collection.add( ids[f{user_id}:{int(time.time()*1000)}], embeddings[embedded], documents[content], metadatas[{user_id: user_id, **metadata}], ) def recall_memory(user_id, query, top_k5): embedded_query client.embeddings.create( modeltext-embedding-3-small, inputquery ).data[0].embedding results collection.query( query_embeddings[embedded_query], where{user_id: user_id}, n_resultstop_k ) return results[documents]落库的内容可以是一条完整对话也可以是提炼后的结构化记忆短语。但及时提炼很重要。如果你存的是“用户第 3 轮对话原文”检索时大概率召回一段废话如果你存的是“用户负责人王强正在推进留存率优化项目目标 18%”那召回出来的就是有效信息。至于怎么提炼第六章展开讲。4.3 长期记忆的写入门槛宁缺毋滥正因为长期记忆的检索质量直接决定 Agent 的“聪明程度”写入内容必须设置门槛。我常用的筛选规则是事实性信息用户的姓名、身份、公司、项目目标、偏好优先写入。稳定的偏好用户明确说的“我喜欢简洁回答”“请用中文回复”写入。临时性闲聊不写入比如“今天天气不错”“哈哈好的”这类。过期即失效的信息可以写但要带上时间戳由后续逻辑判断时效性。如果什么都往长期记忆里塞召回时会发现大量低质量噪声反而干扰大模型判断这个坑相当隐蔽。很多人发现“为什么 Agent 越用越傻”大概率是记忆污染的问题。5. 多智能体框架里的记忆设计看看别人怎么做的5.1 ChatDev用“继承”实现信息传递如果你研究过 ChatDev 这种多智能体协作框架会发现它处理记忆的思路特别有意思。ChatDev 模拟一个软件公司有 CEO、CTO、程序员、测试员等多个角色智能体。他们之间不是简单的消息互发而是通过“聊天链”传递信息上游智能体的输出直接作为下游智能体的输入并且每个智能体都能访问前面所有智能体留下的消息记录。这种设计本质上就是链式记忆。它的优势是上下文非常连贯信息不容易丢失劣势是随着智能体数量增加重复传入的历史会越来越长Token 消耗非常夸张。后来社区讨论这个框架时普遍建议每传一轮就做一次摘要压缩而不是把原始消息一路传递到底。5.2 MetaGPT把记忆写成“文档”MetaGPT 的做法更贴近实际工程它让智能体之间不只靠聊天而是通过结构化文档来协作。比如产品经理智能体写 PRD架构师智能体读 PRD 输出设计文档工程师再基于设计文档写代码。每个文档就是被固化的记忆。这个思路的系统启发是长期记忆不应该只是对话流水账应该优先沉淀为结构化成果。对应到你自己的 Agent 里就是与其在聊天里重复说“上次我们确认了技术栈选型用 Python”不如在对话结束后自动生成一段项目纪要把这个决定记录下来。这样后续任何智能体、任何会话都能直接读文档而不用翻聊天记录。这也是所有知识库类 Agent 设计的底层共识。5.3 AutoGen共享记忆池与多会话写入AutoGen 是我个人用得比较多的框架它的记忆设计相对系统化。AutoGen 支持在多个会话之间共享记忆池也就是说多个不同的 Agent 会话可以同时读写同一个记忆空间。你可以把记忆池细分成用户会话记忆围绕特定用户的历史交互任务状态记忆当前任务的进度、待办事项全局配置记忆系统级约束、团队偏好这样设计有个很实际的好处你不用为每个 Agent 单独复制一套记忆而是共享一个存储通过命名空间隔离。会话 A 里用户说“我把预算加到 50 万”会话 B 里另一个 Agent 在回答预算方案时也能直接读到这条信息。5.4 综合来看给自己的 Agent 设计一套分级记忆看完了这些框架你会发现它们都在做同一个核心动作把“记忆”从单一的上下文窗口升级成一套有命名空间、有层级、有持久化策略的存储系统。我最终落到自己项目里的设计是三层组合短期层Redis 存当前会话消息过期时间 1 小时到 24 小时不等。中期层MySQL 存用户画像、项目信息、偏好设置的字段化记忆。长期层向量数据库存对话摘要、决策记录、语义化记忆。这三层通过一个统一的 MemoryManager 类对外暴露读写接口。上层的 Agent 逻辑不需要关心记忆存在哪只需要调用memory.save(...)和memory.recall(...)底层自动路由到对应存储。这样后续要替换存储方案也只需要改内部实现。6. 从对话里自动提炼记忆信息提取与检索增强6.1 让 Agent 学会“做笔记”长期记忆不能靠人工一条条录入必须让 Agent 从对话里自动提炼。这是让 Agent “记住你”最关键也最容易被低估的一步。我的做法是加一条异步的后处理管线当一轮对话结束后把这一轮的用户输入和 Agent 回复一起丢给一个提炼模型让模型输出候选记忆条目。提示词大致长这样请从下面的对话中提取值得长期记住的事实信息按 JSON 数组输出。 每条包含字段 - content: 记忆内容一句话描述 - category: 可选值为 user_profile / project_info / preference / decision / other - importance: 重要程度 1-55 为最关键 对话内容 {user}: {query} {assistant}: {response}我实际用下来让模型自己判断重要程度再结合一个阈值过滤效果比纯规则好用得多。以下是实际代码示意def extract_memories(query, response): prompt f请从对话中提取值得长期记住的事实信息按JSON数组输出。 每条包含字段 content、category、importance(1-5)。 对话 user: {query} assistant: {response} result llm.chat(prompt, response_formatjson) parsed json.loads(result) # 重要程度4的才写入长期记忆 return [item for item in parsed if item[importance] 4]当然模型提取会有误判。比如用户开玩笑说“我是个天才”模型可能把它当成用户画像写入。我的对策是写入前做一轮规则清洗排除“猜测性表述”、排除“第一人称情绪词”、排除长度过短内容。清洗后的条目进入向量库准确性会大幅提升。6.2 检索增强给召回结果排个序长期记忆存好了召回也做了但我们会发现一个问题向量检索出来的 top_k 结果不一定是最有用的。比如用户现在问的是“帮我看看上次的实验报告”召回的 5 条记忆里可能有一条是半年前的老结论这条显然不该排在前面。所以你要在召回之后再加一道重排序Rerank。常见的做法有几种时间衰减对较新的记忆增大权重公式可以设为 score similarity * alpha ^ (days_ago)。规则加权用户明确提到的实体比如“留存率”“实验报告”如果出现在记忆里加分。模型重排把召回结果连同用户问题一起交给大模型让模型挑出最相关的 2-3 条。这三者我建议至少用第一种加第三种。原因很简单向量相似度衡量的只是语义接近性但不理解“当前任务的时间边界”。而大模型重排虽然聪明但耗时较长加一个时间衰减前置过滤能大幅缩小候选集再让模型从 10 条里挑 3 条成本和效果最平衡。6.3 记忆检索和 RAG 的边界很多朋友把“让 Agent 记住你”和 RAG检索增强生成划等号其实两者侧重点完全不同。RAG 解决的是“Agent 不知道外部知识”检索的是文档库、知识库、官网等静态资料Agent Memory 解决的是“Agent 忘记了你这个人”检索的是历史交互、用户画像、项目决策。我在工程上会给二者分配不同的存储空间和检索链路。但它们的检索链路可以复用同一套向量库。比如我给记忆条目统一增加一个source字段是 user_memory 还是 knowledge_base。查询的时候按 source 分流互不污染。这个细节看起来简单实际非常必要我见过有人把用户个人记忆混进企业知识库结果一个用户问问题回答里蹦出了另一个用户的隐私信息非常危险。7. 记忆系统的排查清单与避坑指南7.1 我踩过的三类典型问题这套系统上线后最常见的故障不是模型问题而是记忆管道的问题。我按出现频率整理了下面这张表现象根本原因排查方向Agent 回答“不记得”长期记忆未命中检查向量库是否写入成功检查召回 top_k 是否太小检查查询是否被 where 条件过滤掉Agent 答非所问内容混乱召回了过期或无关记忆增加时间衰减权重重排结果调低 importance 阈值Redis 内存暴涨会话消息无过期策略统一设置 EXPIRE定期清理空闲会话淘汰老会话数据对话延迟突然变高长期记忆召回后再 Rerank 耗时前置规则过滤缩小候选集Rerank 模型改用小模型可缓存高频用户记忆用户 A 看到用户 B 的信息记忆未按 user_id 隔离检查向量库 where 条件检查 Redis key 命名空间做权限读校验排查时一定要记住一条主线先看“有没有写入”再看“能不能查到”最后才看“查到的对不对”。很多问题卡在第一步模型提取出的记忆条目因为重要性评分太低被过滤了或者写入时 embedding 失败你却在检索环节折腾半天。7.2 避坑心得记忆不是越多越好我做过几个项目后最大的体会是记忆系统的目标不是记住一切而是忘掉大部分留下最关键的一小部分。人的记忆也有遗忘机制Agent 也应该有。设置一个每周清理任务把超过 90 天、importance 低于 3、且从未被召回的冷记忆归档或删除既能降低存储成本也能明显提升召回精度。另外长期记忆里的每条信息都要尽量带上时间戳和来源。只存“用户想要 18% 的留存率”是不够的你还得知道这句话是哪天说的、是在哪个上下文里说的。这样当用户后来改口说“目标调整到 20%”系统才有办法识别冲突并更新旧条目而不是让两条矛盾信息同时留在库里互相打架。7.3 一个小技巧把时间信息展示给模型最后分享一个我这几个月最常用的技巧。在组装 messages 时我不仅会把召回的记忆内容放进去还会把“记忆形成时间”一起展示给模型例如记忆条目2025-03-12用户目标设定为次月留存率提升到 18%。 记忆条目2025-04-20用户提到预算增加到 50 万。这么做的原因是大模型对“信息新旧”的判断能力远比你想象中强。只要把时间戳放在它面前它自己就能合理权衡新旧信息的权重不会机械地认为“所有召回记忆都是当下有效的”。这个技巧几乎零成本但能在很多边界案例上救你一命。个人实际操作里我还会在主对话外独立跑一个小型日志任务把每次“召回记忆的 ID 是否被采纳”记录下来。跑一段时间你就能统计出哪些记忆条目是高频有用的哪些是常年被召回但其实没帮助的。用这个数据反向调整提炼阈值和重排参数比拍脑袋优化靠谱得多。这套体系做完之后Agent 才真正像一个“认识你、记得你、知道你走到哪一步”的搭档而不是一个每次见面都需要重新自我介绍的陌生人。
返回列表