去年我做了一个 AI Agent,能跑几十轮对话完成任务。前 10 轮惊艳,第 50 轮开始犯蠢,第 80 轮已经没法看了。

不是模型变笨了。是我塞了太多历史进去,把模型淹没了。

这篇文章想聊一个反直觉的事:AI Agent 的记忆不是越多越好,学会「遗忘」才是关键。

上下文窗口不是硬盘,是桌面

我跟很多开发者聊过这个问题,大部分人的第一反应是:把所有历史都塞进上下文不就行了?

这个思路错在哪?错在把上下文窗口当成了硬盘。

上下文窗口是工作台,不是档案室。模型推理的时候,所有信息都摊在这个台子上。台子就那么大,东西堆多了,重要的资料就被埋住了。

我见过最夸张的情况:一个 agent 跑了 200 多轮,上下文里塞了 12 万 token 的历史记录。模型把 80% 的推理能力花在"从一大堆文字里找到有用的那几句"上,真正干活的脑力所剩无几。

你可以做个简单实验:给 Claude 或 GPT 一个 10 轮对话的历史,准确率可能是 95%。把同样的任务放到 100 轮对话的历史里,准确率能降到 70% 以下。不是模型变差了,是噪音太多。

遗忘的三种方式

先说清楚三件事,因为很多人把它们混为一谈。

清除(Clearing)

/clear 一下,历史全没了。干净利落,但代价也大。切换任务时用可以,中途来一下,agent 会断片。

截断(Trimming)

删掉最早的消息,保留最近的。简单、快、便宜。但它的问题也很明显:最早的那条消息,可能恰恰是整个任务的关键决策。

压缩(Compaction)

用模型把大量历史改写成一个更短的摘要,保留精髓,丢掉细节。这是这篇想重点聊的。

它们的区别可以用一个例子说明:

Agent 帮用户排查了一个数据库连接问题,花了 20 轮才定位到是 SSL 证书过期。Trimming 会删掉前面的 18 轮——包括"SSL 证书过期"这个核心结论。Clearing 会全删——包括用户刚刚提到的"还需要检查 Redis 连接"。Compaction 会把 20 轮对话压缩成一句:"用户已排查并修复 SSL 证书过期问题,现在需要检查 Redis 连接。"

哪一种保留的信息更有用,一目了然。

压缩的四个坑

Compaction 听起来简单——"加个 summarize 不就好了?"——但实际做起来全是坑。

Drew Breunig 对长上下文退化有一个简洁的分类,而这四种退化都可能因为粗糙的压缩被引入:

1. 毒化(Poisoning)

压缩时模型产生了幻觉,把错误信息写进了摘要。然后这个摘要又被当成事实依据,被后续的推理使用。

我见过一个真实案例:Agent 在早期对话中猜测"可能是数据库连接池满了",后来验证发现是索引问题。但压缩时,模型把"可能是数据库连接池满了"这句话留了下来。结果 30 轮之后,agent 还在围绕"连接池满了"做排查。

应对:只压缩"确定的信息",把推测和假设分开处理。

2. 膨胀(Distraction)

压缩完和原来差不多长——恭喜你,白干了。

这通常是因为压缩时保留了太多"可能有用"的信息。50K 的对话压缩到 48K,没意义。

应对:给压缩设定明确的长度目标,比如"压缩到不超过原始长度的 20%"。

3. 混乱(Confusion)

摘要里塞了太多不必要的细节,把模型的注意力引向无关的方向。

比如一个压缩器认为"用户在第 5 轮提到过他用的是 MacBook Air"很重要(其实跟当前任务完全无关),结果每次推理时模型都会分心考虑"MacBook Air 有没有兼容性问题"。

应对:压缩时基于当前任务目标做筛选,不是做全量摘要。

4. 冲突(Clash)

摘要和最新消息矛盾了,模型需要在两个"事实"之间做选择。

常见场景:Agent 早期决定要实现方案 A,做了大半。用户中途改主意要换方案 B。老摘要里全是"方案 A 的进展",新消息里是"用方案 B 重新开始"。模型夹在中间纠结。

应对:压缩时标注"已废弃"、"已被覆盖"的信息,优先采用最新的指令。

