讨论如何精化 RAG 检索逻辑,去除冗余信息,提升答案质量和系统响应效率的实用技巧。
我们如何教会一个小型 LLM 丢弃 68% 的 RAG 上下文
将 Agent 上下文裁剪到回答真正需要的内容,同时保留 96% 的召回率
编辑:欢迎参与 Hacker News 首页上的讨论。
Kapa 构建的 AI 助手能够基于大型产品知识库回答复杂问题。这些知识库包括技术文档、API 参考资料、PDF、论坛和支持工单等。开发者使用我们的检索 API,为自己的 Agent 提供与产品相关的上下文;我们的端到端助手也使用同一套检索层。
2026 年,关于 Agent 是否仍然需要 RAG 的争论很多。但在我们所处的领域,当知识库变得庞大而复杂时,没有什么方案能够接近 RAG 的效果。我们的检索有多种形式,有些是 Agentic 的,有些是单次检索,但它们都采用相同的基本结构:首先由 retriever 找出与问题相关的文档 chunk,然后由 generator,也就是 LLM,根据这些 chunk 编写答案。
这篇文章的简要结论是:我们在两者之间增加了第三个步骤。一个小型、低成本的 LLM 会同时读取问题和所有检索到的 chunk,并在昂贵的模型看到它们之前,丢弃回答不需要的 chunk。它能删除大约 68% 的上下文,同时保留约 96% 的召回率;扣除自身成本后,还能将单次查询的成本降低三分之一。本文将介绍我们是如何做到的。
Retriever 就像一个漏斗。Embedding 搜索和关键词搜索先把包含数十万个 chunk 的知识库缩小到几百个候选项,reranker 再对它们排序,最终排名前 15 左右的 chunk 会被送到 generator,也就是整个链路中规模最大、成本最高的模型。即便如此,generator 读到的大部分内容仍然不是回答当前问题所必需的。这是有意为之:retriever 的目标是尽可能提高召回率,然后相信 generator 能够忽略噪声。
但 generator 忽略的每一个 chunk 都会产生费用。在我们的助手中,检索到的 chunk 约占单次查询成本的三分之二,比答案、对话历史和 system prompt 的成本总和还高。每减少一个 chunk,查询成本就能降低约 4%。而在 Agent 中,每次工具调用都会把输出塞进同一个上下文,因此上下文会迅速膨胀;更精简的检索结果能够为 Agent 必须保存的其他信息腾出空间,同时减少上下文腐化。
问题在于召回率。如果丢掉了回答所需的 chunk,就相当于为了节省几分钱,换来了一个错误答案。衡量 pruner 好坏的标准,正是这种权衡:每损失一个百分点的召回率,能够换来多少压缩收益。
我们在返回 top K 之前已经执行了 rerank,因此有时会有人建议我们直接公开 rerank 分数,让调用方根据分数截断:保留所有高于 0.7 的内容,丢弃其余内容。这个方法不可行,原因有两个,其中第二个原因决定了我们后来构建的整个方案。
首先,rerank 分数表达的是排序,而不是测量结果。它只能说明在当前查询中,chunk A 优于 chunk B,除此之外没有更多含义。不同查询之间的分数并未校准,Cohere 也明确指出了这一点,因此不存在适用于所有查询的固定阈值。排序唯一支持的截断方式是按位置截断,也就是 top-N;但这种方式无论最后一个 chunk 是噪声还是答案所需内容,都会将它丢弃。
其次,即使分数得到了完美校准,这个问题依然存在:相关性并不是单个 chunk 自身的属性。大多数检索流水线中的 reranker 都是 pointwise cross-encoder。它们会单独为每一组 query-chunk 配对打分,而不会把一个 chunk 与同时检索到的其他 chunk 放在一起判断。下面是一个经过匿名化处理的生产环境示例:
第二个 chunk 从未提到审计日志,因此它被判定为噪声;但它实际上构成了答案的一半。任何 pointwise 分数都无法发现这一点,因为只有把这个 chunk 与第一个 chunk 放在一起时,它才具有相关性。多个 chunk 也可能分别承载一个多部分问题的不同答案,任何一个单独拿出来都毫无用处。真正的问题从来都不是某个 chunk 本身是否相关,而是它是否属于一个能够共同回答问题的 chunk 集合。
在放弃 reranker 之前,我们尝试过 anchor documents(Sinhababu 等人提出的方法):在排序中植入相关性已知的合成 chunk,让 reranker 的评分尺度变成绝对尺度。从 Essential 到 Unrelated,每个相关性级别分别编写一个合成 chunk,然后丢弃所有排在你希望保留的最低相关性级别锚点之后的真实 chunk。只需在已有的 rerank 之上增加一次 LLM 调用,而且这个思路确实很优雅。
但它没有奏效,根本原因与前面相同。锚点可以解决校准问题,却无法修复分数本身。Reranker 仍然会把部分相关和间接相关的 chunk 排在明显无关的 chunk 后面。为了保住这些 chunk,锚点必须放得非常靠后,结果几乎裁剪不掉任何内容。
这次失败带来了一个有用的结论:无论使用什么方案进行裁剪,它都必须同时看到问题和所有 chunk,因为真正需要判断的是整个集合。
我们最终上线的方案,是在 reranker 和 generator 之间增加一次 listwise LLM 调用。它会接收问题和所有 chunk,并根据 prompt 中定义的五级量表,为每个 chunk 评分:
达到或超过某个阈值的 chunk 会被保留。这个设计同时解决了前面提到的两个问题。由于每个等级都使用文字进行了定义,因此无论面对什么查询,4 分都代表相同的含义,固定阈值终于可以发挥作用。又因为模型能够同时看到问题和所有 chunk,所以它可以对整个集合进行判断,部分相关和间接相关的内容也终于有了合适的归类位置。
模型:pruner 的成本必须由它节省下来的费用覆盖,因此旗舰模型从设计之初就被排除在外;各个小型快速模型的判断结果都很接近,所以我们选择了在低 reasoning effort 下速度最快、成本最低的模型。
模型:pruner 的成本必须由它节省下来的费用覆盖,因此旗舰模型从设计之初就被排除在外;各个小型快速模型的判断结果都很接近,所以我们选择了在低 reasoning effort 下速度最快、成本最低的模型。
阈值:用于调节压缩率与召回率之间权衡的主要旋钮。
阈值:用于调节压缩率与召回率之间权衡的主要旋钮。
keep-top-k:无论评分如何,rerank 排名前几位的 chunk 都会直接通过,从而保护最重要的 chunk,避免因评分错误而被删除。
keep-top-k:无论评分如何,rerank 排名前几位的 chunk 都会直接通过,从而保护最重要的 chunk,避免因评分错误而被删除。
为了确保评估足够客观,我们还测试了两个更简单的设计。第一个是 Budget-select:先保留排名最前面的几个 chunk,然后允许 LLM 最多再添加 N 个。它的结果大小可以预测,但一旦预算用完,后续所有 chunk 无论多么相关都会被丢弃。第二个则是最简单的 pruner:直接询问 LLM 应该保留哪些 chunk,不提供任何评分量表。如果一个方案连直接询问 LLM 都无法胜过,就不值得构建。
我们使用一组经过标注的真实问题来测量召回率,这些问题的答案具体需要哪些 chunk 都是已知的。随后,我们选取随机一个月的生产环境对话,针对每个查询当时实际发送给 generator 的 chunk,重放每一种配置,以验证压缩率、成本和延迟。
图中的每个点代表一种配置,并按照最重要的两个指标绘制。横轴是压缩率,表示 pruner 丢弃的检索 chunk 比例。纵轴是保留的召回率,表示经过裁剪后,仍然完整保留回答所需全部 chunk 的问题比例:100% 表示没有任何问题丢失必要的 chunk;90% 表示每十个问题中有一个丢失了必要的 chunk。越靠右上方越好。每条线连接了相应策略表现最好的配置,灰色虚线则表示任何 pruner 都必须超越的基线:朴素的 top-N 截断,也就是直接减少 reranker 返回的 chunk 数量。
所有方案都大幅优于这个基线。将召回率保持在 98% 时,截断策略只能丢弃一个 chunk,压缩率约为 7%。每一种 LLM 策略都能达到 30% 或更高的压缩率,而相关性评分方案能丢弃接近一半的 chunk。在所有压缩率水平上,评分曲线也都优于另外两种方案,因此唯一剩下的问题就是应该选择曲线上的哪个点。
我们选择了一个接近激进端的位置:保留约 96% 的召回率,同时丢弃约 68% 的 chunk。每 25 个问题中会有一个丢失所需的 chunk;作为交换,三分之二的上下文被移除,扣除 pruner 自身的成本后,单次查询的费用降低了约 34%。
Pruner 运行在检索与生成之间,位于关键路径上,因此每次查询都会增加一次模型调用,而模型的速度决定了这次调用的代价。在生产数据集中,我们选择的配置平均每次查询耗时约 0.7 秒。更重的配置耗时会迅速增加,因此,采用低 reasoning effort 的小型模型,是将额外延迟控制在一秒以内的关键。
作为回报,生成阶段几乎没有明显提速:chunk 变少意味着 generator 的输入 token 更少,所以它会稍早一点开始响应,但只快了几分之一秒,远远不足以抵消 pruner 自身调用带来的延迟。
因此,裁剪以少量且固定的额外延迟换来了上下文压缩;在我们上线的配置中,这一延迟远低于一秒。对于延迟敏感的单次调用路径来说,这确实是一项需要权衡的成本。但在每一轮本来就要进行多次模型调用的 Agent 中,多增加一次轻量调用的边际影响很小。
我们首先在检索只是众多工具之一的场景中上线了这个功能,也就是那些基于我们的检索能力构建 Agent 的客户。一个 Agent 会携带数十种工具,每次调用都会把输出塞进同一个上下文;如果文档搜索返回的内容减少三分之二,就能为其他所有信息腾出空间。在这种场景下,召回率损失的危险性也更低:如果 Agent 发现缺少某些内容,它可以再次搜索。
在我们的 Product Agent SDK 中,知识库搜索默认启用裁剪;在 retrieval API 和 MCP servers 中,该功能为可选项。
Silicon Labs
问任何问题……
Silicon Labs
问任何问题……
Logitech
问任何问题……
Logitech
问任何问题……
monday.com
问任何问题……
monday.com
问任何问题……
将技术文档转化为面向客户的 AI 助手