ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent记忆管理实战:从全量拼接陷阱到分层存储与向量检索

Agent记忆管理实战:从全量拼接陷阱到分层存储与向量检索

1. 从一个真实翻车现场说起

去年冬天,我帮一个做智能客服的朋友排查线上问题。他们的系统接了一个大模型驱动的对话 Agent,上线头两周效果惊艳,第三周开始用户投诉量暴涨——同一个用户反复问同一个问题,Agent 每次回答都不一样,有时候甚至自相矛盾。更离谱的是,一个用户聊到第 40 轮的时候,Agent 突然把前面 30 轮里用户提到的订单号、收货地址、投诉诉求全忘了,开始重新问“请问您遇到了什么问题”。

朋友的第一反应是“模型不行”,想换一个更大的模型。我让他先把每轮请求的 token 数打出来看看。结果一看就明白了:他们的做法是把整个对话历史原封不动地拼进 prompt,每轮都全量塞进去。到第 40 轮的时候,单次请求的输入 token 已经逼近模型上下文窗口的上限,系统为了不报错,在代码里做了一层“截断保护”——从最早的对话开始丢。于是用户最早说的那些关键信息,恰好就是被丢掉的那部分。

这个案例几乎每天都在不同的团队里重演。Agent 的记忆管理,是当前所有做智能体应用的团队绕不开的一道坎。很多人下意识觉得“上下文窗口越大越好,那就全塞进去呗”,但真正跑过生产环境的人都知道,这条路走不通。这篇文章就把这件事掰开揉碎讲清楚:为什么不能全塞、全塞会出什么问题、业内在用什么思路解决、以及你可以怎么落地。

不管你是刚接触 Agent 开发的新手,还是已经踩过几次坑的老手,下面这些内容应该都能帮你少走一段弯路。我会尽量用生活化的类比把原理讲透,同时给出可以直接参考的方案和参数。

2. 为什么“全塞进上下文”是个陷阱

2.1 上下文窗口不是免费的仓库

先纠正一个特别常见的认知偏差。很多人把上下文窗口想象成一个“仓库”,觉得只要没装满就能一直往里放。但上下文窗口更像是一张工作台,而不是仓库。工作台的特点是:你放的东西越多,找东西越慢,而且台面本身是有物理边界的。

从技术层面看,上下文窗口有三个硬约束。第一是长度上限,这个最直观,不同模型的窗口大小不一样,从几万到几十万 token 不等,但再大也有天花板。第二是注意力衰减,这是很多人忽略的。Transformer 架构在处理长序列时,对中间位置信息的关注度会明显下降,业内常说的“lost in the middle”现象就是指这个——你把关键信息放在一长串内容的中间,模型很可能视而不见。第三是成本,输入 token 是要计费的,全量拼接意味着每一轮都要为之前所有轮次的内容重复付费,对话越长,单轮成本越高,而且是线性甚至超线性增长。

我做过一个粗略的测算。假设一个 Agent 平均每轮对话产生 200 token 的内容,用户和 Agent 各占一半。到第 50 轮时,如果全量拼接,单次请求的输入大约是 50 × 200 = 10000 token。看起来还好?但如果你的系统提示词本身就有 2000 token,再加上工具调用的返回结果、检索到的文档片段,很容易就冲到 15000 以上。按主流模型的输入价格算,单次请求成本可能是首轮的十几倍。一个日活几千的客服系统,光这一项每月多出来的开销就相当可观。

提示:判断你的 Agent 是否已经掉进“全塞陷阱”,最简单的信号是——单次请求的输入 token 数随着对话轮次线性增长,且没有上限控制。

2.2 全量拼接会引发四类典型故障

我把实际项目中遇到过的故障归了归类,基本逃不出下面四种。

第一类是信息淹没。关键信息被大量无关内容稀释。比如用户在第 3 轮说了“我对花生过敏”,后面 40 轮都在聊别的,等到第 45 轮用户问“这个菜能吃吗”,模型很可能因为中间隔了太多内容而忽略掉那条过敏信息。这不是模型笨,是注意力机制的特性决定的。

第二类是自相矛盾。长对话里用户的需求会演变,早期说的和后期说的可能冲突。全量拼接时,模型面对的是两套甚至多套互相矛盾的信息,它不知道该以哪个为准,输出就会飘忽不定。前面提到的客服案例就是典型。

第三类是成本失控。这个不用多解释,token 账单会教你做人。

