Agent 为什么会反复做同一件事?一次上下文压缩问题复盘

44 阅读10分钟

之前在做一个 BI 智能体项目时,我们遇到过一个比较典型的长任务问题:

Agent 执行时间越长,越容易重复前面已经做过的计算和推理。

表现很明显:

  • 前面已经分析过的数据,后面又重新分析;
  • 已经得到的结论,过一段时间后又重新推导;
  • 执行链路越来越长;
  • Token 消耗持续增加;
  • Agent 最后甚至会逐渐偏离原来的任务。

我们最开始怀疑过模型、Prompt 和任务规划,后来把完整上下文拆开分析,最终定位到了 Context Compaction(上下文压缩)。

问题的本质是:

压缩发生以后,一些已经确认的重要结论没有被保留下来。Agent 在后续阶段看不到这些结论,只能重新计算。

针对这个问题,我们主要做了两个调整:

问题原来的方式调整后的思路
压缩时机不合理Token 达到阈值立即压缩让模型感知任务阶段,在阶段切换时优先压缩
压缩内容不准确使用框架通用压缩 Prompt固定业务 Prompt + Agent 动态生成压缩要求

调整之后,项目里原来比较明显的重复计算、重复推理问题减少了很多,长任务执行也稳定了一些。

下面具体说这两个问题。


1. 我们最开始遇到的问题

这个智能体主要会执行一些比较长的数据分析任务。

一个任务不是简单问一句、答一句,而是可能经历:

理解问题
→ 查询数据
→ 分析数据
→ 得出阶段结论
→ 根据结论继续查询
→ 再分析
→ 生成最终结果

随着任务执行,上下文会越来越长。

Agent 框架为了避免上下文无限增长,会在达到一定条件后对前面的内容进行压缩。

正常情况下,这没有问题。

问题出在压缩结果。

举个简化后的例子。

Agent 在第一个阶段完成了:

查询数据 A
→ 查询数据 B
→ 对比分析
→ 得出结论:

“指标下降主要来自区域 X,
后续应该重点分析区域 X。”

接着发生了一次上下文压缩。

压缩结果可能保留了:

已经查询指标数据。
对多个区域进行了分析。
下一步继续进行详细分析。

但真正重要的:

指标下降主要来自区域 X。

被压掉了。

进入下一个阶段以后,Agent 看到当前上下文,只知道:

前面分析过,但不知道分析结果是什么。

那它最合理的选择就是重新分析。

于是又出现:

重新查询
→ 重新计算
→ 再次比较
→ 再次得出区域 X 的结论

如果后面再次压缩时又丢失类似信息,就会继续重复。

最终我们看到的执行链路就变成:

分析
→ 得出结论 A
→ 压缩
→ 结论 A 丢失

→ 重新分析
→ 再次得出结论 A
→ 再次压缩
→ 结论再次丢失

→ 再次分析……

这也是为什么当时 Agent 看起来像“走偏了”。

实际上它并不一定是真的推理错了。

它只是已经不知道自己前面做过什么。


2. 第一个改造:优化压缩触发时机

我们先看了框架原来的压缩机制。

比较常见的一种实现方式是根据 Token 数量触发。

例如:

上下文 < 50K Token
继续执行

上下文 >= 50K Token
触发压缩

这个方案最大的优点是简单。

对于通用 Agent 框架来说也很合理,因为框架不需要理解具体任务,只需要保证上下文不要超过限制。

但在我们的场景里,这种方式有一个问题:

Token 阈值只知道上下文有多长,不知道 Agent 当前执行到了哪里。

例如 Agent 正在完成:

查询数据
→ 计算
→ 分析
→ 形成最终结论

执行到“分析”这一步时,刚好达到 Token 阈值。

框架此时直接触发压缩,就相当于在一个任务还没有真正结束的时候,把当前上下文重新总结了一遍。

而这个阶段最重要的结论可能还没有完全形成。

即使已经形成,也可能和大量过程数据混在一起。

这时候进行压缩,比较容易丢掉任务状态。

我们的处理方式

后来我们的思路是:

不要只让 Token 数量决定压缩时机,而是让模型参与判断当前是否适合压缩。

因为模型能够通过上下文判断当前任务阶段。

例如:

阶段一:经营数据分析

查询数据
→ 数据计算
→ 原因分析
→ 输出阶段结论
→ 阶段完成

接下来准备:

阶段二:基于阶段结论进行深入分析

“阶段一结束、阶段二开始”这个位置,就比“Token 恰好达到 50K”更适合做压缩。

于是我们的压缩思路从:

Token 达到阈值
→ 立即压缩

调整成更偏:

上下文已经较长
+
当前任务阶段已经完成
→ 执行压缩

核心不是取消 Token 控制,而是尽量避免在一个关键任务执行到一半的时候直接压缩。

在我们的场景里,这个调整比较重要。

因为很多业务分析任务本身就天然存在阶段:

取数
→ 分析
→ 得出结论

结论
→ 下一轮分析
→ 新结论

最终汇总
→ 输出结果

既然任务本身存在阶段,压缩最好也尽量沿着任务阶段发生。


3. 第二个改造:优化压缩 Prompt

解决压缩时机以后,我们继续看另一个问题:

压缩的时候到底保留什么?

