☰
上下文压缩实战指南:从Token优化到长期记忆
2026/10/23 6:53:59 网站建设 项目流程

上个月我在处理一个客服机器人项目时踩了个坑:用户和机器人来回聊了四十多轮,系统把所有历史消息一股脑拼进提示词,结果单次请求的输入token直接逼近一万二。更让人头疼的是,模型并没有因为拿到这些历史而变聪明——前面确认过的规格型号,后面问到的时候照样答错。

也就是从那次开始,我认真研究起一项听起来朴素、做起来门道颇多的技术:上下文压缩。它的核心思路,是在大语言模型真正“读”之前,先把上下文里的水分挤掉,让模型减负增效——少烧token、少等时间、多抓住重点。文章后面我会从原理、技术路线、工程落地和踩坑几个维度,把近半年来实验、应用的经验完整拆一遍。适合做对话系统、RAG应用和大模型上层产品的人参考。

1. 长上下文不是万能解药:先算清楚三笔账

1.1 token成本账:长对话如何偷偷烧钱

大语言模型按token计费,输入和输出都算钱。很多人有个误解:只要模型支持128k上下文,那我就把全部历史传进去,反正窗口够大。问题是窗口大不等于费用低,更不等于效率高。

举一个具体例子。假设客服场景里,用户每轮消息约80个token,机器人回复约120个token,一轮就是200个token。聊到第50轮时,如果全量拼接,请求里自然包含前49轮约9800个token,再加上当前这条消息。真实产品里用户连续聊几百轮的情况很常见,这意味着每一轮请求都要重新发送越来越长的历史。累积下来,单日请求量一大,token费用就会膨胀得非常快。我在模拟项目里测过类似场景,全量历史拼接的费用大约是只保留最近5轮的6到8倍。

这里还要算上重试成本。一旦历史过长导致模型超时或者输出被截断,整个请求作废,这一大坨历史token就白烧了。上下文压缩的价值首先体现在这里:用少得多的token,装下对当前任务最有用的信息。

1.2 延迟与KV cache膨胀:上下文越长,首字节越晚

模型生成回答的延迟,很大一部分取决于“读入上下文”的时间。Transformer结构下,每个token在计算注意力时都要和所有历史token交互。虽然实际推理时靠KV cache做了加速,但历史token的Key和Value都要缓存在显存里。上下文翻倍,KV cache显存占用基本翻倍;上下文达到100k级别时,显存占用往往会超过模型参数本身带来的压力。

我针对本地部署的模型做过一次测量:把输入从2k增加到30k,首token返回时间从0.8秒涨到7秒多。这还只是单条请求。生产环境一旦并发上来,GPU的显存和算力被长上下文大量蚕食,整个服务的吞吐量会明显下降。用户感知到的是“回答变慢”,背后其实是上下文膨胀在拖累推理链路。这类问题在应用层已经加了别的优化也无济于事,瓶颈就在输入长度上。

所以做上下文压缩时,不要只盯着一时的token账单,还要考虑服务端压力和用户可感知的延迟。压缩做得好的系统,token少,延迟低,吞吐也友好,这属于一次改造同时收益多个指标。

1.3 注意力稀释:“看不见”的关键信息

长上下文还有一个容易被忽略的问题:模型对上下文的关注是有限的。注意力机制最后要经过softmax归一化,当序列里塞入大量冗余token时,真正重要信息得到的注意力权重会被摊薄。尤其那些位于长文本中间位置的信息,模型经常“视而不见”。

我在实践中碰到过:把一份产品使用手册全文塞给模型,问题问的是第18页才出现的某个参数,模型反而答得不好;把手册先压缩成两三百字的要点,同一个参数问题它一次就答对。这说明“减负”的另一个意义:不是窗口不够大,而是窗口里噪音太多,模型没法聚焦。

想判断自己的项目是否需要上下文压缩,可以先看三个信号:历史token经常超过窗口的一半;模型回答开始忽略早前轮次里的关键信息;接口费用随对话轮数快速上升。中了任意两条,就值得在上压缩方案。

2. 上下文压缩的四条技术主线

2.1 提示词级压缩:零改造的权宜之计

第一类做法不引入任何新模型,只靠提示词让模型自己“边压缩边回答”。典型做法是在system prompt里加一句:“请先阅读对话历史,忽略闲聊和重复信息,提炼出与用户当前问题相关的背景,再作答。”

优点显而易见:实现成本几乎为零,任何调用大模型接口的项目都能立刻用上。缺点同样明显:压缩效果不稳定,模型可能对指令“理解不到位”;而且每一次请求都让模型多干一步“提炼”的活,输出token可能变多,费用不一定下降。