第四类是延迟恶化。输入越长,推理耗时越长。用户等 2 秒能接受,等 8 秒就开始骂人了。而且长输入的延迟增长不是线性的,到后面会越来越慢。

2.3 一个容易被忽视的隐性成本:调试难度

这一点很少有人提,但我觉得特别重要。当你的 prompt 里塞了几十轮对话,一旦输出有问题,你根本无从下手排查。是系统提示词的问题?是某轮用户输入的问题?是工具返回结果的问题?还是信息之间的相互干扰?全量拼接把所有变量搅在一起,调试就像在一锅乱炖里找一根特定的面条。

我自己的习惯是,任何 Agent 的 prompt 里,同时存在的“信息块”不超过 5 个。超过这个数,我就会考虑做记忆分层或者摘要压缩。这个数字不是拍脑袋来的,是踩了很多次坑之后总结的经验值——5 个块以内,我还能在脑子里追踪每个块的作用;超过 5 个,排查问题的时间会指数级上升。

3. 记忆分层的核心思路

3.1 把记忆当成一个分级存储系统

解决思路其实不复杂,核心就一句话:不是所有记忆都值得放进上下文,不同重要性的记忆应该放在不同的层级。

这个思路借鉴的是计算机体系结构里的存储层次。CPU 有寄存器、L1/L2/L3 缓存、内存、硬盘,越靠近 CPU 的越快越小越贵,越远的越慢越大越便宜。Agent 的记忆管理完全可以照搬这套逻辑。

我通常把 Agent 记忆分成四层。工作记忆是当前这一轮必须用到的信息,直接进上下文,比如用户当前的问题、最近一两轮的对话。短期记忆是本次会话内的重要信息,比如用户在这次对话里表明的偏好、提到的关键实体,需要的时候调进上下文。长期记忆是跨会话的持久信息,比如用户的历史偏好、过往问题的解决记录,存在外部存储里,按需检索。归档记忆是几乎不用的冷数据,只做合规留存,平时不参与推理。

这样分层之后,上下文里永远只放“当前最相关”的那一小部分,长度可控,成本可控,调试也清晰。

3.2 什么信息该进哪一层:一个判断框架

分层说起来容易,具体到某条信息该放哪层,很多人会卡住。我总结了一个简单的判断框架,问自己三个问题。

第一个问题:这条信息在当前这一轮对话里用得到吗?用得到,进工作记忆。用不到但本次会话可能还会用到,进短期记忆。本次会话大概率用不到,进长期记忆。

第二个问题:这条信息的时效性有多强?比如“用户当前正在咨询的订单号”,时效性极强,用完就该从工作记忆里清掉。“用户偏好的沟通语气”,时效性弱,可以进长期记忆。

第三个问题:这条信息出错的代价有多大?代价高的,比如医疗过敏史、金融账户信息,宁可多占一点上下文也要保证它在场,甚至可以强制常驻。代价低的,比如闲聊内容,可以大胆压缩或丢弃。

这三个问题问下来,大部分信息的归属就清楚了。我建议你把这个框架做成一个简单的决策表,贴在团队文档里,新同学上手会快很多。

3.3 分层带来的额外好处:可观测性

分层之后还有一个意外收获——可观测性大幅提升。因为每一层的信息来源和更新时机都是明确的,你可以在每一层加日志、加监控。工作记忆的长度、短期记忆的命中率、长期记忆的检索准确率,这些指标都能单独观测。

我之前带的一个项目,就是靠监控“长期记忆检索命中率”发现了一个隐蔽的 bug:检索模块的向量模型和写入模块的向量模型版本不一致,导致检索出来的内容总是差那么一点。这个问题在全量拼接的方案里几乎不可能被发现,因为所有信息都在,检索模块根本没被用上。

4. 落地实操:从摘要压缩到向量检索

4.1 摘要压缩:最容易被低估的基础手段

如果只能选一种记忆管理手段,我会选摘要压缩。它的性价比最高,实现简单,效果立竿见影。

具体做法是:当对话轮次超过某个阈值(我一般设 10 轮),就把最早的若干轮对话交给模型做一次摘要,用摘要替换原始对话。摘要要保留什么?我的经验是保留四类信息:用户的核心诉求、已经确认的事实、达成的结论、未解决的问题。其他的寒暄、重复确认、中间过程都可以丢。

摘要的 prompt 我一般这么写:

请将以下对话压缩成一段不超过 200 字的摘要,必须保留: 1. 用户提出的所有明确诉求 2. 对话中已经确认的客观事实(如订单号、时间、金额) 3. 已经给出的结论或方案 4. 尚未解决的问题 不要保留寒暄、重复确认和中间推理过程。

