☰
Agent上下文压缩实战:三种方法与一整套可落地流水线
2026/10/14 22:27:56 网站建设 项目流程

做Agent开发的人迟早会撞上同一堵墙:上下文窗口又双叒叕不够用了。我最早是在一个模拟项目X里做客服助手,系统提示词、用户历史对话、工具返回结果堆在一起,还没聊几轮,上下文就顶到了窗口上限。后来开始做上下文工程,才慢慢理解压缩不是简单“删掉一些字”,而是要让Agent在有限的内存里尽量记住真正重要的事情。这篇文章聚焦上下文压缩方法,分享我实际用过的提取式、摘要式、结构式三条路线,以及一整套可以直接抄进代码里的压缩流水线。适合正在做Agent应用、被token长度和成本问题困扰的开发者,也适合刚入坑想了解上下文工程是怎么回事的新手。

1. 为什么上下文压缩成了Agent绕不开的坎

1.1 一个真实场景:系统提示词+历史对话+工具返回

先看一个最常见的Agent运行循环:接收用户问题,带上系统提示词和历史消息,调用大模型;模型觉得需要查数据,就发起工具调用;工具返回一段长长的JSON或数据库结果;模型再根据这些内容组织回答。问题就出在这个“长长”上面。

我遇到过这样一种情况:某个查询工具返回了3000多字的商品详情,包含规格参数、库存状态、物流配置、优惠策略。模型真正用来回答用户问题的只有“库存是否充足”和“价格区间”这两点,其余2900字全是干扰项。结果模型有时候会被无关字段带偏,回答里混进过时信息,甚至因为上下文里有大量重复的字段名,反而忽略了用户真正关心的参数。

这种情况下,上下文压缩要解决的不只是“塞不下”,还有“注意力被稀释”。窗口是有物理上限的,就算窗口够大,模型的注意力也会在超长上下文里衰减。你去做长文本问答就会明显感觉,等到中间位置的内容时,回答质量明显下降。这就是为什么压缩不是可选项,而是Agent工程里的必需品。

1.2 token账本:成本、延迟、准确率的三角关系

每个token都对应真金白银。我常用的一个模型,输入价格折算下来,1万token大概要几块钱。看起来不贵,但如果Agent每天处理几万次请求,历史对话平均3000token,那光重复传递历史消息的支出就很可观。更麻烦的是,每次请求都要把全部上下文从头处理一遍,上下文越长,首字延迟越高。我实测过一个长对话场景,上下文从2000token涨到8000token,首字响应时间从0.8秒涨到2.3秒,用户体验一下子就不一样了。

所以压缩是在同时优化三件事:省钱、降延迟、保准确率。但三件事之间存在拉扯。压缩太狠,token省了,关键信息丢了,准确率崩了;压缩太轻,准确率保住了,但延迟和成本没降下来。这个平衡点不是拍脑袋定的,需要根据具体场景去调。

我自己的习惯是先设定一个预算:这条上下文的token上限是多少,超过上限就必须压缩。比如把历史对话压缩到2000token以内,工具返回结果裁剪到800token以内,系统提示词压缩到500token以内。有了预算,压缩的目标就清晰了,不是“尽量压”,而是“在预算内保留最大信息量”。

1.3 压缩不等于傻删:与检索、缓存的关系

刚开始做压缩时,我误以为压缩就是原始的截断——只留最近几轮对话。结果用户问起三天前提到的一个需求,模型完全失忆。后来才意识到,压缩应该和检索、缓存配合起来。

我的理解是:缓存解决“重复计算”的问题,常用的系统提示词和工具定义可以缓存,不用每次都重新编码;检索解决“大海捞针”的问题,长历史对话里真正相关的片段通过向量检索捞出来;压缩解决“内存过载”的问题,把检索结果和不得不保留的信息压缩得更紧凑。三者是递进的关系,不是互相替代。