更重要的是,这类方法解决不了KV cache膨胀和服务端延迟问题,因为原始历史还是完整传给了模型。它只面向输出端的“精炼”,对系统底层的压力没有任何帮助。我的建议是:只把它当临时应急方案,用来快速验证压缩思路是否可行,不适合作为生产环境的长期方案。

2.2 摘要式上下文压缩:让模型给历史“做笔记”

第二类是最常见、也是最推荐先从它入手的方案:在对话过程中周期性调用大模型,把已经结束的若干轮历史生成摘要,下一轮请求用摘要替换原始文本。

工程上通常叫“滚动摘要”。我建议每隔固定轮数或token阈值触发一次压缩。比如每5轮,或历史累到3000 token时。摘要里要分类保存:用户的基本属性、明确的偏好、待办事项、已经确认的事实、仍悬而未决的问题。不是单纯让模型概括一遍,而是要生成结构化内容。

我常用的压缩提示词大致长这样:

你是一个对话摘要引擎。请阅读以下对话历史,生成结构化摘要。 要求: 1. 保留所有数字、日期、金额、否定词、专有名词; 2. 把代词替换为具体实体,例如“他说的那个方案”改写为“用户提到的方案B”; 3. 按三类组织内容:用户画像、已确认事实、待办事项; 4. 不要输出无关内容。

这段提示词里的关键不是“摘要”两个字,而是“结构化”“三类组织”“代词替换”这些约束。它们直接针对后面要讲的几个坑:数字丢失、指代混乱、信息偏科。

摘要式压缩的优点是与模型无关、任何产品都能接;缺点是需要维护压缩触发逻辑,并且摘要本身也是一次模型调用,有额外延迟。这个延迟可以在工程上异步化处理,后面会展开。

2.3 KV cache压缩:在系统层给模型“物理瘦身”

第三类绕开文本层,直接在推理的KV cache上做压缩。KV cache是Transformer解码时缓存下来的中间表示,可以理解为模型对上下文的一种“编码后的记忆”。对它做剪枝、量化或淘汰,能显著降低显存占用和延迟,而且对上层应用完全透明。

比较常见的思路有两类。一类是量化,把KV cache从fp16压到int8甚至int4,显存直接减半甚至更多,对精度的影响在多数场景下可控。另一类是token淘汰,根据注意力分数保留重要token、丢弃不重要的token。

做token淘汰时有一个藏在细节里的坑:无论如何不能把所有早期token都删掉,尤其是序列的第一个token。因为注意力计算有“关注起始符”的倾向,把它的KV删掉后,模型输出质量会明显劣化。很多推理框架在实现时会给这部分token开“免死金牌”。

KV cache压缩属于推理框架和模型服务层的工作,普通应用开发者一般不直接写这部分代码。但理解它有助于定位问题:当你在文本层做了压缩,延迟依然偏高时,瓶颈很可能在KV cache层,而不是提示词层。

2.4 专用压缩模型:把上下文“翻译成自己的语言”

第四类是训练专门的模型做压缩,压缩结果不再要求是人类可读的文本。有些研究会让小型模型读取长上下文,输出一串“软提示”(压缩后的向量或伪token),下游大模型再基于这些软提示生成答案。

这种做法的理论上限最高,因为压缩表示能保留许多文本摘要说不清的细节。但工程复杂度也最高,需要对模型结构做改造或微调,不是搭好接口就能用。目前更适合研究型和实验型项目。业务系统现阶段不建议碰这条路线,等技术沉淀成成熟的库再考虑。

对我个人来说,前三条路线的落地顺序是:先用提示词级方案做验证,再切到摘要式压缩作为生产主线,同时观察KV cache层的状态。专用压缩模型保持关注,但不作为近期依赖。

3. 实战选型:不同场景下该用哪种压缩

3.1 多轮对话:热窗口加冷摘要的双层结构

多轮对话场景我直接推荐“热窗口+冷摘要”双层结构。热窗口指最近几轮对话保留原文;冷摘要指更早的历史压缩成摘要。热窗口解决“即时上下文”问题——用户刚才说的内容、语气、措辞都要保留;冷摘要解决“长期记忆”问题——用户三天前提过的偏好、承诺过的事项不能丢。

我在模拟对话系统里常用这组参数:

  • 热窗口保留最近8轮原文;
  • 当历史总token超过6000时触发一次冷摘要压缩;
  • 每次压缩只处理“热窗口之外、上一次压缩点之后”的新增历史,避免重复压缩。

这套结构稳定跑过两个月,输入token总量比全量拼接减少60%以上,同类问题的答对率反而提升了。原因不复杂:模型不再被几十轮重复寒暄分散注意力,可以把资源集中在真正相关的信息上。