这里有个细节要注意:摘要本身也会累积误差。如果你对摘要再做摘要,几轮之后信息会严重失真。我的做法是,摘要只做一层,原始对话保留在外部存储里,摘要里如果提到某个关键事实,同时存一个指向原始对话的引用 ID,需要的时候可以回溯。

注意:摘要压缩的阈值不要设得太小。我见过有人设成 3 轮就压缩,结果摘要比原文还长,纯属浪费。10 到 15 轮是比较舒服的区间,具体看你的单轮信息密度。

4.2 向量检索:让长期记忆真正可用

短期记忆靠摘要,长期记忆就得靠向量检索了。这块的坑最多,我重点讲。

基本流程是:把历史信息切块、向量化、存入向量库;需要的时候用当前问题去检索最相关的若干块,拼进上下文。听起来简单,但每一步都有讲究。

切块策略是第一个关键点。切太大,检索出来的内容冗余;切太小,语义不完整。我的经验值是 200 到 500 字一块,块与块之间留 10% 到 20% 的重叠,避免关键信息被切断。对于对话记录,我倾向于按“话题”切而不是按“轮次”切,同一个话题的几轮对话合成一块。

检索数量是第二个关键点。检索太多,上下文又被塞满了;检索太少,可能漏掉关键信息。我一般检索 top 5 到 top 8,然后做一次重排序,最终只把 top 3 放进上下文。重排序这一步很多人省掉,但我觉得不能省,向量相似度高不代表真的相关,加一个交叉编码器做精排,准确率能提升不少。

检索时机是第三个关键点。不是每一轮都要检索。我的做法是,先用一个轻量判断——当前问题是否涉及历史信息?如果只是“好的”“谢谢”这种,根本不用检索。这个判断可以用规则做,也可以用一个小模型做,成本很低。

下面是一个检索模块的伪代码,展示整体流程:

def retrieve_memory(query, user_id, top_k=8, final_k=3): # 1. 向量粗排 candidates = vector_store.search( query_embedding=embed(query), filter={"user_id": user_id}, top_k=top_k ) # 2. 交叉编码器精排 reranked = rerank_model.sort(query, candidates) # 3. 取最终结果,并做长度控制 selected = reranked[:final_k] return truncate_by_token(selected, max_tokens=1500)

4.3 参数怎么定:一份可参考的配置表

参数不能拍脑袋,我整理了一份自己常用的配置,你可以作为起点,再根据实际效果微调。

参数建议值说明
摘要触发轮次10-15 轮低于 10 轮压缩收益低,高于 15 轮上下文压力大
摘要长度上限200-300 字太长失去压缩意义,太短信息丢失严重
向量块大小200-500 字按话题切分,块间重叠 10%-20%
粗排数量5-8 条给精排留足候选
精排后入上下文2-3 条控制上下文长度
检索结果 token 上限1000-1500超过这个数就该考虑压缩
工作记忆保留轮次最近 3-5 轮保证当前对话连贯性

这张表里的数字不是金科玉律,但都是我实际跑过、验证过能用的。你如果发现效果不好,优先调“摘要触发轮次”和“检索结果 token 上限”这两个,它们对整体表现影响最大。

4.4 一个完整的记忆流转示例

光讲模块可能还是抽象,我串一个完整流程。

用户第 1 次来咨询,问了一个产品问题。系统把这次对话存入短期记忆。用户第 2 次来,问的是另一个问题。系统检索长期记忆,发现这个用户上次问过相关问题,把上次的结论调出来,拼进上下文。用户第 3 次来,聊到第 12 轮,触发摘要压缩,前 10 轮压成一段摘要,最近 2 轮保留原文。同时,这次对话里用户表明的偏好(比如“我喜欢简洁的回答”)被写入长期记忆。

整个过程中,上下文里始终只有:系统提示词 + 当前问题 + 最近几轮原文 + 相关摘要 + 检索到的长期记忆。长度稳定在一个可控范围内,不会随对话轮次无限增长。

5. 常见问题与排查技巧实录

5.1 摘要丢信息怎么办

这是最高频的问题。摘要压缩之后,模型突然“忘了”之前确认过的事实。排查思路分三步。