举个例子,用户的历史对话有200轮,我不会全部压缩成一篇摘要,因为摘要会损失细节。我会先用检索找出与当前问题最相关的10轮对话,再对这10轮做压缩,这样既保住了关键细节,又把长度压了下来。这个思路在后面的压缩流水线里会反复出现。

2. 三种主流压缩路线,到底该怎么选

2.1 提取式:黑白名单加重要性排序

提取式压缩是最容易理解的方式:从原始文本里挑出重要的句子、字段、片段,丢掉其余的。它的核心是“判断什么重要”。

实际操作起来有两类做法。一类是规则式:如果上下文里包含JSON,就只保留特定的key;如果包含日志,就只保留ERROR级别以上的行;如果包含对话,就只保留用户消息和最后的助手回复。这个做法稳定、可解释、不消耗额外的大模型调用,适合结构化的工具返回结果。

另一类是基于模型的抽取:把原始文本切成长度为100到200字的块,让模型给每个块打分,按分数从高到低取前面的块,拼成压缩后的上下文。这种方式适合非结构化的对话文本。

提取式的优点是不会凭空生成新信息,所以保真度高。缺点是碎片化严重,句子之间原有的逻辑关系会丢。比如两个连续的句子,前一句是“库存不足”,后一句是“预计三天后补货”,抽取式可能只留下前一句,后一句被丢掉,模型就会给出错误结论。

2.2 摘要式:用生成换空间

摘要式压缩让模型把大段文本重新组织成简短摘要。这是最常用,也是风险最大的方式。

好处是压缩率极高。一段5000字的对话可以压成200字的要点列表,信息密度大幅提升。但风险在于模型可能“脑补”,也就是摘要里出现原文没有的内容。我见过最离谱的一次,模型把“用户希望收到红色包装”摘要成“用户偏好红色包装且喜欢礼盒”,这个“礼盒”就是凭空出现的,直接导致后续推荐策略出错。

所以我用摘要式压缩有个前提:原文本身就是可容忍部分失真的场景,比如闲聊类的历史对话、固化的产品背景介绍。一旦涉及精确数字、状态标志、时间点这些关键字段,摘要式必须和提取式配合使用,或者直接改用结构化的键值对输出。

还有一个技巧:不要一次压一整篇,而是分层摘要。先把每一轮对话压成一句,再把若干句合并成一段摘要。这样能减少模型在超长输入下的幻觉概率,也方便后续做局部更新。

2.3 结构式:把上下文拆成可管理的块

结构式压缩是我后来重点研究的方向。思路是把上下文按照功能拆成独立的块,然后对每个块用不同的策略处理。

比如一个Agent的完整上下文可以拆成四块:系统提示词、工具说明、历史对话、当前任务的临时数据。系统提示词是高度稳定的,适合用固定模板加少量变量,几乎不需要压缩;工具说明可以用一句话概括加参数列表;历史对话则分成“最近几轮完整保留”和“更早的压缩成摘要”;临时数据只保留当前任务真正需要的字段。

这样做的好处是灵活。我可以在同一个压缩流程里,对系统提示词不做任何压缩,对工具返回结果做提取式压缩,对老对话做摘要式压缩。每一块都用最适合自己的方式,而不是整个上下文一刀切。

结构式压缩的难点在于“拆”这个动作本身需要逻辑清晰。我通常会在Agent初始化阶段就把上下文组织成字典结构,每个key对应一个块,压缩器按key分别处理,最后再拼接起来。这比处理一大段纯文本要可维护得多。

3. 一整套可落地的压缩流水线

3.1 第一层:规则裁剪,先做减法

我的压缩流程不是一上来就调大模型,而是先用规则把明显没用的东西剪掉。规则裁剪速度快、零成本、不引入幻觉,是必须做的基础层。

具体来说我会做这几件事:

  • 去掉多余的空白字符、重复的分隔符、空字段;
  • JSON和工具返回值只保留预先定义的必要key,其他key全部删除;
  • 日志只保留ERROR、WARN级别,INFO和DEBUG直接丢弃;
  • 对话消息里只保留文本内容,去掉多余的元数据;
  • 超过一定长度且不包含关键字的代码块直接截断。

