上下文窗口总是不够用?79.2K Star项目用Prompt压缩省掉98%Token

发布时间:2026/9/24 20:45:41
上下文窗口总是不够用?79.2K Star项目用Prompt压缩省掉98%Token 做 Agent 开发最糟心的不是模型不够聪明而是上下文窗口动不动就满。你先跑几轮工具调用再塞几段文档模型转头就忘了前面聊了什么轻则答非所问重则直接给你抛出一个上下文超长的报错。我前段时间在 GitHub 上翻到一个 79.2K Star 的项目专门治这个病它能在回答之前先把你的 Prompt 压缩一遍官方展示的数字是最高省掉 98% 的 token。这个工具不是简单地截断文本而是用小模型把长上下文重新提炼成关键信息再喂给大模型。这篇文章我就把这个工具的来龙去脉、工作原理、复现步骤以及几个我在接入真实项目时踩过的坑完整地过一遍。1. 项目概述与核心需求解析1.1 先说说那个让人血压飙升的“上下文已满”你肯定遇到过这种情况一个多轮 Agent 跑到第 8 轮前面调了三次工具、返回了一大段 JSON你接着还想让它分析一下这份结果。结果请求刚发出去API 直接告诉你 context length exceeded。更隐蔽的是另一种情况模型不报错但已经开始牛头不对马嘴——你问的是 A 产品的数据它回答的是 B 产品的结论因为它早把前面几轮的细节忘干净了。为什么会出现这种问题因为上下文窗口本质上是个有限的工作台。你把一堆历史消息、工具返回值、检索出来的文档片段全都堆上去模型能同时关注到的信息量是有限的先放进去的内容会被后面的内容“挤”出注意力范围。学术界管这个叫 lost in the middle意思是模型对长输入中间部分的利用率断崖式下跌越长的上下文越容易出现这种情况。就算模型性能还撑得住成本也撑不住。现在的 API 基本都是按 token 计费输入和输出都算钱。一个 10k token 的 Prompt 如果被反复调用几十次一天下来账单数字会非常难看。所以我现在做 AI 应用第一原则已经不是“模型支不支持长上下文”而是“能不能别把这些 token 喂进去”——省下来的每一个 token都是利润。1.2 它到底是个什么思路先压缩、再回答我这次要聊的工具跟平时理解的“对话总结”是两码事。对话总结通常是事后补救比如对话结束后把内容存成一个 summary下次再取出来用。而这个工具的思路是在推理之前就做一次信息提炼把即将发给大模型的 Prompt 先压缩一遍再让模型看到压缩后的版本。它训练了一个专门的压缩器。这个压缩器可以按照你给定的预算比如 500 token把一大段原始输入压缩成一段高信息密度的短文本。压缩后的文本再拼上任务指令一起喂给大模型。关键点在于压缩器是独立于大模型之外的它是一个小得多的模型负责“提炼”而不是“回答”所以跑起来又快又便宜。这种方案的适用场景非常明确长文档问答、多轮 Agent 对话、批量文本处理。你要是只写个一次性的小工具Prompt 总共也就几百 token那完全没必要压缩。但如果你在做知识库问答系统、客服机器人、自动编程 Agent折腾过一次上下文爆炸之后就会明白这个思路有多值钱。这篇内容适合所有被 token 账单和上下文溢出折磨过的 AI 应用开发者也适合想搞懂“上下文工程”这个概念到底在解决什么问题的人。2. 核心原理拆解为什么一张提示词能省掉 98% 的 Token2.1 长上下文里到底有多少“水分”我在做一个内部工具的时候用户会把一份 200 页的行业报告整个贴进来然后问第一页提到的某个指标。大模型在回答之前要把 200 页全都读完但实际上对答案有帮助的可能只有那个数字加上旁边一小段解释。你算算这个信息密度有多低。长上下文里的“水分”主要有四类。第一类是过程性内容Agent 调工具的过程日志、中间结果、错误重试记录这些对最终答案往往没用但占的 token 特别多。第二类是重复信息用户复制粘贴时多带出来的段落、多轮对话里反复确认的内容、模型自己之前生成过的完整代码又被塞回上下文的。第三类是与当前目标无关的内容整篇文档里只有一小段和当前问题相关其他部分都是噪音。第四类是结构噪音各种格式标记、无意义换行、过长的引用开头。你把这四类水分挤一挤会发现一个残酷的事实真正有价值的信息可能只占原始上下文的 10% 到 20%。压缩工具做的事情就是把这个比例重新拉高让大模型把注意力集中在那 20% 上。我习惯把它类比成开会你是要看三个小时完整录像还是看一份三页纸的会议纪要录像当然信息最全但真到做决策的时候纪要反而更好用。2.2 压缩机制到底是怎么运作的这个工具的压缩器不是简单地从原文里挑几个句子拼一起它有两种工作模式分别对应不同的场景。抽取式压缩是在原文里挑选关键句子和短语基本保持原有措辞。这种方式的好处是不容易引入错误信息因为选出来的都是原话适合事实型任务比如文档问答。缺点是不够“狠”如果原文里没有一句能独立概括大意抽取出来的片段可能会比较碎。生成式压缩是小模型用摘要生成的方式把一段文本重新组织成更短的表达。信息密度更高往往能把几千 token 压缩到几百 token但代价是细节可能被丢掉压缩器如果理解错了某句话还会“脑补”出原文没有的内容。所以这种方式适合那些上下文里存在大量冗余、只需要保留整体语义的场景。这两种方式之上还有一个核心机制叫做预算控制。你可以显式指定压缩后的目标长度比如要求只能输出 200 token。压缩器会在预算内做取舍先把最重要的信息放进去省下空间再放次要信息。这个过程很像你朋友圈发九宫格照片位置有限你得先保证主角在里面然后才考虑配图。压缩器本身是怎么训练出来的项目采用的是蒸馏思路先用大模型把一批长 Prompt 压缩成理想结果生成一个“标准答案”数据集然后拿这个数据集去训练一个小模型让它学会模仿大模型提炼信息的方式。训练完成之后压缩器只需要理解“这段在讲什么”不需要真正完成任务所以底座模型不用太大。T5 系列、LongT5、mT5 这类 seq2seq 模型常被拿来当底座效果和速度都比较平衡。2.3 压缩比、准确率与成本的三方权衡官方宣传里那个最高省 98%别理解成任何场景都能到。这个数字通常是在极度冗余的输入上拿到的比如长对话历史、多文档聚合问答。我实际用下来不同压缩比对应的风险和收益有很大差异。压缩比例典型场景token 降幅风险等级50% - 70%日常对话历史、短文档省一半以上很低基本不影响效果80% - 90%长文档问答、Agent 历史省到原来的 1/5 到 1/10中等需要验证关键信息是否保留90% - 98%高冗余日志、多轮工具调用记录省到只剩零头偏高必须有测试支撑我判断压缩比合不合适的标准很简单压缩前模型能答对的问题压缩后还答不答得对。拿你自己的真实数据先压二十条对比一下输出质量再决定把压缩比定到哪一档。别一上来就追求极限压缩省下的 token 可能还不够弥补模型答错一次带来的返工成本。3. 实操复现把压缩器接到你自己的项目里3.1 环境准备与最小项目结构动手之前先说需求。Python 3.10 以上PyTorch 和 transformers 这两个库是必须的。有显卡最好没有的话纯 CPU 也能跑只是压缩几千 token 的文本时可能需要多等一两秒。项目仓库在 GitHub 上直接搜工具名就能找到clone 下来之后先装依赖然后会从 Hugging Face 拉取压缩器的模型权重首次运行要下载几百 MB 到几个 GB 不等属于正常现象。复现流程我来拆成四步。第一步clone 官方仓库安装 requirements.txt 里的依赖第二步把你要压缩的内容整理成纯文本格式建议先用一小段对话历史或者长文档试水第三步加载压缩器跑一个压缩函数观察输出结果和 token 数量变化第四步在真实场景里反复调整预算参数找到适合你任务的压缩比。整个过程大概一两个小时就能跑通。3.2 跑通一个最小示例先看一眼 token 是怎么降下来的为了把链路讲清楚这里先用 t5-small 做一个演示。正式复现时你可以换成仓库 release 出来的压缩 checkpoint但这一段的目的是理解“压缩→替换”这个流程本身。from transformers import AutoTokenizer, AutoModelForSeq2SeqLM # 正式复现时把这里换成项目仓库提供的压缩模型权重 compress_tokenizer AutoTokenizer.from_pretrained(t5-small) compress_model AutoModelForSeq2SeqLM.from_pretrained(t5-small) long_text 我们是一家人工智能初创公司正在做企业级知识库问答系统。最近我们在测试不同的向量检索方案 遇到了几个问题。第一个问题是召回率不够稳定同样的 query 在不同数据集上表现差异很大 第二个问题是中文分词的边界情况比如“南京市长江大桥”这类歧义第三个问题是多模态数据 比如 PDF 里的图表信息很难统一进向量库。目前我们使用的是开源的 Embedding 模型效果还算可以 但是长文档场景下往往需要把整个段落都塞进上下文才能回答准确。 def compress_for_context(text: str, budget: int 128) - str: inputs compress_tokenizer( text, return_tensorspt, truncationTrue, max_length1024, ) outputs compress_model.generate( **inputs, max_new_tokensbudget, num_beams2, do_sampleFalse, ) return compress_tokenizer.decode(outputs[0], skip_special_tokensTrue) result compress_for_context(long_text) print(原始文本 token 数:, len(compress_tokenizer.encode(long_text))) print(压缩后 token 数:, len(compress_tokenizer.encode(result))) print(压缩后文本:, result)这段代码跑完之后你会看到原始文本的 token 数量和压缩后的 token 数量。像上面这种只有几百 token 的短文本压缩空间不大可能只省个两三成。但如果你换成几千 token 的长文档或者几十轮对话历史同样是生成 128 个 token 的摘要省掉的量就非常可观了。需要提醒一点t5-small 对中文的支持比较一般这段演示代码跑出来的中文摘要只能算“能看懂”离高质量压缩还有距离。真实中文场景建议换成中文摘要模型或者直接用官方压缩器对英文内容做压缩。中文压缩这块我后面会在常见问题里详细说。3.3 在 Agent 工作流里加一个“自动压缩阀”单独压一段文本价值有限真正实用的是把它接到 Agent 的循环里。我现在的做法是在每次调用大模型之前先估算当前消息列表的总 token 数超过阈值就触发压缩把最早的一批非系统消息压缩成摘要然后继续跑。# 伪代码达到阈值就压缩最旧的历史 def trim_context(messages, compressor, max_tokens6000): # 1. 分离出不能压缩的系统指令和工具 schema fixed [m for m in messages if m[role] system] history [m for m in messages if m[role] ! system] # 2. 从最旧开始批量取出要被压缩的片段 while estimate_tokens(history) estimate_tokens(fixed) max_tokens: batch history[:4] # 每次拿最早 4 条 original_text \n.join( f{m[role]}: {m[content]} for m in batch ) summary compressor.compress(original_text, budget200) # 3. 用一条“摘要记录”替代这批原始消息 history [ {role: assistant, content: f[此前内容摘要] {summary}} ] history[4:] return fixed history这里有几个设计决策值得展开。第一个是触发阈值的设定我习惯在预算用到 60% 到 70% 的时候就触发压缩别等快满才动手。真等到上下文报错的时候压缩器能保留的信息已经不完整了。第二个是每次压缩多少条拿太多了摘要会丢细节拿太少又需要反复调用压缩器增加整体延迟。我试下来每次压 4 到 6 条比较平衡。第三个是哪些内容坚决不压缩系统指令、用户明确提出的输出格式要求、工具调用的 schema这些必须原样保留否则模型行为会漂移。4. 踩坑复盘压缩工具在真实场景里的问题与排查4.1 压缩后答案质量下降先别急着骂工具我一开始用压缩器也遇到过效果变差的情况当时第一反应是压缩策略有问题。后来排查才发现问题往往出在“关键信息被意外丢掉”这件事上。有一次我压了一段多轮对话摘要文本本身很通顺但把用户最开始指定的输出格式丢了导致后续 Agent 按错误格式生成了很久最后返工浪费的时间远大于省下的 token。现在排查压缩质量问题我会按下面这个顺序来。第一步直接读压缩结果检查有没有丢实体、数字、条件。这个最笨但最有效一眼就能看出问题。第二步把压缩比从 90% 降到 70% 再试很多时候不是压缩器不好是预算定得太紧关键的限定条件被挤掉了。第三步对关键片段做豁免在压缩前把含有用户明确要求、关键数字、文件名的段落单独抽出来不参与压缩。第四步如果效果还是不行就考虑换模式从生成式压缩切到抽取式压缩或者针对你的领域数据微调一下压缩器。4.2 延迟、成本与收益到底怎么算压缩器本身也有延迟它不是免费的。我实测下来在 GPU 上压缩 1k 到 3k token 的文本大约需要 0.1 到 0.3 秒CPU 上会慢一些可能要 1 到 2 秒。这个延迟换来的是一次大模型调用中 80% 以上的输入 token 被消掉。方案输入 token主要延迟来源一次调用输入成本近似直接喂原始 10k10000大模型处理 10k 输入高先压缩再喂压缩器读 10k 大模型只读 200压缩器 0.3 - 1 秒低约省 95% 输入费用结论很直接对于单次短 Prompt不要折腾压缩省不了多少还平白多一次延迟对于反复调用的 Agent、批量处理任务压缩带来的收益非常可观。还有一个经验是把压缩器做成异步预压缩在等待大模型返回结果的时候就提前把下一轮要用的历史记录压缩好这样用户几乎感知不到压缩的延迟。4.3 GitHub Issues 里的高频坑这个项目里被问得最多的几个问题我也都踩过列一下给后来人避坑。中文支持差是最常见的。官方压缩器默认是在英文语料上训练的直接拿来压中文摘要质量会明显下降容易丢实体。解决思路是换用 mT5 这类多语言序列模型做底座或者在中文数据上做一轮微调效果会好很多。压缩代码和 JSON 结构是另一个大坑。压缩器重写文本之后代码缩进、括号配对、JSON 键名都可能被改掉模型拿到这种“被润色过的代码”反而读不懂。我的做法是压缩前把代码块和 JSON 片段提取出来用占位符替换压缩完再还原只让压缩器处理自然语言部分。还有版本兼容问题。transformers 版本太低的话加载官方权重的代码可能直接报错最好按照 requirements.txt 里锁定的版本装别用太老的环境跑。流式输出也要注意。压缩器是独立的大模型调用如果把它放在生成的主链路里同步执行第一个 token 迟迟出不来用户体验会非常差。正确做法是单独跨线程或者跨服务调用压缩器别让它阻塞主流程。最后分享一个实操体会。上下文压缩的本质不是把历史“丢掉”而是把它“转述”成更省空间的版本。最核心的一条经验是压缩之前先想清楚哪些信息绝对不能丢。你可以像前面说的那样在压缩器前面加一层规则把用户明确提到的需求、格式要求、关键数字单独摘出来剩下的才交给压缩器。这样既能享受最高省 98% 的红利又不会因为压得太狠把该留的信息压没。这套方案后续还能继续往“按查询相关性排序后压缩”的方向演进那就是另一个有意思的话题了。