先看摘要 prompt 有没有明确要求保留事实类信息。很多人写的 prompt 太笼统,只说“总结一下”,模型自然会把事实当细节丢掉。然后看摘要长度是不是压得太狠,200 字塞不下关键事实,那就放宽到 300 字。最后看是不是摘要做了多层,多层摘要必然失真,改成单层加引用回溯。

我自己的兜底方案是:关键事实不进摘要,单独存一个结构化字段。比如订单号、金额、时间这些,用 JSON 存,需要的时候直接拼进上下文,不依赖摘要。摘要只负责承载那些不好结构化的语义信息。

5.2 检索总是召回不相关内容

向量检索召回不准,八成是切块或 embedding 的问题。先检查切块是不是把语义切碎了,一个完整的诉求被切成两半,检索出来自然不相关。再检查 embedding 模型是不是适合你的领域,通用模型在垂直领域往往表现一般,有条件的话用领域数据微调一下。

还有一个容易被忽略的点:查询改写。用户当前的问题可能很短,比如“那这个呢”,直接拿去做向量检索,召回质量肯定差。我的做法是先用模型把当前问题结合最近几轮对话改写成一句完整的查询,再去检索。这一步能显著提升召回准确率。

5.3 上下文长度还是压不下来

如果做了摘要和检索,上下文还是长,那要检查是不是有“隐形大户”。常见的隐形大户有三个:系统提示词太长、工具返回结果没做裁剪、检索结果没做 token 控制。

系统提示词我一般控制在 1000 token 以内,超过就拆分,把不常用的指令做成按需加载。工具返回结果一定要裁剪,比如搜索工具返回 10 条结果,只保留前 3 条的关键字段。检索结果的 token 控制前面提过,用truncate_by_token这类函数硬性截断。

5.4 一份速查表

现象可能原因排查方向
模型忘记早期确认的事实摘要丢信息 / 检索没召回检查摘要 prompt、关键事实结构化
回答自相矛盾新旧信息同时在场检查是否有冲突信息未清理
检索结果不相关切块碎 / embedding 不匹配调整切块、查询改写、换模型
上下文压不下来隐形大户 / 参数没调检查系统提示词、工具返回、检索上限
响应越来越慢输入 token 增长监控单轮输入长度,加硬性上限

提示:这张表建议直接贴到你的监控看板上,出问题时按行排查,比从头分析快得多。

5.5 几个我踩过的坑

第一个坑:摘要和原文同时在场。早期我做过一个“保险”设计,摘要进上下文的同时,原文也保留一份,想着万一摘要漏了还能兜底。结果上下文直接翻倍,而且模型面对摘要和原文两套表述,反而更容易混乱。后来改成摘要进上下文、原文只存外部,需要时按引用 ID 回溯,问题就没了。

第二个坑:检索没有做用户隔离。多用户场景下,如果检索时不加 user_id 过滤,A 用户的记忆可能被检索到 B 用户的对话里。这个 bug 很隐蔽,因为大部分时候检索结果看起来“也说得通”,但偶尔会冒出莫名其妙的信息。加上过滤条件就好了。

第三个坑:长期记忆只写不清理。用户三个月前的一个临时偏好,一直留在长期记忆里,导致 Agent 的行为越来越“固执”。长期记忆一定要有 TTL(过期时间)或者定期清理机制,我一般设 90 天,重要的偏好可以手动标记为永久。

6. 我个人的一些实践体会

做 Agent 记忆管理这几年,最大的体会是:不要追求“记住一切”,要追求“在对的时候想起对的事”。人类的大脑也不是把所有经历都存着,而是记住关键的、遗忘次要的,需要的时候通过线索回忆起来。Agent 的记忆系统应该模仿这个机制,而不是模仿硬盘。

另一个体会是,记忆管理没有银弹,只有权衡。摘要压缩会丢信息,向量检索会召回不准,分层会增加复杂度。你要做的是根据自己业务的特点,选择能接受的权衡。客服场景对准确性要求高,那就多留一点上下文;闲聊场景对成本敏感,那就压得狠一点。

最后一个建议:从最简单的方案开始。不要一上来就搞四层记忆加向量库加重排序,先用摘要压缩跑起来,观察效果,再逐步加复杂度。我见过太多团队,记忆系统设计得无比精巧,结果因为调试困难、维护成本高,最后又退回全量拼接。简单能跑通的方案,永远比复杂但跑不起来的方案有价值。

如果你现在正在为 Agent 的记忆问题头疼,不妨先做一件事:把单轮请求的输入 token 数打出来,看看它是不是在随对话轮次增长。如果是,那你就已经找到了优化的起点。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进