ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent记忆系统设计:从短期上下文到长期记忆的完整实现

AI Agent记忆系统设计:从短期上下文到长期记忆的完整实现 1. 为什么Agent必须拥有记忆——从“工具”到“伙伴”的分水岭先跟你交代一个背景。我前两篇写完了Agent的基本骨架和工具调用能力很多朋友照着搭出来的Demo都能跑通“查天气、写周报、调API”这类流程。但真正拿去用了两周大家反馈出奇一致第一次用觉得惊艳第十次用就觉得它“像个傻瓜”。原因就一句话——它不记得你是谁。你跟它说过三遍“我平时喝美式不加糖”它每次点单还要再问一遍你上周让它帮你整理过竞品资料这周问它“上次那份报告结论是什么”它一脸茫然。这种体验就像你每次走进同一家咖啡馆店员都像第一天上班你的口味、习惯、偏好统统清零。这不是模型笨是架构上压根没给Agent设计记忆通道。这篇是“走进AI Agent”系列的第三篇专门聊怎么让Agent真正“记住你”。不是简单地把历史消息堆进上下文而是建立一套完整的记忆体系短期上下文、长期记忆、用户画像以及围绕它们的写入、召回、更新和遗忘机制。适合正在做Agent产品原型、或者想把自己搭的Agent从“能用”升级成“好用”的人。看完这篇你至少能独立设计一套可落地的记忆方案并在代码层面实现它。1.1 没有记忆的Agent本质上还是高级搜索引擎很多第一次接触Agent的人会混淆一个概念Agent能对话、能调用工具是不是就等于“具有智能”我的判断标准很简单——你能不能让它在没有提示的情况下主动复用你过去的偏好和决策。没有记忆的Agent工作模式是“一问一答一清空”。它每次接收到的只有当前这轮对话模型感知不到三天前你们聊过什么更不知道你已经是一个工作了八年的电商运营还是正在备考的大学生。它的回答是基于“一个普通用户”的概率分布生成的而不是基于“你”生成的。这就导致一个很现实的问题所有个性化的东西都得靠用户每次重复交代。你设计Agent的时候可能觉得“用户多写几句prompt不就得了”但实际上用户根本不会这么做。斯坦福和Google的研究里有个词叫“冷启动体验”意思是用户刚开始用新产品时耐心极低只要发现“我都说过一遍了你怎么还记不住”大概率直接弃用。所以记忆不是附属功能它就是Agent能否从“工具”走向“伙伴”的分水岭。工具不需要记得你每次用都重新开一张工单就行但伙伴必须记得你否则所有互动都停留在“陌生人社交”层面。1.2 记忆的核心价值不是储存而是“人格连续性”我见过很多团队给Agent加记忆做法就是开一张MongoDB表把用户聊天记录往里一塞然后告诉我说“记忆功能做完了”。但实际使用的时候效果跟没做几乎一样。为什么因为存储本身没有价值价值在于“提取信息—建立关联—影响后续行为”这条链路。举个我实际调过的案例。一个知识付费类Agent用户第一天问“适合初学者的Python书单”Agent推荐了三本书第三天用户说“看到你推荐的书了有本读完前两章有点吃力”好的Agent这时候应该联想到“用户基础比较薄弱”在后续推荐练习项目时自动降难度甚至主动解释基础概念。这个能力靠的是什么靠的就是把“用户基础薄弱”这条信息从对话里提炼出来存成长期记忆然后在后续某一轮被召回、注入当前决策。我把这个能力叫作“人格连续性”。就是Agent在不同时间点对同一个用户应该保持一致的认知和一致的回应风格。用户不需要每轮对话都重新介绍自己Agent也不需要每次都从零开始理解用户。这个“连续性”就是记忆系统最核心的产品价值。1.3 记忆与知识检索的本质区别一个是“懂你”一个是“查资料”这里必须澄清一个高频误区。很多人一说记忆就联想到向量数据库、Embedding、相似度检索然后直接把公司知识库文档切成块怼进去告诉我说“RAG方案就是记忆”。这个理解偏差非常大。知识检索解决的是“外部知识怎么进上下文”的问题它服务的是“准确回答事实性内容”这个目标。比如Agent内部的资料库里存了产品手册用户问“退款政策”召回相关片段拼进上下文这是知识检索。而记忆解决的是“用户信息怎么沉淀和复用”的问题它服务的是“个性化、连续性、主动性”这些目标。比如用户说过“我在上海”“我养了两只猫”“我讨厌电话沟通”这些信息被沉淀下来在后续对话中影响Agent的语气、建议和行动这才是记忆。如果你还不理解可以这么想知识库像图书馆谁去查都能查到一样的资料记忆像你自己的笔记本上面记的是别人不知道的、关于你自己的事情。两者可以放在同一个技术底座上都用向量数据库但采集逻辑、存储结构、更新策略完全不同。后面我会详细拆这两套链路千万别混为一谈。2. 记忆系统的整体架构三层记忆模型从零设计一套能落地的记忆方案我建议先别急着写代码先把模型想清楚。我目前在项目中用的是一套“三层记忆模型”不是我自己发明的而是融合了多份记忆相关论文和工程实践后整理出来的也经过了多次线上迭代的检验。这套模型由三层组成工作记忆Working Memory、长期记忆Long-term Memory和用户画像User Profile。每一层的职责边界非常清晰后续加功能、理Bug都会轻松很多。2.1 工作记忆当前任务的“便签纸”工作记忆对应Agent在单次会话内需要“临时记住”的信息。它最典型的载体就是对话历史上下文Message History也就是喂给大模型的聊天记录。它的特点是容量有限、寿命短、丢失成本低。容量有限是因为大模型有上下文窗口限制比如当前主流模型的窗口可能是128K tokens看起来很大但塞上系统提示词、工具定义、检索结果真正留给对话历史的空间并不宽裕。寿命短是因为它只需要维持“当前这次对话”的连贯性用户关掉页面、新开会话工作记忆就可以宣告结束了。丢失成本低因为丢了也不影响长期关系顶多这轮要重新聊几句。但工作记忆也不是无脑存原始消息就完事。真实项目中窗口很快会被撑爆所以我通常会在工作记忆上做两件事一是设一个“核心上下文窗口”只保留最近几轮完整消息二是超过窗口的早期消息会触发“滚动摘要”Rolling Summary把旧对话压缩成一段摘要继续保留。这套机制兼顾了连贯性和容量具体代码在第四章先记住这个设计思路。2.2 长期记忆跨会话稳定的“人生履历”长期记忆是整个记忆系统的重头戏。它的职责是保存那些“跨会话依然有效”的信息让Agent能在下一次对话中想起用户是谁、之前聊过什么。什么信息适合进长期记忆我总结了三类事实型信息用户提到的个人信息、偏好、重要日期、居住城市等事件型信息用户近期做过的事、项目进展、上一次交代的任务关系型信息用户与第三方人物、组织、产品之间的关联长期记忆存储在技术上通常用向量数据库承载配合Embedding模型做语义检索。用户说过的信息被抽取、清洗后写入记忆库等到需要的时候再根据当前对话内容做相似度召回找到最相关的若干条记忆拼接到提示词里。这里要特别强调一个工程经验不要试图把用户说过的话原封不动地存进去。原始聊天记录噪声太大经过抽取、改写后的结构化记忆召回效果和命中率会好很多。我自己踩过这个坑最早直接存原始消息导致召回结果里全是“嗯”“好的”“哈哈哈哈”这种无效内容后来改成抽取式存储才真正跑通。2.3 用户画像从数据中提炼“你是谁”如果说长期记忆是零散的“事实卡片”那用户画像就是把这些卡片整合起来的人格档案。它回答的是“用户是个什么样的人”这个问题属于更高层次的抽象。画像里通常包含这些维度基本信息年龄区间、职业、地点、家庭情况沟通偏好喜欢简洁还是详细、用词正式还是随意、能不能接受玩笑兴趣领域技术、健身、育儿、理财等消费或决策偏好注重性价比、重品质、怕麻烦禁忌与雷区不想讨论的话题、反感的信息画像的构建方式有两种一种是显式的用户明确告诉你“我是做设计的”“我不喜欢喝甜的”另一种是隐式的Agent通过分析用户历史对话推断出“这个用户最近三个月都在聊AIGC方向的内容可能是个AI从业者”。显式信息直接记录隐式信息需要LLM做一轮分析抽取然后在后续对话中验证。画像跟长期记忆的分工是长期记忆回答“用户经历了什么”画像回答“用户是怎样的一个人”。在实际应用中画像用来调整个性化回复风格和推荐策略长期记忆用来提供具体的事实线索。两者配合才能达到“既懂你为人、又记得你的事”的效果。2.4 记忆的完整生命周期写入、召回、更新、遗忘三层记忆模型不是静态表结构而是一套持续运转的流程。完整生命周期分四个环节写入从当前对话中判断哪些信息值得沉淀。这一步通常由LLM完成设计一套提取规则或Prompt让它把对话里的“长期有效信息”抽取出来分类后写入对应位置。召回在生成回复之前用当前对话内容去检索相关的长期记忆和画像信息命中后注入到上下文。注入不是越多越好要有优先级和上限。更新已有记忆和当前对话发生冲突时怎么处理。我一般用“置信度”机制单次提及记低置信度多次验证后升为高置信度冲突信息以最近一次为准但会保留历史版本。遗忘这是最容易被忽略的一环。记忆不是越多越好过期的、被否定的、久未命中的记忆应该被降权或清理否则系统会越来越“固执”甚至用陈旧印象误导决策。一个健康可用的记忆系统一定是这四个环节都在正常工作缺一个都会在实际使用中暴露出问题。后面第四章我会把每个环节的代码实现都写出来。3. 核心实现让Agent具备短期与长期记忆架构想清楚之后落地就是纯工程活。这一章我从实现角度拆解几个关键模块把每一步涉及的技术选型、参数依据和常见坑位都讲透。3.1 短期上下文管理窗口、滚动摘要与关键信息保护短期上下文管理是最基础但也是最容易翻车的模块。很多人直接写死“保留最近10轮对话”结果用户在前面的对话里说过的关键信息比如“我预算在三万以内”在第12轮被Agent彻底遗忘回复质量直线下降。我的标准做法有两层第一层是设置一个动态窗口。窗口大小不固定而是根据输入消息的长度动态调整。如果用户一次性发了一大段文本系统会自动压缩历史消息轮数优先保住“最靠近当前”的信息。实现上就是先计算前置固定内容的token数系统提示词工具定义画像召回记忆用模型总上下文减去这些再按比例倒推能放多少轮对话。第二层是滚动摘要机制。当对话轮数超过窗口上限时把最早的几轮消息丢进一个“摘要模型”生成一小段关键信息摘要替换掉原始消息继续留在上下文里。比如前面12轮聊了一堆项目需求细节摘要可能就压缩成“用户正在开发一个面向中小企业的报销系统重点关注审批流和移动端体验”。这样既保住了关键信息又不占太多token。还有个细节容易被忽略摘要里要标记时间。因为不同时间点说的话可信度和优先级完全不同。三个月前的“我最近在减肥”可能今天已经作废了如果摘要不带时间模型就会拿着过时信息当常识用。3.2 长期记忆的向量化召回Embedding模型选择与相似度检索调优长期记忆落地核心是“文本向量化相似度检索”这套组合拳。我选型时踩了不少坑分享几个关键决策Embedding模型的选型考虑的是“对中文的支持度”和“语义理解能力”。目前主流方案有通用文本向量模型和闭源Embedding接口两类。我的建议是先拿自己真实的记忆数据做一个百条的召回效果评测不要只看榜单指标。有些模型跑学术基准很高但在你的业务场景里召回结果就是不对味这是很常见的事。向量数据库选型我用过的有Chroma、Milvus、Qdrant等。如果项目刚起步、数据量在百万条以内就选最轻量、最省事的方案如果数据量大且对并发有要求再上分布式的。不要项目刚起步就给自己上重工程优先跑通闭环。召回时的相似度阈值非常关键。阈值太高容易啥都召回不了阈值太低召回一堆噪声挤占上下文。我的调参经验是先离线跑一批数据把“人工判断相关”和“人工判断不相关”的相似度分布画出来选择两者分离度最好的点作为初始阈值然后上线后用“用户点赞/点踩”信号持续微调。实际上分数在0.75到0.85之间但不要照搬一定要测自己的数据分布。另外召回结果不要只用相似度分数排序最好加上时间衰减。两条相似度相近的记忆三天前的内容和三周前的内容优先级应该有差别。做法是用相似度分数乘以一个时间衰减系数比如exp(-days/30)让近期的记忆更容易被召回。3.3 用户画像的自动构建与更新用户画像构建我的做法是在对话的“非生成阶段”执行——也就是用户发完消息、Agent还没有生成回复之前或者在回复完成后开启一个异步任务专门跑一次“画像更新”。这个异步任务做的事情是把当前这轮用户消息以及最近的上下文丢给LLM让它判断是否存在值得更新画像的信息。Prompt里会定义好画像的维度例如基本信息、偏好、风格、禁忌并要求模型按JSON结构输出更新内容。一个关键工程经验画像更新要引入“确认机制”。模型在对话中推断出的画像信息可信度并不高比如用户开玩笑说“我最讨厌世界上的所有猫”如果你真的把它写进画像后面所有涉及猫的建议都会出问题。我的做法是模型第一次推断出的画像记录标为“待验证”只有在后续对话中再次被用户明示或暗示确认后才会升级为“已确认”状态。这样能大幅降低画像噪声。画像信息的存储结构我会建议做版本化。用户今天说“我喜欢极简风”一个月后说“最近迷上了复古风”如果画像只有最新值没有历史轨迹Agent会显得“只认当下、不懂变化”。但保留历史版本也不是要全部堆上去而是保留最近几次变更的时间点方便模型判断用户偏好的稳定性和演变趋势。3.4 记忆优先级与Token预算的取舍记忆系统做得再完善最终的载体还是上下文窗口。窗口有限但记忆可能无限所以必须有优先级机制决定“什么优先放进上下文”。我自己项目里的排序逻辑大致是用户画像中最核心的几条比如称呼、职业、明显偏好优先级最高最近命中的、与当前话题强相关的长期记忆当前会话内的滚动摘要和历史关键信息其他低相关或过时的记忆不进入上下文快到窗口上限时优先丢弃的是第4类然后是第3类中非关键部分。画像核心信息要保证永远能放进去这是底线。这里我建议你做一个记忆预算表先在系统里定义好各类内容的最大tokens。我常用的预算是系统提示词1500 tokens、用户画像800 tokens、召回记忆1200 tokens、滚动摘要1000 tokens剩下的都给当前对话历史。不同项目可以自己调但一定要有明确预算否则记忆注入就是失控的。4. 实操从零实现一个“记住你”的Agent附Python代码理论讲得再多不如直接看代码。这一章我带你从零搭一个带记忆的Agent完整覆盖短期上下文、长期记忆和用户画像三个模块。我会把关键代码贴出来每一步解释在做什么、为什么这样做。4.1 技术选型与运行环境演示环境我尽量保持轻量方便你直接复现Python 3.10FastAPI提供Web服务接口你也可以换成Flask或直接脚本跑通OpenAI兼容接口用于对话生成、摘要生成、记忆提取如果你用国产大模型也没问题只要是OpenAI兼容接口就行Chroma本地向量数据库用于长期记忆存储和检索一个Embedding模型调用接口用于向量化依赖安装就一行命令。我用了一个轻量的同步版本先把逻辑讲清楚生产环境再换异步也不迟。pip install fastapi uvicorn chromadb openai pydantic4.2 第1步设计记忆数据结构开工先别急着调API先把数据模型定清楚。长期记忆和画像分开存用不同的collection承载。长期记忆使用Chroma的collection来存每条记忆的文档内容是抽取后的结构化文本metadata里带记忆类型、时间戳、置信度这些字段。from datetime import datetime import chromadb from chromadb.utils import embedding_functions # 初始化Chroma选一个本地目录 client chromadb.PersistentClient(path./agent_memory_db) embed_fn embedding_functions.OpenAIEmbeddingFunction( api_keyyour-api-key, model_nametext-embedding-3-small # 按你的实际模型调整 ) # 长期记忆集合记录具体事件、事实 long_term_mem client.get_or_create_collection( namelong_term_memory, embedding_functionembed_fn, metadata{hnsw:space: cosine} ) # 用户画像是更结构化的信息我用JSON文档管理并同步写入向量库 profile_mem client.get_or_create_collection( nameuser_profile, embedding_functionembed_fn, metadata{hnsw:space: cosine} )每条记忆写入时的标准结构我用一个函数封装好def add_memory(text: str, mem_type: str fact, confidence: float 0.7, user_id: str default): text: 抽取后的记忆文本比如用户是Java开发工程师工作年限6年 mem_type: fact(事实) / event(事件) / preference(偏好) confidence: 置信度 0~1单次提及默认0.6多次确认后可升到0.9 now datetime.now().isoformat() doc_id f{user_id}_{now}_{abs(hash(text)) % 10000} long_term_mem.upsert( ids[doc_id], documents[text], metadatas[{ user_id: user_id, type: mem_type, confidence: confidence, created_at: now, updated_at: now }] ) return doc_id为什么用upsert而不是add因为同一条记忆如果被反复写入会变成冗余向量召回时会批量命中同义内容白白浪费上下文预算。通过doc_id做幂等控制后续更新能直接覆盖。4.3 第2步对话记忆提取与写入记忆从哪来从用户的对话里来。但原始对话不能直接入库必须经过LLM抽取这一步是整个系统的“信息守门员”。我设计了一个提取函数输入是最近的对话历史输出是“本条对话中值得长期保留的记忆列表”。这里的关键是Prompt写得具体让模型知道什么该记、什么不该记。MEMORY_EXTRACT_PROMPT 请从以下对话中提取需要长期记住的关于用户的信息。 只提取那些对后续对话有长期价值的信息包括但不限于 - 用户的个人身份信息职业、城市、家庭成员、年龄阶段等 - 用户明确表达的偏好、喜好、厌恶 - 用户正在做的重要事项、项目进展、目标 - 用户与其他人或组织的关系 不要提取 - 一次性的对话内容比如今天天气真不错 - 情绪化的临时表达除非是长期偏好比如我讨厌被打断 - 对话中已有的系统操作记录 输出严格为JSON数组格式每个元素包含 {content: 抽取后的记忆内容第三人称陈述句, type: fact|event|preference, confidence: 0~1的数值} 对话历史 {conversation} def extract_memories_from_conversation(conversation_text: str): resp chat_model.chat( modelyour-model, messages[{role: user, content: MEMORY_EXTRACT_PROMPT.format(conversationconversation_text)}], temperature0.2 # 温度要低抽取要稳定不能随机发挥 ) content resp.choices[0].message.content # 解析JSON如果有风险就返回空列表宁可漏记不可乱记 import json try: return json.loads(content) except Exception: return []这里temperature设0.2的目的很明确抽取任务需要确定性不能让模型同一段对话两次抽出来的内容差异巨大。如果你担心模型返回的JSON格式不对可以在Prompt里加few-shot示例或者用支持结构化输出的模型调用方式。写入阶段我会做一次“去重更新判断”。调用Chroma的query方法拿待写入的内容先查一遍有没有高度相似的历史记忆def upsert_after_dedup(new_text: str, mem_type: str, confidence: float, user_id: str default): # 先查一下有没有相似度很高的已有记忆 results long_term_mem.query( query_texts[new_text], n_results3, where{user_id: user_id} ) docs results.get(documents, [[]]) distances results.get(distances, [[]]) if docs and docs[0]: # 相似度小于0.15注意Chroma返回的是距离距离越小越相似 best_distance distances[0][0] if best_distance 0.15: # 已有相近记忆直接更新原记录而不新增 existing_id results[ids][0][0] long_term_mem.update( ids[existing_id], documents[new_text], metadatas[{ user_id: user_id, type: mem_type, confidence: max(confidence, results[metadatas][0][0].get(confidence, 0)), updated_at: datetime.now().isoformat() }] ) return existing_id # 没有相近的新增 return add_memory(new_text, mem_type, confidence, user_id)这个去重逻辑非常关键。没有它用户的同一条信息被抽出来10次库里就有10条几乎一模一样的向量召回的时候会把这10条同时塞进上下文纯纯浪费窗口资源。4.4 第3步记忆召回与提示词注入对话生成之前Agent要先“回忆”。我封装了召回函数用用户最近一条消息作为query去长期记忆和画像里检索相关内容。def recall_memories(user_input: str, user_id: str default, top_k: int 5): # 长期记忆召回按相关度和时间衰减综合排序 results long_term_mem.query( query_texts[user_input], n_resultstop_k * 2, # 多召回一些再综合排序 where{user_id: user_id} ) docs results.get(documents, [[]]) metas results.get(metadatas, [[]]) dists results.get(distances, [[]]) if not docs or not docs[0]: return , [] scored [] from datetime import datetime, timezone now datetime.now(timezone.utc) for idx, doc in enumerate(docs[0]): meta metas[0][idx] dist dists[0][idx] similarity 1 - dist # cosine距离转相似度 # 时间衰减日期越久衰减越明显 created datetime.fromisoformat(meta.get(created_at, now.isoformat())) age_days max((now - created).days, 0) decay 0.9 ** age_days final_score similarity * decay * meta.get(confidence, 0.7) scored.append((final_score, doc, meta, similarity)) scored.sort(keylambda x: x[0], reverseTrue) # 取前top_k拼接成记忆片段 recall_text \n.join([f[{m.get(type,)}] {d} for _, d, m, _ in scored[:top_k]]) return recall_text, scored[:top_k]召回结果怎么变成提示词我会在System Prompt里加一块“记忆区”把画像核心信息和召回的长期记忆拼进去。注意提示词的措辞要让模型知道哪些是“事实记忆”哪些是“可能过时的信息”避免它盲信。def build_system_prompt(user_input: str, user_id: str default): # 召回长期记忆 recall_text, _ recall_memories(user_input, user_iduser_id) # 拉取画像核心信息我简化为从profile collection查top1条最近画像快照 profile_resp profile_mem.query( query_texts[user_input], n_results1, where{user_id: user_id} ) profile_text if profile_resp.get(documents) and profile_resp[documents][0]: profile_text profile_resp[documents][0][0] system_prompt f 你是一个具备长期记忆能力的AI助手。在回答用户问题之前你会先收到关于该用户的记忆信息。 【用户画像】 {profile_text if profile_text else 暂无画像信息需要你在对话中逐步了解用户} 【相关历史记忆】 {recall_text if recall_text else 暂无相关历史记忆} 【使用规范】 1. 优先使用上面的记忆信息来个性化你的回复。 2. 如果记忆与当前对话矛盾以当前对话为准不要强行坚持记忆。 3. 记忆信息可能过时如果用户明确纠正请礼貌道歉并记住新的信息。 4. 不要主动提及“根据我的记忆”直接自然地运用这些信息。 5. 如果没有相关记忆也不用强行编造正常回答即可。 return system_prompt这里有个经验不要让模型把“记忆”当负担。你如果写了“你必须引用记忆”模型就会生硬地复述记忆内容而不是自然运用。比如用户喜欢喝美式好的回复是“还是老样子帮你点一杯美式”而差的回复是“根据我的记忆你是一个喜欢喝美式的人所以我可以帮你点美式。”加粗那个“使用规范”第4条就是我踩过好多次坑之后总结出来的。4.5 第4步用户画像更新与遗忘策略最后是画像更新。我的方案是异步执行每次对话结束后把当前轮次的对话记录丢给一个画像更新函数让它判断是否需要更新画像。PROFILE_UPDATE_PROMPT 请根据用户最近这条消息更新对用户的画像理解。 已知画像信息 {current_profile} 目标画像维度 - basic_info: 基本信息职业、地理位置、家庭、年龄等 - preference: 明确偏好喜欢、不喜欢、习惯 - communication: 沟通风格简洁/详细、正式/随意等 - taboo: 禁忌不想讨论的话题、反感的事项 要求 1. 只提取有明确依据的信息不要过度推断。 2. 如果信息已有且没有变化不重复输出。 3. 如果信息与旧画像冲突请以新信息为准并在notes里说明冲突。 4. 输出JSON {{ updates: [{{dimension: preference, content: ..., confidence: 0.8}}], notes: ... }} 如果没有任何需要更新的信息返回 {updates: [], notes: } 用户消息 {user_message} def update_user_profile(user_message: str, user_id: str default): # 1. 读取当前画像简化处理取最近一条 current_profile 暂无 resp profile_mem.query( query_texts[user_message], n_results1, where{user_id: user_id} ) if resp.get(documents) and resp[documents][0]: current_profile resp[documents][0][0] # 2. LLM判断是否更新画像 parsed chat_model.chat( modelyour-model, messages[{role: user, content: PROFILE_UPDATE_PROMPT.format( current_profilecurrent_profile, user_messageuser_message )}], temperature0 ) result json.loads(parsed.choices[0].message.content) # 3. 有更新就写入画像库保留版本 if result.get(updates): newest_content current_profile \n if current_profile ! 暂无 else for upd in result[updates]: newest_content f[{upd[dimension]}] {upd[content]}\n now datetime.now().isoformat() profile_mem.upsert( ids[f{user_id}_profile], documents[newest_content], metadatas[{ user_id: user_id, updated_at: now, version: result.get(notes, ) }] ) return result至于遗忘策略我的实现是在“召回”这一步自然完成大部分。时间衰减会让久远的记忆权重越来越低最终排不进top_k相当于被“软遗忘”。对于明确的过期信息比如用户说“我换工作了现在在杭州”通过去重更新机制覆盖旧记忆。我一般不搞物理删除除非涉及用户主动删除或合规要求因为有些看似过期的信息在未来某天还可能成为关键线索。4.6 完整效果演示隔天对话的“记住你”体验把上面所有模块串起来一个带记忆的Agent就成型了。我用一个FastAPI端点展示对话流程from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ChatRequest(BaseModel): user_id: str message: str conversation_id: str default class ChatResponse(BaseModel): reply: str recalled_memories: list # 便于调试时检查召回效果 app.post(/chat) def chat(req: ChatRequest): user_id req.user_id # 1. 召回记忆构建带记忆的system prompt sys_prompt build_system_prompt(req.message, user_id) # 2. 拼接当前消息调大模型 messages [ {role: system, content: sys_prompt}, {role: user, content: req.message} ] reply chat_model.chat(modelyour-model, messagesmessages) # 3. 异步更新长期记忆和画像 conversation_text f用户{req.message}\nAI{reply} new_memories extract_memories_from_conversation(conversation_text) for mem in new_memories: upsert_after_dedup(mem[content], mem[type], mem.get(confidence, 0.6), user_id) update_user_profile(req.message, user_id) return ChatResponse(replyreply, recalled_memories[...])演示一下实际效果第1天用户问“我是一名健身教练最近想帮会员设计减脂饮食计划”Agent正常回答。异步任务把“用户是健身教练”“正在做减脂饮食计划”写入长期记忆画像里加了职业维度。第2天用户只发了一句“继续昨天的计划”Agent通过召回自动定位到“健身教练”“减脂饮食计划”直接延续话题并提供更具体的建议。第3天用户问“顺便推荐一款适合我的运动手表”Agent因为知道用户职业和需求推荐的逻辑就完全不一样了。这种“跨会话连续性”一旦跑通Agent的产品形态就会发生质变。用户不用再重复背景不用再“教”Agent真正开始有“被记住了”的感觉。5. 常见问题与排查技巧实录记忆系统属于那种“架构看起来不难、跑起来全是坑”的模块。我把自己实际运营中遇到的典型问题整理成一份清单包含现象、原因和解决方案方便你遇到问题时直接定位。5.1 记忆污染记了一堆不该记的现象Agent开始“记住”用户随口说的一句话甚至在后续对话里反复提起这些无关内容。比如用户说“今天地铁上人好多”Agent隔天问“您今天坐地铁还顺利吗”——这条信息就属于一次性情绪表达不该进长期记忆。原因记忆提取Prompt写得不够严格模型把临时性、情绪化表达也当成了长期偏好。还有一个常见原因是提取时只看了单轮对话缺少“至少两次提及才确认”的机制。解决方案一是强化提取Prompt明确区分“一次性表达”和“长期信息”并给示例二是引入置信度门槛置信度低于0.6的记忆不进入长期库或者只标记为“待验证”三是定时检查记忆库随机抽几条看看质量如果噪声率高重点检查提取Prompt和质量分。5.2 召回不准阈值怎么调才能“刚好想起”现象用户明明之前说过去哪里Agent却想不起来或者召回了三条记忆里只有一条相关其余全是利用相似度硬凑的。原因Embedding模型不能完全理解中文的语义差异加上业务领域的词汇分布和通用场景差异大固定的相似度阈值很难适用。解决方案建一个“召回评测集”。模拟业务里可能出现的对话场景准备30-50组问题和对应的“应该召回”的记忆文本。离线跑一遍recall5指标观察相似度分布。如果相关和不相关的分数区域严重重叠考虑换Embedding模型或者增加元数据过滤条件比如限制召回范围是fact还是event。线上再加一个“召回判题”的轻量任务让LLM判断“当前上下文中的记忆与用户消息是否真的相关”过滤掉不相关的内容再拼接prompt。5.3 记忆膨胀长期记忆越来越多怎么办现象系统运行一个月后用户记忆库累计了上千条记录召回的top_k里全是“噪音”关键信息反而被淹没。原因只写不清理记忆库缺乏生命周期管理。长期记忆虽然按相关度排序但同主题的旧记录会累积成“记忆海绵”把新信息的显示空间挤掉。解决方案建立两级归档机制。召回频率低、时间超过90天的记忆自动移动到“冷存储”集合不参与常规召回如果用户重新提到相关内容再通过检索把冷存储的记录唤醒。同时周期性跑一次“记忆合并”任务把重复度高、语义接近的多条记录合并成一条综合记录减少冗余。我给自己的项目的目标是把活跃记忆控制在单用户500条以内。5.4 多轮对话后仍然“失忆”排查链路现象对话进行到中段Agent突然忘了用户几分钟前刚刚说过的关键信息。比如用户在第3轮说“我在上海办公”第8轮问“那我们在哪见面”Agent居然说“请问您在哪个城市”。原因这个“失忆”通常不是长期记忆的问题而是短期上下文的窗口管理出了问题——mid对话历史被滚动摘要压缩过度或者窗口设置太小早期的关键信息被截掉了。解决方案一条条排查。第一步看对话日志里实际传给大模型的messages列表确认用户信息是否还在上下文里第二步检查窗口动态计算的逻辑是否为超长输入的极端场景预留了空间第三步检查滚动摘要是否保留了“实体信息”——“用户在上海”这种关键信息绝对不能丢。另外建议在摘要Prompt里强制要求“必须保留人名、地名、时间、数字、明确偏好”。这个坑我踩过不止一次每次都是摘要把关键实体给“概括”没了。5.5 成本与性能控制记忆系统会不会拖慢响应现象加了记忆功能后对话延迟变高了token消耗变大了。原因每次对话都要做记忆提取一次LLM调用、画像更新一次LLM调用、召回检索一次向量DB查询这几个环节全部串行执行的话延迟自然就上去了。解决方案把“提取”和“画像更新”改成异步任务用户在等回复时系统在后台完成记忆沉淀前端只等待“召回生成回复”这条主链路。召回检索本身很快主要开销是Embedding查询可以做一个简单的缓存机制相同或高度相似的用户消息直接走缓存。另外记忆提取不是每条消息都要跑可以在用户消息达到一定长度或者对话轮数达到N轮之后再做一次批量提取也能省不少token。6. 我自己的几点实战体会做记忆系统做到现在我最深的感觉是技术方案本身不难难的是对“记忆”这件事的理解深度。很多团队把记忆做成了“聊天记录归档”恨不得把用户说过的话全存下来结果就是存储庞大、召回无效、体验稀碎。真正好用的记忆系统是克制而精准的——它知道什么值得记住什么应该遗忘什么需要确认后再记。两个小经验最后分享给想动手做的朋友。第一记忆的质量控制大于数量。一套“少而精”的记忆系统远胜“多而杂”的记忆系统。宁可让Agent暂时不记得也不要让它记错、记乱。所以每次提取都要带着“这条信息下次真的有用吗”这个标准去过滤。第二记忆系统的效果一定要用“跨会话任务”来衡量。不要只在单次对话里测试而是设计一个需要隔天才能完成的任务比如“用户今天说了一个偏好明天看Agent是否主动运用”。这种测试才能真正反映记忆系统的价值。按照这套思路你完全可以先跑一个最小闭环用Chroma做存储、一个LLM做提取和生成、一个Embedding做召回。跑通之后再逐渐加入用户画像、版本管理、冷热归档这些进阶能力。AI Agent的竞争短期拼的是模型能力长期拼的就是谁更“懂用户”而记忆一定是那条绕不开的护城河。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进