叶小钗

第 11 篇 · 上下文压缩:让 Agent 在长会话中持续工作

前面几篇已经实现了 Agent 的基本循环,可以和用户多轮对话,也可以调用工具、读取文件、执行命令,再根据工具结果继续处理任务。

对于一般的任务,这个Agent已经很不错,可以帮助我们完成任务,提高工作效率。 如果要处理复杂任务,Agent需要持续多轮的推理,工具调用,随着会话变多,一个新的问题会逐渐出现,发送给模型的消息越来越多,最终会超过模型能够处理的上限。

这一篇内容我们把它分为两部分。

  • 第一部分介绍上下文窗口和上下文压缩的基本原理。
  • 第二部分再回到 mini-openclaw这个项目,结合源码说明压缩的触发时机、消息切分、摘要生成和上下文重建过程。
Image

第一部分:上下文压缩原理

什么是上下文窗口

每个大模型都有一个 上下文窗口(context window),它决定了模型在一次请求中最多能够处理多少 token。每当模型厂商推出新的模型的时候都会着重介绍他们的上下文窗口大小,上下文窗口越大,能够处理的内容就越多。

目前主流模型的上下文窗口已经普遍提升到 100 万 token 左右。例如:

  • DeepSeek V4 Flash / V4 Pro:1M,上限输出 384K
  • GPT-5.6:1.05M,上限输出 128K
  • Claude Sonnet 5:1M,上限输出 128K
  • Gemini 3.5 Flash:1,048,576 输入 token,上限输出 65,536 token

我们可以把一次模型调用上下文窗口理解成:

上下文占用 =system prompt+ user 消息+ assistant 历史消息+ tool definitions+ tool 调用结果+ 图片、文件等多模态内容+ 模型本轮生成的输出

也就是说,我们每次请求时传递的 messages、工具定义和文件内容,都会占用上下文空间。模型还需要为本轮输出预留一部分 token。

我们可以简单的理解成:

输入 token + 本轮输出 token ≤ 模型允许的总上下文长度

历史消息如果太多,本轮请求可能无法提交,被API拒绝回复,也可能没有足够空间生成完整回答。

输入占用太多之后,那么同样输出的空间就会被压缩。

Agent 每次调用模型时,都要关心这些问题:

  • 当前输入大约占用了多少 token
  • 工具定义需要占用多少token
  • 模型输出需要预留多少空间。

Agent 的上下文为什么增长得很快

大模型是没有状态的,每一次调用模型API,它都不会自动记住之前发生过什么。

每次调用模型时,Agent都要重新组装本轮对话需要的信息,把之前发声的历史消息全部添加到消息数组message里面

而且这个动作时累加的,Agent处理的任务越复杂,推理轮次,对话轮次越多这个消息就会逐步累加到模型的上下文中

Image

这个里面产生的消息,用户的输入和模型的输出,只能占用小部分的上下文窗口,更多的是工具调用产生的结果 例如,用户让 Agent 分析三个文件:

Image

这一轮对话里面,三个文件的完整内容通常比用户的问题占用更多上下文。

Agent 上下文中常见的大块内容包括:

  • 文件全文;
  • 搜索结果;
  • 命令行输出;
  • API 或 MCP 返回的 JSON;
  • 工具调用参数;
  • 模型的中间回复和推理内容。

这些内容在刚返回时很重要,因为模型要依靠它们继续判断。但任务进入下一阶段以后,模型可能只需要保留其中的结论,不再需要每次都读取原文。

上下文过长会带来什么问题

上下文超过模型上限后,API 请求会直接失败。在达到硬上限之前,过长的上下文也会影响速度、成本和回答质量。

Image

请求延迟增加

每一轮都要重新处理历史消息。历史消息越长,模型根据历史消息推理的时间就越长

调用成本增加

那么按照模型厂商的计费方式,token越多,每一轮请求的价格就越贵,历史消息不断累加,成本就会越来越贵

关键信息被稀释

用户的目标、限制条件和当前任务状态,可能被大量旧工具结果淹没。虽然模型还是能在历史消息中看到这些关键信息,但是在推理的时候,不一定会特别关注它们,消息太多了,模型的注意力也会被稀释。

输出空间减少

上下文窗口是有限的。输入占用越多,能够留给模型输出的空间就越少。

上下文管理的核心就是 既要避免请求超限,也要让模型在长任务中始终拿到一份适合继续工作的上下文。

常见的上下文管理方法

实际系统通常会组合使用以下几种方法处理长上下文。

滑动窗口

滑动窗口只保留最近 N 条消息,或者最近 N 个 token,较早的消息直接丢弃。

原始历史:M1 M2 M3 M4 M5 M6 M7 M8
保留结果:            M5 M6 M7 M8

这种方法实现简单,也不需要额外调用模型。由于它只看消息的位置,不理解消息的含义,用户最早提出的目标、约束和已经完成的工作可能一起被删掉。

工具结果截断

为什么需要对工具的结果进行截断,因为上下文是稀缺的,有的工具读取文件,获取网页,一次性返回太多的内容,但是这些内容并不是所有都是本轮任务所需要的,所以需要对工具返回的内容进行裁剪,通过计算返回内容长度,做一些特殊处理。例如:直接截断,保留开头和结尾,内容查找匹配,模型压缩等方式来减少工具结果注入上下文。

