资讯动态

Agent记忆系统从0到1:前端转LLM必懂的短期、情景与语义记忆架构

发布时间:2026/10/17 12:02:38 来源:尧图企业网站定制
做 Agent 开发前五篇聊了 Prompt 工程、工具调用、结构化输出这些基础但这些都不是 Agent 区别于普通脚本的真正分水岭。到第六篇终于要碰那块最硬、也最容易被忽略的骨头——Agent Memory记忆系统。我的感受很直接开发过带记忆和不带记忆的 Agent两者完全是隔代产品。没有记忆的 Agent 能回答你的问题有记忆的 Agent 才像一个真正与你长期共事的人。这篇系列文章就是面向正在从纯前端转向 Agent 开发的朋友把我自己从 0 到 1 搭建记忆系统的完整路径、踩坑记录和最终方案全部摊开照着做你也能让你的 Agent 从“聊完就忘”进化到“越来越懂你”。1. 前端转型的第六站为什么说记忆系统才是 Agent 智能的天花板1.1 从 React 状态管理到 Agent 记忆前端人最好懂的思维迁移如果你是前端出身一定对 React 的useState和 Redux 不陌生。组件要记住用户点过哪个按钮、表单填了什么值靠的是状态。可一旦页面刷新状态清零用户就得重来一遍。解决的办法是localStorage或后端数据库把状态持久化下来。Agent 的记忆系统本质上就是把这套思路搬到 LLM 世界。LLM 本身是个无状态的计算引擎你发一段 Prompt它回一段文字完事。它不记得你上一轮说过什么也不记得三天前的偏好设置更不记得上个月你给它纠过的错。想让 Agent 真正“懂你”就必须在模型外面搭建一套记忆层把信息和状态缓存下来在下一次对话开始前再塞回上下文。这套思路前端工程师理解起来几乎零成本。把 LLM 想象成一个每次刷新都清空的组件把记忆系统想象成你为这个组件定制的数据持久层。剩下的问题就是——记住什么、存在哪里、什么时候取出来、怎么保证取出来的是对的东西。这四个问题就是本文要解决的。1.2 没有记忆的 Agent 是什么体验一场让人抓狂的重复对话我先还原一个场景你立刻就能感受到问题的严重性。某团队给内部客服系统做了一个 Agent 模拟项目 X最初版本没有记忆系统。用户在对话框里说“我叫李明我的会员编号是 10086以后请直接称呼我李工”Agent 礼貌回复“好的李工已记录您的会员编号”。每隔十分钟用户问“我叫什么名字”Agent 一脸茫然“很抱歉我目前看不到您的个人信息。”用户再报一次Agent 又谢一遍。来回三次之后用户基本就骂人了。这个场景还是轻的。更严重的是多轮任务类的 Agent——比如让 Agent 帮忙筛选简历、规划旅行、写代码如果没有记忆系统每轮对话它都要重新理解上下文。上一轮确认了预算 8000 以内这一轮它可能推荐 12000 的套餐上一轮明确排除了某框架这一轮它又把这个框架写进方案。气得人想砸键盘。所以我的结论是Prompt 决定 Agent 的“表达水平”工具调用决定 Agent 的“行动能力”而记忆系统决定 Agent 的“人格连续性”。前两者让 Agent 好用后者让 Agent 可信。没有记忆的 Agent本质上就是个每次对话都重启的无情回答机器。1.3 记忆系统需要解决的三类具体问题在设计模拟项目 X 之前我先花一天时间把“需要记忆什么”拆成了三类这也是后来我做架构的分工依据记忆类别解决什么问题典型例子埋在哪里短期记忆当前会话连续性上一轮说的预算、当前正在写哪段代码模型上下文Prompt情景记忆跨会话回放历史上周聊过什么、上次排除了什么方案持久化存储 检索语义记忆提炼并长期稳定保存偏好用户姓名、喜欢简短回答、代码风格结构化存储 更新策略这三类不是物理隔离的而是分工协作。短期记忆靠 Prompt 里的多轮对话历史维持情景记忆靠把历史对话切成片段存进存储然后按需检索语义记忆则要把零散的对话内容“咀嚼”成可以长期复用的用户画像。下面每一节都会对应讲清楚它的实现方式和我的实测心得。2. 记忆到底存在哪工作记忆、情景记忆、语义记忆的分层拆解2.1 工作记忆模型上下文窗口里的“临时便签”工作记忆也叫短期记忆就是当前这次会话里 LLM 能直接看到的全部信息。它的物理载体是 Prompt 中的对话历史——你发送给模型的 messages 数组。前端转型的读者可以把 messages 理解成一个不断push的数组const messages [ { role: system, content: 你是一个…… }, { role: user, content: 帮我推荐三本书 }, { role: assistant, content: 好的推荐《重构》《代码整洁之道》《人月神话》 }, { role: user, content: 第一本和第二本之间我更推荐哪本 }, ];模型回答最后一个问题时确实能看到前面的所有轮次。但这不代表它“记住”了它只是这次计算时把这些内容当成了输入——每一次发起请求都是全量重算。所以工作记忆的特征是信息量大、完全受限于上下文窗口、成本高、且无法跨会话留存。我在做模拟项目 X 时有个体会不要试图优化模型的“记忆力”模型的上下文不是无限的就算现在各家都推百万 token 上下文把七天前的完整对话全部塞进去也是浪费钱且拖慢响应。工作记忆只负责“当前正在进行的任务”跨时间的信息交给下面两层。2.2 情景记忆按时间轴回放“发生了什么”情景记忆英文叫 Episodic Memory对应的是人类对具体经历的回溯能力。你会记得“上周二和同事讨论过如何优化接口性能当时提了三个方案”。这就是一段情景记忆。对 Agent 来说情景记忆就是历史对话的存档。但它不能像工作记忆那样全量重放必须经过切片、索引、检索三步。我的切法是这样每一个完整问答对作为一个记忆单元起点是用户提问终点是 Agent 完成回复每个记忆单元打上时间戳、会话 ID、任务主题标签定时或按需把多个记忆单元打包成“事件”事件可以是一个用户需求从提出到满足的全过程。为什么非要打成事件而不只是存每条对话因为单独一句“用户说预算八千以内”脱离了场景就没有意义而和后续的“排除某方案”“最终确定某酒店”放在一起才是完整可复盘的情景。检索的时候也是按事件维度去召回这样返回给模型的是一段有因果关系的叙述而不是零散句子。2.3 语义记忆把零散对话提炼成“用户画像”语义记忆是三层里最抽象、但长期价值最高的一层。它不记录“发生了什么”而是记录“用户是什么样的人”。比如用户说“回复别太啰嗦直接给结论”这条偏好就应该沉淀为一条语义记忆communication_style concise。再比如用户三次提到“我在用 Node.js不想引入重框架”语义记忆里就应该存在一条技术栈约束tech_stack: Node.js, prefer lightweight。这个提炼过程不能靠硬编码规则一条条截取而是要用模型来做信息压缩。我常用的方法叫“反思式沉淀”每隔几轮对话系统另开一个轻量调用把最近的对话片段发给一个专门的提炼提示词让它输出“用户偏好”“当前任务状态”“需要长期记住的事项”三类结构化的 JSON。我这里贴一份我调试过很多版的提炼提示可以直接抄你是一个记忆整理助手。请阅读下面的对话历史提取需要长期保留的记忆。 只输出 JSON不要解释。 规则 1. 用户明确表达的偏好、身份、技术栈、业务信息必须提取。 2. 暂时性信息如“今天下雨”这类一次性状态不提取。 3. 如果你认为已提取内容与之前记忆冲突以新内容为准并标记 conflict: true。 输出格式 { preferences: [{ key: communication_style, value: concise }], facts: [{ key: user_id, value: xxxx }], task_state: 当前任务描述, conflict: false }实测下来这个“模型帮你归类记忆”的方式比单纯把原始对话塞进数据库再期望检索出有意义内容要靠谱得多。它真正把 Data 变成了 Knowledge。2.4 一个统一记忆模型的字段设计分层归分层落到代码里还是得有一个统一的数据结构去承载不同种类的记忆。我在模拟项目 X 里用的记忆实体比较精简下面这个结构基本够用interface MemoryItem { id: string; // 唯一标识如 uuid type: episodic | semantic; // 情景记忆 or 语义记忆 content: string; // 记忆的完整文本 embedding: number[]; // 向量表示用于相似度检索 importance: number; // 重要度 0~1决定是否会进入长期层 accessCount: number; // 被检索命中的次数用于热度衰减 createdAt: number; // 创建时间戳 lastAccessedAt: number; // 最后一次被用到的时间 sessionId: string; // 来源会话 metadata: Recordstring, any; // 扩展字段会话主题、标签、情绪等 }我强烈建议一开始就把embedding字段加上哪怕你当时还没接向量检索。原因很简单如果之后要给 Agent 加“跨时间的相似场景联想能力”向量字段就是唯一的主干索引没有它你只能靠关键词硬匹配效果差一个量级。3. 一个能跑的短时记忆原型从会话状态到窗口管理3.1 最朴素的做法把所有对话塞进数组我们先实现最直觉的版本。前端转型的人最容易写出的第一版短时记忆逻辑就是一个数组不断 pushclass ShortTermMemory { private messages: Array{ role: string; content: string } []; add(role: string, content: string) { this.messages.push({ role, content }); } getMessages() { return [{ role: system, content: SYSTEM_PROMPT }, ...this.messages]; } }这个版本 30 行代码就能跑通 Demo。如果对话只有三五轮完全够用。但只要你开个测试脚本连续追问 20 轮很快就会触到两堵墙第一堵墙是 token 超限模型直接报错第二堵墙更隐蔽——即使没超限十几轮之后模型对最早信息的注意力已经明显下降提问初期的用户偏好它会回答得颠三倒四。所以从真实项目的角度短时记忆的核心不是“存”而是“管”。管得住上下文窗口Agent 才不会在长对话中越聊越蠢。3.2 上下文窗口的精打细算token 预算表我给模拟项目 X 定的上下文管理策略是给不同信息分区配预算。一个 128K 上下文窗口的模型我不会真的用到 128K实际维护一个软上限通常是窗口的 70% 左右。具体来看这张预算表区域预算占比内容说明系统提示词约 5%人物设定、工具清单、输出规则基本固定很少变化当前任务指令约 10%用户最近一次的具体指令必须完整保留短时对话历史约 45%最近几轮问答按窗口滑动裁剪检索召回记忆约 30%从长期记忆中取回的信息动态替换每次不同缓冲余量约 10%留给模型输出防止超限这个分配比例不是拍脑袋定的是跑了大概两个星期、观察了 200 多轮真实对话后调出来的。之前我把检索召回记忆的预算压得太低导致模型即使拿到了长期记忆也因为没有足够篇幅展示细节而用不上。后来干脆把检索内容的呈现做了一次裁剪每条检索结果强制压缩成三行以内的摘要省下的 token 全部让给对话历史效果反而变好了。3.3 滑动窗口与摘要压缩的取舍预算定好了接下来是“对话历史满了怎么办”。两个主流思路滑动窗口和摘要压缩。滑动窗口的实现非常简单——保留最近的 N 轮更早的全部丢弃class SlidingWindowMemory { private messages []; private readonly maxRounds 10; add(role: string, content: string) { this.messages.push({ role, content }); if (this.messages.length this.maxRounds * 2) { this.messages.splice(0, 2); // 每次丢最早的一问一答 } } }窗口思路的好处是简单、不改原文、不会引入新的错误。坏处是早期关键信息比如开头的预算、目标可能被丢得无影无踪。摘要压缩则是把前面的对话喂给模型让模型总结成一段几百字的“历史纪要”以后把纪要放在系统提示词里。好处是不丢关键信息坏处是每次摘要都有一定概率引入模型自己的改写偏见而且摘要完成前那一次额外调用有延迟和成本。我的折中方案被自己称为“两级记忆”对话前 10 轮完整保留超过 10 轮的部分每 5 轮生成一次增量摘要摘要插入系统提示词原始轮次继续丢弃。这样既保住了最新细节又用摘要维持了长程上下文。你可以直接抄这个思路不一定需要超长上下文模型——最近的感受是工程上把信息组织好比单纯买大窗口省心得多。4. 长期记忆的骨架从文件存储到向量检索的实现全过程4.1 记忆对象序列化与目录设计短时记忆管的是“当前这轮对话”长期记忆管的则是“这个用户下辈子还要用的信息”。第一版我建议先别上数据库直接上文件系统——把每个记忆实体序列化成一个 JSON 文件归到用户目录里。原因很简单可调试性极强打开文件就能看到 Agent 心里到底记住了你什么比连数据库然后查表直观得多。目录结构长这样memory/ users/ u_10086/ profile.json # 用户画像语义记忆 episodes/ 20250628_1030.json # 某次会话的情景记忆包 20250628_1530.json indexes/ episodic_index.json # 情景记忆索引 semantic_index.json # 语义记忆索引每次会话结束后系统把该会话里新提炼出的语义记忆追加到profile.json把打包好的情景记忆写入episodes/并更新索引文件。整个读写逻辑跟前端写localStorage没什么本质区别只是目录层级更规整。4.2 向量化原理为什么记忆需要“坐标”文件能存但怎么找这就是向量检索登场的时刻。简单说向量化就是让模型把一段文字转换成一串数字数组比如 1536 维的浮点数。这串数字就是文字在高维空间里的“坐标”。语义相近的句子坐标距离就近。比如“用户喜欢简洁回复”和“用户嫌回复太啰嗦”字面完全不同但向量距离非常近。这也是为什么我坚持在前转型的第一篇里就让大家去理解“嵌入模型”Embedding Model——它跟对话模型完全是两码事。对话模型负责生成文字嵌入模型只负责把文字变成坐标。第一次看到这层抽象的人可能会绕晕但一旦接受了“文字可以被投影成空间坐标”这个设定后面看所有基于向量的东西都会通。我用的嵌入模型来自某个普通的文本向量接口没有特殊之处。关键在于你要为每一条记忆生成 embedding 并在写入时存好async function saveMemory(userId: string, item: MemoryItem) { item.embedding await embeddingClient.createEmbedding(item.content); const filePath memory/users/${userId}/episodes/${item.id}.json; await writeJson(filePath, item); await updateIndex(userId, item); }4.3 余弦相似度 Top-K 检索的完整代码检索阶段我把用户当前的问题做同样的向量化然后计算向量库里每条记忆的余弦相似度取最高的 K 条返回。余弦相似度的公式不复杂就是两个向量的余弦夹角值越接近 1 越相似。不依赖外部向量数据库纯 JS 也能实现。数据集不大比如几千条时全量暴力算一轮也就是几十毫秒function cosineSimilarity(a: number[], b: number[]): number { let dot 0, normA 0, normB 0; for (let i 0; i a.length; i) { dot a[i] * b[i]; normA a[i] * a[i]; normB b[i] * b[i]; } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } async function retrieveMemories(userId: string, query: string, topK 5) { const queryEmbedding await embeddingClient.createEmbedding(query); const allMemories await loadAllMemories(userId); const ranked allMemories .map((mem) ({ mem, score: cosineSimilarity(queryEmbedding, mem.embedding), })) .sort((a, b) b.score - a.score) .slice(0, topK); return ranked; }这段代码非常直白关键点在于.slice(0, topK)之前的那一整个排序实际上就是决定 Agent“想起来什么”的核心逻辑。你会发现它没有任何规则纯粹是向量空间里的距离计算。这也是 Agent 开发比传统后端开发有意思的地方——很多智能行为不是显式编码的而是从数据里流出来的。4.4 混合检索关键词过滤 向量召回纯向量检索也有它的盲区典型问题是精确信息命中率低。比如用户问“我的会员编号是多少”语义上这句话和当初“我的会员编号是 10086”高度相关向量检索应该能命中。但如果用户问的是“10086 是什么”模型需要的是精确的数字匹配向量检索可能召回一些语义相关但不含正确数字的记忆这时就得出组合拳。我的做法是先用关键词过滤建立候选集再做向量排序。例如把包含“会员编号”“预算”“排除”等业务关键词的记忆先挑出来再在这些候选里按向量相似度排序。const KEYWORD_FILTER [会员编号, 预算, 技术栈, 偏好, 排除]; async function hybridRetrieve(userId: string, query: string, topK 5) { let candidates await loadAllMemories(userId); const hitKeyword KEYWORD_FILTER.filter((k) query.includes(k)); if (hitKeyword.length 0) { candidates candidates.filter((mem) hitKeyword.some((kw) mem.content.includes(kw)), ); } const queryEmbedding await embeddingClient.createEmbedding(query); return candidates .map((mem) ({ mem, score: cosineSimilarity(queryEmbedding, mem.embedding) })) .sort((a, b) b.score - a.score) .slice(0, topK); }这套混合检索上线后模拟项目 X 的“答非所问”率明显下降。此前某个用户问“上次我让你排除的框架是什么”纯向量检索总计给了三条边缘记忆硬是把正确答案挤出了 Top5。加上关键词过滤后跟“排除”有关的记忆直接锁进候选排序结果立刻准确了。5. 混合记忆架构让 Agent 在“记住什么”和“忘记什么”之间做权衡5.1 记忆写入时机观察 → 提炼 → 沉淀三步记忆系统最容易犯的错误是什么都想记结果什么都记不准。我自己在模拟项目 X 里的教训是Agent 的记忆不是越大越好它必须在“记住”和“忘记”之间做一个符合业务目标的权衡。为此我设计了一个三步写入机制每一步都有明确的触发条件。观察系统监听每一轮用户消息只要发现用户消息里出现了明确指令、偏好声明、纠错行为就触发下一步。这里可以用简单的规则检测比如出现“以后/下次/记得/我喜欢/我不喜欢/请直接”等触发词。也可以用一个小模型做分类判断我当时为了省成本先用规则效果不够再加模型。提炼触发后把最近几轮对话交给提炼模型生成结构化记忆。这步上一节讲过输出的是 JSON而不是自由文本。务必强调提炼模型输出必须强约束为 JSON否则后续解析会疯狂出 bug。沉淀把提炼出的语义记忆合并进用户画像文件同时把整段对话打成情景记忆包写入 episode 目录。合并的时候要处理冲突——同一个 key 前面是communication_style: formal这次是communication_style: casual以新值为准并把profile.json里对应字段更新同时保留一条旧值记录方便回溯。5.2 记忆的权重与衰减策略存储层建好之后又一个重要问题浮上来长期记忆是不是永远保留我给每条记忆维护一个importance权重和accessCount访问次数。写入时importance由提炼模型根据“这条信息是否影响多数后续对话”给出比如用户身份、技术栈、禁忌事项importance 直接拉满而某次聊到喜欢的电影这种场景性偏好importance 给中等。检索命中一次accessCount 1长时间没被命中则定期执行一次衰减function decayMemories() { for (const mem of allMemories) { const daysSinceLastAccess (Date.now() - mem.lastAccessedAt) / 86400000; const decayFactor Math.pow(0.9, daysSinceLastAccess); mem.importance mem.importance * decayFactor; if (mem.importance 0.2) { archiveMemory(mem.id); // 移入归档区不作为检索候选 } } }0.9这个衰减系数是我试出来的太快会把用户的长期偏好忘掉太慢会让早期无关记忆一直占用检索资源。按天为单位衰减10 天不访问大概降到原来的 0.35第 20 天左右就跌破 0.2 被归档。归档不等于删除必要时还能捞出来但默认检索视野里看不到它。5.3 让 Agent 主动“想起来”记忆触发的三种方式记忆写进去了怎么保证它在正确的时间被想起来这步如果做得生硬Agent 会在每条回复里都硬塞记忆读起来像个查户口的。我把触发方式分成三种主动注入会话开始前把用户画像和最近的高权重记忆自动放进系统提示词让 Agent 从一开始就“知道在跟谁说话”。这适用于身份信息、沟通风格这些稳定偏好。按需检索在任务执行流程里设置一个判断节点当用户提问中存在涉及历史、偏好、对比需求时调用hybridRetrieve拉取相关记忆注入临时上下文。这适用于跨会话的任务连续性。被动确认当新增记忆与已有记忆冲突时模型需要主动向用户确认。比如用户上次说“技术栈用 Node.js”这次说“我们要整体切到 Java”Agent 应该在回复中回应“收到已更新技术栈偏好。需要我把以前基于 Node.js 的方案备注成历史方案吗”这一步虽然多一次对话但避免了错误记忆覆盖正确信息的风险。我测试过一个记忆机器人的体验闭环第一天让它记住用户是咖啡爱好者第二周用户在对话里提“帮我选支豆子”Agent 直接定位成“上次聊到喜欢耶加雪菲”这个主动想起来的小瞬间正是记忆系统的价值感爆发的时刻。6. 真机实测里的四类问题上下文膨胀、记忆污染、隐私红线与成本账单6.1 上下文膨胀13 轮对话后 Prompt 从 2K 涨到 11K 的实测真实跑起来最原始的问题还不只是 token 超限而是“虽然没超限但响应越来越慢、越来越贵”。我做过一次记录模拟项目 X 的每轮 Prompt用户上来之前就带着 2K 左右的系统提示和记忆注入连续聊 13 轮之后不裁剪对话历史Prompt 涨到 11K 左右。11K token按当时的单价一次普通问答光输入成本就翻了五六倍响应时间也变长了将近一倍。裁剪之后把历史窗口从“全部保留”改到“最近 6 轮 增量摘要”同样 13 轮结束后 Prompt 稳定在 4.5K 左右。这个数字是长对话 Agent 能否落到生产环境的分水岭。我给所有做 Agent 开发的朋友一句口诀Prompt 的大小就是 Agent 的成本和延迟。写代码之前先写 token 预算表。6.2 记忆污染用户说“我没说过”的场景怎么处理记忆系统上线后我遇到的第一个口碑事故是 Agent 自信满满地说出一句用户从未说过的信息。复盘下来问题出在提炼模型把一次场景假设当成了事实。用户原话是“如果预算够的话我们也考虑过云原生方案。”结果提炼模型把“考虑过云原生方案”直接抽成了facts: { preferred: cloud_native }后续 Agent 便坚定地认为用户偏好云原生甚至开始主动推荐相关课程。用户最终忍无可忍“我说的是考虑过不是定了”解决这个问题的办法是给提炼模型加一层“事实置信度”字段要求它对每一条抽取内容标注置信度凡是来自条件句、虚拟语气的记忆置信度一律压低只有明确陈述句才允许进入高置信区。同时高置信区的写入必须经过“用户确认”这一步——模型在回复里带上“我刚刚记录了xxx如果理解有偏差请纠正我”把纠错入口交还给用户。6.3 隐私红线本地存储与云端存储的取舍记忆系统天然就是一台收集用户隐私的机器越精准越敏感。在模拟项目 X 的开发里我对用户敏感信息的处理定了一条硬规矩敏感记忆默认只存本地不上云真要上云必须做字段级加密和脱敏。怎么落地呢把记忆按敏感级别分级。身份类信息姓名、手机号、公司属于高敏感写入时用对称加密存文件检索后仅在用户授权会话内使用偏好类信息咖啡口味、沟通风格属于低敏感正常向量化即可。嵌入模型调用时必须注意不要把高敏感原文直接发给第三方嵌入服务实在要检索就先把原文做哈希映射或脱敏之后再向量化。这条线守不住记忆系统做得越成功产品死得越快。也是从项目中期开始我特别重视“最小记忆原则”能记的少记该清的定时清用户想导出记忆时一键导出想删除时彻底删除。6.4 成本账单一次检索 VS 每次全塞的费用对比记忆系统不是免费的它由三笔钱构成嵌入模型生成向量的成本、存储成本、以及每次检索时把召回内容塞进 Prompt 带来的增量 token 成本。我做过一个对照实验同样 50 轮跨会话任务方案 A 每次把最近 30 条记忆全量塞进 Prompt方案 B 每次只做一次混合检索取 Top5。最终账单差异非常明显——方案 B 在输入 token 上只花了方案 A 的约五分之一而任务完成质量并不差某些场景甚至更稳因为冗余信息少模型反而少了被干扰的几率。所以记忆系统里“取什么”比“存多少”更影响成本。我现在做任何 Agent 项目第一步就是先画一张 token 流向图把每一次调用模型的输入输出都标出来再回头优化记忆策略。这个习惯强烈建议一上来就养成。最后再分享一个实际调试经验。记忆系统刚写完时我根本不知道 Agent 脑子里到底记了什么只能靠对话去猜。后来我给自己加了一个调试命令输/mem dump系统直接打印当前用户的完整记忆文件。这个方法让我在半天时间内就把提炼模型的十几个槽点全找出来了。如果你也在做记忆系统务必给自己留一个类似后门否则只能两眼一抹黑地盲调。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