1. 聊了十轮就失忆:Agent 落地的最大短板
我在本地跑一个模拟项目 X 的时候,遇到过一件特别窝火的事。那个项目是一个多轮对话助手,负责帮用户整理会议纪要和待办事项。前九轮都好好的,它能准确记住用户在第一轮提到的"下周三下午三点开周会"、"这份合同要重点审第 4 条第 2 款"这类关键信息。结果到第十轮,用户随口问"我上次说的那个合同条款,帮我拟个修改意见",Agent 直接愣住了——不是它不会拟,而是它根本想不起来"上次说的"是哪条。
这个场景如果是你自己在用某个 AI 应用,多半也遇到过。对话一长,模型就开始"翻车",要不就是答非所问,要不就是把之前已经确认过的信息重新问一遍。很多人把锅甩给上下文窗口不够大,然后拼命买更长上下文的模型。但说实话,上下文窗口再大,也只是把问题往后推,没有从根本上解决"Agent 该怎样管理自己的记忆"这件事。
1.1 一次会话里的"短期失忆"现场
先说清楚什么是短期失忆。我拆解刚才那个模拟项目 X 的日志,发现它的问题不是模型能力不够,而是提示词里塞的内容太多太杂。系统对外宣称支持 200K 上下文,但实际单轮对话只用了不到 8K token 就触发了一系列问题:较早轮次的关键信息被后续大段内容挤出了注意力范围,模型在处理后续问题时没有明确的"记忆检索"入口,所有历史记录一股脑堆在系统提示词尾部,类似于把整本字典丢给你,让你在 3 秒内回答页码。
后来我给这个项目加了"会话滚动摘要"机制,每几轮就对前面的关键信息做一次压缩提炼。问题缓解了一部分,但新的麻烦又冒出来了:摘要压得太狠,把"合同第 4 条第 2 款"这种细节弄丢了;压得太松,摘要本身又要占掉很多 token。这件事让我意识到,Agent 的记忆根本不是"缓存历史对话"这么简单,它需要一整套"存什么、怎么存、怎么取、什么时候忘"的管理逻辑。
1.2 记忆缺失的三个层次
如果把 LLM Agent 比作一个刚入职的实习生,你会发现它缺的不是智商,而是三样东西:一是"心里装不下太多事"的工作记忆,二是"过去做过的事要能随时查"的长期记忆,三是"同一件事记住一个版本就够"的信息一致性。这三样分别对应三个层次的技术问题:
第一层是上下文管理,解决的是"一次会话内怎么把最重要的信息保留在模型可视范围内"。这是最基础的,工程上用提示词压缩、摘要、滑动窗口就能做到。
第二层是跨会话记忆,解决的是"上次聊完的事,下次还能接上"。这里需要把历史信息持久化,并在新会话启动时把相关片段重新注入。
第三层是记忆的组织与检索,解决的是"记忆越来越多之后,怎么快速找到当前任务真正需要的那几条"。这也是整个架构里最容易被低估、也最决定体验的一层。
我的经验是,如果只做第一层,Demo 能跑通,但一到真实业务场景就会崩。真实的用户不是只跟你聊一轮,他会隔几天回来,告诉你"上周那个方案我改主意了"、"那个客户的名字我又记错了"。Agent 要接住这些话,靠堆上下文窗口是没用的,必须建立一套分层记忆系统。
2. 把"记住"拆开:Agent 记忆的四类模型与存储选型
人类记忆其实不是一个单一的黑盒。你记得昨天午饭吃了什么,记得圆周率前几位,记得怎么骑自行车,这三种记忆在神经科学里分属不同的系统。AI Agent 要做记忆系统,第一步也是把"记忆"拆开,而不是笼统地说"我要做一个记忆库"。
2.1 工作记忆、情景记忆、语义记忆、程序记忆怎么对应到工程模型
业内讨论 Agent 记忆时,经常引用认知科学里的分类框架,我把它翻译成工程语言,方便落地。
工作记忆对应的是当前任务上下文。它的特点是容量小、实时性强、用完就该释放。在工程上,工作记忆通常由 LLM 的上下文窗口直接承担,但你要主动管理,不能让它被历史对话塞满。我的做法是给工作记忆设一个 token 预算,超过阈值就把最老的内容移出,转存到下一层。
情景记忆对应的是"发生过的事",也就是具体的时间、人物、事件。比如"我在 3 月 20 号和某客户开会讨论了升级方案"。这类记忆在 Agent 系统里通常以会话记录、事件日志的形式存在,特征是具体、可检索。
语义记忆对应的是"从发生过的事里提炼出的知识",比如"该客户偏好保守的报价方案"、"这个项目的负责人很在意交付时间"。
语义记忆不绑定具体时间,它是一种抽象出的用户画像或业务规则。工程上,这类记忆是实现个性化体验的关键,通常以结构化记录的形式存在,并且会通过摘要、聚合不断更新。
程序记忆对应的是"怎么做一件事的流程",比如"处理退款时先校验订单状态再调用支付接口"。在 Agent 系统里,这其实是工作流和工具调用的范畴,不太需要向量检索,更适合用函数、插件、工作流编排器来承载。
我在给模拟项目 X 做记忆改造时,第一个动作就是把这四类记忆分开建表。以前所有对话历史塞进一个集合,检索时既检出"3 月 20 日开过会"这种情景记忆,又混着"客户偏好稳健方案"这种语义记忆,结果相互干扰,相关性反而变差。分开存储之后,每一类记忆的检索策略都可以单独调优,效果立刻不一样。
2.2 向量库、结构化数据库、原始文本:三种存储介质怎么搭配
确定记忆分类之后,下一个问题是存储介质。我的经验是,不要迷信向量数据库。向量检索是召回手段,不是存储方案。一个健壮的记忆系统,往往是三种存储共存。
向量存储负责承载"无法用规则描述相似性"的记忆片段,比如一段用户说过的话、一篇文档的内容摘要。它的优势是支持语义召回,缺点是精度有限、容易被相似但不相关的信息干扰。
结构化数据库承载的是"有明确字段归属"的记忆,比如用户偏好、任务状态、关键实体关系。这类记忆用 SQL 或 NoSQL 管理更可靠,更新一条记录时可以直接覆盖,不必担心新旧版本同时在库里。
原始文本日志是兜底方案。所有对话历史、中间结果、工具调用日志,至少存一份原文。向量化和结构化提取都有可能出错,出问题时还能回溯原文排查。
这三种存储的写读路径不同。我建议的记忆写入链路是:原始对话先落到日志存储;异步任务从原始对话里抽取结构化信息(实体、偏好、任务状态)更新到结构化数据库;同时把关键内容切成片段做向量化,存入向量库;最后由一个定期任务把旧对话滚动压缩成摘要。这样每个存储层各司其职,互不阻塞。
这里最容易踩的坑是把原始对话直接丢给向量化模型,不分层。这样存的后果是检索时命中一堆琐碎内容,比如"好的""嗯""稍等"这种语气词片段,而真正重要的信息被淹没。我做模拟项目 X 时吃过这个亏,后来强制规定:凡是进向量库的文本,必须先经过信息密度过滤,过滤不掉的高频噪音词要从索引里剔除。
3. 写入端:Agent 怎么判断"这条信息值得记下来"
记忆系统的写入端是最容易被忽略的环节。很多人觉得记忆嘛,把对话原文存下来不就行了。但真实场景里,如果什么对话都存,链路会迅速被垃圾信息淹没;如果存得太少,又会漏掉关键信息。写入端的核心问题,是让 Agent 具备"判断信息价值"的能力。
3.1 触发写入的四种信号
我给模拟项目 X 设计了一套写入触发机制,不是每轮都写,而是检测到四种信号之一才触发记忆写入。
第一种是实体信号。用户明确提到新的人名、机构名、项目名、日期、金额,尤其是首次出现的实体,几乎一定要写入。比如"张经理下周出差"这条里,"张经理"和"下周"都值得记录。
第二种是决策信号。用户做出了选择、拍板、确认或否决,比如"就用第二版方案吧"、"这次先不涨价"。这类信息是后续交互的重要基准,必须持久化。
第三种是偏好信号。用户表达了对某种风格的倾向,比如"我不喜欢太正式的开场白"、"报告里多放图表少放文字"。
第四种是任务状态信号。一个多步任务进行到哪一步了,哪些已完成、哪些待办。这对跨会话恢复任务至关重要。
这四种信号不一定由 Agent 自由判断,也可以先用规则引擎或一个较小的模型做初筛,再决定是否进入写入流程。自由判断的优点是灵活,缺点是漏判和误判都存在,我实测漏判率在初版高达三成,加规则初筛后降到一成以内。
3.2 摘要压缩:如何只留"骨架"不留"废话"
触发写入之后,需要把原始信息转成适合存储的形态。这里分两路:一路保留原始文本片段,另一路生成摘要和结构化信息。
摘要生成要注意一个原则:按时间衰减分配权重。最近的对话保留更多细节,较早的对话只保留结论和状态。我早期做摘要时是全局无差别的,结果远期细节占用大量 token,近期关键信息却被压掉了。后来改成"滑动摘要窗口":最近 5 轮对话几乎不压缩,再往前的对话先压成段落摘要,再往前的压成一句话标签,超过一定轮次后只保留结构化状态。
具体实现时,我给每轮对话打一个"重要性分",分数由几个维度加权算出:是否包含新实体、是否涉及当前活跃任务、是否包含决策词、用户是否明确强调。分数超过阈值才进入摘要池,低于阈值的零散内容直接进日志,不进记忆库。这样可以显著降低写入开销。
3.3 元数据标签:检索时成败的关键细节
很多人做记忆系统只存文本和向量,不存元数据。这是个大问题。元数据是检索时的过滤条件,没有它,检索就是大海捞针。
我建议每条记忆至少带这几类元数据:时间戳(发生时间,不只是写入时间)、来源会话 ID、记忆类型(情景/语义/程序)、关联实体(人、项目、文档)、置信度(这条记忆有多可靠)、活跃状态(是否已被后续信息推翻)。这些字段看起来不起眼,但会直接决定后面检索时能不能精确圈定范围。
举个例子,用户在 3 月说过"倾向低价策略",4 月又说过"预算充足,可以接受高价方案"。如果没有时间戳和活跃状态,检索会把两条记忆同时返回,Agent 就会困惑。有了这些元数据,系统可以标注后一条为"活跃",前一条为"已过期",检索时优先返回活跃状态。
4. 读取端:检索不是调用一次向量相似度那么简单
写入端做得再好,读取端拉胯,整个记忆系统也白搭。读取端的任务,是在合适的时机把最合适的记忆片段送到 LLM 的上下文里。
4.1 混合检索:向量召回、关键词匹配、规则过滤三层并行
如果只用向量检索,会遇到一个经典问题:语义相近但事实不同。比如用户问"上次说的那笔退款",向量检索很可能召回所有和退款相关的记录,包括退款政策、退货流程、历史退款案例。而关键词检索能精确锁定"退款"加特定订单号的组合,但无法处理"上次说的那笔"这种指代。
所以我现在设计读取端一律走混合检索:第一层用向量做语义召回,召回 Top N;第二层用关键词和实体匹配做精确过滤,比如用户当前任务关联的订单号、客户名;第三层是规则过滤,按照时间范围、记忆类型、置信度、活跃状态把不符合条件的候选剔除。最后把三层的结果做融合排序。
融合排序不要用死板的公式,实践上我用的是一个加权评分,向量相似度占 0.5,实体匹配占 0.3,时间新鲜度占 0.2。这个权重可以调,不同业务场景侧重点不一样。比如客服场景时间新鲜度可能更重要,而知识问答场景向量相似度权重更高。
4.2 注入上下文的格式:结构比内容更影响模型表现
检索到记忆之后,怎么把这些记忆片段呈现给 LLM,直接决定模型能不能用上它们。我的经验是三条。
第一,每条记忆前加一个固定前缀标签,比如[记忆] 时间:2025-03-20 | 相关人:某客户 | 类型:决策,需要让模型明确知道这是一条记忆,而不是用户说的话。
第二,记忆区与当前对话区用明确的系统提示分隔,比如"以下是系统已有的记忆,供回答问题时参考,不要复述这些记忆给用户"。
第三,控制注入条数。很多开发者的误区是觉得检索到的全塞进去,反正上下文够大。但记忆太多会让模型分不清主次。我一般设定注入上限:核心记忆 5 条、辅助记忆 10 条,超过上限就只保留评分最高的。实测下来,注入 15 条以上时,回答准确率反而下降。
4.3 可信度标签:比"记住多少"更重要的是"信多少"
记忆不是百分之百可靠的。用户之后说了"更正一下",说明旧记忆失效;用户在某条记忆所涉事件中没有表态,那只是观察,不是确认。如果 Agent 把每条记忆都当成铁板钉钉的事实,它很容易一本正经地误导用户。
我的做法是在检索结果里附带一个可信度字段,取值范围 0 到 1。明确确认过的记忆可信度设为 1,用户主动陈述但没有确认的设为 0.8,从行为推断的设为 0.5,被后续信息推翻的设为 0.1 且标注"已过期"。注入上下文时,可以把可信度直接写在标签里,让模型自行权衡;也可以在系统提示里加一句"如果记忆与当前对话冲突,以当前对话为准"。
最开始我觉得加可信度是过度设计,直到模拟项目 X 出现了一次事故:用户说"价格方面我们接受上浮",系统把三个月前一次未成交讨论中的"客户对价格敏感"当成既定事实,给了一个很保守的方案。加可信度机制之后,这类问题基本被杜绝了。
5. 遗忘与更新:记忆系统没有保鲜机制迟早腐坏
很多人设计记忆系统时只想着"如何记住",从不考虑"如何忘记"。但遗忘不是缺陷,而是对抗信息腐化的重要手段。
5.1 主动遗忘与衰减遗忘两条路径
我采用两条遗忘路径叠加。
主动遗忘是指当新信息与旧记忆冲突时,系统显式地把旧记忆标记为过期或删除。比如用户明确说"不用考虑 A 方案了",之前关于 A 方案的记忆如果仍被检索回来,就是干扰项。此时要在结构化数据层面做过期标记,而不是靠"让向量相似度自己变低"。
衰减遗忘是指没有冲突、但长期未访问的记忆,随着时间推移降低重要性。我给每条记忆加一个 last_access_time,检索排序时对时间做衰减加权:30 天内活跃的记忆权重不变,90 天以上未访问的权重降到原来的三成,180 天未访问的直接移出热索引,只保留在冷存储里。
这套机制实施起来会有个取舍:衰减太快,用户几个月前提过的一个偏好可能被忘掉,再次提起时 Agent 茫然;衰减太慢,陈旧信息在检索中的排名仍然靠前,干扰判断。我目前的经验是,把衰减阈值开放成配置项,不同记忆类型用不同的衰减速度。程序记忆几乎不衰减,用户偏好半年衰减,情景记忆三个月衰减。
5.2 冲突处理策略:新旧信息打架怎么办
冲突处理是记忆更新里技术含量最高的部分。我总结了三类冲突及对应策略。
第一类是否定冲突,用户明确否定了旧决策。处理策略是直接废弃旧记忆,写入新记忆,并且在新记忆里注明"替代了某条记忆"。
第二类是范围冲突,新信息比旧信息更细或更广。比如用户先说过"我们支持所有主流支付方式",后来又说"我们只支持微信和支付宝"。策略是更新旧记忆为更精确的版本,保留历史版本但标记为"已局部覆盖"。
第三类是模糊冲突,新信息和旧信息方向不完全一致,但无法确定谁对谁错。这时策略是两条都保留,但在检索排序时,以后出现的记忆为优先。
有一个细节需要提一下:向量数据库的"更新"并不是真正的覆盖,它只是新增了一个向量。如果新写入的向量和旧向量语义很接近,检索时两条都会召回。所以冲突处理一定要在结构化数据层面做,并且给向量记录做失效标记,检索时过滤掉。
5.3 会话归档与定期重组
从长远看,记忆系统必须有一个类似"归档与重组"的周期任务。我会在每晚对当天的记忆做一次离线处理:把零散的情景记忆聚合成语义记忆,比如过去十天里有五次和某客户讨论报价,系统应该提炼出一条"该客户近期在频繁询价,可能正在做采购决策",而不是把五条孤立记录留在库里。
这个聚合任务可以交给 LLM 批量执行,把一组相关记忆连同上下文发给模型,让它提炼出更高层的趋势和结论,再写回语义记忆库。要注意的是,聚合结果不能替代原始记忆,原文始终保留在日志层。聚合的目的是让检索时能召回"更高维度的洞察",而不是把事实细节全部吞掉。
6. 从零搭一个记忆模块:架构与最小实现
讲完原理和设计取舍,最后分享一套我可运行的最小实现方案。这套方案不依赖任何重型框架,用 Python 加一个轻量向量库,再加一个 SQLite 就能跑通。
6.1 分层架构与模块划分
我把记忆模块拆成四个组件。
记忆写入模块负责接收原始对话、判断写入触发条件、生成摘要和结构化信息、写入各存储层。记忆检索模块负责在请求到来时执行混合检索、融合排序、组装上下文。记忆更新模块负责处理冲突、执行过期标记、定期归档。工作记忆管理器负责维护当前会话上下文窗口,把检索到的记忆和最近的对话拼装成最终发给 LLM 的提示词。
这四个组件相互独立,之间通过接口调用。好处是可以单独升级某一层,比如换更好的向量模型时,不需要重写其他模块。
6.2 核心代码骨架
memory 表的设计我放在下面。
我实际用的建表语句大概是这样的:
CREATE TABLE memory ( id INTEGER PRIMARY KEY AUTOINCREMENT, memory_type TEXT NOT NULL, -- episodic / semantic / procedural content TEXT NOT NULL, -- 原始记忆文本 summary TEXT, -- 压缩摘要 entities TEXT, -- 关联实体 JSON confidence REAL DEFAULT 0.8, status TEXT DEFAULT 'active', -- active / expired / archived importance REAL DEFAULT 0.5, created_at TEXT, last_access_time TEXT, replaces_id INTEGER, -- 替代了哪条旧记忆 source_session_id TEXT );检索过程我封装成一个函数,它接收当前用户消息和上下文信息,返回组装好的记忆片段列表。核心逻辑是混合检索加融合排序。
向量部分我用的是一套轻量本地实现,避免引入过重的服务依赖。核心思路很简单:把文本切块后向量化,检索时先向量召回,再按元数据过滤,最后按加权评分排。
def retrieve_memory(query, current_session, top_k=5): # 向量召回 vec_results = vector_index.search(query, top_k=top_k*2) # 关键词/实体过滤 filtered = [] for item in vec_results: if item["status"] != "active": continue if item["expires_at"] and item["expires_at"] < now: continue filtered.append(item) # 加权排序 for item in filtered: score = 0 score += 0.5 * item["vec_score"] score += 0.3 * entity_match_score(item, current_session["entities"]) score += 0.2 * recency_score(item["last_access_time"]) item["final_score"] = score filtered.sort(key=lambda x: x["final_score"], reverse=True) return filtered[:top_k]召回之后的记忆组装,我会用一个统一的模板把它格式化成提示词片段,保证模型能区分记忆与当前对话。
MEMORY_HEADER = "[系统记忆] 以下为与当前问题相关的历史记忆,仅供回答参考,请勿向用户复述。" def format_memories(memories): blocks = [] for m in memories: blocks.append( f"- [记忆] 类型:{m['memory_type']} | " f"时间:{m['created_at']} | " f"置信度:{m['confidence']}\n" f"{m['content']}" ) return MEMORY_HEADER + "\n" + "\n\n".join(blocks)组装好的记忆片段会作为 system message 放在提示词最前面,然后是最近的工作记忆内容最后是用户当前输入。
6.3 实测效果与三个踩坑点
这套体系在模拟项目 X 上跑了一个多月,多轮记忆准确率从初版不到五成提升到八成以上。中间遇到过三个比较有代表性的坑,值得单独说。
第一个坑是摘要粒度不好控制。后来我采用"分层摘要"解决:每写完一层摘要就压缩成更短的版本,按时间窗保留多个层级,检索时先取精确层,不够再取更粗的层。
第二个坑是新写入记忆的"新鲜度溢价"过高,导致每次检索都偏向最新记忆,而忽略了相关性。后来我在排序公式里给时间因子加了个比较低的上限,保证它不会压过语义相似度。
第三个坑是工作记忆和长期记忆的边界模糊。有时候系统把长期记忆当成了当前对话的一部分,导致模型以为用户刚刚说过这句话,回答的时候产生混淆。解决办法是在提示词里明确写明记忆的时间点和来源,并强调它属于历史记录。
7. 补充一些调优经验:记忆系统上线之后要做的事
架构能跑通,只算是完成了六成。拼好一套记忆系统,要在真实流量里持续观察和调优,才能稳定发挥价值。
我的习惯是给记忆系统加两个观测指标。第一个叫记忆召回利用率,定义是"单次对话中检索到的记忆里,被最终注入上下文的条数除以总条数"。如果这个值长期低于三成,说明排序策略有问题,要么是召回不相关,要么是注入上限设得太保守。第二个叫记忆写入准确率,定义是"写入的记忆中,在后续对话里被有效引用的比例"。这个值低于两成时,需要回头检查触发条件和摘要质量。
具体调优时,别只看离线测试集的准确率,离线数据永远覆盖不了真实用户的表达方式。我在模拟项目 X 里就试过好几版离线评估都很好,上线后被用户一句口语化的"上次内个事咋样了"打回原形。最终只有让系统沉淀真实对话日志,定期抽样看检索结果,才能发现并修掉这类问题。
安全也值得一提。记忆系统存储了用户的历史行为和偏好,前提一定是用户知情和授权。我的项目中用了比较保守的策略:超出会话本身的历史记忆默认不启用,用户主动打开"记住我"才允许系统跨会话使用记忆;同时提供一键清空记忆的能力,避免隐私隐患。技术再强,尊重用户边界这条底线不能丢。
最后分享一个串联工作的心得:做 Agent 记忆系统,最忌讳"一步到位"的思维。先只用工作记忆加简单摘要,跑通之后再加向量检索,再逐渐引入冲突更新和衰减遗忘。每加一层,你都会更清楚地看到当前瓶颈在哪里。这个系统的复杂度是一点点长出来的,不是设计出来的。我做模拟项目 X 时,如果一开始就上全套架构,估计到现在都还在调试,而不是像现在这样,架构稳定、指标可控、后续可以放心扩展成更通用的记忆服务。