上下文压缩是如何工作的?

上下文压缩是如何工作的?

本文将介绍大语言模型中的上下文压缩,以及它是怎样工作的。我们还会看看:长对话为什么会占满上下文窗口,怎样把对话中的旧消息概括成更短的摘要,同时保留重点,以及实际的 AI agent 会在哪里用到上下文压缩。

本文将介绍以下内容:

  • 什么是大语言模型
  • 什么是上下文窗口
  • 什么是上下文
  • 长对话带来的问题
  • 最容易想到的办法,以及它为什么行不通
  • 什么是上下文压缩
  • 如何通过摘要实现上下文压缩
  • 分步演示
  • 用代码实现上下文压缩
  • 实际 AI agent 中的上下文压缩
  • 为什么上下文压缩很重要

我是 Amit ShekharOutcome School 的创始人。我教过并指导过许多开发者,他们凭自己的努力拿到了高薪技术工作。我也帮助多家科技公司解决过各自的问题,并开发了多个被知名公司采用的开源库。我热衷于通过开源项目、博客和视频分享知识。

我在 Outcome School 教授 AI 和机器学习

我们开始吧。

什么是大语言模型

在介绍上下文压缩之前,我们需要先了解什么是大语言模型。

大语言模型(Large Language Model,LLM) 是一种读取文本并预测下一个词的模型。

简单来说,它是一台很会猜词的机器。我们给它一些词,它会猜下一个词是什么;然后把猜出的词加到文本里,再继续猜。这个过程不断重复,每次生成一个词。

假设我们输入“I want to learn”(我想学习……)。模型猜测下一个词是“machine”(机器),于是文本变成“I want to learn machine”。模型再把整段文字读一遍,继续猜下一个词。长回答就是这样一个词接一个词生成的。

注意: 模型实际处理的是一种叫作 token 的文本小单元。一个 token 可以是完整单词、单词的一部分,甚至单个字母。为了便于理解,我们可以暂时把 token 看作一个词。

大语言模型就是这样工作的。下面来了解上下文窗口。

什么是上下文窗口

模型无法读取无限多的文本。它一次能容纳的词数有上限。

上下文窗口是模型在同一时刻最多能够保留的 token 数量。

简单来说,上下文窗口就是模型短期记忆的容量。如果上限是 4000 个 token,模型最多只能在上下文中保留最近的 4000 个 token,再往前的内容就放不下了。

可以把上下文窗口想成一块小白板。白板上能写下的内容有限,一旦写满,就必须先擦掉一些内容,才能再写新的东西。

所以,上下文窗口的大小是固定的。我们接下来要解决的问题,根源就在这里。下面看看,这个窗口里实际装了些什么。

什么是上下文

我们一次发送给模型的全部内容,统称为 上下文

简单来说,上下文就是模型回答问题前需要读取的全部文本。对当前任务来说,它就是模型的工作记忆。

也就是说,上下文是模型此刻需要查看的所有文字。在聊天中,它包括系统指令、之前的每条消息,以及我们刚刚提出的新问题。

假设我们正在和聊天机器人交谈。上下文就是截至目前的完整对话记录。模型每次回复前都会重新读取这份记录,因此它才能记得我们之前说过什么。

这里有一个重点:上下文装在上下文窗口里。窗口是盒子,上下文就是我们放进盒子里的东西。

决定上下文里放什么、怎样管理这些内容,本身就是一门学问。我们另有一篇文章深入介绍上下文工程

以上就是上下文的工作方式。下面看看,为什么我们需要上下文压缩。

长对话带来的问题

假设我们正在构建一个聊天机器人或 AI 编码助手,并希望连续使用它几个小时。对话会越来越长,我们发出的每条消息和模型给出的每个回答都会加入上下文。

问题在于,装上下文的盒子大小固定,对话却会不断增长。盒子迟早会被装满。

下图展示了盒子是怎样一点点装满的:

