做 RAG检索增强生成的同学应该都对这一套流程不陌生用户问一个问题我们把问题向量化去库里召回 top_k 条文档切片然后把这些切片一股脑塞给大模型生成答案。听起来很顺但长期做知识库问答、长文档分析的人一定踩过同一个坑——检索回来的文本往往又长又乱真正和问题相关的可能就一两句话其余全是背景铺垫、举例说明、免责声明。把这些噪音直接丢给大模型token 成本上去了回答质量反而下来了模型很容易被一堆无关信息带偏。Contextual Compression也就是“上下文压缩”就是来解决这个问题的。它不改变检索环节而是专门在“召回之后、生成之前”加一道工序先判断哪些内容真的和当前问题相关再把相关的部分压缩成更精简的片段最后才把压缩后的文本交给大模型。换句话讲检索器负责“把可能相关的找回来”压缩器负责“把真正有用的挑出来、说人话”。这篇文章我会从原理、代码到调优把我实际搭建上下文压缩链路时积累的经验完整梳理一遍适合正在做知识库问答、客服机器人或长文档分析且对 token 成本和答案质量有要求的开发者参考。1. 为什么 RAG 链路需要“上下文压缩”1.1 传统 RAG 检索链路的瓶颈常规 RAG 管道的设计思路是把检索当作一个“尽可能召回相关材料”的过程。召回阶段为了保证不高漏通常会设置一个比较宽松的相似度阈值或者直接固定取前 k 条。这个策略在召回阶段是合理的但问题在于检索器返回的单元是“文档切片”而不是“问题的答案”。一个切片是多大取决于切分策略短的几百 token长的上千 token。也就是说哪怕只有一条切片真正命中了用户的问题我们传送给生成模型的也可能是一整块长文本。我在某公司的内部知识库项目里就遇到过类似问题员工问“退款周期是几个工作日”系统召回的五条切片里有三条都是商品活动介绍只有一段提到了退款规则。如果直接把全部内容送进模型实测下来模型有时会把“赠品发放时间”和“退款时间”混在一起回答因为上下文里干扰信息太多注意力被分散了。这里涉及一个很核心的问题——大模型处理长上下文时不是“信息越多越好”。过多的冗余文本会稀释关键信息的注意力权重尤其是在开源模型或参数量较小的部署场景下这种稀释效应非常明显。与此同时每次请求的 token 成本也在线性上涨。如果你在做一个调用量大、业务又要求低延迟的问答系统这个问题会被放大到不得不解决的地步。1.2 上下文压缩的解决思路上下文压缩的核心思路可以用一句话概括把“召回的材料”变成“可用的证据”。它把压缩逻辑拆成两步。第一步是判断对召回来的每个切片做相关性评估把那些明显与当前问题无关的内容过滤掉第二步是精简对保留下来的相关切片只抽取其中真正表达核心信息的部分可能是几句关键句子也可能是一个简洁的摘要。这里有一个很容易被忽略的设计取舍压缩器的目标不只是“减少文本量”还要“保留证据感”。我在项目里对比过两类方案一类是让大模型把相关切片改写成一个概括性摘要另一类是让模型标出哪几句话与问题直接相关并保留原话。前者文本更短但生成阶段容易丢细节比如精确的数字、日期、专有名词后者虽然压缩率没那么高但答案生成时更有依据引用原文时也更可信。打个生活化的比方从图书馆借来一本五百页的书你要找的其实是关于某个城市房价变化的五页内容。传统 RAG 是把整本书复印给用户上下文压缩则是先翻目录定位章节再只复印关键页面甚至把其中的核心段落用荧光笔标出来。用户拿到手的是能直接用于决策的材料而不是一堆等着他自己翻找的信息。2. 压缩器内部是怎么工作的2.1 两类核心子组件过滤器与提取器我实际用下来的经验是一套可靠的上下文压缩方案通常由两类子组件组成相关片段过滤器和文本提取压缩器。两者职责不同缺一不可。相关片段过滤器负责从召回的切片中筛掉无用信息。它的实现方式很多最轻量的是用嵌入模型对“用户问题”和“每个候选切片”分别向量化计算相似度后把低于阈值的切片丢弃。这类方案的好处是速度快、可解释性强缺点是无法处理“相关性藏在原文的语境中”的情况。举个例子用户问“发货怎么变慢了”问题和一条切片表面没有太多词面重合但切片内容讲的是物流方仓库爆仓语义上其实高度相关。这种情况下仅靠嵌入相似度很容易误杀。文本提取压缩器则负责从保留的切片中提取或重写唯一对回答问题有用的句子。常见的做法是调用一个大模型让它阅读切片后提取所有与当前问题相关的句子也有用问答链做抽取式摘要的会告诉你“根据这个切片问题的答案是什么、支持这个答案的原文是什么”。这类方案更精准但代价是增加了模型调用耗时和成本。我通常会把两者串联起来用先用嵌入过滤器快速砍掉明显无关的切片再对剩余切片跑模型提取。这样做的好处是“粗筛”阶段很便宜能把召回数量从几十条降到三五条后续“精提”阶段即使调用大模型需要处理的文本量也大幅减少整体链路开销反而可控。2.2 数据流转全过程拆解为了把完整流程讲清楚我举个例子。假设我们的知识库里存了一批产品操作手册按每个主题章节做了切分切片大小大约 200 到 400 token。用户提问“如何重置设备密码”第一步检索器执行向量相似度检索从知识库中返回 8 个候选切片其中 3 个讲的是设备登录流程2 个讲的是固件更新2 个讲的是售后服务政策还有 1 个介绍了产品整体参数。第二步相关片段过滤器登场。把用户问题分别与 8 个切片算相似度设定阈值为 0.3。此时 4 个切片被过滤掉保留了固件更新相关的 2 个切片、参数介绍的 1 个切片以及登录流程相关的部分切片。第三步文本提取压缩器处理保留下来的切片。它逐个切片阅读内容并从中抽出“按住侧边按钮十秒”“在设置页选择重置并输入默认密码 1234”这样的关键句子。那些“固件更新日志见官网”之类的背景信息则被丢弃。第四步把提取出来的句子拼接成一段干净的文本传给生成模型。最终进入生成阶段的文本可能只有原始文本量的三分之一甚至更少但信息密度大幅提升。我统计过一个内部项目的数据切片压缩前的平均长度是 380 token压缩后平均长度降到 120 token 左右生成模型的答案得分反而提升了 11%这主要归功于移除了干扰信息。2.3 为什么压缩器不能“一把梭”到底另一个我在实际中反复体会到的细节是不能把“压缩”做成“二次总结”。如果单纯让大模型“用一句话概括这段内容”生成模型在理解问题之前也许已经丢失了关键信息。比较典型的是时间线、金额、编号这类数字信息它们容易在概括中被省略或改写。我踩过一次比较惨的坑某项目需要回答“上一财年华东区的销售总额”压缩器对相关切片做了摘要式改写结果把“去年营收 3.86 亿”改写成了“相比前年有较大增长”。下游回答虽然语句通顺但关键的金额数字丢了。后来我改用抽取式方案让压缩器把原文中和问题数字相关的句子原样保留问题才彻底解决。这个改动还带来一个额外好处答案生成时如果引用了切片原文做“检索内容引用标注”会非常自然因为证据链是完整的。3. 手把手搭建一个可用的上下文压缩链路3.1 核心组件与依赖准备如果你用的是业界常见的开源编排库后面我统一称它为 LC 库配套搭建上下文压缩链路会很快。为了不依赖任何闭源服务我们的目标是搭建一条完整的 RAG 压缩链路核心组件包括一个向量数据库存储文档切片索引一个嵌入模型用于计算问题与切片的相似度一个轻量分类或评分机制用于相关性过滤一个大模型实例既用于片段提取也用于最终答案生成LC 库环境用于串联检索器、压缩器、生成链路要提醒的是嵌入模型的选择直接影响过滤效果。我的建议是优先选择语义理解能力强、且对中文友好的向量模型而不是只追求速度的模型。因为在上下文压缩场景里嵌入相似度承担了“粗筛”的重任筛错了后面再怎么提取都补不回来。3.2 关键实现代码与参数选择下面这段代码可以看作一个最小可运行的上下文压缩链路骨架。它演示的是先初始化一个基础检索器再包装一个压缩器最后让压缩检索器在收到用户问题后返回压缩过的文档列表。from lc_lib.schema import Document from lc_lib.retrievers import ContextualCompressionRetriever from lc_lib.document_compressors import EmbeddingsFilter from lc_lib.document_compressors import LLMChainExtractor # 1. 初始化向量存储与基础检索器 embedder EmbeddingModel(your-embedding-model-name) vectorstore VectorStore.load(your-index-path, embedderembedder) base_retriever vectorstore.as_retriever(search_kwargs{k: 8}) # 2. 配置一个基于嵌入相似度的过滤器 emb_filter EmbeddingsFilter( embeddingsembedder, similarity_threshold0.3, k5 ) # 3. 配置一个大模型驱动的片段提取器 extractor_llm ChatModel(temperature0) llm_extractor LLMChainExtractor.from_llm(extractor_llm) # 4. 组装压缩管道 compressor_pipeline DocumentCompressorPipeline( transformers[emb_filter, llm_extractor] ) # 5. 用压缩器包装基础检索器 compression_retriever ContextualCompressionRetriever( base_compressorcompressor_pipeline, base_retrieverbase_retriever ) # 6. 实际查询 compressed_docs compression_retriever.get_relevant_documents( 如何重置设备密码 )这段代码里有几个参数值得展开讲。similarity_threshold0.3是嵌入过滤器的保留阈值含义是“只有当切片与问题的相似度高于 0.3 时才保留”。这是我经过大量实验后比较常用的一个起点值。阈值太高可能误杀语义相关但表述差异大的文档阈值太低又起不到过滤作用。我在不同业务上测试过0.25 到 0.4 之间是比较安全的区间需要结合嵌入模型自身的分数分布来调。k8是基础检索器的召回数量k5则是过滤后希望保留的切片数量。这里有一个小经验基础检索器的召回量可以适当放宽因为压缩层会做二次筛选。传统 RAG 为了省 token 不敢召回太多但在压缩链路上你可以先把召回量调大到 8 到 10 条再让压缩器筛选到 3 到 5 条。这样能在不增加生成阶段成本的前提下降低“相关文档没被召回”的风险。LLMChainExtractor内部的实现本质是将每个文档切片与大模型提示词组装起来要求模型抽取与问题最相关的句子。这里要格外注意temperature务必设置为 0。因为提取工作不允许有随机性模型一旦“自由发挥”就可能导致提取结果不稳定。3.3 从一份样例看压缩前后的效果差异为了直观说明压缩链路的收益我在模拟项目 X 里构造了一个测试样本。知识库中存入某产品的完整使用文档用户问题是“如何在订单完成后申请开发票”。未压缩时直接检索到的 8 条切片拼接起来约 3200 token其中混杂了产品介绍、退换货规则、会员积分说明等无关内容只有两条切片真正涉及开票流程。将这些文本全部传给生成模型最终答案中出现了退换货时限的描述明显是受到了干扰。使用上下文压缩后先经嵌入过滤器筛掉 3 条完全不相关的切片再经提取器保留与开票相关的原句最终进入生成模型的文本约 900 token。模型给出的答案准确点出了“订单完成后 48 小时内在订单详情页点击申请开票”这一关键信息并且没有夹杂任何无关内容。这个对比已经说明问题。上下文压缩不是简单地对文本做“删减”而是让下游生成模型专注在真正有用的信息上。对于追求高质量输出的 RAG 应用这一步几乎不能省。4. 调优经验与常见问题排查实录4.1 三个关键参数怎么调上下文压缩链路最核心的三个可调参数是召回数量 k、相似度阈值、提取器长度控制。我根据自己的实践整理了一套比较通用的参考值。召回数量 k建议控制在 6 到 12。k 太小会漏召回k 太大则压缩阶段的过滤压力陡增甚至因为候选切片太多导致过滤分数整体被拉低。如果检索阶段本身效果就不稳定可以适当调大 k但不要超过 20否则即使有压缩层也会引入过多“看似相关”的干扰。相似度阈值这是最容易调坏的一个参数。嵌入模型的分数分布在不同业务数据上完全不一样——有的知识库相似度普遍偏高平均都在 0.6 以上有的知识库内容风格差异大0.3 就算很高的相似度。我的建议是在拿到真实查询和切片后计算一批相似度分数看一下分布再定阈值。先粗筛看丢掉的全是无关文档还是误杀了关键文档再逐步调整。提取器长度控制我常用的配置是限制每次提取不超过 3 到 5 句话。如果提取内容超过这个长度说明这个切片本身信息混杂需要回看切分策略是否需要调整。提取器的目标不是把所有相关句子都找出来而是只找那些“足以支撑当前答案”的句子。4.2 高频踩坑压缩过头与上下文丢失上下文压缩最大的坑是“压过头”。特别是在使用大模型做片段提取时如果提示词写得不清晰模型可能会把多个切片的信息合并成一段概括导致实际生成的文本虽然流畅却丢失了关键专有名词和数字。我前面提到的“销售总额”案例就是典型。要避免这个问题我一直坚持一个原则提取器的输出应当尽量保留原文表达而不是让模型自由改写。落实到提示词上我会明确要求“请从原文中逐字摘抄与问题相关的连续句子禁止改写”。对中文场景还要注意一点模型摘抄时容易把引号错乱或吞掉句尾数字调试时可以定期抽查提取结果看一眼提取后的文本是否保留完整的数字和单位。另一个常见的坑是压缩后上下文仍然太长。有些开发者以为加了压缩器就万事大吉结果发现生成阶段的上下文还是动辄几千 token。这通常是因为过滤器的阈值设置得太低大量内容穿过了第一道粗筛。我的处理方式是两阶段配合嵌入过滤阶段宁可多杀几条也要把候选收窄提取阶段再尽量保留原文不要贪多求全。4.3 常见问题速查表我在表里整理了日常支持时被问到最多的问题这些都属于“压缩链路调一调就能解决”的类型。现象可能原因排查方向压缩后答案丢失关键数字提取器做了改写式压缩改用抽取式提取禁止模型改写原文过滤后上下文还是很长相似度阈值过低查看分数分布适当调高阈值或限制提取句子数相关文档被误杀嵌入模型语义理解能力不足更换更强的嵌入模型调低相似度阈值提取阶段响应太慢切片数量太多或切片过长先在过滤阶段减少切片限制提取器的输入长度答案比不压缩时还差压缩器破坏原文结构检查提示词要求逐字摘抄对照压缩前后文本如果排查完仍然异常我的一个笨办法是把压缩前后的文档差出来人工看一遍。压缩链路本身就是一个“输入可解释、输出可退溯”的环节保留日志、对比原文和压缩结果是调优最可靠的方式。从长远来看上下文压缩的价值会越来越大。随着大模型应用逐渐进入企业级场景不单是 token 成本生成质量、可解释性以及对知识库内容更新的适应能力都要求我们在检索之后、生成之前多一道“克制”的工序。我自己在搭建知识库问答系统的过程中把这条链路从一个简单的“检索拼接”演进到“检索过滤提取生成”每一步都踩了坑但每一步的收益都是可量化的。哪怕你的项目目前数据量不大我也建议提前把压缩环节纳入架构设计里。因为它不只影响成本更决定你的系统能不能在复杂知识场景里保持稳定可靠。
阅读完成 · 觉得有帮助?