当前位置:首页 > 编程开发 > 正文

上下文压缩三种策略实测:省到 100 token,就得接受会漏

约 4329 字 · 预计阅读 11 分钟
一句话总结:我拿一个 30 轮、3122 token、含 3 条硬约束的会话测了三种压缩策略。滑动窗口压到 833 token 但丢了 2 条约束;全文摘要压到 106 token 保留 2 条(按 30% 漏检模拟);分层压缩 1146 token,一条不丢。省 token 和保信息是两条相反的路,分层是唯一在现实条件下稳的。

Grok Build 0.2.60 的那次更新,把三件事列为重点:会话恢复、上下文压缩、MCP 工具输出。

前两件其实是同一件事的两面——压缩是为了让会话活得更久,恢复是为了让活下来的会话能接着用。

我拿一个典型场景测了一下:一个编码 Agent 跑 30 轮,中途定下 3 条硬约束,怎么压才不丢。

一、先看三种上下文压缩策略差在哪

滑动窗口。 只保留最近 N 轮,更早的直接扔掉。实现最简单,成本最低。

全文摘要。 把整个会话压成一段话。token 省得最多,但摘要器会漏东西。

分层压缩。 硬约束原样保留,最近几轮原样保留,中间无关紧要的部分压成一句。

三种策略的差别不在算法复杂度,在"丢什么"。

滑动窗口按时间丢——它假设越早的内容越不重要。这个假设在编码任务里经常不成立:第 3 轮定的技术选型,到第 30 轮还在约束你。

摘要按重要性丢——它假设摘要器能判断什么重要。这个假设依赖摘要器的质量,而摘要器本身是个模型,也会出错。

分层不丢硬约束,只丢"过程"。它假设你能识别出什么是硬约束。

二、实测:833 和 1146 的 token 差别

测试设定:30 轮对话,共 3122 token,其中 3 轮含硬约束:

  • 第 3 轮:用 Python 写
  • 第 12 轮:不准改数据库 schema
  • 第 25 轮:目标是下午 5 点前上线

上下文压缩三种策略实测:省到 100 token,就得接受会漏

图 1:横轴往右是花更多 token,纵轴往上是不丢约束,三个点各占一角

结果:

策略 压缩后 token 压缩率 硬约束保留
滑动窗口(留 8 轮)83326.6%1/3
全文摘要(30% 漏检)1063.4%2/3
分层压缩114636.7%3/3

滑动窗口只留住了第 25 轮那条(因为它落在最近 8 轮里),前面两条全丢。

摘要丢了一条,因为我按 30% 漏检率模拟——真实的摘要器漏检率不好说,可能更低也可能更高。

分层一条不丢,代价是 token 只压到 36.7%。

这个 36.7% 看着不划算,但它保住了最关键的东西。 一个丢掉了"不准改数据库"的 Agent,后面可能真的去改数据库。

三、分层上下文压缩怎么实现

核心是"分类",不是"压缩"。你得先能判断哪些轮次是硬约束。

def is_hard_constraint(turn: dict) -> bool:
    """判断一轮是不是硬约束。真实实现用模型分类,这里用规则演示"""
    markers = ("不准", "必须", "不要", "只能", "截止", "目标")
    return any(m in turn["text"] for m in markers)


def compact(turns: list[dict], keep_recent: int = 8) -> list[dict]:
    recent = turns[-keep_recent:]
    recent_idx = {t["n"] for t in recent}

    # 关键点:已经在最近 N 轮里的,不要再算进 keys,否则会被重复计入
    keys = [t for t in turns
            if (t.get("hard") or is_hard_constraint(t))
            and t["n"] not in recent_idx]

    middle = [t for t in turns
              if t not in keys and t not in recent]
    if middle:
        summary = {
            "n": 0,
            "text": f"中间 {len(middle)} 轮讨论实现细节,无新硬约束",
            "is_summary": True,
        }
    else:
        summary = None

    out = keys + ([summary] if summary else []) + recent
    return sorted(out, key=lambda t: t["n"])

跑这 30 轮,输出是:

3 条硬约束原样保留
1 条摘要(覆盖 19 轮)
最近 8 轮原样保留
合计 1146 token