Context window (a fixed box that holds 8 messages)

Early in chat:   [ msg1 msg2 ........................ ]   plenty of room
Later in chat:   [ msg1 msg2 msg3 msg4 msg5 msg6 .... ]   filling up
Very long chat:  [ msg1 msg2 msg3 msg4 msg5 ... msg8 ]   FULL, no room

New message msg9 arrives ->  it does not fit. Now what?

可以看到,对话越长,盒子里的内容就越多。如果新消息到来时盒子已经满了,就必须先删掉一些旧内容,才能把新消息放进去。

上下文溢出会带来三个问题。第一,模型放不下新消息,于是会报错或截断旧文本。第二,上下文中的内容越多,模型运行得越慢、成本也越高,因为它每一轮都要读取更多 token。第三,过多的旧文本可能干扰模型,让它抓不住当前真正重要的内容。

所以问题是:怎样才能让对话持续几个小时,又不让这个盒子溢出?

最容易想到的办法,以及它为什么行不通

最容易想到的办法很简单:盒子满了之后,丢掉最旧的消息,只保留最近的消息。这叫作 滑动窗口(sliding window)

简单来说,滑动窗口只保留最近的一批消息。随着窗口向前移动,旧消息会从另一端掉出去,就像坐在行驶的火车上,看着旧景色不断从窗外退去。

来看一个例子。假设盒子只能容纳 4 条消息,对话内容如下:

msg1 msg2 msg3 msg4 msg5 msg6

这里共有六条消息,但盒子只能容纳四条。msg5 到来时,我们丢掉 msg1;msg6 到来时,再丢掉 msg2。这样一来,盒子里始终只有最近 4 条消息。

听上去很完美,但问题在于,旧消息里往往保存着我们仍然需要的信息。

假设 msg1 是“My name is Amit and I am building an Android app in Kotlin.”(我叫 Amit,正在用 Kotlin 开发一款 Android 应用。)几小时后,我们问:“我用的是什么编程语言?”如果 msg1 已经被删掉,模型就无从得知答案。这个信息已经丢失,模型要么猜错,要么说不知道。

这种方法的问题在于,丢掉旧消息,也会丢掉其中的重要信息。我们失去的不只是文字,还有记忆。下面看看另一种办法如何解决这个问题。

如果你想深入学习上下文工程、agent 的记忆与架构,并从零构建一个 AI 编码 agent,可以了解 Outcome School 的 AI 和机器学习课程

什么是上下文压缩

这时,上下文压缩就派上用场了。

上下文压缩会把之前的对话浓缩成一份简短摘要,让重要信息保留下来,也让盒子里重新有了空间。

简单来说,我们不直接删除旧消息,而是把它们压缩成一份简短摘要,用它替换大段旧文本。盒子里的内容变少了,重要信息却保留下来。

为了便于理解,我们把这个术语拆开来看。

上下文压缩 = 上下文 + 压缩

“上下文(Context)”是模型需要读取的全部文本,“压缩(Compaction)”就是把这些文本变得更短。因此,上下文压缩就是在保留重点的前提下,减少模型需要读取的内容。

假设我们用 20 页日记记录了一天的经历。到了第二天,我们不再需要全部 20 页,只需写一份半页长的摘要,留下重要事件,再丢掉原来的 20 页。我们会失去一些细节,但仍然记得事情的来龙去脉。这半页摘要就是压缩后的上下文。

可以把它想成收拾行李箱:

收拾行李箱 上下文压缩
行李箱 上下文窗口(大小固定)
所有衣物 完整对话记录
衣物多得装不下 上下文窗口逐渐装满
折叠衣物并用真空袋压缩 为旧消息生成摘要
收拾整齐的行李箱 压缩后的上下文
丢掉垃圾、留下衣物 丢掉无关内容、保留重要信息

可以看到,行李箱装满时,我们不会把衣服扔掉,而是把它们叠好、压紧,腾出空间。上下文压缩对文字做的也是同一件事。