最大的坑:累积侵蚀

上面四个问题,在单次压缩里问题不大。但如果你反复压缩,问题就来了。

第一次压缩损失 20% 的信息,第二次损失 20%,第三次再损失 20%。三轮下来,原始信息的有效内容只剩一半。十轮之后,agent 在跟自己玩"传话游戏"——原始意图已经严重失真。

这不是理论推演,这是我的 agent 项目真实踩过的坑。跑了几天之后回头看日志,发现 agent 已经在执行一个完全变了形的任务。原因很简单:每一次 compaction 都是 lossy 的,我把它凑合用了 50 次。

实战:我实现的四种遗忘策略

下面是我在自己项目中验证过的几种方案,各有各的适用场景。

策略一:滑动窗口 + 渐进压缩

最简单的实现方式。维护一个消息队列:

原始窗口:最近的 10 轮完整消息
压缩区域:更早的消息压缩成摘要

每次新消息进来,检查窗口大小。如果超过阈值,把最老的一轮消息移出窗口,加入到待压缩区。待压缩区积累到一定量后,进行一次压缩。

class SlidingWindowMemory:
    def __init__(self, window_size=10, compression_threshold=20):
        self.recent = []  # 原始消息窗口
        self.summary = ""  # 压缩后的历史摘要
        self.buffer = []   # 待压缩的消息
        self.window_size = window_size
        self.compression_threshold = compression_threshold

    def add(self, message):
        self.recent.append(message)
        self.buffer.append(message)

        # 窗口超出,压缩
        if len(self.recent) > self.window_size:
            moved = self.recent.pop(0)

        # 缓冲区积攒足够,执行压缩
        if len(self.buffer) >= self.compression_threshold:
            self._compact()

    def _compact(self):
        # 用 LLM 把 buffer 里的消息压缩成摘要
        compressed = llm.summarize(self.buffer)
        # 合并到已有的摘要中
        self.summary = llm.merge_summaries(self.summary, compressed)
        self.buffer = []

    def get_context(self):
        return {
            "history_summary": self.summary,
            "recent_messages": self.recent
        }

优点是实现简单,开销可控。缺点是无法避免累积侵蚀。建议设置最大压缩轮数,超过就做一次"全量回顾"来修正失真。

策略二:语义剪枝

不让模型"总结"历史,而是让模型"判断每段信息是否还有用"。

def semantic_prune(messages, current_task):
    """判断每条历史消息是否与当前任务相关"""
    for msg in messages:
        relevance = llm.judge(
            f"这条消息与当前任务 '{current_task}' 的相关性(0-10):\n{msg}"
        )
        if relevance < 3:
            messages.remove(msg)  # 直接丢弃
    return messages

比起全量压缩,语义剪枝更轻量——因为不需要生成新内容,只是做二元判断。适合那些历史消息量大但大部分已经没用的场景。

缺点是每次调用都有模型开销。另外如果任务上下文发生了变化,之前被认为"不相关"的信息可能又重新变得重要——所以别删太狠,考虑用"归档"而不是"丢弃"。

策略三:基于遗忘曲线的记忆衰减

人脑的遗忘是有规律的——我们不会均匀地忘记所有事情。刚发生的记得清楚,久了就模糊。艾宾浩斯遗忘曲线描述的就是这个规律。

把这个思路用到 agent 上:

class DecayMemory:
    def __init__(self):
        self.memories = []  # (content, timestamp, importance)

    def add_memory(self, content, importance=0.5):
        self.memories.append({
            "content": content,
            "timestamp": time.now(),
            "importance": importance,
            "access_count": 0
        })

    def recall(self, query, threshold=0.3):
        results = []
        for m in self.memories:
            # 计算记忆强度
            hours_elapsed = (time.now() - m["timestamp"]).hours
            # 重要性越高的记忆,衰减越慢
            strength = m["importance"] * 0.9 ** (hours_elapsed / (24 * m["importance"]))
            # 被访问的次数越多,记忆越强
            strength *= (1 + 0.1 * m["access_count"])

            if strength > threshold:
                results.append(m)

        return results

    def access_memory(self, memory):
        memory["access_count"] += 1
        memory["timestamp"] = time.now()  # 重新复习,延长记忆

