大模型Agent记忆技术实战:从原理到代码的完整指南
很多刚开始玩大模型 Agent 的朋友第一次做出来的机器人像个金鱼聊完上一句就忘。这不是模型笨而是我一开始也没搞懂 Agent 记忆技术到底在解决什么问题。这篇指南按照我自己从零到能跑通的路线整理面向小白程序员逻辑讲透代码可以直接抄。我会从原理、分层、代码、参数、踩坑五个角度把“大模型 Agent 怎么记住东西”这件事拆开讲清楚。你不需要一开始就懂向量数据库也不需要背一堆新名词跟着顺序看下来基本就能在自己的项目里给 Agent 装上“记忆”。适合谁读刚接触大模型应用开发、想让自己的聊天机器人具备跨会话记忆、做私人知识助手、或者想在团队里搭一个带记忆的客服问答系统的人。就算你只写过一点 Python也一样能跟上。1. 先搞清楚 Agent 为什么需要记忆1.1 大模型生来就是“无状态”的大模型本质上是一个函数给它一段文字它返回一段文字。它不会在上一次调用结束后保存任何东西。你发两条消息它不会自动记得第一条聊了什么除非你把第一条的内容重新塞进第二条的输入里。这一点和人的对话体验差别巨大。人聊天时会自然记住“刚才聊到了什么”“对方叫什么名字”“他上次说过喜欢简洁”。但模型没有这个概念。所谓“模型有记忆”实际上是在模型外面加了存储和检索再把检索到的内容放回输入里。我刚开始做 Agent 时犯过一个经典错误以为把历史消息存进数据库Agent 就有记忆了。结果发现存了等于没存因为模型根本不会自己去查数据库。它只认当前请求里的内容。所以你真正要做的事情只有一件在合适的时机把合适的记忆内容变成当前请求的一部分。1.2 记忆的本质是上下文管理不是单纯存储很多人把“记忆系统”理解成“聊天记录存储系统”这是误区。存储只是最底层记忆系统的核心是上下文管理。上下文窗口就像桌面记忆库像档案柜。档案柜再满也没关系但桌面只有那么大面积。你不能把所有档案都摊在桌面上不然真正有用的东西反而找不到。Agent 记忆要做的工作是判断哪些档案现在有用、把哪几份抽出来放到桌面上、用完后再收回去。换句话说记忆系统不是“存得越多越好”而是“放得越准越好”。精确地决定“放什么进 Prompt”比“存了多少条历史”重要得多。1.3 这篇指南解决三类问题一类是“记不住”用户昨天告诉你他喜欢简短回复今天你又说了一大篇。这类问题源于跨会话信息丢失。另一类是“记太乱”历史记录、用户偏好、临时状态全混在一起塞进 Prompt 后模型被旧信息带偏回答前后矛盾。还有一类是“记不起”记忆明明存了但检索的时候找不准。比如用户问“上次那个报错解决了吗”系统搜不到因为当时的记录里根本没有“报错”这个词。后面所有内容基本都在围绕“怎么记住、怎么存、怎么取、怎么用、怎么忘”这五件事展开。2. 记忆体系的四层拆分2.1 第一层会话工作记忆第一层是“正在聊的这段对话”也就是当前 prompt 里的消息列表。它由模型自带的上下文窗口承载需要我们自己管理长度和裁剪策略。典型问题用户连续聊二十分钟后历史太长超过窗口上限。简单粗暴的做法是“只保留最近 N 条”但这样会导致更早的关键信息丢失。稍好一点的做法是把早期内容压缩成摘要再把摘要放在消息列表最前面。这一层的管理频率非常高几乎是每次请求都要做。所以它必须足够轻不能依赖外部数据库查询尽量在内存里完成。2.2 第二层情景记忆情景记忆存的是“发生过的事件”。比如“用户上午反馈了支付失败”“上次对话里用户要求换个方案”“上周已经发送过一封确认邮件”。这类记忆的特点是有时间、有事件、有结果通常按发生顺序检索。情景记忆解决的是连续性。没有它用户下次打开对话时Agent 完全不知道你们之前发生过什么。有了它Agent 可以说出“上次你提到支付失败我查了一下可能是银行接口超时”。实现时一般会给每条记忆打上时间戳、事件类型、关联对象这样既能按时间排序也能按事件类型过滤。2.3 第三层语义记忆语义记忆存的是“事实和偏好”不依赖某个具体事件。比如“用户喜欢简洁回复”“项目部署在测试环境”“用户所在行业是教育”。这些内容即使不提时间也长期有效。语义记忆通常适合用向量检索。因为用户问法千变万化“我上次说过的偏好你还记得吗”“回复别那么啰嗦”和“用户喜欢简短回答”这句话在字面上差别很大但语义距离很近。向量检索能抓这种近似关系。语义记忆要特别注意覆盖和去重。同一条偏好可能被写进去三次如果不去重检索时会同时冒出三条重复记忆既占空间又干扰模型。2.4 第四层程序性记忆程序性记忆是“怎么做某件事”的经验比如“遇到数学计算先调用计算工具别自己心算”“生成周报时按数据、结论、行动计划三段来写”。在小项目里这部分通常被写死在代码逻辑或系统提示词里。但做得复杂的 Agent 会把它沉淀为可检索的技能模板。比如一个 Agent 遇到 Excel 处理任务时会自动调出“读取表格-清洗-分析-出报告”的流程模板。程序性记忆的本质是知识复用。它能让 Agent 不去重复摸索流程而是直接使用已经验证过的方法。四层记忆可以共存。我的经验是先把会话工作记忆和语义记忆做扎实再考虑情景和程序性记忆。因为前两个直接决定用户体验后两个更多是锦上添花。记忆类型典型内容存储方式主要用途会话工作记忆当前对话上下文内存消息列表保证当前对话连贯情景记忆某时某刻发生的事件数据库记录时间索引跨会话回溯语义记忆用户偏好、事实知识向量库规则过滤回答个性化问题程序性记忆任务流程、技能模板配置/模板库自动决定执行路径3. 手把手实现一套可用的记忆组件3.1 最小版会话窗口管理先写一个最简单的会话窗口。作用是控制消息列表长度超出的部分挤压成摘要而不是直接扔掉。import json from collections import deque class SessionWindow: def __init__(self, max_messages20): self.max_messages max_messages self.messages deque(maxlenmax_messages) def add(self, role, content): self.messages.append({role: role, content: content}) def to_prompt(self): return list(self.messages) def compress_old(self): # 保留最近一半消息把更早的文本拼成一条摘要占位 keep_count self.max_messages // 2 old list(self.messages)[:-keep_count] recent list(self.messages)[-keep_count:] summary_text 之前对话摘要 .join(m[content] for m in old if m[content]) self.messages.clear() self.messages.append({role: system, content: summary_text}) for m in recent: self.messages.append(m)这里用deque(maxlen...)可以自动丢弃最旧的。但只丢弃不总结会丢关键信息。compress_old就是把早期内容压成一条摘要再把摘要放到 system 里。注意摘要不能太频繁做。每轮都压缩等于把每次回答都再调用一次模型做总结成本和延迟都会上去。建议当消息条数达到上限时才触发。3.2 升级版向量检索长期记忆当需要跨会话记住用户偏好和事件时纯会话窗口不够。我们需要一个“写进去-查出来”的机制。最通用方案是向量检索把文本变成向量用余弦相似度找最接近的内容。下面用一个纯 Python 标准库演示核心逻辑。生产环境一般会用现成的向量库或模型服务提供的 embedding原理一样。import hashlib import json def tokenize(text): # 简单按空格和标点切分真实场景可以用分词库 return [t.lower() for t in text.replace(,, ).replace(。, ).split()] def hashed_embedding(text, dim128): vec [0.0] * dim for token in tokenize(text): digest hashlib.md5(token.encode()).hexdigest()[:8] index int(digest, 16) % dim vec[index] 1.0 return vec def cosine_similarity(a, b): dot sum(x * y for x, y in zip(a, b)) norm_a sum(x * x for x in a) ** 0.5 norm_b sum(x * x for x in b) ** 0.5 if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b) class VectorMemory: def __init__(self, pathmemory.json): self.path path self.items self._load() def _load(self): try: with open(self.path, r, encodingutf-8) as f: return json.load(f) except FileNotFoundError: return [] def _save(self): with open(self.path, w, encodingutf-8) as f: json.dump(self.items, f, ensure_asciiFalse, indent2) def add(self, text, categorygeneral): embedding hashed_embedding(text) self.items.append({ text: text, category: category, embedding: embedding, access_count: 0 }) self._save() def search(self, query, top_k3): query_vec hashed_embedding(query) scored [] for item in self.items: score cosine_similarity(query_vec, item[embedding]) scored.append((score, item)) scored.sort(keylambda x: x[0], reverseTrue) return scored[:top_k]这个写法可以跑通但有个明显缺陷哈希词袋向量对“语义相似”不敏感比如“喜欢简短”和“别啰嗦”几乎匹配不到一起。真实项目里应该把hashed_embedding替换成 embedding 模型生成的向量。替换后其他逻辑不用变。搜索后把返回结果按指定格式拼进 Prompt 就行memory VectorMemory() results memory.search(用户喜欢什么风格) memory_section \n.join(f- [{r[1][category]}] {r[1][text]} for r in results) prompt f以下是长期记忆\n{memory_section}\n\n请基于记忆回答当前问题。每次命中时最好更新access_count后面做遗忘策略会用到。3.3 进阶版写入与遗忘策略长期记忆最怕两件事什么垃圾都存、存了永远不删。所以要加两道流程写入前过滤定期遗忘。写入前过滤举例def should_store(text, min_length10): # 太短的内容没有存储价值 if len(text) min_length: return False # 一些临时寒暄不值得记 useless [你好, 哈哈, 再见, 不知道] if any(u in text for u in useless): return False return True遗忘策略可以用“低频且长期未访问优先清理”的思路import time def clean_memory(memory, max_items200, max_age_days30): now time.time() kept [] for item in memory.items: last_access item.get(last_access_ts, now) age_days (now - last_access) / 86400 # 访问次数低且超过30天未用准备淘汰 if age_days max_age_days and item.get(access_count, 0) 3: continue kept.append(item) memory.items kept memory._save()这只是示例生产里更常用加权分数综合“最近访问时间”“访问次数”“内容重要性”算一个衰减分低于阈值就删除。操作心得遗忘一定要做成定时任务而不是每轮对话都扫一遍。原因很简单记忆表小的时候怎么扫都无所谓但数据到几万条后每次扫描都是性能开销。4. 记忆链路的四个关键步骤与参数选择4.1 写入判断什么值得记记忆写入最容易犯的错是“全量写入”。把每轮对话都原样存进去短期内没感觉时间一长向量库塞满了“今天天气不错”“好的”“嗯嗯”这类无意义内容检索质量直线下降。我自己的筛选标准有三条包含事实比如用户所在行业、项目名称、联系方式、产品版本。包含偏好比如“回复要简短”“不喜欢 emoji”“报告要带数据表”。包含状态变化比如“支付失败已解决”“计划从下周二开始实施”。凡是用户明确表达、重复提到、并且后续可能再次使用的信息才写入。相反寒暄、临时情绪、一次性的流程确认通常不写。写入时还要做归一化。比如用户说“以后叫我老王就行”和“请称呼我为老王”本质是同一件事至少要把称呼统一成“用户希望被称呼为老王”。不做归一化检索时会同时命中两条类似内容白白浪费上下文空间。4.2 检索如何让“相关”变成“精准”检索是记忆系统里最影响体验的环节。相关不代表有用。用户问“昨天那个问题解决了吗”检索出十条“昨天聊过的东西”模型反而不知道具体是哪件事。通用做法是“先过滤再排序”。先用元数据缩小范围比如时间范围、对话场景、记忆分类然后做向量检索按相似度排序最后按分数阈值截断。相关参数我建议这样设置top_k从 3 开始调。太少容易漏太多容易杂。min_score低于 0.2 的记忆不要放进上下文。这个值需要配合你用的 embedding 模型来定不同模型分数分布差很多。recency_bonus给近期内容加一点权重。例如最终得分 相似度 * 0.8 时间新鲜度 * 0.2。调参时建议每次只改一个变量别同时动 top_k 和阈值不然你不知道是哪个改动带来的效果。4.3 融合检索结果如何安全放进 Prompt检索结果进了 Prompt不代表模型会正确使用。你需要告诉模型“哪些是记忆哪些是当前对话”。我常用的格式【长期记忆】 - 用户偏好喜欢简短回复 - 事实当前项目使用测试环境 【当前对话】 用户昨天那个报错后来怎么解决的这样模型可以清楚区分信息来源不容易把旧记忆当成新输入。如果记忆内容和当前对话冲突比如记忆说“用户喜欢详细报告”但当前用户说“别废话直接给结论”我会在 Prompt 里加一句“当长期记忆与当前对话矛盾时以当前对话为准”。融合阶段还要控制体量。我给自己定过一个预算记忆内容不超过整个 Prompt 的 30%。超过这个比例模型会迷失重点回答又臭又长。4.4 遗忘记忆系统必须会做减法遗忘和写入同样重要。没有遗忘记忆库就是垃圾堆。旧信息会干扰新判断甚至让 Agent 用过时信息回答当前问题。我项目里至少维护三种遗忘机制过期删除比如“临时任务”类记忆 24 小时后失效。低频删除超过 30 天没被检索到的记忆权重下调最终清理。冲突覆盖当新记忆与旧记忆语义高度相似且信息更新时删除旧记录。遗忘是记忆系统和日志系统的分界线。日志是”全都要存永不删除“记忆是”只保留对后续决策有用的信息“。很多人把这两者混在一起最后系统既不像日志也不像记忆。5. 落地实践五个经典问题5.1 检索不到用户觉得你“失忆”了现象是用户明确告诉你“我上次说过”Agent 却说“抱歉我没有找到相关信息”。先检查三步写入有没有成功、检索条件是不是太严、文本有没有分词或编码问题。我踩过一个坑是 embedding 用的是旧模型新对话里的词分布变化太大导致相似度普遍偏低。换新 embedding 模型后问题消失。如果检索确实没问题但用户换了说法可以加一层“关键词向量混合检索”。关键词负责精确匹配向量负责语义近似两边各自返回一批候选最后合并排序。这样能覆盖“字面不同但意思相近”的情况。5.2 检索出一堆但全用不上现象是明明返回了记忆模型还是答非所问。原因通常是 top_k 太大或者检索时没有限定记忆类型。比如用户问的是项目状态结果系统把用户闲聊偏好、历史情绪、任务记录全检索出来了。正确做法是按“场景 分类”过滤。查询前先识别意图这是任务咨询那就只检索“任务类”记忆这是闲聊那就只检索“偏好类”记忆。过滤后再做向量排序准确率和效率都会好很多。5.3 上下文越来越长费用吃不住随着记忆累积每次请求都会变得越来越长。如果不对上下文做限制Token 费用会线性上涨。我的做法是三层存储热记忆最近 1 小时内的会话直接放内存。温记忆跨天但频繁使用的内容放向量库按需检索。冷记忆一个月前的内容压缩成摘要后归档。每次请求前先算一次总 Token。如果超过窗口预算就降低“温记忆”的召回条数。比如正常取 5 条超预算时就只取 3 条。优先保当前对话长期记忆永远让路。5.4 记忆被污染越修越错这是最隐蔽的问题。“污染”指错误的记忆被写入后模型每次都会基于错误信息回答用户纠正完后旧的错误还在又会被检索出来。避免污染的关键是更新机制。当出现一条新记忆与旧记忆明显矛盾时不应“并列存储”而应该做覆盖。比如旧记忆是“项目部署在测试环境”用户今天说“项目已经上线生产了”这时直接删除旧条目或标记为过期只保留新条目。另外必须给用户提供“删除记忆”的能力。哪怕只是提供一个简单的“清除我的所有历史记忆”按钮也能兜底解决大部分污染问题。5.5 安全与隐私怎么处理记忆里往往包含很多个人信息和业务敏感信息。至少要遵守这几条不记录密码、密钥、Token 等敏感字段。写入前先做脱敏手机号、身份证号、银行卡号用掩码替换。记忆数据按用户隔离不能在多用户间串数据。明确告知用户我们存储了哪些信息并支持一键清除。合规这块不能等项目上线才补。从第一版设计记忆组件开始就要把数据隔离和删除机制考虑进去。否则后期改造成本会很高。症状可能原因排查顺序快速对策完全检索不到写入失败或阈值过高先查写入日志再查检索分数放宽 min_score启用混合检索检索结果不相关top_k 过大、未按场景过滤看返回条目的类型分布先按记忆分类过滤再向量排序上下文超预算记忆未分级、没有限制条数统计每次请求 Token 消耗三层存储限制温记忆召回记忆越用越错冲突信息未覆盖、污染写入检查是否有重复矛盾记录增加覆盖机制提供用户删除入口数据安全风险敏感字段明文入库检查入库前是否有脱敏逻辑脱敏隔离可删除6. 进阶方向与选型建议6.1 从向量检索迈向知识图谱向量检索擅长回答“语义相近”但不擅长回答“多跳关系”。比如“A 用户负责的 B 项目里有哪些模块是用 C 技术实现的”这种问题如果只靠向量匹配很难一次命中。知识图谱则可以把实体和关系串起来支持精确的关系查询。但知识图谱的构建成本高需要维护实体、关系、属性和更新逻辑。我的建议是先跑通向量记忆等确实遇到“关系推理”类需求再引入图结构。小项目上来就做图谱大概率会被建图工作量拖垮。6.2 记忆压缩的两种路线一是摘要压缩。把长段对话交给模型生成一段摘要放入长期记忆。优点是保存的信息密度高缺点是有损细节会丢失。二是结构化压缩。从对话中抽出结构化信息如字段、时间、意图、结论保存成 JSON。这种格式适合程序处理也适合做条件查询。一条可行路线是“先结构化再按需摘要”。重要内容抽成结构化字段长期不用的内容再合并成摘要。这样兼顾了即时检索和长期归档。6.3 什么时候该自研什么时候用现成组件如果是快速做 Demo、验证想法直接用现成框架自带的记忆模块最省事。但默认实现通常比较粗糙比如全量存历史、简单截断、不做遗忘。一旦你的用户量上来或者对记忆准确率有要求就值得自己写一套轻量记忆模块。我个人的判断标准是记忆逻辑不超过几百行代码时自研更划算。你能精确控制什么时候写入、检索多少条、如何融合、如何遗忘。而使用重型框架时光是理解它的记忆生命周期就要花很久。如果你打算在自己的项目里长期迭代 Agent我建议至少把“写入过滤检索召回遗忘清理”这三个核心组件自己写一遍。这样独立于任何框架后续迁移也方便。最后再分享一点个人体会我最初做的记忆系统很蠢一个 JSON 文件存所有历史每次对话都把所有历史塞进 Prompt。结果模型越聊越乱成本越烧越高。后来慢慢改成“写入前过滤、检索时排序、融合前控制体量、定期做遗忘”整个 Agent 才稳定下来。如果你也在做类似项目我的建议很直白不要一开始就上重型设施。先在本地跑一个 JSON 存储 向量检索的最小闭环把“写入-检索-融合-遗忘”体会一遍。跑通之后自然知道哪些地方必须换数据库、哪些地方必须加规则。记忆系统的价值从来不是存得多而是用得准。希望这份指南能让你少走一点弯路。