规则裁剪的核心是维护一份“保留清单”。我会花时间去看工具返回的真实结构,搞清楚哪些字段会在后续决策里被真正用到,把它们列入白名单。这个工作很枯燥,但收益很大。我做过一个数据查询Agent,工具返回的JSON原本平均1200token,通过白名单裁剪后压到300token左右,准确率不但没降,反而因为去掉了干扰字段小幅上升。

3.2 第二层:用一个小模型做关键信息抽取

规则裁剪处理了结构化的部分,但对话文本和自然语言描述还需要更聪明的处理。这里我会用一个轻量级的小模型来做抽取,比如7B到14B量级的开源模型,或者API里更便宜的模型。

抽取任务我一般设计成这样:给模型一段对话和当前用户问题,让它输出一个JSON,包含三个字段:用户意图、关键实体、已确认的事实。用户意图用于保留对话中真正重要的需求;关键实体包括人名、订单号、型号、数量等,必须一字不差地保留;已确认的事实是对话里双方明确达成的结论。

举个例子,一段2000字的客服对话,用户先问了A产品,后来改主意要B产品,还问到了发票问题。抽取模型会输出:

{ "用户意图": "购买B产品并咨询开发票流程", "关键实体": ["A产品", "B产品", "发票"], "已确认的事实": ["用户最终选择B产品", "发票需在订单完成后申请"] }

这样原始2000字就压缩成了大概150字的结构化信息,而且保留了核心语义。我实测小模型做这个任务效果还不错,因为任务足够聚焦,不需要太多推理能力,只要忠实抽取即可。

3.3 第三层:按对话轮次做动态摘要

对于更早的历史对话,抽取出来的信息可能还不够,因为这些对话里可能藏着用户没有明说的偏好,比如语气、情绪、多次提到的需求。这些用抽取很难量化,更适合做摘要。

我会把历史对话按时间分成两个区域:最近N轮(通常N=3到5)完整保留,这部分是当前任务的直接上下文,压缩它反而会丢细节;更早的部分按会话(session)做摘要,每个会话输出一个结构化摘要,包含会话目标、关键事件、遗留问题。

动态摘要的做法是:当历史消息数量超过阈值时,触发一次摘要。摘要由大模型生成,生成后替换掉原始消息,而不是简单附加在后面。这个操作属于主动压缩,必须放在一个独立的方法里执行,不能在每次请求的路径上同步跑,否则会影响延迟。

这里要特别注意:摘要不是一劳永逸的。如果之后用户又问起一件早期对话中的具体事情,摘要里可能没有,那就需要回到原始日志里搜索。所以我在实现时会保留一份未经压缩的完整日志,只有Agent运行时才使用压缩后的版本。

3.4 压缩率与保真度的平衡经验

压缩率不是越高越好。我自己的经验是把整体压缩率控制在60%到75%之间,也就是压缩后token数是原来的25%到40%。这个区间内,保真度基本可控,模型能力下降不明显。一旦压缩率超过80%,也就是只剩原文20%以下,信息丢失的风险就急剧上升。

保真度怎么衡量?我通常用三类指标:实体保持率,也就是原文里的关键实体有多少还出现在压缩结果里;事件一致性,压缩后的摘要是否还保留原文的因果关系;任务完成率,跑一批固定测试问题,看Agent在压缩上下文下的成功率。前两个指标可以用脚本半自动对比,第三个必须人工跑。

我踩过的坑是单一追求token下降。有一次我把历史对话压成了总共不到500字的摘要,token节省了非常多,但用户后续问到一个之前提过的收货地址,Agent直接给出了一个编造的地址。原因就是摘要里没有保留地址这个实体,模型只能猜。从那以后,我把实体保持率作为不可突破的底线,一旦低于90%,就调整压缩策略。

4. 手写一个压缩器的核心实现

4.1 压缩Prompt模板,直接抄

