FEATURED · 精选文章

Agent长期记忆架构设计:从记忆分层到向量混合检索与遗忘机制

发布时间 / 2026/8/31 13:53:20
来源 / 创域科博编辑部
栏目 / 资讯中心
Agent长期记忆架构设计:从记忆分层到向量混合检索与遗忘机制 LLM 的上下文窗口再大关掉会话就“失忆”。这是当前 Agent 落地时最常见的痛点。很多团队把“上下文窗口”当成“记忆”来用结果对话一长就开始漏信息、重复提问甚至把上一轮的错误决策带进下一轮。真正能撑起复杂任务的 Agent需要一套独立于模型上下文之外的记忆系统有写入、有检索、有更新、有遗忘。这次我们来完整拆一遍 Agent 长期记忆架构。文章会先讲清短时记忆与长期记忆的边界再给出一套可落地的记忆分层模型然后逐步展开记忆写入、混合检索、遗忘机制的实现思路最后给出一个可以照着改的工程模块设计和接口示例。内容偏系统设计也包括可以直接抄的代码骨架。1. Agent 记忆架构核心思路速览先给一张总览表后面所有章节都围绕这张表展开。设计项推荐方案记忆分层工作记忆 / 情景记忆 / 语义记忆 / 技能记忆短时记忆存储会话上下文、任务栈、Redis 缓存长期记忆存储关系型数据库 向量索引或专用向量数据库写入触发用户显式表达、任务完成、事件发生、定时回顾检索链路向量召回 关键词召回 时间过滤 重排序遗忘策略时间衰减、重要性评分、容量上限、显式删除接入方式记忆服务 API或接入 LangGraph 等框架的 memory 节点合规要求最小必要存储、支持用户删除、敏感信息脱敏这套结构解决的核心问题是Agent 不再只有“上下文窗口里的临时文本”而是拥有一个跨会话、可检索、可管理的事实库。短期记忆负责“当前任务跑得顺”长期记忆负责“这个用户是谁、之前做过什么、结论是什么”。2. 为什么 Agent 需要独立于大模型之外的长期记忆大模型本身是没有状态的。你调用一次接口传入完整的对话历史模型才能“知道”前面聊了什么。这也是为什么很多人把上下文窗口当成 Agent 的记忆因为从表现上看只要把历史消息全部拼进 promptAgent 好像就记得住。但这种方式有四个硬伤。第一成本随轮数膨胀。每一轮对话都把全部历史发给模型token 消耗持续增长。第二窗口有上限超出之后只能截断。截断会把关键结论丢掉而且往往是对话中期的信息先丢。第三无法区分“值得记的事”和“临时的一句话”。用户在某轮随口说“我比较喜欢简洁的回复”这句话如果不在窗口里下一轮就没了如果把所有话都留着又会污染 prompt。第四不同任务之间无法共享记忆。用户上一个 Agent 里确认过的偏好下一个 Agent 完全不知道。所以长期记忆的定位应该是把对话中真正有长期价值的信息抽取出来存到记忆系统里等到下一次需要时通过检索把相关的记忆片段重新放回 prompt。也就是“唤醒”而不是“全量携带”。这也正是 LangChain、LangGraph 等 Agent 框架开始强调长期记忆能力的原因——模型本身负责推理外部记忆模块负责记住两者各司其职。3. 短时记忆与长期记忆的区分很多入门设计会漏掉一个重要问题什么样的信息算短时记忆什么样的信息必须进长期记忆区分标准不能只看“时间长短”还要看信息是否具备跨任务复用价值。短时记忆的特征是只在当前任务内有效例如用户正在填写的表单内容、中间推理步骤、临时文件路径容量小生命周期短任务结束即失效复用的价值低绝大多数中间状态不需要进入长期库。长期记忆的特征是可以跨会话、跨任务使用例如用户偏好、项目背景、已确认的业务规则时效性可能很长也可能有有效期例如“用户当前正在使用免费套餐”从原始对话中抽取出来之后仍然需要继续维护包括冲突处理和过期淘汰。下面用一张表做对比维度短时记忆长期记忆生命周期会话内或任务内跨会话、跨任务存储位置内存、Redis、会话文件数据库、向量索引数据形态原始消息、任务状态、临时变量结构化事实、摘要、用户画像写入方式直接追加抽取、去重、冲突合并检索需求无直接读取需要检索只取相关片段遗忘方式会话结束即清理需要显式遗忘策略如果把这个边界搞混会出现很典型的问题。把临时信息写进长期库长期记忆会被噪声淹没检索时永远命中一堆无关记录把长期信息全部留在短时上下文里token 成本暴涨而且会话结束之后这些信息就永久丢失。所以分层设计不是理论洁癖是实际工程必须做的第一步。4. 记忆分层工作区、情景区、语义区、技能区我建议把 Agent 记忆分成四层。每一层解决一类问题存储介质、写入触发条件和维护策略都不同。4.1 工作记忆层L0工作记忆层对应短时记忆存的是当前正在执行的会话状态和任务状态。包括用户最新输入、Agent 的推理中间结果、待处理队列、工具调用记录等。实现上不需要复杂数据库通常用内存对象或 Redis 就行每个会话一个键。要注意的是必须设置生命周期例如会话闲置 30 分钟后清理或者任务完成后压缩。工作记忆层虽然技术含量不高但它是整个记忆系统的入口。很多长期记忆的抽取工作就是从这里拿到原始素材的。4.2 情景记忆层L1情景记忆层存的是跨会话但没有经过太多抽象的事件记录。例如“用户在 3 月 10 日要求生成一份月度报表”“用户在上一次对话中提到了项目 A 的截止时间是月底”。这些记录是“发生过的事实”保留原始语义。情景记忆通常会被转成长期记忆之后再做清理而不是一直无限堆积。它的价值在于当 Agent 需要回答“我的项目甲方之前提过什么要求”这种问题时情景记忆是最终的事实来源。存储建议使用普通数据库表每条记录包含时间、事件摘要、关联实体、重要性分数和过期时间。4.3 语义记忆层L2语义记忆层是长期记忆的核心存的是经过抽取和抽象之后的稳定事实。例如“用户偏好简洁回复”“用户负责的项目叫 x 项目”“团队使用 PostgreSQL”。这些事实不再依赖某一次对话而是像用户画像一样长期存在。语义记忆的写入需要经过 LLM 抽取和冲突处理。比如用户上一轮说“我讨厌长邮件”下一轮说“重要客户可以发长邮件”这两条信息需要合并而不是覆盖因为后者是对前者的补充。语义记忆的存储一般用关系型数据库加向量索引或者直接用向量数据库。检索时既要支持按实体属性精确匹配也要支持按语义相似度召回。4.4 技能记忆层L3技能记忆层存的是“怎么做”的经验。例如某个工具调用流程在什么条件下有效、某个常见任务的处理步骤、哪些 workflow 在上一次执行时成功了。这一层和 Agent 的 skill/plugin 机制可以结合。它可以不存自然语言描述而是存可执行的模板或工作流引用。技能记忆的意义在于Agent 在遇到重复任务时可以直接复用成功路径而不是每次重新探索。四层记忆之间不是孤立的。典型流程是工作记忆中的对话内容被抽取出事件进入情景记忆情景记忆经过摘要和去重升级为语义记忆多次成功的任务路径沉淀为技能记忆。这样记忆就有了“从原始到抽象从临时到稳定”的完整生命周期。5. 记忆写入什么时候记、记什么、怎么去重长期记忆架构里最容易被忽略的是写入策略。没有好的写入策略检索做得再漂亮也没用因为库里装满了噪声。5.1 写入触发时机不建议把每一轮对话都写进长期记忆。比较合理的触发点有四个用户显式表达偏好。例如“以后都用中文回复”“我不喜欢看超过 500 字的总结”。任务完成时抽取关键结论。例如“本次任务生成了 3 份周报存档位置是 xxx”。跨会话关键事件发生。例如用户创建了一个新项目、修改了截止时间。定时回顾。每隔一定轮数对近期工作记忆做一次摘要把有价值信息抽取出来。5.2 记忆条目数据结构一条长期记忆应该至少包含下面这些字段{ memory_id: mem_xxx, type: semantic, subject: user, predicate: prefers_language, object: chinese, importance: 0.9, created_at: 2025-01-01T10:00:00Z, last_accessed_at: 2025-01-01T10:00:00Z, expires_at: null, source_session: session_xxx, content_embedding: [] }subject/predicate/object 结构适合表示“主谓宾”三元组既方便精确查询也方便做冲突检测。importance 是重要性分数遗忘机制会用到。expires_at 是过期时间适合临时性事实。这个结构是示例实际可以根据业务调整。但建议至少保留类型、实体、属性、值、重要性、时间戳这几个核心字段。5.3 写入去重与冲突合并写入前先做相似度检查。如果新记忆的向量和已有记忆的相似度超过阈值就不要直接插入而是做合并。例如“用户喜欢简洁回复”和“用户喜欢简短回复”语义高度相似就应该保留一条或者把两者合并成“用户喜欢简洁、简短的回复”。合并规则建议按重要性优先新值重要性更高时更新旧值否则保留旧值并补充说明。要避免“每次写入都新增一条”这样长期记忆会快速膨胀检索时返回一堆重复内容。代码层面写入流程可以抽象成这样一个函数def remember( memory_store, embedding_model, item: MemoryItem, duplicate_threshold: float 0.85, ): if not item.content: return query_vec embedding_model.embed(item.content) item.content_embedding query_vec # 先查重再决定新增还是合并 candidates memory_store.search_similar( vecquery_vec, top_k5, thresholdduplicate_threshold ) for cand in candidates: if is_semantic_similar(item, cand): memory_store.merge(item, cand) return memory_store.insert(item)这个查重逻辑非常重要。实际运行时你可以在同一个任务里快速验证连续写入两条语义相近的记忆观察最终库里是否只保留了一条或者变成一条合并后的记录。6. 记忆检索向量 关键词多路召回 重排序长期记忆如果只有写入没有检索就是一个死库。检索的目标是给定当前用户问题从记忆库中找出最相关、最有用的记忆片段组装到 prompt 里。整个检索流程我分成五步。6.1 查询改写用户的问题通常是口语化的例如“帮我把上次给财务的方案找出来”。直接拿这句话做向量检索语义能命中一部分但精确命中“财务方案”这种实体关键词会较弱。所以第一步是查询改写用 LLM 把用户问题转换成适合检索的形式例如原始问题帮我把上次给财务的方案找出来检索 query财务 方案 上次 时间检索意图查找用户和财务相关的历史方案这一步可以简单调用一次 LLM也可以根据意图模板生成。查询改写不是必须的但在命中率不足时优先检查这里。6.2 多路召回向量检索 关键词检索单靠向量检索不够。向量检索适合语义相似但不擅长精确匹配人名、编号、专有名词。比如用户问“项目X的负责人是谁”如果记忆里存的是“项目X由张三负责”向量检索能命中但如果存的是“张三 项目X 负责人”关键字权重较高时BM25 召回会更准。所以建议做双路召回向量召回embedding 模型编码 query在向量索引中取 top_k关键词召回使用 BM25 或全文索引按词频和逆文档频率召回包含关键实体的记忆。热搜里提到的“向量混合检索加 bm25 多路召回”就是在讲这种方案。两面召回之后合并、去重再进入重排序阶段。一个简化版的双路召回伪代码如下def search_memories(memory_store, query: str, top_k: int 10): query_vec embedding_model.embed(query) vector_hits memory_store.vector_search(query_vec, top_ktop_k) keyword_hits memory_store.bm25_search(query, top_ktop_k) # 合并候选集按来源去重 candidates merge_by_memory_id(vector_hits, keyword_hits) return rerank(candidates, query, query_vec, top_ktop_k)6.3 时间过滤与重要性加权召回之后的候选集不能直接进 prompt。需要做一轮过滤已经过期的记忆直接丢弃距离过期时间很短的记忆降权重要性分数低的记忆只有在没有其他候选时才返回。这里引入一个简单的评分公式score semantic_score * alpha keyword_score * beta importance * gamma - time_decay * delta其中 alpha、beta、gamma、delta 是权重time_decay 是距离创建时间或最后访问时间的衰减函数。具体数值要在你自己的数据集上调先固定一个可用版本再看命中率调整。6.4 重排序Rerank多路召回召回的数量可以放大比如 top 50然后重排序后再取 top 5。重排序可以使用专门的 rerank 模型也可以直接用 LLM 对候选记忆打分。对小规模应用用 LLM 打分成本可控大规模应用建议用 lightweight rerank 模型避免每次检索都调用大模型。重排序的目标不是“找到语义最像的那条”而是“找到当前任务最需要的那条”。例如用户问“这个项目什么时候截止”记忆库里有一条“项目截止时间是月底”还有一条“用户是项目负责人”前者应该排得更靠前尽管后者的实体关联更强。6.5 组装上下文检索到的记忆要以紧凑文本的形式插入到 system prompt 中而不是把完整原始对话塞回去。建议给每条记忆加一个元信息前缀例如【记忆-语义】用户偏好使用简洁中文回复重要度0.9记录于2025-01-01 【记忆-情景】上次对话用户要求生成了3份周报存放在 /reports/weekly/这样可以告诉模型“这些是历史记忆”避免模型混淆记忆和当前对话。同时控制召回条数例如默认返回 5~8 条每条记忆控制在一两句话整体记忆 prompt 保持在几百 token 以内。7. 遗忘机制不让记忆变成包袱长期记忆系统如果没有遗忘机制早晚会失控。遗忘不是删除而是对记忆进行“降权、归档、最后删除”的生命周期管理。遗忘机制要解决几个问题存储无限膨胀、过时信息产生误导、用户隐私数据存留过久。7.1 遗忘的三个层次软失效把记忆标记为“不活跃”或“低置信度”检索时降低权重但不物理删除。适合暂不确定是否还有用的记忆。归档把长期不访问的记忆移到冷存储如从热向量索引迁到离线表。保留记录但不再参与实时检索。硬删除物理删除记忆记录及其向量索引。适用于过期隐私数据、用户主动要求删除的数据、重复错误数据。7.2 遗忘评分策略给每条记忆维护一个遗忘分定期扫描并处理低分记忆。遗忘分可以由这几个因素决定重要性importance越低越容易被遗忘最后访问时间last_accessed_at越久越容易被遗忘访问频率高频访问的记忆要优先保留是否过期expires_at过期记忆直接进入删除或归档流程。一个简单的遗忘评分函数可以这样实现import math from datetime import datetime, timezone def forget_score(item, nowNone): now now or datetime.now(timezone.utc) age_hours (now - item.last_accessed_at).total_seconds() / 3600.0 # 时间衰减age 越大score 越小半衰期设为 7 天 half_life 24.0 * 7 time_factor math.pow(0.5, age_hours / half_life) # 综合分数importance 越高越不容易被遗忘 return item.importance * time_factor * (1.0 item.access_count * 0.1)这里的半衰期 7 天是示例参数真实业务需要按用户习惯和记忆类型调整。语义记忆的半衰期应该比情景记忆长用户画像比临时任务结论长。7.3 定期整理与摘要合并遗忘机制不能只被动等待记录过期还要定期主动整理。例如每周对情景记忆做一次摘要将多条相似事件合并成一条语义记忆然后删除原始事件记录。这样既能压缩存储又能提升检索质量。整理任务可以做成一个后台定时任务例如每天凌晨运行扫描低遗忘分记忆标记归档或删除对相似记忆做聚类合并对过期记忆执行硬删除更新用户记忆画像摘要。注意一点用户主动要求删除的记忆必须真实删除不能只做“降权”。尤其是涉及个人信息的数据除了删除主记录还要同步删除向量索引、缓存和归档副本。这是合规底线。8. 工程实现记忆模块的落地骨架下面给出一套可以照着改的工程骨架。这里不绑定具体框架核心接口是通用的写入、检索、遗忘、整理。8.1 存储选型参考不同规模的项目选型差异很大给一个保守参考规模短时记忆长期记忆单机小项目内存字典 pickle/JSON 落地SQLite 文件向量索引团队中等项目Redis JSONPostgreSQL pgvector大规模服务Redis Cluster 会话缓存PostgreSQL pgvector / Milvus 等向量库选择存储时先不要追求“最先进的向量数据库”。用 PostgreSQL 自带 pgvector或者 SQLite 加向量扩展就足够启动一个真实项目。把记忆 schema 设计好后面迁移到专用向量库并不难。8.2 核心接口设计class MemoryStore: def insert(self, item: MemoryItem) - str: ... def search_similar(self, vec, top_k, threshold) - list: ... def search_keyword(self, query, top_k) - list: ... def update(self, memory_id: str, updates: dict) - None: ... def delete(self, memory_id: str) - None: ... def bump_access(self, memory_id: str) - None: ... def get_expired(self, now) - list: ...8.3 一个简单的写入实现这里用 SQLite 做示例方便大家快速跑通。实际生产可以用 PostgreSQL pgvector接口保持一致。import sqlite3 import json class SqliteMemoryStore(MemoryStore): def __init__(self, db_path: str): self.conn sqlite3.connect(db_path) self.conn.execute( CREATE TABLE IF NOT EXISTS memories ( memory_id TEXT PRIMARY KEY, type TEXT, content TEXT, importance REAL, created_at TEXT, last_accessed_at TEXT, expires_at TEXT, access_count INTEGER DEFAULT 0, embedding TEXT ) ) self.conn.commit() def insert(self, item: MemoryItem) - str: cur self.conn.execute( INSERT INTO memories (memory_id, type, content, importance, created_at, last_accessed_at, expires_at, access_count, embedding) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?) , ( item.memory_id, item.type, item.content, item.importance, item.created_at, item.last_accessed_at, item.expires_at, json.dumps(item.content_embedding), ), ) self.conn.commit() return item.memory_id搜索需要把 embedding 存成 JSON 还是用专门向量列取决于你选的存储方案。生产环境建议直接使用数据库原生向量索引避免每次全量扫描。8.4 与 Agent 框架的接入方式LangGraph 这类框架已经把“长期记忆”放进了官方能力体系里但核心思路是一样的你需要注册一个 memory store在每次对话轮次前后触发写入和检索。典型的接入点是在对话开始时读取当前用户的历史记忆组装到 system prompt在对话结束时抽取本次对话的长期记忆写入记忆库在工具调用失败或多次重复问题时主动检索记忆补充上下文。不要把这个模块做成不可替换的“大胶水”。记忆模块应该只依赖两个输入当前对话历史和用户标识。这样无论是接入 LangGraph、自研 Agent还是未来换成另一个框架都只需要改接入层不需要重写记忆逻辑。9. 记忆服务 API 与批量任务当记忆模块从 Agent 进程里抽离出来它就是一个独立的记忆服务。这样做的好处是多个 Agent 可以共享一套记忆前端可以独立查看和删除记忆批量整理任务也可以在服务端统一执行。9.1 接口设计示例下面给出一组示例接口字段和路径请按实际项目调整。这里只是为了说明接口形态方法路径作用POST/memories写入一条记忆POST/memories/search检索记忆POST/memories/forget遗忘/删除记忆GET/memories/{user_id}查看某个用户的记忆列表POST/memories/consolidate触发一次整理任务搜索接口请求体示例{ user_id: user_123, query: 财务方案, top_k: 5, include_expired: false }9.2 Python 调用示例记忆服务启动后Agent 侧可以通过 HTTP 调用。import requests url http://127.0.0.1:8000/memories/search payload { user_id: user_123, query: 财务方案, top_k: 5, include_expired: False, } resp requests.post(url, jsonpayload, timeout5) if resp.status_code 200: memories resp.json()[items] for mem in memories: print(mem[content], mem[importance])9.3 批量任务设计批量任务通常有两类。一类是批量写入例如从历史对话日志中回填记忆。这种任务要求写入接口支持批量和幂等否则重复回填会产生大量重复记忆。另一类是批量整理例如每晚对全部用户的记忆执行遗忘扫描和摘要合并。批量整理任务要特别关注两个问题失败重试整理任务涉及删除和合并中途失败容易造成数据不一致。建议每一步操作先记录操作日志再执行变更全量扫描成本随着记忆量增加每天全量扫描所有记忆会越来越慢。可以按用户分组每天只处理一部分或者按“最近有活跃记录的用户”优先处理。接口服务的稳定与否直接决定了记忆模块能不能被其他系统复用。所以最好把写入去重、检索超时、遗忘删除这几个流程都在服务端固定下来而不是每个调用方各写一遍。10. 性能观察与常见问题排查长期记忆系统上线后不要只关心功能是否“看起来能跑”要看几个关键指标。10.1 关键指标指标观察方式说明写入耗时记录每次 remember 的 P95 耗时太高说明查重或 embedding 调用成为瓶颈检索耗时记录 search 的 P95 耗时多路召回后如果超过几百毫秒需要优化索引记忆总量每天记录新增和删除数量持续增长说明遗忘策略没生效或查重没生效命中率人工抽样判断检索返回值是否相关可以用“检索是否被 Agent 采用”做间接指标embedding 调用量记录每日 embedding 接口调用次数写入和检索都依赖 embedding费用需要关注这些指标不需要一次性全做但至少要记录日志。否则上线之后系统效果变差很难定位是记忆写入的问题、检索的问题还是 prompt 组装的问题。10.2 常见问题排查表问题现象可能原因排查方式解决方案检索结果和当前问题无关向量阈值设置过低召回大量弱相关记忆检查候选记忆的相关性分数分布调高相似度阈值增加关键词召回权重记忆库快速增长写入去重失效查看是否有大量相似记忆同时存在加强写入前向量查重和合并逻辑某些用户记忆过多没有按用户设置容量上限查看单个用户记忆数量按用户设置容量上限超出触发遗忘记忆 prompt 占用过大召回条数过多或单条记忆太长检查最终插入 prompt 的记忆内容长度降低 top_k控制每条记忆长度用户删除记忆后仍被检索到只删除主表未清理向量索引或缓存检查删除逻辑覆盖范围同步删除向量索引、缓存和归档副本检索延迟偏高embedding 调用慢或向量索引未优化分段测量查询改写/embedding/检索耗时对向量索引做量化和分片必要时增加缓存11. 最佳实践与合规边界长期记忆看起来只是技术模块但涉及用户数据必须把合规和隐私问题放在一起设计。第一坚持最小必要存储。不要把所有原始对话全部入库只存储抽取后对任务有复用价值的结构化事实。这样既能降低存储成本也能减少隐私风险。第二敏感信息必须脱敏。用户姓名、电话、地址、身份证号、财务信息等尽量不要以明文形式存进长期记忆。如果业务确实需要要做加密存储和访问审计。第三支持用户自主查看和删除。Agent 应该允许用户查看“你还记得关于我的哪些信息”并且允许一键删除全部记忆。这不仅是合规要求也能提升用户信任。涉及人脸、声音、私有文档等敏感内容时必须获得明确授权后才能写入记忆库。第四内存和数据库权限要隔离。记忆服务如果是对外的需要做用户身份校验防止一个用户检索到另一个用户的记忆。即使是内部服务也建议按用户维度做隔离避免误查询导致的数据泄露。第五介入人工审核。对重要事实和用户画像类记忆可以在抽取后做低置信度标记由人工或后台校验后再进入线上检索。不要完全信任 LLM 抽取结果它在某些场景会幻觉出用户从未说过的信息。12. 总结与下一步这套 Agent 长期记忆架构最值得记住的三个设计点是分层、混合检索、遗忘机制。分层的本质是让不同类型的记忆各归其位避免把临时信息当成核心事实混合检索解决的是“语义相似”和“精确匹配”不能互相替代的问题遗忘机制则是让记忆系统长期可用的前提。最先应该验证的功能是一个最小闭环在一个会话里告诉 Agent 一条用户偏好关闭会话开启新会话后问一个需要用到这条偏好的问题看 Agent 能否通过长期记忆检索正确回答。这个闭环能跑通架构就已经成立了一大半。最容易踩的坑是把短时记忆直接当成长期记忆写入导致记忆库快速膨胀以及不做遗忘策略导致越用越慢。这两个问题在系统初期很难发现等记忆量上去了再改成本会高很多。下一步你可以继续扩展的方向为记忆增加置信度评估和人工审核流程将多用户的记忆做权限隔离在不同业务场景里设置不同的记忆 TTL以及把记忆检索结果做成可观测的日志方便持续调优检索权重。等这些做扎实了这个长期记忆模块就可以真正接到生产 Agent 里。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