资讯动态

AI Agent记忆系统实战:从工作记忆到长期记忆的完整落地指南

发布时间:2026/10/19 13:45:28 来源:尧图企业网站定制
直接开始。AI Agent 圈子里最近聊得最热的一个话题就是记忆系统。很多人做着做着就发现Agent 单轮对话能力已经够强了可一旦聊到第十轮它能把最开始你交代的背景忘得一干二净。你让它写一份方案写到一半它忘了之前约定的格式你让它帮你跟进一个项目它盯着当前这封邮件就回完全不顾前面几周讨论的结论。说到底缺的就是“记忆”。我自己在搭 Agent 工作流的时候早期用的方案非常简单粗暴把整个对话历史一股脑塞进上下文。结果窗口一满系统就开始胡言乱语要么把早期信息截断掉要么被无关的闲聊冲淡重点。后来我专门花了一段时间把记忆系统完整做了一遍这才把 Agent 从“金鱼脑”拉回“人脑”的状态。这篇文章不搞理论堆砌就是把我实际落地时用到的思路、代码、坑全部拆开手把手给你讲清楚。适合刚接触 Agent 开发、想给自己的项目加上长期记忆能力的朋友也适合已经在用 LangChain 或自建框架、但对记忆模块怎么设计还比较模糊的开发者。1. 核心思路拆解记忆系统到底在解决什么问题想清楚“为什么做”比直接敲代码更重要。Agent 的对话模型本质上是无状态的模型不会记得上一次调用发生了什么每一次调用都像第一次见面。所以 Agent 需要一个外层系统来模拟“记忆”这个能力本质上就是把模型从完全无状态改造成有状态协作。1.1 如果不做记忆系统Agent 会踩哪些坑首先是最直接的上下文长度问题。现在主流的大模型上下文窗口已经不算小但上下文是个有限的资源不是无限的水池。长对话中早期信息被截断是最常见的事故。比如用户在第 40 轮问起“我之前让你记录的预算上限是多少”Agent 早把这条信息忘干净了。这类问题靠加大窗口不能根治窗口越大成本越高、响应越慢而且模型对长文本中关键信息的注意力也会被稀释。其次是无差别记忆带来的信息污染。如果每次都把全部历史塞进去那么历史里那些闲聊、临时纠错、旧结论都会和当前有效的约束混在一起。模型分不清什么是“当时说了一嘴的”什么是“长期有效的规定”于是经常出现前后矛盾的回答。这个现象很典型上午你还告诉它“不要用某个外部接口”下午它又给你推荐那个接口。再者是缺少遗忘和更新的机制。真实的人类记忆不是录像带而是不断重写、调整。用户修正了一个偏好系统应该在下一次决策时立刻采用新偏好而不是继续沿用旧数据。如果没有记忆系统这些修正要么只能在当前这一轮生效要么会随着对话历史滚动消失完全无法沉淀下来。1.2 记忆的两种基本形态工作记忆和长期记忆在设计记忆模块前先把记忆分类。我习惯把它拆成两层对应人脑的工作记忆和长期记忆。工作记忆就是一问一答范围内需要保留的信息。它不需要持久化用完就能丢。最简单的实现就是把最近的几轮对话拼成一段文本连同当前问题一起提交给模型。这一层的核心是控制窗口大小和顺序太短记不住前文太长会浪费 token 且引入噪声。对于大多数轻量级场景保留最近 6 到 12 轮通常够用。长期记忆才是真正的难点。它需要保存的是跨会话、跨轮次仍然生效的信息用户身份、项目背景、已完成的事项、明确偏好、关键约定。这些信息通常不随某轮对话结束而失效可能这个星期确定了下个月还在用。长期记忆的存储就不能是“聊天记录的堆叠”而必须抽成结构化、可检索的知识。我的做法通常是把信息按语义拆成“记忆条目”一条记忆就是一个短文本附加属性存入向量数据库需要时通过相似度检索召回让 Agent “想起来”该想的事情。1.3 一句话说清楚这个方案的架构整个记忆模块可以抽象成三层写入层从对话中提取值得长期记住的信息给它生成向量存进索引。存储层一个带相似度检索能力的数据库负责保存和查询向量。召回层每次收到新问题把问题向量化和历史记忆做相似度匹配取回最相关的若干条注入提示词。这套结构最大的好处是解耦。存储层用什么数据库、嵌入用什么模型、召回策略怎么调每一环都能独立替换和优化。我最早用一个字典加列表模拟存储后面才换了真正的向量库代码结构几乎没动只替换了存储实现。2. 技术底座怎么选嵌入模型、向量库、相似度计算记忆系统的核心依赖两块把文本变成向量的嵌入模型以及能把向量存下来并高效检索的向量数据库。选型直接影响效果和成本值得多花一点时间。2.1 嵌入模型的选择思路嵌入模型负责把“语义”转换成数字向量。这里的要求是语义相近的文本向量距离越近越好。比如“用户喜欢简洁的邮件风格”和“用户偏好简短回复”在语义上是近似的向量检索才可能在用户问“回复邮件时注意什么”时把这条记忆捞出来。嵌入模型我建议优先考虑开源的中文友好模型。常见的 BGE 系列、m3e 系列都有不错的中文效果可以本地跑不依赖外部 API。不要一上来就追求最好效果先把流程跑通后续换模型只是一行配置的事。如果对效果要求高可以用量化版或在消费级显卡上跑 small 版本成本很低。嵌入的粒度也很关键。有人习惯把整段对话记录直接向量化这样做只能做整段召回精度很差。我的建议是写入之前先做信息切分一条记忆只保存一个明确的事实。如果一段对话里包含多个要点就拆成多条。这个先决条件决定了后面召回质量的下限。2.2 向量数据库怎么选从零依赖开始向量数据库本身不需要想得太复杂。如果你的项目刚起步或者只是做个个人知识库直接在内存里用 numpy 计算余弦相似度就够了。我自己最早就是用列表存向量然后暴力计算数据量几百条时完全没问题。追求精确的工程落地再上真正的向量数据库。选型时主要看四个维度部署成本、检索性能、过滤能力、生态成熟度。给小项目或学习用Chroma 这类嵌入式向量库就很合适它支持持久化、相似度检索、元数据过滤并且接口简单。如果数据量真的大到百万级再考虑 FAISS 或专门的数据库服务。这里不展开对比我给一个简单的选型表场景推荐方案理由学习/原型验证内存列表 numpy 余弦相似度零依赖、逻辑透明小项目本地部署Chroma 嵌入式模式持久化简单API 友好中大规模生产FAISS 或专业向量库检索性能高支持分布式扩展从学习角度出发我认为先用最简单的方式实现一遍再迁移到正式向量库能让你理解最透彻。很多人上来就引 Chroma结果连向量怎么算相似度都没搞清出了问题无从排查。2.3 相似度计算里的关键参数向量检索的核心是把“回忆”变成一个数学问题给定一个查询向量找到库里最接近的向量。常用的计算方式是余弦相似度取值范围在 -1 到 1 之间越接近 1 表示越相似。实际使用时还有一个重要参数相似度阈值。这个阈值决定了“多像才算想起来”。阈设得太低检索结果里全是无关记忆会污染上下文阈设得太高又容易什么都召不回Agent 表现出失忆。我在实践中的经验是中文通用领域余弦相似度阈值先设在 0.4 到 0.45 之间然后根据实际结果慢慢调。为什么不是一个绝对数因为不同嵌入模型的分布不同有的模型哪怕完全无关的文本都可能在 0.35 以上这种必须通过抽样测试来确定。后面我会专门讲怎么用一组测试文本来校准阈值。3. 从零手写一个记忆模块完整代码实操下面直接给出一个最小可用的记忆系统实现。我用 Python尽量少用第三方库方便你理解每一步。完整代码不追求华丽但我会把写入、存储、召回三个核心流程全部走通。3.1 环境准备与依赖安装建议先新建一个虚拟环境然后安装以下依赖pip install numpy pip install sentence-transformers pip install chromadbsentence-transformers负责加载嵌入模型chromadb负责持久化向量。我这套代码里用的模型是在本地运行的轻量级中文嵌入模型具体模型名可以换成你手头已有的同类模型。安装完成后先做一个快速验证看看模型能不能正常加载from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-small-zh-v1.5) vec model.encode(用户喜欢简洁的回复风格) print(vec.shape) # 输出一个向量维度如果你在初始化模型时第一次需要下载权重网络慢的话耐心等一会儿。实际项目中也可以把模型提前下载到本地目录再指定路径加载。3.2 定义记忆数据结构严格来说一条记忆不只是字符串它应该包含记忆原文、创建时间、更新时间、来源会话、重要程度等。但为了讲清主流程我先保留最小字段后面的进阶部分再补充。from dataclasses import dataclass from datetime import datetime dataclass class MemoryItem: content: str created_at: str datetime.now().isoformat() metadata: dict None def to_dict(self): return { content: self.content, created_at: self.created_at, metadata: self.metadata or {} }这里把metadata单独留出来是为了以后做过滤。比如一个 Agent 同时服务多个任务可以用metadata里的task_id做区分召回时只查当前任务相关的记忆。这样做的价值会在实战中很快体现。3.3 实现向量检索层我直接用 Chroma 作为存储检索单元因为它在本地跑起来简单而且支持按元数据过滤。import chromadb from chromadb.config import Settings client chromadb.Client(Settings( chroma_db_implduckdbparquet, persist_directory./memory_store )) collection client.get_or_create_collection( nameagent_memory, metadata{hnsw:space: cosine} ) def write_memory(content: str, metadata: dict None): vector model.encode(content).tolist() item_id str(hash(content str(datetime.now().timestamp()))) collection.add( ids[item_id], embeddings[vector], documents[content], metadatas[metadata or {source: manual}] ) return item_id def recall_memory(query: str, top_k: int 5, threshold: float 0.4): query_vector model.encode(query).tolist() result collection.query( query_embeddings[query_vector], n_resultstop_k ) docs result[documents][0] distances result[distances][0] recalls [] for doc, dist in zip(docs, distances): # cosine distance 转相似度 score 1 - dist if score threshold: recalls.append({content: doc, score: score}) return recalls注意这里 Chroma 返回的是余弦距离不是相似度我需要把它转换成相似度1 - distance。threshold的默认值先设为 0.4不代表所有场景都适用后面会讲怎么校正。3.4 从对话中抽取长期记忆这一步是记忆系统里最依赖模型能力的部分。真实对话通常不是一条干净的事实陈述里面夹杂着语气词、闲聊、临时信息。直接整段存进去召回时就会带回很多噪音。最简单的思路是让大模型自己判断哪些信息值得写入记忆。我在实际项目中通常写一个小函数输入对话历史输出若干条结构化记忆。下面是一个示意省略了具体模型接口调用重点关注流程from typing import List def extract_memory_from_dialogue(history: List[dict]) - List[str]: # history 形如 [{role: user, content: ...}, {role: assistant, content: ...}] # 写一个 prompt让模型返回 JSON 数组每一项是一条需要长期记忆的事实 prompt f 你是一个记忆抽取器。请从以下对话中提取需要长期记住的信息。 只提取明确的偏好、约束、关键事实。忽略寒暄和临时问题。 返回 JSON 数组每一项是一个字符串。 对话记录 {history} 输出 # 调用大模型接口拿到 JSON 结果 # json_result call_llm(prompt) # return json.loads(json_result) pass实际运行时我会让模型输出严格 JSON再用json.loads解析。这里有三个细节会显著影响效果抽取时把“当前对话的最后一轮”和“历史沉淀”分开最后一轮往往有临时的操作指令不应该都进长期记忆约定了新规则才写入。如果一条事实同时包含多个约束强制模型拆成多条。二选一的偏好和“必须同时满足 A 和 B”是两类信息存成一条将来不好检索。给抽取结果附上置信度或来源标签用于后面冲突处理。3.5 组装完整的 Agent 记忆交互流程把上面这些模块串起来一个完整流程如下先召回记忆再把记忆和最近对话一起组装成提示词最后让模型生成回答。生成后再抽取新记忆写回库中。def build_agent_response(user_input: str, history: List[dict]): # 1. 召回长期记忆 memories recall_memory(user_input, top_k5) # 2. 组装系统提示词 if memories: memory_text \n.join([f- {item[content]} for item in memories]) else: memory_text 暂无相关长期记忆 recent_history history[-6:] # 工作记忆保留最近 6 轮 system_prompt f 你是智能助手。请参考以下长期记忆回答用户问题。 长期记忆 {memory_text} 最近对话 {recent_history} 请基于这些信息自然回答。 # 3. 调用大模型此处省略接口细节 answer call_llm(system_prompt, user_input) # 4. 抽取并写入新长期记忆 new_memories extract_memory_from_dialogue(history [ {role: user, content: user_input}, {role: assistant, content: answer} ]) for mem_content in new_memories: write_memory(mem_content, metadata{source: agent_interaction}) return answer这段代码结构很清晰召回 → 组装 → 生成 → 抽取 → 写入。实际接口调用需要你自己对接具体的大模型但记忆层的逻辑是通用的换任何模型都可以复用。我自己用这套流程试过多个模型效果上的差异主要集中在抽取和生成阶段记忆层的稳定性很好。到这里一个能跑起来的记忆系统已经成型。把上面几个函数放进同一个 Python 文件加一个输入循环就可以做简单的对话测试了。测试时你会明显感觉到隔了几个小时后重新启动程序它还能记得之前记录下来的偏好。4. 进阶优化让记忆系统真正“好用”起来基础链路能跑通只是第一步。从工程落地角度还有很多问题会逼着你优化。我把踩过的坑和最终采用的方案整理成几个方向。4.1 阈值怎么调才靠谱很多人第一次跑起来发现召回的结果里混着明显无关的记忆或者该被召回的记忆找不到首先想到的就是调阈值。但阈值不是拍脑袋定的。我会准备一组人工标注的测试对比如 20 个查询每个查询配 5 条应该召回的“正样本”和 5 条无关的“负样本”。跑一遍检索记录所有结果的相似度分布画出正负样本的分界线。通常正样本的相似度集中在 0.4 到 0.75 之间负样本集中在下 0.3 以下中间有一段重叠区。阈值就取重叠区偏保守的位置宁可少召回也别把噪声带进上下文。这个校准过程需要一定耐心但一次校准可以长期复用。采用这个做法后我的记忆召回准确率有明显提升。早期我依赖默认阈值差不多有 30% 的召回结果要在生成前人工过滤。校准阈值之后这个比例降到了 5% 以内。4.2 记忆更新与冲突处理长期记忆里最麻烦的不是写不进去而是信息变了怎么办。用户昨天说“项目预算上限 5000 元”今天又说“预算调整到 8000 元”。如果系统只会追加不会更新Agent 召回时会同时看到两条冲突记忆结果完全取决于哪条排在前面这就很难稳定。我的处理方法分两步。第一步写入前先做一次“冲突检测”拿新记忆作为查询去库里检索最相似的已有记忆。如果相似度极高且元数据里有对同一主题的记录就视为需要更新而不是新增。第二步更新时把旧记忆标记为不可用或者物理删除。对大多数场景直接删除旧记录是最省事的。这里的关键是检测冲突时的相似度阈值要设得比召回阈值高比如 0.8 以上否则容易误删一些正常的新记忆。4.3 给记忆加上重要度和生命周期不是所有记忆都值得永久保存。比如“用户今天临时喜欢用表格回复”可能过两天就失效了。我给记忆结构里增加了两个字段importance重要度和expire_at过期时间。重要度由抽取阶段模型判断取值范围 0 到 1过期时间由业务规则决定。检索时先过滤未过期记忆如果召回的总体数量不够再考虑降级使用低重要度记忆。这个设计用起来非常自然。比如“用户的真名”和“用户的临时偏好”重要度不同在系统里的预期生命周期也不同。对于重要度较低的记忆我还会定期做清理防止库被无效信息占满。4.4 记忆丢失与幻觉并存的排查思路当 Agent 出现“明明存了记忆却像没存一样”的情况时问题不一定出在存储而是出在“召回之后有没有真正进入提示词”。我排查时有一个固定的顺序先看检索结果再看提示词最后看生成结果。具体来说把这个问答链路里的每一步都打日志记录查询向量对应的原文是什么检索召回了哪些记忆相似度分别是多少最终注入提示词里的记忆条目有几条分别是哪些。有一次我觉得系统“失忆”了查日志发现召回结果里确实有正确记忆但组装提示词时被截断逻辑错误地裁掉了。所以有问题时先不要怀疑模型把链路日志拿出来很容易定位。4.5 常见问题速查表我把日常维护中碰到频率最高的问题整理成一个速查表方便后面直接对照。现象可能原因解决方法召回结果大量无关阈值设太低校准阈值或提高 top_k 后重新过滤该召回的一条都没有阈值太高/记忆未写入先检查写入日志确认记忆是否确实落库模型前后矛盾旧偏好反复出现记忆更新失败存在冲突项增加冲突检测删除旧记录提示词太长成本飙升召回条数过多或单条记忆过长调低 top_k限制单条记忆长度控制历史轮数不同会话之间记忆串味缺少元数据过滤写入和查询时都带上 session / task 维度过滤向量库文件越来越大无清理机制增加过期和重要度过滤定时清理这套速查表基本涵盖了我遇到过的九成问题。剩下的一成属于模型抽取质量不佳通常靠优化抽取提示词解决而不是调检索逻辑。5. 工程落地中的成本与隐私考量做记忆系统时成本往往不体现在模型调用上而是体现在存储冗余和无意义写入上。每一条记忆都要经过嵌入模型计算和向量存储看起来单价低但长期累积的数据量会持续增长。如果每一轮对话都往库里面写 5 到 10 条记忆那么一个月下来就是几千甚至上万条。其中大部分可能都是废话检索效率自然下降。5.1 写入前多加一道“价值判断”我在抽取记忆之后、写入向量库之前增加了一个判断步骤这真的是一条值得长期保存的信息吗判断规则可以很简单包含明确的偏好词喜欢、讨厌、希望、不要、项目参数预算、时间、截止时间、身份属性职位、团队、联系人时才值得写入。只包含临时操作指令的比如“请现在帮我把这份文件转成 PDF”完全没必要进入长期记忆。这道过滤看似不起眼却能显著减少存储膨胀。我在实际项目中加上这层规则后记忆库的体积增速下降了超过一半。5.2 隐私信息怎么处理记忆系统涉及的隐私问题比普通对话更多。因为记忆往往长时间保留姓名、公司、账号、内部数据都可能存下来。工程上的基本要求敏感字段进行脱敏或加密存储记忆库的访问权限必须单独控制不能和业务数据库混在一起用户明确要求删除时要能按会话或按内容维度批量清除。向量数据库天然不支持“按内容模糊删除”所以要设计好元数据标签删除时按元数据过滤后逐条删除。5.3 混合检索策略到数据量更大时可以考虑混合检索向量相似度召回为主关键词匹配作为补充。原因是某些精确信息如编号、邮箱、日期用向量检索并不一定精确。关键词匹配能直接命中两者取交集再排序召回质量会更稳。实现上可以在 Chroma 的where_document过滤里做关键词匹配或者额外用一层倒排索引。这个属于进阶玩法项目没到一定规模前不必着急上。6. 多 Agent 场景下的记忆共享与隔离如果你做的不是单个 Agent而是一组 Agent 协作的系统记忆就要考虑共享和隔离的问题。不同 Agent 之间有些信息应该互通比如用户的基础偏好有些信息应该隔离比如某个任务内部的临时状态。我采用的方案是在每条记忆的元数据里标记scope字段可取值为global、task、user。召回时根据当前 Agent 的角色过滤 scope。比如负责邮件撰写的 Agent只召回scopeemail或scopeglobal的记忆不掺入代码开发 Agent 的记录。这样就避免了多 Agent 之间的信息串扰。实际做下来比单纯给每个 Agent 建一个独立库更灵活同库存储但逻辑隔离后续也方便做跨 Agent 的知识迁移。7. 未来可以扩展的方向最后再分享一个我可以确认的扩展点。现在的记忆系统主要依赖文本向量检索但它有个天然局限无法把记忆组织成结构化的知识网络。比如“项目 A 的负责人是 XX 偏好每周一汇报”这两条记忆在向量空间里可能离得挺远但它们在逻辑上关联。未来方向之一是用知识图谱加向量检索的混合存储先用图谱做逻辑关联再用向量做语义召回。另一个方向是记忆的层次化压缩。对历史对话做摘要、再对摘要做摘要形成一个锥形结构越远的历史越抽象。这样即使不引入复杂模型也能保留更长时间尺度的信息。我目前还在测试中但已经验证了它能把单条长对话的召回质量提升不少。如果你做到这一步说明你的记忆系统已经进入比较深的领域了。我在实际落地中的最大体会是不要急着堆技术组件先把自己的对话场景盘清楚明确哪些信息必须记住、记多久、给谁用。记忆系统的难点不在于“能不能存”而在于“存得准、取得到、用得对”。把这三点做扎实你的 Agent 就能真正从“金鱼脑”进化成“过目不忘”。

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

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

免费获取报价 →
↑