我们用一个小模型,扔掉了 68% 的 RAG 上下文
我们用一个小模型扔掉了 68% 的 RAG 上下文在 kapa.ai我们每天要处理成千上万个来自 Docker、CircleCI、Reddit 等产品的技术问题。我们的 RAG 系统在检索阶段会捞回大量文档块但说实话——很多都是噪音。这篇文章讲讲我们怎么用一个小模型在答案生成之前先把垃圾扔掉。先说说我们的 RAG 长什么样我们的检索有好几种形式有些是 agentic 的有些是单次检索但本质上都是同一个模式用户提问 → 检索器捞文档块 → LLM 看这些块 → 写出答案检索器从文档库里捞出和问题相关的块然后生成模型拿这些块来写答案。看起来挺直接的对吧但这里藏着一个问题——只要你稍微多捞一点噪音就进来了。多捞是为了不遗漏关键信息这没错。但问题在于那些不相关的块并不会被模型无视——它们会污染上下文让模型分心甚至直接导致幻觉。传统的做法rerank业界比较成熟的做法是 rerank。在检索之后、生成之前加一个 reranker对捞回来的块重新排序把最相关的排前面然后截断只保留 top-k。Rerank 当然有用但它有一个根本性的限制它只能在块级别上做取舍。一个文档块可能几百个 token里面真正有用的可能就一两句话。rerank 要么全留要么全扔颗粒度太粗了。我们的做法用一个小模型做 pruning我们的思路不一样。我们放了一个小模型在检索器和生成器中间。这个小模型很便宜跑一次几乎不花钱。它的任务很简单读一遍问题再读一遍所有捞回来的块然后告诉生成器——这些句子留下那些扔掉。流程变成了这样用户提问 → 检索器捞文档块 → 小型 pruner 逐句筛选 → 只保留有用内容 → LLM 写答案关键点不是块级别的丢弃是句子级别的。一个块里可能三句话两句有用一句没用pruner 会把没用的那句扔掉而不是把整个块扔掉。pruner 模型本身很小很便宜和生成的 LLM 比起来几乎可以忽略不计。pruner 同时看到问题和所有块所以它能做全局判断——哪些内容真正对回答问题有帮助。我们用的模型Provence我们在生产里用的是 ProvencePruning and Reranking Of retrieVEd relevaNt ContExts名字挺长的反正就是个缩写。Provence 相比之前的方法有几个关键改进1. 序列标注它把 pruning 当成一个序列标注任务来做。每个句子进来模型给它打一个标签保留 or 丢弃。这听起来简单但实际效果比之前那些全局打分的方法好得多。2. 把 pruner 和 reranker 统一了之前的研究把 pruning 和 reranking 分成两个独立的阶段。Provence 把两者统一到一个模型里——同一个模型既能做句级别的筛选也能做块级别的重排序。这在实际部署中省了很多事。3. 训练数据多样性Provence 的训练数据覆盖了多种领域和上下文长度。这意味着它不会在你的文档表现很好、换个领域就崩掉。我们测下来它在各种场景下的表现都很稳定。效果怎么样我们在一个标注数据集上测了 recall——这些问题是真实的用户问题我们人工标注了每个问题到底需要哪些块才能正确回答。结果压缩率 68%平均扔掉 68% 的上下文留给生成模型的内容不到原来的三分之一。Recall 几乎没有下降丢掉的内容确实都是不相关的。延迟几乎没增加pruner 模型小跑一次几毫秒到几十毫秒和生成的 LLM 调用时间比起来可以忽略。成本下降明显生成模型看到的内容少了 68%token 花费也少了 68%。为了验证不是标注集上的巧合我们把各种配置在一个随机月份的完整生产流量上做了回放——用的就是每条查询当时实际发送给生成器的那些块。结果和标注集上的表现一致。生产部署一些实际经验说几个在实践中踩过的坑1. Pruner 和生成模型的配合Pruner 的核心目标不是尽可能多扔而是在保证 recall 的前提下尽可能多扔。我们调了一个阈值参数来控制 pruning 的激进程度。刚开始我们设得太激进了把一些模棱两可的内容也扔了结果发现某些边缘 case 的回答质量下降了。最后的做法是设一个保守一点的默认值同时在 API 里暴露这个参数让用户根据自己的场景调整。2. 不同类型的块需要不同的处理有些文档块是纯文本有些是表格有些是代码片段。Provence 在纯文本上表现最好在表格和代码上稍微差一点——因为这些内容的语义密度很高几句话之间可能没有明显的冗余可以剪。我们的解决办法是对不同类型的块使用不同的 pruning 策略。代码块和表格的 pruning 力度小一些纯文本的力度大一些。3. 长上下文场景下的收益更大我们发现当检索器捞回的块数比较多比如超过 10 个时pruning 的收益是最明显的。因为块数少的时候每个块可能都有用但块数一多重复信息和冗余内容就出现了。事实上我们的数据也印证了这一点——那些检索返回块数最多的查询pruning 的压缩率最高。什么时候不该用 pruning虽然 pruner 很便宜但也不是零成本。如果你遇到下面这些情况可能需要小心检索已经极度精确如果你的检索器只返回 1-2 个块而且几乎不会漏掉关键信息pruning 的收益很小。你用的生成模型本身就很强像 Claude Opus 这种级别的模型对噪音的容忍度其实很高。pruning 能省钱但对质量的提升可能不明显。延迟敏感度极高虽然 pruner 很快但毕竟多了一次模型调用。如果每一毫秒都很关键可能不值得。怎么用目前在 kapa 的产品里pruning 是默认开启的。如果你在用我们的 Product Agent SDK知识库搜索已经自动带了 pruning。如果你在用 Retrieval API 或 MCP 服务器也可以通过参数开启。代码大概长这样# 开启 pruningretrieval_resultkb.search(query如何配置 OAuth2?,enable_pruningTrue,# 默认就是 Truepruning_threshold0.35# 控制激进程度0~1)最后RAG 发展到今天单纯在检索层面做优化已经不够了。当模型可以吃下几百万 token 的时候喂什么比能喂多少重要得多。Pruning 不是什么魔法它就是很朴素的思路——让便宜的模型干脏活累活让贵模型只看到精华。但就是这个朴素的思路在生产里给我们省了 68% 的 token同时没有牺牲回答质量。对我们来说这已经足够了。如果你也在做 RAG 系统可以试试这个方向。不一定非要用 Provence——Voyage 也有类似的能力或者你自己 finetune 一个小 BERT 做二分类也能跑。关键不是模型而是不要在生成的这一步浪费钱处理噪音。本文翻译整理自 kapa.ai 技术博客 “How we prune RAG context”数据和案例均来自 kapa.ai 生产环境。