news 2026/10/4 6:57:47

Agent长对话失忆怎么办?Context Editing与Compaction实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent长对话失忆怎么办?Context Editing与Compaction实战

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 Prompt)5%~15%否包含角色设定、工具说明、输出格式约束
工具定义(Tool Schema)10%~30%否工具越多、参数越复杂,占用越夸张
历史对话消息30%~60%是主要增长源
工具调用结果10%~40%是搜索结果、文件内容、API 返回,极易膨胀
检索增强内容(RAG)0%~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 压缩对象:优先压“过程”,保留“结论”

压缩不是把所有旧消息一视同仁地总结。我的优先级是:

  1. 优先压缩:Agent 的中间推理、试错过程、被否决的方案。
  2. 谨慎压缩:用户的明确指令、确认过的参数、最终结论。
  3. 绝不压缩:结构化状态、当前任务目标、安全约束。

一个实用的压缩 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 最容易做砸的地方是“什么都往里写”。我的原则是只在信息具备长期价值时才写入,判断标准有三条:

  1. 这条信息在未来对话中可能被再次引用吗?
  2. 它是稳定的,还是临时的?
  3. 它是否已经被结构化状态覆盖了?

只有前两条为“是”、第三条为“否”时才写入长期记忆。否则宁可让它随对话自然衰减。

5.3 检索回来的内容要“再压缩”

从记忆里检索出来的内容,不能原样塞回上下文。我的做法是检索后先做一次“相关性重排 + 摘要”,只把最相关的 2~3 条、每条压到 100 字以内注入。这样既用上了长期记忆,又不会把窗口重新撑爆。

提示:Memory Tool 的检索质量高度依赖写入时的元数据。写入时一定要带上时间、任务类型、涉及实体这些标签,否则后期检索只能靠语义相似度,召回会很不稳定。

6. 一套可落地的上下文管理流水线

把前面几块拼起来,我给出一套我自己在用的流水线。它不是唯一解,但每个环节的取舍逻辑我都验证过。

6.1 每轮请求的组装顺序

  1. System Prompt:角色 + 工具说明 + 输出约束(固定)。
  2. 结构化状态:当前任务的事实状态(精简 JSON)。
  3. 长期记忆注入:检索到的 2~3 条相关记忆(已压缩)。
  4. 分层对话历史:近期原文 + 中期摘要 + 远期摘要。
  5. 当前用户输入。

这个顺序有讲究:把最稳定、最重要的放前面,把最易变的放后面。这样即使后面被截断,前面的核心约束也不会丢。

6.2 每轮结束的收尾动作

  1. 抽取本轮产生的状态增量,合并进结构化状态。
  2. 判断是否有值得写入长期记忆的信息。
  3. 更新分层摘要的边界(是否需要把某轮从“原文”降级为“摘要”)。
  4. 记录本轮 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,会稳很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 6:54:54

基于OpenClaw搭建8人AI开发团队:多Agent协作实践与编排指南

如果你手里有一个正经的开发需求,同时又有七八个不同专长的 AI 助手可以用,你会怎么安排它们干活?是一个一个轮流来,还是让它们各司其职、并行配合?我这次用 OpenClaw 把后者真正跑起来了——一个由 8 个 AI Agent 组成…

作者头像 李华
网站建设 2026/10/4 6:54:23

Hindsight:基于时间戳锚点的分布式任务回溯分析系统

1. 什么是 Hindsight?它不是“事后诸葛亮”,而是一套可落地的工程化回溯分析系统 Hindsight 这个名字乍一听容易让人联想到英文里“hindsight is 20/20”(事后看得清)那句老话——但在这类技术项目语境中,它绝非一句空…

作者头像 李华
网站建设 2026/10/4 6:53:55

MRAM替代EEPROM与Flash:RA2E1+MR25H40CDF工业存储方案实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 6:51:25

QuickFix Java消息收发与查看实战指南

1. 项目概述:为什么FIX协议的收发与查看是Java金融系统开发的“呼吸感”环节QuickFix Java 讲解(五)消息的收发与查看——这个标题里藏着一个被很多Java初学者低估、却被高频交易系统、券商柜台、基金估值引擎等真实生产环境反复锤炼的核心能…

作者头像 李华
网站建设 2026/10/4 6:50:39

Claude插件市场与MCP协议实战:从配置到避坑全指南

「AI扩展开大会,Claude这套玩法跟别人不太一样。别人是给模型加功能按钮,Claude是干脆把“外部世界”标准化成了一堆可插拔的插件——也就是MCP Server。你可以在Claude Code里挂数据库、读文件、操作浏览器、调Git,甚至让它自己写一个工具再…

作者头像 李华