1. 为什么上下文窗口越堆越大,Agent 反而越用越“健忘”
做 Agent 开发的人大概都经历过这个阶段:一开始觉得上下文窗口越大越好,模型从 4K 一路开到 128K、200K,心想这下总算能把整段对话、整份文档、整个工具调用历史全塞进去了。结果真跑起来才发现,窗口是大了,Agent 的表现却没跟着线性变好,反而出现一种很诡异的现象——前面聊过的重要约束它记不住,中间某次工具返回的关键 ID 它转头就丢,到了后面几轮开始胡编乱造,甚至把已经确认过的结论又推翻重来。
这不是模型变笨了,而是上下文管理这件事本身没做好。窗口大不等于有效信息密度高,把一堆无关紧要的历史、冗余的工具输出、重复的寒暄全堆进去,等于在一杯水里兑了一桶杂质,模型注意力被稀释,真正该记住的东西反而被淹没。业内有个很形象的说法叫“lost in the middle”——关键信息夹在长上下文中段时,模型的召回率会明显下降,这跟窗口大小关系不大,跟信息排布和压缩策略关系极大。
所以这篇想聊的,就是 Agent 上下文压缩与记忆策略这套东西。核心围绕三块:5 种压缩手段、3 类记忆形态、4 条选型红线,顺带把 MemGPT、ACON、Context Editing 这几个常被提到的方案拉出来对比一下,看看它们各自解决的是哪一段问题。适合正在做 Agent 落地、被上下文长度和记忆一致性折磨过的开发者,也适合刚接触这块、想搞清楚“到底该不该上记忆系统”的朋友。下面不讲空理论,全部按我实际踩过的坑和验证过的做法来说。
2. 五种上下文压缩手段:从“无脑截断”到“结构化重写”
压缩的本质是在有限的 token 预算里,尽可能保留对当前任务有用的信息。不同手段的取舍点不一样,有的胜在简单,有的胜在保真,有的胜在能长期跑。我按从粗暴到精细的顺序排一下,每种都说清楚它适合什么场景、坑在哪。
2.1 滑动窗口截断:最省事,也最容易丢关键约束
滑动窗口就是只保留最近 N 轮对话,超出的直接扔掉。实现简单到几行代码,很多框架默认就是这个策略。它的假设是“越近的信息越重要”,这在闲聊场景基本成立,但在 Agent 任务里经常翻车。
问题在于,Agent 的很多关键信息是早期一次性给出、后期反复依赖的。比如第一轮用户说“所有金额单位用万元,保留两位小数”,后面十几轮都在处理数据,滑动窗口一截,这条约束就没了,模型开始输出“12345.67 元”。我踩过最惨的一次是任务跑到第 20 轮,模型突然把前面确认过的 API 版本号改了,排查半天才发现是那条约束被窗口挤出去了。
所以滑动窗口不是不能用,而是必须配合“钉住”机制:把系统提示、核心约束、当前任务目标这几类信息单独拎出来,永远不参与截断。剩下的对话历史才走滑动窗口。这样至少保证底线不丢。
2.2 摘要压缩:把历史“翻译”成一段话,但摘要本身也会失真
摘要压缩是让模型把较早的对话总结成一段简短描述,用摘要替代原始历史。比截断优雅,能保留语义,但有两个隐藏成本。
第一,摘要是有损的,而且损失不可控。你让模型总结“用户改了三次需求”,它可能只写“用户调整了需求”,具体改成什么样全丢了。第二,摘要会累积误差。如果每轮都对上一轮的摘要再摘要,几轮下来信息会像复印机复印一样越来越糊。我见过一个跑了 50 轮的任务,最后的摘要已经跟原始需求对不上了。
我的做法是分层摘要:近期对话保留原文,中期对话做一次摘要,远期对话做二次摘要但保留关键实体(人名、ID、数值、约束)。而且摘要 prompt 里要强制模型输出结构化字段,比如“已确认约束”“待办事项”“关键数值”,而不是一段自由文本。结构化之后,后续检索和拼接都方便得多。
2.3 关键信息抽取:只留“骨架”,把冗余全砍掉
这个比摘要更激进,也更精准。思路是不做整体总结,而是从历史里抽取结构化的关键信息,比如实体、关系、状态变更、决策记录,存成一张表或一段 JSON,原始对话直接丢弃。
举个例子,一个订票 Agent 跑了十几轮,抽取出来的关键信息可能是:
{ "user_constraints": ["靠窗", "上午出发", "预算不超过800"], "confirmed": {"flight": "CA1234", "date": "2024-06-01", "seat": "12A"}, "pending": ["是否需要接机"], "rejected": ["红眼航班", "中转超过一次"] }这段 JSON 可能只有 100 token,但承载的信息量远超几千 token 的原始对话。它的优势是信息密度极高、可检索、可校验。难点在于抽取的准确性——抽错了比不抽还糟,所以抽取 prompt 要设计得足够严谨,最好让模型输出“不确定”而不是硬编。
2.4 语义检索压缩:不是全留,也不是全丢,而是“按需召回”
前面三种都是“决定留什么”,语义检索是“先都存着,用的时候再捞”。把历史对话切块、向量化,存进向量库,当前轮需要时用相似度检索出最相关的几块拼进上下文。
这套在知识库问答里很成熟,搬到 Agent 记忆里也成立,但有个关键区别:Agent 的检索不能只看语义相似,还要看时间新鲜度和任务相关性。纯向量检索容易捞出“语义很像但已经过时”的信息,比如用户上周说“我想去北京”,这周改成了“去上海”,检索“目的地”时两条都可能被捞出来,模型就懵了。
我的经验是混合打分:语义相似度占大头,但叠加时间衰减因子和“是否被后续信息覆盖”的标记。被覆盖的旧信息要么不召回,要么召回时明确标注“此信息已被更新”。这样能大幅减少自相矛盾。
2.5 结构化重写:把上下文当成一份“活的文档”来维护
这是我认为最优雅、也最费功夫的一种。不把上下文看成对话流水,而是看成一份持续更新的结构化文档,每轮对话后由模型把新信息合并进文档,冲突的地方做覆盖或标注。
比如维护一份这样的状态文档:
## 任务目标 帮用户规划一次三日游 ## 已确认 - 目的地:杭州 - 出行人数:2人 - 预算:3000元以内 ## 待确认 - 具体日期 - 是否需要含餐 ## 已否决 - 跟团游(用户明确不要)每轮对话后,模型读一遍文档 + 新对话,输出更新后的文档。这样上下文永远是“当前最新状态”,没有冗余历史,也没有摘要失真问题。代价是每轮多一次模型调用,而且文档 schema 要设计好,否则会越写越乱。MemGPT 的核心思路其实就接近这个,只不过它把文档分成了主上下文和外部存储两层。
3. 三类记忆形态:工作记忆、情节记忆、语义记忆怎么分工
压缩解决的是“怎么少占地方”,记忆解决的是“该记什么、记多久、怎么取”。这两件事经常被混为一谈,其实得分开设计。我习惯把 Agent 记忆分成三类,对应不同的生命周期和访问方式。
3.1 工作记忆:当前任务的“草稿纸”,用完即弃
工作记忆就是当前这一轮或当前这个子任务正在用的上下文,生命周期最短,可能就几轮对话。它的特点是读写频繁、容量小、不需要持久化。比如 Agent 正在调用一个工具,工具返回的原始 JSON、中间计算结果、临时变量,都属于工作记忆。
这块的管理重点是及时清理。很多 Agent 跑着跑着上下文爆炸,就是因为工作记忆里的临时数据没清,一直堆着。我的做法是给工作记忆设一个明确的“作用域”,子任务结束就整体丢弃,只把结论写进上一层记忆。就像函数里的局部变量,函数返回就销毁,不会污染全局。
3.2 情节记忆:发生过什么,按时间线存
情节记忆记录的是“事件”,比如“用户在 10:05 确认了航班”“Agent 在 10:07 调用了支付接口并成功”。它是有时间顺序的,适合回答“刚才发生了什么”“上一步做了什么”这类问题。
情节记忆的关键是可追溯。出问题时要能回放整个决策链路,所以每条记录最好带上时间戳、动作类型、输入输出摘要。但情节记忆不能无限增长,否则检索会变慢、噪声会变多。我一般给它设一个保留窗口,比如最近 100 条事件,更早的做归档或摘要。
3.3 语义记忆:沉淀下来的“事实”和“偏好”,跨任务复用
语义记忆是最高层、最持久的,存的是从多次交互中提炼出来的稳定事实和用户偏好。比如“这位用户偏好靠窗座位”“这个项目的 API 基地址是 xxx”“团队约定金额单位用万元”。它不依赖具体某次对话,而是跨会话、跨任务复用。
语义记忆的难点在于写入时机和冲突消解。什么时候该把一条信息从情节记忆提升为语义记忆?我的判断标准是:重复出现、且被明确确认过的信息才值得沉淀。单次出现的信息先放情节记忆,出现两次以上再考虑提升。冲突时以最新确认的为准,但保留历史版本以便回溯。
三类记忆的关系可以这样理解:工作记忆是草稿纸,情节记忆是日记本,语义记忆是档案库。草稿纸用完就扔,日记本按时间翻,档案库按主题查。三者配合,才能既省 token 又不丢关键信息。
4. 四条选型红线:踩过才知道哪些方案不能碰
前面讲了手段和形态,但真正落地时,选型比实现更容易出错。我总结了四条红线,都是实际项目里用血泪换来的,碰到就得停下来重新想。
4.1 红线一:压缩率不能只看 token 数,要看任务成功率
很多人选压缩方案时盯着“压缩到原来的 10%”这种指标,觉得压得越狠越牛。但压缩的目的是让 Agent 把活干对,不是省 token 省着玩。我做过对比,某个摘要方案能把上下文压到 8%,但任务成功率掉了 30 个百分点,因为关键约束被压没了。
正确的做法是用任务成功率反推压缩率。先定一个可接受的成功率下限,然后在这个约束下找压缩率最高的方案。通常压缩到 30% 到 50% 是个比较稳的区间,再往下就要非常小心地保护关键信息。
4.2 红线二:记忆写入不能“来者不拒”,否则噪声会淹没信号
有些方案为了“不丢信息”,把所有对话、所有工具返回都往记忆库里塞。短期看很安全,长期看是灾难。记忆库会迅速膨胀,检索时捞出一堆无关内容,反而干扰模型判断。我见过一个跑了三个月的 Agent,记忆库里存了几万条记录,检索出来的十条有八条是噪声。
记忆写入必须有准入规则:只写被确认的事实、明确的偏好、可复用的结论。临时数据、中间过程、失败尝试,要么不写,要么写到低优先级的存储里,检索时不参与或降权。
4.3 红线三:检索不能只靠向量相似度,时间与状态必须参与排序
纯向量检索在 Agent 场景里不够用,前面提过。这里再强调一次,因为这是最容易踩的坑。用户的需求是动态变化的,旧信息和新信息语义可能高度相似,但旧信息已经失效。如果检索不区分时效和状态,模型就会拿到互相矛盾的信息,输出自然不稳定。
我的排序公式大致是:最终得分 = 语义相似度 × 0.6 + 时间新鲜度 × 0.3 + 状态有效性 × 0.1。状态有效性指这条信息是否被后续信息覆盖,被覆盖的直接归零。权重可以根据场景调,但时间维度不能省。
4.4 红线四:不要在没有评估集的情况下上复杂记忆系统
这条最容易被忽视。很多人一上来就搭向量库、写摘要流水线、搞多层记忆,结果根本不知道效果好不好,因为没有评估集。改了一版策略,感觉好像好了一点,但到底好了多少、有没有引入新问题,全靠感觉。
我的建议是,动手做记忆系统之前,先攒一个 50 到 100 条的真实任务评估集,覆盖长对话、多轮修改、跨会话复用这些场景。每改一版策略就跑一遍,看成功率、平均 token 消耗、关键信息召回率这几个指标。没有评估集,所有优化都是盲人摸象。
5. MemGPT、ACON、Context Editing 三条路线的取舍
这三个名字经常被放在一起提,但它们解决的问题其实不完全一样。我把它们放在同一节里对比,是因为很多人在选型时会纠结“到底该学哪个、用哪个”。搞清楚各自的定位,选择就清晰了。
5.1 MemGPT:把上下文当“虚拟内存”来管理
MemGPT 的核心思路很有启发:它把 LLM 的上下文窗口类比成计算机的物理内存,把外部存储类比成磁盘,然后设计了一套“分页”机制——主上下文放当前最相关的信息,外部存储放全量历史,模型自己决定什么时候把信息从外部调入主上下文、什么时候把主上下文的信息写回外部。
它的价值在于给了 Agent 自主管理记忆的能力,而不是靠外部硬编码规则。模型可以通过工具调用主动“检索记忆”“写入记忆”,这让长期运行的 Agent 有了自我维护的可能。代价是复杂度高,而且模型自主决策本身会引入不确定性,调不好会出现频繁读写、记忆混乱的情况。适合对长期记忆有强需求、且愿意投入调优的场景。
5.2 ACON:聚焦“压缩”这一环,把上下文精简做到极致
ACON 这类方案的重点不在记忆分层,而在上下文压缩本身。它关注的是如何在保证任务信息不丢的前提下,把上下文压到最小。常见做法包括对工具输出做结构化裁剪、对历史做增量摘要、对冗余内容做去重。
它的优势是目标单一、容易集成,不需要重构整个记忆架构,在现有 Agent 上加一层压缩管道就能见效。适合那些上下文爆炸主要来自“工具返回太长”“历史堆太多”的场景。局限是它不解决跨会话记忆问题,压缩完的信息还是留在当前上下文里,会话一结束就没了。
5.3 Context Editing:把上下文当成可编辑的对象
Context Editing 的思路是让上下文可增删改,而不是只能追加。传统对话历史是只追加的,越滚越长;Context Editing 允许 Agent 或开发者主动删除过时信息、修改错误信息、合并重复信息。它更像是一种“上下文维护”的操作范式,而不是一个具体算法。
这套思路和前面说的“结构化重写”很接近,区别在于它更强调编辑动作的显式化。什么时候删、删什么、改成什么,都有明确的记录。好处是可审计、可回滚,坏处是需要一套编辑规则和冲突检测机制,否则容易改乱。
三条路线对比下来,我的判断是:ACON 解决“当前上下文太胖”,Context Editing 解决“上下文内容不对”,MemGPT 解决“长期记忆怎么管”。它们不是互斥的,实际项目里经常组合使用——用 ACON 做压缩,用 Context Editing 做维护,用 MemGPT 式的分层做长期存储。
6. 落地时最容易翻车的几个细节
理论和方案讲完了,最后说几个实操里反复踩的坑。这些细节文档里一般不写,但真跑起来一个比一个要命。
6.1 压缩触发时机:别等爆了才压,要提前压
很多人设的压缩触发是“上下文用到 90% 才开始压”,这时候往往已经晚了,因为压缩本身也要消耗 token,而且压缩过程中模型可能已经因为上下文过长而表现下降。我的做法是在 60% 到 70% 就开始渐进式压缩,把较早的历史慢慢摘要掉,保持一个平稳的上下文水位,而不是等到临界点做一次大压缩。
6.2 记忆检索的“空结果”处理:捞不到时别硬编
检索记忆时经常出现捞不到相关结果的情况。这时候如果直接让模型“根据记忆回答”,模型可能会硬编一个答案。正确做法是明确告诉模型“未检索到相关记忆”,让它基于当前上下文回答,或者主动询问用户。这个小小的提示词改动,能显著减少幻觉。
6.3 多轮修改场景下的“版本覆盖”标记
用户改需求是常态,但记忆系统如果不标记版本,就会出现新旧信息并存。我的做法是给每条记忆加一个superseded_by字段,新信息写入时把旧信息标记为“已被覆盖”,检索时默认过滤掉被覆盖的,需要历史时再显式召回。这样既保证当前状态干净,又保留可追溯性。
6.4 评估集要包含“长尾失败案例”
评估集不能只放顺利跑通的任务,要专门收集那些失败过的、边界模糊的、多轮反复的案例。这些长尾案例才是真正检验记忆策略的地方。我一般会把线上跑挂的任务手动整理进评估集,每次改策略都重点看这些案例有没有改善。
6.5 别忽视 token 成本之外的延迟成本
压缩和记忆检索都会增加延迟。摘要要调模型,检索要查向量库,这些都会让单轮响应变慢。如果 Agent 是实时交互的,延迟可能比 token 成本更敏感。我的经验是把压缩和检索做成异步或预取,不要卡在用户等待的主链路上。比如上一轮结束时就预压缩下一轮可能用到的历史,用户提问时直接取用。
这套东西说到底没有银弹,5 种压缩、3 类记忆、4 条红线,都是帮你在“记多少”和“记得准”之间找平衡的工具。真正跑起来,还是得靠评估集说话,靠一次次失败案例把策略磨出来。我自己到现在也还在调,每次觉得稳了,换个场景又冒出新问题——这大概就是 Agent 记忆这件事的常态。