关键是 keys 那一步。它决定了压缩的上限——识别不出来的约束,再好的压缩策略也保不住。

实际产品里这一步通常交给模型做,判断"这轮说话是不是在定规矩"。规则匹配只能兜底。

四、会话恢复比压缩更难

Grok Build 把"会话恢复"和"上下文压缩"并列,但我觉得后者比前者简单。

压缩是"把长的变短",纯计算问题。

恢复是"把停下的状态接上",涉及状态一致性。难在哪:

一是压缩必须可逆到"够用"。 恢复之后 Agent 要能接着干,它不需要知道第 15 轮聊了什么,但必须知道那 3 条约束。所以压缩时丢的东西,必须是恢复时用不上的。

二是工具调用状态要单独存。 会话里不只有文本,还有"调用了什么工具、返回了什么、哪个还没完成"。这些状态压缩不掉,得序列化。

三是恢复点要能对齐检查点。 如果恢复时 Agent 正做到一半(比如改了一半文件),得知道从哪继续。这要求每次改动前记快照。

第三点最容易出 bug。代码库健康度那篇讲的"状态复杂"就是这个意思——代码库的状态不是几个变量能描述的。

五、坑清单

1. 关键轮可能和"最近 N 轮"重叠,重了就会重复计入。 我第一次写的时候没做去重,第 25 轮的约束既在 keys 里又在 recent 里,算出来 4 条约束、1251 token。真实场景里这个 bug 不会报错,只会让 token 数字虚高。

2. 硬约束的识别不能只靠关键词。 上面那段代码是演示用的,真实场景里"尽量别动那个文件"也是约束,但不含任何标记词。要靠模型判断。

3. 压缩之后要重算 token。 按字符估 token 会有偏差,尤其是中英混排。压缩后必须用真实 tokenizer 数一遍,否则你以为压到 1200,实际是 1800。

4. 摘要里要保留原始轮次编号。 不然恢复时顺序会乱,Agent 可能以为约束是后来才定的。

5. 别在压缩后再压缩。 多轮压缩会累积信息损耗,第二遍摘要会把第一遍的细节再模糊一层。要么一次性压到位,要么保留原始会话做冷备。

6. 恢复后第一件事是确认状态。 让 Agent 复述一遍当前的约束和进度,比直接让它接着干安全。

六、边界

这组数字是本地模拟的,摘要的漏检率(30%)是我拍的,不是实测。

真实的压缩效果取决于三件事:模型识别硬约束的准确率、摘要器的质量、任务本身的信息密度。任务越"无状态"(比如纯问答),压缩越容易;任务越"有状态"(比如改代码),越难。

另外我只测了文本轮次,没有测工具调用的结果。实际会话里工具返回的内容往往占大头,压缩它们需要另外的策略(比如只留关键字段、丢掉冗余日志)。

结语

选哪种策略,看一个判断:丢了会不会出事。

丢了没事,用滑动窗口,最省。

丢了会出事但能接受偶尔漏,用摘要。

丢了绝对不能接受,用分层。

分层压不到 40%,这是它的成本。但这个成本比"Agent 改错数据库"便宜太多。

参考资料

目录导航
  • 一、先看三种上下文压缩策略差在哪
  • 二、实测:833 和 1146 的 token 差别
  • 三、分层上下文压缩怎么实现
  • 四、会话恢复比压缩更难
  • 五、坑清单
  • 六、边界
  • 结语
    • 参考资料
  • 相关文章:

  • 腾讯混元 Hyra 2.0:三层 Agent 做科研,从解题变成造工具
  • DeepSeek 的 Creator Mode:说一句话,生成一个能用的插件
  • Agent 开始有身份了:内容站该怎么声明谁能来、能干什么
  • Agent 的算力账单:一个任务跑到 1333 步,整月订阅就没了
  • Anthropic message threads:20 轮少传 96%
  • Kimi 连干 20 小时,SlopCodeBench 无一通过:验收口径不同
  • 过度封装的下场:一个检查代码的活拆成六个组件,改需求改了两天
  • 让 Agent 操作桌面应用:走无障碍树比截图省 5 倍,但省的不是最关键的
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。