3.2 RAG与文档问答:检索后的句子级精炼

RAG场景的上下文冗余更明显。检索器一次返回5到10个段落,每段少说几百字,拼在一起可能三四千token,但真正回答问题往往只需要一两句话。直接全部塞给模型,等于让模型再经历一次注意力稀释。

我在RAG链路里通常做三步:召回、重排序、段落压缩。重排序之后,再对入选段落做一次“句子级精炼”,用轻量模型或规则提取关键句。对同一个问题的多个段落,优先保留包含关键实体和数字的句子。这样进入Prompt的不是“相关段落”,而是“可信证据”。

有一个收益容易被忽略:压缩后的段落里如果统一保留来源标记,比如“[文档A-第3节]”,模型回答出错时排查会快很多。这在压缩链路里算小细节,但长期维护价值很高。

3.3 离线摘要与批量处理:先分层再压

遇到一次性处理超长文档的场景,比如几十页合同、论文或会议纪要,我推荐Map-Reduce式的分层压缩:先把文档切成若干块,每块生成局部摘要;再把局部摘要拼接起来,生成全局摘要。不要企图让模型一口吃掉全文。

这样做的好处是每一层上下文都在可控范围内,压缩质量稳定。批量场景不太关心延迟,因此压缩参数可以调得更激进,允许模型多花一些输出token生成更详细摘要,为后续下游任务保存尽可能多的信息。分层压出来的摘要,层级之间的包含关系也更清晰,后续做事实核验时能找到源头。

3.4 三类场景的组合策略

这里我把自己的选型经验整理成一个表格,方便对不同场景做快速决策。

场景推荐方案核心目标预估Token节省
多轮对话热窗口+冷摘要保留语义,控制成本50%~70%
RAG问答重排序+句子级压缩聚焦证据,减少噪音40%~60%
离线长文档分层Map-Reduce摘要信息保全,结构清晰80%以上

表格里的数值是基于我测试项目得到的经验范围,不是通用承诺。真正落地时还要看模型能力、业务对信息完整度的要求。但大方向是明确的:压缩不是一刀切,必须按场景调策略。

4. 落地实操:在业务系统里接入上下文压缩

4.1 会话状态的数据结构设计

压缩要有效,数据模型必须先改。我不建议把会话历史直接存成字符串数组,然后一股脑拼进Prompt。更好的做法是维护一个带角色的结构化状态。

我用一段简化JSON示意:

{ "session_id": "sess_10001", "user_profile": { "name": "某用户", "preference": "倾向性价比高的型号", "budget": "约3000元" }, "hot_history": [ {"role": "user", "content": "我想要红色那款"}, {"role": "assistant", "content": "好的,红色那款目前有现货"} ], "cold_summary": "用户在3月10日咨询过A型号,确认预算约3000元;3月12日反馈B型号缺货,转向考虑C型号。", "last_summary_at": "上次压缩完成的时间点" }

这里的user_profile是从摘要中单独抽取出的高频稳定信息;hot_history保留原文;cold_summary是早期历史的压缩产物。组装Prompt时,按“user_profile + cold_summary + hot_history”的顺序拼接即可。

数据结构本身不复杂,但它是所有压缩方案的落点。没有这个结构,摘要生成得再好也无处安放;有了它,后续的增量更新、检索、溯源都变得清晰。

4.2 压缩触发与增量更新

压缩既不应当每轮都做,也不应等历史堆到天量才做。我建议同时设两个阈值:轮数阈值和token阈值。轮数好理解,token阈值则通过统计当前上下文总长度得到。比如达到6000 token就触发一次。

触发时,只压缩“上次总结点之后新增的部分”,老摘要保持不变。这个增量策略能避免一个经典问题:每次压缩都对全部历史重算,导致同一份信息被反复压缩、失真不断累积。

压缩任务的执行建议放到异步链路里,由单独worker完成,并把结果缓存到Redis或内存。原因很简单:压缩本身也是一次模型调用,如果把它放到用户请求的同步链路上,一次压缩可能额外增加1到3秒延迟,用户会明显感到卡顿。异步压缩把成本挪到请求间隙,用户下一轮对话时拿到的是已经压缩好的上下文。

4.3 压缩效果评估:用两类指标把关

压缩做得好不好,不能只凭感觉。我习惯用两个指标来判断:

一是压缩率,定义为压缩后token数除以原始token数。比如原始3000token、压缩后800token,压缩率约27%,相当于节省73%。

二是信息保留率。这个指标更难自动化,但更不能省。我的做法是:抽出一批真实历史对话,人工列出“必须保留的关键点”,压缩后再人工核对这些关键点是否还在。关键点包括数字、时间、否定词、实体名、明确的偏好和承诺。

