
1. 从“第20轮失忆”说起Agent上下文管理的真实痛点如果你正在做 Agent 开发大概率遇到过这个场景前几轮对话里Agent 还能准确引用用户十分钟前提到的偏好、约束和中间结论聊到第 15 到 20 轮之后它开始答非所问把之前确认过的参数忘得一干二净甚至把用户明确否定的方案又拿出来推荐一遍。这不是模型“变笨”了而是上下文窗口被塞满之后系统做了某种粗暴的截断或压缩把关键信息一起丢掉了。我最早踩这个坑是在一个多轮任务型 Agent 上。用户让它帮忙规划一份跨三周的学习计划前 12 轮都很顺到第 18 轮用户问“那第三周周末的复习安排呢”Agent 直接回了一句“我们还没有确定第三周的内容”。实际上第三周的框架在第 6 轮就已经敲定了。排查下来发现当时用的方案是“超过 token 阈值就把最早的消息整段删掉”而第 6 轮那条消息恰好落在被删的区间里。这件事让我意识到一个核心区别管理历史和管理上下文是两件完全不同的事。管理历史是“把对话记录存下来、按时间顺序拼进去”管理上下文是“在有限的 token 预算内动态决定此刻模型应该看到什么”。前者是存储问题后者是调度问题。标题里说的“高手管理的是上下文”指的就是后者。这篇内容适合三类人正在做 Agent 应用开发、被多轮对话稳定性折磨的工程师准备从零搭建 Agent 框架、想提前避开上下文坑的开发者以及已经在用现成 Agent 平台、但发现长对话质量下滑、想搞清楚底层机制的产品和技术同学。我会围绕Context Editing上下文编辑、Compaction压缩、Memory Tool记忆工具这几个关键词把“为什么会失忆”“怎么判断该用哪种策略”“具体怎么落地”讲透并给出可以直接参考的实现思路和参数取舍逻辑。需要先明确一点上下文管理没有银弹。任何方案都是在“信息完整性”“token 成本”“响应延迟”“实现复杂度”这四个维度之间做权衡。下面我会逐个拆解。2. 上下文窗口到底被什么吃掉了一次 token 账单的拆解很多人以为上下文就是“用户说的话 模型回的话”实际远不止。一个真实运行的 Agent每一轮请求里塞进去的内容通常包括以下几类我按占用比例从高到低排内容类型典型占比是否随轮次增长备注系统提示词System Prompt5%~15%否包含角色设定、工具说明、输出格式约束工具定义Tool Schema10%~30%否工具越多、参数越复杂占用越夸张历史对话消息30%~60%是主要增长源工具调用结果10%~40%是搜索结果、文件内容、API 返回极易膨胀检索增强内容RAG0%~20%视情况每轮可能重新检索当前用户输入1%~5%否通常最小这张表里最容易被低估的是工具调用结果。我见过一个 Agent单次网页抓取返回的正文就有 8000 token连续抓三次就把 32k 的窗口吃掉大半。还有工具定义如果你接了 20 个工具每个工具 schema 平均 200 token光工具定义就 4000 token还没开始聊天就没了。所以“第 20 轮失忆”往往不是第 20 轮才发生的而是从第 8 轮开始系统就在悄悄做取舍了。常见的粗暴做法有三种滑动窗口只保留最近 N 轮超出的直接丢。问题是早期确认的关键约束比如“预算不超过 5000”“不要用某个库”会被丢掉。头部截断保留 system prompt 和最近消息中间挖空。问题是中间恰恰是任务推进的核心过程。无脑摘要把旧消息丢给模型总结成一段话。问题是摘要会丢细节而且摘要本身也可能越滚越大。提示判断你的 Agent 是否已经“隐性失忆”可以在 system prompt 里加一条指令要求模型在每轮回答前先复述当前已知的关键约束。如果复述开始缺项说明上下文已经在丢信息了。理解了 token 账单才能理解为什么“管理上下文”必须是有策略的而不是简单的“存下来再拼回去”。3. Context Editing不是删消息而是重写消息Context Editing 这个词听起来很玄本质上是在把消息发给模型之前对消息列表做一次有目的的重写。注意是“重写”不是“删除”。删除是不可逆的信息损失重写是把信息换一种更省 token 的形态保留下来。我常用的 Context Editing 手段有这么几类按侵入性从低到高3.1 消息合并与去冗余多轮对话里大量 token 浪费在客套和重复上。比如用户说“帮我查一下北京明天的天气”Agent 回“好的我来帮您查询北京明天的天气”这一来一回里有效信息只有“北京”“明天”“天气”。Context Editing 的第一步就是把这类冗余压掉。具体做法是维护一个“消息规范化”函数在写入历史前先处理def normalize_message(msg): # 去掉纯确认类回复 if msg.role assistant and is_pure_acknowledgement(msg.content): return None # 合并连续的同角色消息 # 压缩空白和重复表述 msg.content collapse_whitespace(msg.content) return msg实测下来光这一步在长对话里能省 15%~25% 的 token而且几乎不损失信息。3.2 工具结果的按需保留工具调用结果是重灾区。我的做法是给每个工具结果打一个“保留等级”关键结果如用户明确要求的数据、最终计算值完整保留。中间结果如搜索到的候选列表只保留前若干条 一句概括。过程性结果如“正在执行”“已提交”只保留状态不保留内容。比如一次搜索返回 20 条结果模型真正需要的可能只是前 5 条的标题和链接剩下的可以压成“另有 15 条类似结果”。这个策略我在多个项目里用过单次能省 60% 以上的工具结果 token。3.3 结构化状态外置这是我认为最有效的一招把对话中沉淀下来的“事实”抽出来存成结构化状态而不是留在对话历史里。举个例子用户和 Agent 聊了 15 轮确定了这些事实目标城市北京出行日期3 月 15 日预算5000 元以内禁忌不吃辣与其让这些信息散落在 15 轮对话里不如维护一个 JSON 状态{ destination: 北京, date: 2025-03-15, budget: 5000, dietary_restriction: [no_spicy] }每轮请求时把这个状态以精简形式注入 system prompt 或作为一条特殊消息。这样即使原始对话被压缩关键事实也不会丢。这就是“管理上下文”和“管理历史”的分水岭——历史可以丢状态不能丢。注意结构化状态需要一套抽取和更新逻辑。我的经验是让模型在每轮结束时输出一个“状态增量”由代码合并进主状态而不是让模型每轮重新生成完整状态后者容易漂移。4. Compaction什么时候压、压什么、压到什么程度Compaction压缩是 Context Editing 里最需要拿捏分寸的部分。压得太轻没效果压得太狠丢信息。我把它拆成三个决策触发时机、压缩对象、压缩目标。4.1 触发时机别等爆了才压最常见的错误是“等 token 超过阈值再压缩”。这时候往往已经晚了因为压缩本身也要消耗 token 和一次模型调用而且压缩过程中如果上下文已经接近上限压缩请求本身可能失败——热词里那个error running remote compact task: fatal error: remote compaction v2 expected就是这类问题的典型表现。我的做法是设置双阈值软阈值如窗口的 60%开始做轻量 Context Editing去冗余、压工具结果。硬阈值如窗口的 80%触发 Compaction对早期对话做摘要。这样压缩发生在还有余量的时候压缩请求本身不会因为窗口不够而失败。4.2 压缩对象优先压“过程”保留“结论”压缩不是把所有旧消息一视同仁地总结。我的优先级是优先压缩Agent 的中间推理、试错过程、被否决的方案。谨慎压缩用户的明确指令、确认过的参数、最终结论。绝不压缩结构化状态、当前任务目标、安全约束。一个实用的压缩 prompt 长这样以下是一段 Agent 与用户的历史对话。请压缩为要点列表要求 1. 保留所有用户明确提出的约束和偏好 2. 保留所有已确认的结论和参数 3. 丢弃试错过程和被否决的方案只保留曾尝试X但被否决的一句话 4. 输出不超过 300 字4.3 压缩目标分层摘要而非单一摘要单一摘要的问题是它会随对话持续增长最后又变成新的负担。我用的是分层摘要最近 5 轮原文保留。第 6~15 轮一段中等粒度摘要。第 15 轮以前一段高粒度摘要只保留结论和约束。这样 token 占用是可控的而且越久远的信息越精简符合“近期细节重要、远期只需结论”的直觉。压缩策略token 节省信息损失风险适用场景去冗余15%~25%极低所有长对话工具结果裁剪40%~60%低工具调用频繁的 Agent单层摘要50%~70%中对话轮次中等分层摘要60%~80%中低超长对话、任务型 Agent结构化状态外置视情况极低有明确任务状态的场景这张表是我自己项目里实测的粗略区间具体数值会因任务类型差异很大但相对关系是稳定的。5. Memory Tool把“记不住”变成“查得到”前面讲的 Context Editing 和 Compaction 都是“在窗口内做减法”。Memory Tool 是另一条路把信息挪出窗口需要时再查回来。这解决的是“窗口再大也不够用”的根本问题。5.1 记忆的三种类型我在设计 Memory Tool 时会把记忆分成三类分别用不同的存储和检索策略事实记忆用户的偏好、身份、长期约束。存结构化数据库每轮无条件注入精简版。情节记忆某次任务的完整过程。存向量库按语义检索。工作记忆当前任务的中间状态。存内存或 KV任务结束即清。很多 Agent 框架把这三类混在一起塞进向量库结果检索出来的东西要么太泛要么太碎。分开管理之后命中率明显提升。5.2 写入时机比检索更重要Memory Tool 最容易做砸的地方是“什么都往里写”。我的原则是只在信息具备长期价值时才写入判断标准有三条这条信息在未来对话中可能被再次引用吗它是稳定的还是临时的它是否已经被结构化状态覆盖了只有前两条为“是”、第三条为“否”时才写入长期记忆。否则宁可让它随对话自然衰减。5.3 检索回来的内容要“再压缩”从记忆里检索出来的内容不能原样塞回上下文。我的做法是检索后先做一次“相关性重排 摘要”只把最相关的 2~3 条、每条压到 100 字以内注入。这样既用上了长期记忆又不会把窗口重新撑爆。提示Memory Tool 的检索质量高度依赖写入时的元数据。写入时一定要带上时间、任务类型、涉及实体这些标签否则后期检索只能靠语义相似度召回会很不稳定。6. 一套可落地的上下文管理流水线把前面几块拼起来我给出一套我自己在用的流水线。它不是唯一解但每个环节的取舍逻辑我都验证过。6.1 每轮请求的组装顺序System Prompt角色 工具说明 输出约束固定。结构化状态当前任务的事实状态精简 JSON。长期记忆注入检索到的 2~3 条相关记忆已压缩。分层对话历史近期原文 中期摘要 远期摘要。当前用户输入。这个顺序有讲究把最稳定、最重要的放前面把最易变的放后面。这样即使后面被截断前面的核心约束也不会丢。6.2 每轮结束的收尾动作抽取本轮产生的状态增量合并进结构化状态。判断是否有值得写入长期记忆的信息。更新分层摘要的边界是否需要把某轮从“原文”降级为“摘要”。记录本轮 token 占用用于监控趋势。6.3 监控指标我必看的三个指标每轮 token 占用曲线如果持续上升且不收敛说明压缩策略失效。关键约束复述准确率定期用探针问题测试模型是否还记得核心约束。压缩触发频率如果每两三轮就触发一次硬压缩说明软阈值设置太晚或压缩力度不够。# 简化的监控埋点 def log_turn_metrics(turn_id, token_usage, compaction_triggered, constraint_recall_ok): metrics.append({ turn: turn_id, tokens: token_usage, compacted: compaction_triggered, recall_ok: constraint_recall_ok })这套流水线跑下来我那个原本第 18 轮就失忆的 Agent稳定跑到了 60 轮以上关键约束的复述准确率保持在 95% 以上。7. 几个我踩过的坑和对应的处理经验最后分享几个具体教训都是文档里不会写、但实际开发中一定会遇到的。坑一压缩请求本身超窗口。前面提过硬阈值设太晚压缩时窗口已经不够。解决办法是把压缩做成“分段压缩”每次只压一段而不是一次性压全部。坑二摘要越滚越大。单层摘要用久了会膨胀。换成分层摘要后解决核心是给每层设 token 上限超了就往下层合并。坑三结构化状态漂移。让模型每轮重新生成完整状态几轮之后字段就开始乱。改成“增量更新 代码合并”后稳定了。坑四记忆检索污染。早期什么都往长期记忆写结果检索出来的全是无关内容。加上写入门槛和元数据标签后召回质量明显改善。坑五工具定义占太多。接了太多工具光 schema 就吃掉三分之一窗口。后来做了工具分组按当前任务动态加载相关工具不相关的工具定义不注入。这些坑的共同点是它们都不是模型能力问题而是上下文调度问题。这也是为什么我说“高手管理的是上下文”——模型本身没变变的是你喂给它的东西。如果你现在正在被多轮对话的稳定性困扰建议先从“结构化状态外置”这一步做起它的投入产出比最高而且不依赖任何特定框架。等这一步跑顺了再往上叠 Compaction 和 Memory Tool会稳很多。