这就是本文的核心。接下来看看,上下文压缩具体是怎样实现的。

如何通过摘要实现上下文压缩

我们通过 生成摘要 来缩短旧文本。

简单来说,就是让模型自己读一遍旧消息,把其中的重点整理成一份简短摘要。

巧妙之处在于,负责回答问题的那个大语言模型,本身也很会做摘要。所以,这两项工作可以交给同一个模型。我们只要告诉它:“阅读这段旧对话,写一份保留重要信息的简短摘要。”它就会返回一份很短的摘要。

假设旧消息共有 3000 个 token。我们把它们交给模型,请模型生成摘要。模型返回一份只有 200 个 token 的摘要,其中仍然保留着用户姓名、项目、所用的编程语言,以及此前作出的决定。

接下来,我们删掉原来那 3000 个 token 的旧消息,只留下这份 200 个 token 的摘要。这样就腾出了 2800 个 token 的空间,同时保留了重要信息。整个方法就是这么简单。

可以用下面的示意图表示这一流程:

   Old messages          The model            Short summary
   (3000 tokens)        (summarizer)          (200 tokens)
  +-------------+                            +-----------+
  | msg1 msg2   |  -->  +-----------+  -->   | key facts |
  | msg3 ... old|       |  read and |        | name      |
  | raw history |       | summarize |        | project   |
  +-------------+       +-----------+        +-----------+

   Throw away 3000 raw tokens, keep the 200-token note.

可以看到,旧消息送进模型后,出来的是一份很短的摘要。也就是说,负责回答问题的模型,也能替我们把旧内容压缩成一份摘要。

顺便提醒一句

无论你从事哪个技术领域,都应该熟悉下面这些主题:

  • LLM
  • RAG
  • MCP
  • Agent
  • 微调
  • 量化

我们用一个视频把这些内容串到了一起:

AI 工程详解:LLM、RAG、MCP、Agent、微调与量化

不用现在停下来去看视频——先收藏,等有空再看。以后你会感谢现在的自己。

现在回到正题。

还有一点:这个过程可以反复执行。下次盒子再装满时,我们把上次压缩之后积累的那些消息,连同已有摘要一起交给模型,再生成一份新的、甚至更短的摘要。以后每次盒子装满,都可以重复这个过程。

所以,生成摘要是实现压缩的手段,缩短后的上下文才是结果。下面用一组具体数字看看这个过程。

分步演示

通过例子最容易理解。下面用一个小例子,看看这些数字是怎样变化的。

假设上下文窗口可以容纳 1000 个 token。我们设置一条规则:上下文超过 800 个 token 时,就执行压缩。

第 1 步: 对话不断增长。经过多轮交互,旧消息占用 700 个 token,最近的消息占用 150 个 token,总计 850 个 token。上下文已经超过 800 个 token 的阈值,因此需要开始压缩。

第 2 步: 我们拿出占用 700 个 token 的旧消息,让模型为它们生成摘要。模型读完这些消息后,返回一份只有 120 个 token 的摘要,其中保留了用户姓名、项目目标和关键决定。

第 3 步: 我们重新组成上下文:删掉占用 700 个 token 的旧消息,换成 120 个 token 的摘要;最近的消息占用 150 个 token,内容较新、细节也完整,因此原样保留。

新的上下文如下所示:

Before compaction (850 tokens, over the limit):
  [ old messages: 700 ] + [ recent messages: 150 ]   -> 850 (too big)

After compaction (270 tokens, plenty of room):
  [ summary: 120 ] + [ recent messages: 150 ]         -> 270 (lots of space)

We freed up 580 tokens and kept every important fact.

可以看到,上下文从 850 个 token 减少到 270 个 token,腾出了 580 个 token 的空间。最近的消息保留了全部细节,旧消息中的重要信息则被浓缩进摘要。