这样可以减慢上下文增长速度,但是不管怎么操作,截断本身就是有损的,关键内容可能恰好位于被删除的部分。它只能控制单条工具结果,不能解决多轮历史不断累积的问题。

摘要压缩

摘要压缩是把较早的消息交给模型总结,再用一段摘要替换这些原始消息,同时保留最近一段历史消息以控制上下文长度。

Image

这一过程虽为有损压缩,会丢失部分细粒度信息,但若摘要结构围绕“任务目标、已完成工作、当前状态、下一步计划”设计,则能有效留存执行脉络,尤其适合需长期连贯决策的 Agent 场景。

外部检索

外部检索将历史消息持久化至数据库或向量库,在需要时按需查询并取回相关片段。

该方式适合大规模历史记录和跨会话知识复用。但需注意,检索返回的通常是若干独立片段,难以完整还原任务从起始到当前的推进轨迹,它更适合作为知识补充手段,而非会话状态摘要的替代品。

方法对比

下表梳理了四种常见上下文管理策略的核心差异:

方法
模型调用
任务连续性
主要问题
滑动窗口
否
弱
早期目标和限制会直接丢失
工具结果截断
否
中
可能截掉关键原文
摘要压缩
是
强
摘要可能遗漏信息或逐渐失真
外部检索
视实现而定
中
检索片段不一定能还原连续状态

综合来看,单一方法各有短板,但可组合使用以取长补短。例如:先限制单次工具输出量,避免单轮溢出,对长会话进行摘要压缩以维持执行脉络,同时将跨会话的重要知识存入外部记忆,供后续检索增强。

这种组合策略能在控制成本的同时,兼顾连续性与知识覆盖面。

摘要应该保留什么

上下文压缩不能只是把前面的对话缩短一下,要让压缩后的内容还能支撑 Agent 继续执行任务。

比如下面这种摘要,虽然概括了之前聊过什么,但实际作用并不大:

用户和 Agent 讨论了上下文压缩,并查看了一些代码,研究了上下文压缩的实现机制。

问题在于,模型读完这段话,还是不知道现在要做什么。它不知道任务目标是什么,也不知道前面已经完成了哪些工作,更不知道当前卡在哪里、后面应该从哪一步接着做。

Agent 的上下文摘要不能只记录 之前发生过什么,而应该保留任务继续执行所需要的状态。类似这样的一个摘要总结:

1. 任务目标
为 Agent 增加长会话上下文压缩能力。

2. 已完成
已经定位模型调用循环,并确认会话历史来自 JSONL 事件记录。

3. 当前进度
正在设计压缩切分点,需要保证 tool call 和 tool result 不会被拆开。

4. 后续工作
实现尾部消息选择,并将压缩结果重新写入会话历史。

我们在实际实现时,摘要内容不一定只有这四项。只要后续任务还会用到,就应该保留下来。例如:

  • 用户明确提出的要求和限制;
  • 正在修改的文件和代码路径;
  • 已经确定的技术方案;
  • 关键参数和配置;
  • 遇到的错误以及已经排除的原因;
  • 仍未完成的工作。

所以,上下文压缩并不是简单地对历史消息做一次总结。它更像是在任务执行到一半时,整理出一份交接记录:前面的详细对话可以删除,但任务目标、当前进度和接下来的执行线索必须保留下来。

保留最近消息

我们都知道摘要肯定会损失细节,这个是毋庸置疑的,压缩摘要执行的次数越多,细节就损失越多,而最近的消息通常与当前操作关系最紧密。

假设用户刚刚补充:

只改后端,不要动前端;文件名保持不变。

如果我们不保留最近的消息,把它压缩到摘要去,这个用户明确要求的规则,很可能就会丢失掉,特别是已经和Agent对话了一段时间,有大量历史信息的时候。

我们在实际处理时,通常只压缩较早的历史,最近一段仍然保留原文:

新的上下文 = 较早历史的摘要 + 最近一段原始消息

摘要负责保留长期任务状态,最近消息负责保留当前操作需要的精确内容。

最近的消息如何保留

压缩历史消息的时候,我们需要保留最近的历史消息,策略不同,我们使用的方法也不同,比如,保留最近3轮,最近10轮消息,这个很好处理

只需要找到用户消息和模型回复的消息,统计几轮进行裁剪就行,用户消息和模型消息中间的工具调用全部保留。

还有一中方式 不是指定最近几轮的消息,而是保留10% 左右的上下文token,这个就不是通过几轮对话来截取,而是计算token数量来估算截取的位置,这个时候有一个重要的点要做特别的处理,计算出来的位置,大概率在某一轮对话的中间位置,这里有2种方式,一种是之前往前提取更多的信息,到达这一轮对话的开始,另外一种就是直接截取,这个截取的话就需要处理工具调用的关系

工具调用消息存在协议上的对应关系:

assistant(tool_calls: call_123)
tool(tool_call_id: call_123)

如果切分点落在两条消息中间,保留区可能只有一条 tool 消息,却没有发起它的 assistant.tool_calls。有些模型 API 会直接拒绝这类消息,直接报错。

Image

选择压缩边界时,除了 token 数量,还要检查消息结构。从完整的用户轮次开始保留,并逐条验证工具结果与工具调用的对应关系,可以避免切出无效消息。