这两个指标要一起看。压缩率很高但信息保留率很低,等于把上下文压缩成了“正确但没用”的废话;信息保留率很高但压缩率很低,说明方案只是换了种说法,没有真正减负。上线前至少拿二三十条真实业务对话做一次双向对比,心里才有底。

5. 踩坑经历与质量兜底策略

5.1 数字、否定词是怎么悄悄丢掉的

我在一个售后场景里吃过亏。用户说“这个订单超过30天,但你们答应过可以退”,摘要模型把它压成了“用户咨询退货政策”。后续生成回答时,模型以为用户只是随便问问,回复变成了“以页面显示为准”。

问题的根源在于:通用摘要任务天然偏向保留观点、丢失细节,而业务判断恰恰依赖数字、时限、否定词这些细节。兜底方案有两种:一是在摘要提示词里显式要求“所有数字、金额、日期、期限、否定表述必须原样保留”;二是加一层规则扫描,压缩完成后用正则从原文中把数值、否定词提取出来,发现摘要缺失就自动补进去。

我两种都上了,而且规则扫描的优先级更高。因为它不依赖模型状态,任何时候都能兜底。这类问题一旦出现,直接影响业务正确性,光靠提示词是没有百分之百保障的。

5.2 指代混乱:压缩之后,“他”到底是谁

多轮对话里充满代词:“他说的那个方案”“那不行的”“这家店”。这些词在原文里有明确指代对象,但压缩成摘要后,指代关系经常断掉。模型拿到摘要时看到的是一句“用户说那不行的”,根本不知道“那”指的是什么。

解决这个问题没有捷径,只能在摘要生成时要求“把代词替换为具体实体”。例如把“他说的那个方案”改写成“用户提到A客服推荐的方案B”。更保险的做法是维护一张“最近指代映射表”:每次摘要生成后,用规则脚本扫描摘要和原文,把高置信度的实体指代关系补回摘要。

这一步比较费功夫,但值得做。我在项目里测试,加上代词替换要求后,多轮问答的准确率提升了明显一截。原因是模型终于不用自己猜指代关系了。

5.3 重复压缩的信息雪崩

最隐蔽的坑是“重复压缩”。如果压缩总在上一次摘要的基础上再次生成新摘要,信息是逐级衰减的。第一次摘要可能保留90%的关键信息;第二次在摘要基础上压缩,可能只保留第一次的80%;第三次再压就只剩六成多了。这不是模型能力问题,而是链路设计问题。

我的对策是:永远让压缩器读取“原文区间”,而不是读取“上一次的摘要”。具体做法是保留原始消息日志,压缩时只针对新增的原始片段;老摘要仅作为生成摘要时的参考背景,不进入被压缩对象。

这样做之后,每一段信息最多被压缩一次,信息损失可控。把这句话刻在工程文档里:“摘要永远从原文生成,而不是从摘要再生成。”

6. 压缩不是妥协,是长期记忆的必需品

6.1 上下文窗口再大,终究有装不下的一天

有人会觉得,等模型上下文窗口发展到几百万token,上下文压缩就该退休了。我的看法相反:窗口越大,压缩越重要。理由很简单,对话是无限长的,窗口再大也只能容纳一个固定长度的切片;而对某一轮请求来说,最有价值的信息可能横跨数月。

更合理的定位是:把大窗口当成“临时缓冲”,把压缩得到的摘要当成“长期记忆”。大窗口负责接下一次检索回来的大量内容,压缩负责把必须长期保留的内容沉淀下来。二者不是替代关系,而是前后级关系。

6.2 当压缩与检索、记忆协同起来

再往后看一步,上下文压缩会成为记忆系统的基础组件。我在模拟项目里已经尝试把摘要结果同时写入两个位置:一个用于下次Prompt拼装的短时记忆区,一个用于向量检索的长时记忆库。用户再次出现时,系统先检索相关历史摘要,再结合热窗口内容拼装上下文。压缩在这里既提高了检索效率,也控制了进入Prompt的信息总量。

我在实际使用中的体会是:上下文压缩从来不只是让长文本变短。它逼着你去思考一个更本质的问题——对当前这个任务来说,哪些信息是真正重要的?想清楚这一点,不仅省token、省延迟,更重要的是让模型把注意力放在它该放的地方。这才是“减负增效”四个字的真正含义。

最后再分享一个小技巧:任何压缩方案上线之前,先拿二三十条真实业务对话做一次“压缩前后对比测试”,把模型答案并排放在一起看。这一步花不了多少时间,但能帮你拦住大多数压缩质量事故。

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

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

立即咨询