第 4 步: 对话继续进行。模型现在读取这份摘要和最近的消息。它仍然知道我们的姓名和项目,因此能够正确回答。以后盒子再次装满时,只需再压缩一次。

数字就是这样变化的。下面看代码。

用代码实现上下文压缩

先来看最简单但存在缺陷的滑动窗口版本:

def naive_sliding_window(messages, max_messages):
    # keep only the most recent messages
    return messages[-max_messages:]

这里,我们只保留最后 max_messages 个元素。这样会彻底丢掉旧消息,其中包含的信息也会随之消失。这个版本会忘掉之前的信息。

下面是改进后的代码。它会压缩旧内容,而不是直接删除:

def compact_context(messages, recent_count, summarize):
    # split the history into old and recent parts
    old = messages[:-recent_count]
    recent = messages[-recent_count:]

    # turn the old messages into one short summary
    summary = summarize(old)

    # rebuild: short summary first, then the recent messages
    return [summary] + recent

这段代码做了三件事:

  • 把对话记录拆成 oldrecent 两部分:最后 recent_count 条消息放进 recent,其余消息放进 old
  • 调用 summarize(old),让模型把 old 中的消息压缩成一份简短摘要。
  • 重新组成上下文:先放简短的 summary,再接上保留完整内容的 recent

旧消息中的信息保存在摘要里,最近的消息则保留完整细节。上下文又缩短了,对话可以继续,问题也就解决了。

注意: 实际使用时,我们不会在每条消息后都执行压缩。只有当上下文占用达到设定阈值(例如窗口容量的 80%)时,才会执行压缩。这样可以降低成本。

以上就是这段代码的工作方式。下面看看实际的 AI 工具如何使用上下文压缩。

实际 AI agent 中的上下文压缩

上下文压缩并不只是一个设想。如今的 AI agent 和编码助手已经大量采用这种方法。

简单来说,AI agent 是一种利用大语言模型连续完成多项操作的程序,例如读取文件、运行命令和修复代码。每一步都会向上下文加入更多文字。任务持续得越久,上下文就会变得越庞大。

假设一款编码助手已经工作了一小时。它读取了几十个文件,也运行了许多命令。此时,未经压缩的历史记录已经非常庞大,盒子几乎装满。如果不进行上下文压缩,助手就会忘记任务一开始的目标。

因此,助手会在后台执行上下文压缩。它把前面执行过的步骤整理成一份简短记录,例如:“目标:修复登录 bug。已经修改 auth.pylogin.py。测试均已通过。”接下来,它只需把这份记录和最新的执行步骤放进上下文,就能继续工作。

还不止如此。好的压缩机制会完整保留几类内容,不会用摘要替换它们:最初的系统指令、用户的主要目标,以及最新消息的全部细节。只有中间那段杂乱的内容会被压缩。

因此,现代 agent 会把上下文压缩当作一项预先设计的功能,而不是事后补救措施。它们保护最重要的部分,压缩其余内容。这样,agent 才能在长任务中连续工作几个小时,又始终不忘任务主线。

如果你想深入学习上下文工程、agent 的记忆与架构,并从零构建一个 AI 编码 agent,可以了解 Outcome School 的 AI 和机器学习课程

为什么上下文压缩很重要

至此,我们已经完整了解了上下文压缩。最后看看它为什么重要。

  • 它让聊天机器人和 agent 连续运行几个小时,而不会超出上下文窗口。
  • 它能保留重要信息,只丢掉无关内容。
  • 模型每一轮需要读取的 token 更少,因此成本不会太高,速度也不会太慢。
  • 即使记忆容量有限,它也能让对话持续顺畅地进行下去。

这样一来,即使可用的记忆空间很小而且固定,我们仍能维持长对话,同时不忘记重要内容。

准备 AI 工程面试,可以参考:AI 工程面试题

本文到这里就结束了。

谢谢。

Amit ShekharOutcome School 创始人