下面这个模板是我在项目里反复迭代后定下来的,专门用于把一段长对话压缩成结构化摘要。它的特点是:明确要求保留实体,明确要求禁止脑补,明确输出格式。

你是一名上下文压缩器。以下是一段用户与助手的历史对话,以及当前需要回答的问题。 请把历史对话压缩成一份摘要,要求: 1. 只保留与当前问题相关的信息; 2. 关键实体(人名、订单号、型号、日期、金额、地址)必须原样保留,不能改写; 3. 不要补充原文不存在的信息; 4. 保留事实之间的因果关系,例如“因为...所以...”; 5. 输出格式为JSON,包含三个字段:summary(概述),entities(实体列表),facts(已确认事实列表)。 原始对话: {{history}} 当前问题: {{question}} 请直接输出JSON,不要输出其他文字。

这个模板有三个关键设计。第一,加入“当前问题”字段,压缩就有了方向,不会把无关紧要的寒暄也留进来。第二,单独列出entities字段,方便后面做实体保持率检查。第三,加了“不要补充原文不存在的信息”这条,虽然模型不一定百分百遵守,但能明显降低幻觉概率。

4.2 用函数调用把压缩器接进Agent

我建议把压缩器封装成一个独立的工具函数,由Agent在适当时机主动调用,而不是放在主链路里强制每次执行。这样能减少不必要的处理,也更灵活。

实现上有两种接法。一种是在Agent循环里加一个前置检查:在构造请求前,计算当前上下文token数,如果超过阈值,就调用压缩函数,把历史消息列表传进去,得到压缩后的摘要,替换掉旧消息。这种方式适合逻辑可控的Agent框架。

另一种是利用模型自身的函数调用能力,在系统提示词里告诉模型:“如果上下文即将超过限制,调用compress_context函数。”模型会自行决定是否压缩。这种方式更智能,但调试成本高,模型有时候会在不必要的时候触发压缩,或者在信息很重要的时候反而选择压缩,导致后续失败。

我目前更推荐第一种,因为它可控、可测试。关键代码如下(伪代码):

def maybe_compress(messages, max_tokens=2000): if count_tokens(messages) <= max_tokens: return messages history = messages[:-1] # 保留当前用户消息 compressed = compress_dialogue(history, current_question=messages[-1].content) return [{"role": "system", "content": "以下是压缩后的历史对话摘要:" + compressed}] + messages[-1:]

注意压缩后的消息我用了system角色,而不是直接塞进user消息,这样避免污染用户原始输入。同时原来的历史消息被替换成一条摘要消息,后续轮次继续累加。

4.3 压缩质量怎么评估,别只看token

评估压缩质量是我花了不少时间才建立体系的。我最开始只看压缩后token数,觉得压到很低就是成功,后来发现完全不是这么回事。一套可用的评估方法至少包含三部分。

第一是自动指标。准备一个测试集,包含原始对话和对应的关键信息列表,跑压缩器,然后用脚本检查关键信息覆盖率。重点检查实体是否原样保留,日期、金额、编号这类信息最容易出错。我写了一个简单的校验函数,把实体列表里每个词在压缩结果里做精确匹配,匹配率低于90%就报警。

第二是任务指标。在压缩后的上下文上运行Agent,跑真实任务并记录成功率。比如一个查询Agent,给它压缩后的上下文,看它能否正确回答5个问题,全对才算通过。这个指标反映的是压缩对最终结果的影响,比单纯的文本相似度更可信。

第三是人工抽检。每月抽几十条压缩结果,人工对比原文看是否有明显的信息扭曲。我遇到过实体都在,但事件关系被改反的情况,比如原文是“用户先退款后重新下单”,摘要变成“用户先下单后退款”,这样的事只能靠人工发现。

评估不是一次性工作,而是每次调整压缩策略后都要跑的回归测试。我把评估脚本做成命令行工具,改完Prompt直接跑一遍,很快就能看到效果。

5. 实战中踩过的坑和排查思路

5.1 实体丢失与人称混乱