我们查看了之前使用的 Agent 框架底层实现,发现它的压缩 Prompt 比较通用。

这其实很正常。

一个通用 Agent 框架并不知道使用者是在:

  • 写代码;
  • 查资料;
  • 做数据分析;
  • 生成报告;
  • 还是处理业务流程。

所以压缩 Prompt 通常只能要求模型:

总结当前上下文,并保留重要信息。

但这里有一个很关键的问题:

什么叫“重要信息”?

对于不同任务,答案完全不一样。


4. 我们真正想保留的是“结果”,而不是完整过程

拿 BI 分析举例。

一个阶段可能执行了:

调用多个查询
→ 获取大量数据
→ 做多次计算
→ 尝试不同分析方向
→ 排除部分原因
→ 最终得出三个结论

对于后续阶段来说,真正重要的通常不是:

调用过哪些工具
每一次查询返回了什么
中间试过哪些错误方向
每一步计算过程是什么

真正重要的是:

已经确认的事实是什么
已经得出的结论是什么
哪些假设已经被排除
当前任务进行到了哪里
下一步还需要做什么

换句话说:

过程数据可以压缩,但任务状态和关键结论不能丢。

原来的通用 Prompt 很难准确知道这个区别。

所以我们对压缩 Prompt 做了改造。


5. 压缩 Prompt 拆成两部分

我们最终把压缩 Prompt 分成了两部分。

第一部分:固定 Prompt

第一部分是我们根据自己的业务场景写的固定规则。

主要告诉模型:

压缩时重点保留:

1. 已确认的业务事实
2. 已经得出的关键结论
3. 后续任务仍然需要的数据
4. 当前任务已经完成到什么程度
5. 尚未完成的问题
6. 下一阶段需要继续使用的信息

同时允许减少:

1. 重复的数据查询过程
2. 临时计算过程
3. 已经被否定的中间尝试
4. 对后续任务没有影响的过程信息

这一部分相当于给压缩增加了一个比较明确的业务方向。


第二部分:Agent 动态生成压缩要求

只有固定 Prompt 仍然不够。

因为同一个 BI Agent,每一次任务也可能完全不同。

因此我们又增加了一部分:

让 Agent 根据当前完整上下文,自己生成这一次压缩需要重点保留的信息。

原因很直接。

执行到这个阶段时,最清楚当前任务状态的,其实就是模型自己。

它知道:

  • 刚才做了哪些分析;
  • 哪些只是中间过程;
  • 哪些结论已经确认;
  • 哪些信息下一阶段还需要;
  • 当前还有什么没有完成。

所以在真正压缩之前,我们先让 Agent 根据当前任务生成一份“本次压缩要求”。

最终的压缩 Prompt 变成:

固定业务压缩规则
+
当前 Agent 根据任务生成的压缩要求
=
最终 Context Compaction Prompt

固定部分解决:

我们这个类型的业务,一般应该保留什么。

动态部分解决:

这一次具体任务,最不能丢的是什么。

两部分结合以后,比只用一套通用 Prompt 更符合我们的场景。


6. 两个改造其实解决的是两个完全不同的问题

后来复盘这个问题,我觉得有一点比较容易被混在一起。

上下文压缩至少有两个不同的问题:

问题一:什么时候压缩

解决的是:

不要把正在执行的关键任务从中间切断。

我们采用的是:

任务阶段感知。


问题二:压缩什么

解决的是:

压缩以后,后续任务仍然能够知道前面已经完成了什么。

我们采用的是:

固定业务规则 + Agent 动态压缩要求。


可以简单总结成:

Context Compaction

├── When
│   └── 尽量在任务阶段结束时压缩
│
└── What
    └── 保留结论、状态和后续依赖
        减少过程性信息

这也是我们这次改造最核心的两个点。


7. 实际效果

改完以后,我们重点观察的不是“压缩率提高了多少”,而是 Agent 的执行行为。

之前比较明显的问题是:

已经完成分析
→ 压缩
→ 忘记结果
→ 再次分析

调整之后,这类重复计算和重复推理明显减少。

同时因为很多已经没有价值的过程数据不再继续传递,后续阶段的上下文也更干净。

最终带来的变化主要有两个:

  • 无意义的 Token 消耗减少;
  • Agent 长任务的执行链路没有以前那么容易反复。

这对我们的 BI 场景比单纯追求更高的上下文压缩比例更有价值。


最后的一点感受

这次问题让我对 Agent 上下文压缩有了一个比较大的认知变化。

以前很容易把它理解成:

Context 太长了,所以想办法总结短一点。

但真正做长任务以后,我觉得更准确的理解应该是:

上下文压缩其实是在给 Agent 做阶段交接。

上一阶段结束以后,下一个阶段最需要知道的不是:

前面一共发生了多少事情。

而是:

前面已经确认了什么,现在进行到了哪里,下一步应该接着做什么。

如果这些信息丢了,上下文即使压得再短也没有意义。

甚至可能因为丢失状态,让 Agent 花更多 Token 把已经做过的事情重新做一遍。

所以现在我判断一次上下文压缩好不好,不太关注它:

从 50K 压到了 10K,还是压到了 5K。

我更关注的是:

压缩完成以后,Agent 能不能不回头,直接继续往下做。

对长任务智能体来说,我觉得这个指标可能比单纯的压缩率更重要。