这个实现最接近人类的记忆方式:重要的记得久,频繁访问的记得牢,长期不用的慢慢淡出。

但它的缺点是依赖人工标注重要性。我的做法是让模型在每次对话结束时自动标注:"本轮对话中的关键信息(1-10分)"。关键信息包括:用户明确的决策、配置参数、错误根因等。

策略四:主动遗忘

以上三种都是被动遗忘——等到记忆快满了再处理。主动遗忘是在执行过程中就让 agent 自己判断"这个信息值不值得记住"

在每条消息进入记忆系统之前,agent 先问自己三个问题:

  1. 这条信息未来的任务还会用到吗?
  2. 它是一个决策还是一个临时状态?
  3. 如果丢了它,最坏后果是什么?
def active_filter(message, context):
    """在消息进入记忆前就做筛选"""
    assessment = llm.analyze(
        f"评估这条信息是否值得长期记住:\n"
        f"消息:{message}\n"
        f"当前任务上下文:{context}\n"
        f"请返回:[记住/丢弃/暂存] + 理由"
    )
    return assessment

主动遗忘是最聪明的策略,因为它让 agent 有了"元认知"——意识到自己在记住什么、为什么记住。但这样做的代价是每次消息都多了一次 LLM 调用。

我的使用经验是:只在关键决策点开启主动遗忘过滤。比如用户修改配置、改变策略方向、确认问题根因这些时刻。日常对话的零碎信息走滑动窗口就行。

混合策略:我的推荐配置

实际项目中,我通常组合使用以上策略:

层级 策略 容量 成本
短期 滑动窗口(最近 10 轮) ~5K tokens 0
中期 语义剪枝(每 5 轮清理一次) ~10K tokens
长期 遗忘曲线衰减 ~50K tokens
关键点 主动遗忘 ~1K tokens 高,只关健时刻

每次 agent 构建上下文时,按这个优先级组装:

def build_memory_context(agent):
    context = []

    # 1. 关键记忆(主动遗忘策略保留的)
    key_memories = agent.key_memories.recall(query=agent.current_task)
    context.append(format_section("关键信息", key_memories))

    # 2. 长期记忆(衰减后仍保留的)
    long_term = agent.decay_memory.recall(query=agent.current_task)
    if long_term:
        context.append(format_section("历史信息", long_term))

    # 3. 短期记忆(最近的对话)
    recent = agent.sliding_window.get_recent(10)
    context.append(format_section("最近对话", recent))

    return "\n".join(context)

踩坑总结

最后分享几个我在记忆系统上踩过的坑:

坑 1:摘要里混入了代码语境

压缩器把一段代码示例压缩成了"用户讨论了一个函数实现",结果 agent 完全丢失了代码细节。后面需要复用这段代码时,agent 只能重新写一遍——大概率写得不一样。

解决:代码片段不压缩,只压缩自然语言讨论。代码改成引用 (reference) 模式。

坑 2:忘了保留"否决"记录

Agent 和用户讨论过方案 A,最终决定用方案 B。压缩后只保留了"使用方案 B",没保留"否决方案 A"。过了几轮,agent 又自己提出方案 A,浪费了十多轮对话。

解决:额外保留一个"已否决"列表,每次 proposal 先查一下。

坑 3:记忆系统本身成了瓶颈

记忆系统太复杂了——又要衰减、又要剪枝、又要主动过滤。每次构建上下文要调用 3-4 次 LLM,延迟增加了 5 秒。

解决:把记忆系统拆成同步和异步两部分。同步部分只做最轻量的滑动窗口,异步部分在后台做压缩、衰减、主动遗忘,下次对话时再生效。

最后

回到题目:为什么「记得少」比「记得多」重要?

因为 AI Agent 的上下文窗口是有限的工作台,不是无限容量的仓库。学会遗忘——有策略地、有选择地遗忘——不是能力缺陷,是设计智慧。

好的记忆系统,不是把所有东西都记住。而是在需要的时候,能想起该想起的东西。

这也是人脑工作的方式。你的大脑不会记住过去三个月的每一封邮件,但它知道现在该想什么。

所以,教你的 agent 忘记。教它忘记的分寸,就是教它记住的水平。