实体丢失是最常见的问题。某个订单号在原文里出现了三次,摘要里一次都没有,Agent后续就编造了一个。排查这类问题时,我会把原始对话和压缩结果并排对比,一个个查实体是否都在。

另一个很容易被忽略的问题是代词歧义。有些压缩器为了简洁,会把“用户要求明天发货”缩成“要求明天发货”,结果到了模型那里,这个“要求”到底是谁提的就变得模棱两可。所以在Prompt里我会明确要求:“所有动作必须保留主语,不得省略发出者。”

如果你发现压缩结果经常丢实体,建议不要指望模型自觉。更可靠的办法是在提取层单独把实体列表拎出来,拼到压缩结果的最前面。即便摘要本身有缺失,实体列表也能兜底。

5.2 摘要幻觉比不压缩更危险

摘要幻觉是压缩带来的特有风险。原文说“库存暂时缺货”,摘要变成“库存准备中”,看似意思接近,但在一个下单Agent里,这个差异会导致完全不同的动作。更极端的情况是模型把两个不同事件拼在一起,制造出一个原文根本没有的结论。

应对幻觉我总结了几条经验:不要一次性让模型压缩超过3000token的内容,分段压缩;摘要输出必须基于原文,可以在Prompt里要求模型逐句对照原文检查;在Agent流程中,如果后续操作依赖某个关键事实,尽可能保留原文片段,而不是依赖摘要。

还有一招是给摘要内容带上置信度。我做过一个扩展,让模型在输出摘要时,对每一条事实给出“原文可见”或“推测”的标记。对于标记为推测的内容,Agent在决策时降低权重,需要求证时再回查原始日志。

5.3 这四种场景千万别强行压缩

不是所有上下文都适合压缩。我权衡之后,认为以下场景宁愿让上下文长一点,也不要压缩。

涉及金额、日期、地址、订单号等精确信息的任务,必须保留原始字段,压缩导致数字错一位就是事故。多步推理任务中,中间结果被摘要掉,后面的步骤就没法衔接,这类任务应该保留完整的推理链。处理用户情绪或敏感沟通时,压缩容易去掉语气信息,导致Agent显得冷漠或冒犯。当前正在执行的工具调用结果,不能压,因为下一步马上要用,一压就废。

遇到这些场景,我的策略不是压缩,而是“隔离”——把精确信息存成单独的变量,决策时直接读取变量,不放进上下文文本里;把推理链拆成多步,每步使用独立的小上下文;工具调用结果做成结构化对象,需要哪个字段取哪个字段。这些方法能避开压缩风险,又不会撑爆窗口。

5.4 一些经验数据与使用建议

最后分享一组我在模拟项目X里实测的数据。有一个用户支持Agent,原来历史对话平均8500token,通过分层压缩后,平均降到2100token,压缩率75%。首字响应时间从1.9秒降到1.1秒,单次请求成本降低约70%。任务成功率从94.3%下降到92.8%,但在实体保持率被专项优化后,回升到96.1%,对比未压缩版本反而略高。

这组数据说明一件事:压缩带来的准确率下降不一定是必然的,因为上下文变短后,模型注意力更集中,反而能表现更好。关键在于压缩要保得住真正重要的信息。

给正在做上下文工程的朋友一个建议:起步阶段不要一上来就追求极致压缩。先把规则裁剪做好,把白名单列清楚,把保存原始日志的机制搭好,再逐步引入模型摘要。每一步都加评估,确认没有明显回退再进入下一步。上下文压缩不是一个独立模块,它是Agent整个记忆系统的一部分,需要和缓存、检索、日志一起设计。

我个人在实际操作中的体会是,压缩器的核心不是技巧,而是取舍。你得清楚每个token背后对应的业务价值,才能决定什么该留、什么该丢。多跑几轮真实业务场景,多看几次压缩结果和原始对话的差异,比埋头调Prompt参数有用得多。最后再分享一个小技巧:给压缩器单独建一个日志,记录每次压缩前后的内容片段。刚开始觉得麻烦,后来排查问题全靠它,能省下大量猜测的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询