登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

RAG 检索结果太多时怎么用 reranker 控制上下文长度

来源:17golang原创

时间:2026-09-08 14:02:42 281浏览 收藏

RAG 检索结果太多时,最稳妥的做法不是把向量检索的 top_k 直接塞进提示词,而是把流程拆成“扩大召回、reranker 重排、按 token 预算装配”三层。比如先召回 30 个片段,再用 reranker 排出相关性顺序,最后只装入不超过 3000 token 的 5~8 个片段。这样既保留了召回的余量,也不会让无关内容挤占模型真正需要的上下文。

reranker 负责回答“哪些片段更相关”,不负责回答“还能放多少文字”。最终上下文数量必须由 token 预算、单片段长度和重复来源共同决定。
要点速览
  • 向量召回的 top_k 是候选池大小,不是最终上下文数量。
  • reranker 先给候选片段排序,再按分数、token 和来源去重做二次筛选。
  • 日志至少记录召回数、重排数、最终片段数、累计 token 和被跳过原因。

先把召回数和上下文预算分开

向量检索适合快速缩小范围,但它通常只根据 embedding 相似度做初筛。查询中同时出现“安装失败”“权限”和“回滚”时,相关片段可能分散在多个 chunk 中,top_k 过小会在第一层就丢证据。因此可以让召回池略大一些,再把更昂贵的 reranker 用在这批候选上。

环节建议记录它解决什么问题
向量召回top_k=30避免候选池过窄
reranker保留 15~20 个排序候选把 query 与片段放在一起判断相关性
上下文装配预算 3000 token,约 5~8 个片段控制提示词长度和重复内容
RAG 用户查询经过向量召回、候选片段、reranker 和 token 预算进入最终上下文的静态关系图
图1:召回候选、相关性排序和上下文装配是三个边界,top_k 不应直接等同于最终片段数。

这里的数字只是起始配置,不是通用标准。长文档、短问句和多轮对话的预算完全不同。生产环境应把输入提示、历史消息、工具说明和模型输出预留一起算入,而不是只给检索片段留一个孤立的上限。

reranker 只做相关性排序

reranker 可以理解为一个把“查询+候选片段”放在一起打分的模型适配器。Hugging Face 的 Transformers 文档把文本分类模型的推理结果表示为标签和分数,也支持用 tokenizer 截断输入;项目可以据此封装自己的 pair scorer。下面的代码故意不绑定某个模型名称,避免把一个 checkpoint 当成所有语料的答案。

from dataclasses import dataclass

@dataclass
class Chunk:
    text: str
    source: str
    vector_score: float
    rerank_score: float = 0.0

def rerank(query: str, chunks: list[Chunk], pair_scorer) -> list[Chunk]:
    # 让重排模型同时看到查询和片段,而不是只比较两个向量分数。
    pairs = [[query, chunk.text] for chunk in chunks]
    scores = pair_scorer(pairs)
    for chunk, score in zip(chunks, scores):
        # 分数只用于当前查询内排序,不跨查询比较阈值。
        chunk.rerank_score = float(score)
    return sorted(chunks, key=lambda item: item.rerank_score, reverse=True)

重排后仍然要保留来源、chunk_id 和原始分数。它们是后面做去重、调参和排查“为什么答案缺证据”的依据。不要因为 reranker 给出了 0.82 就直接认为它能占用 820 个 token;分数和长度是两条独立的轴。

按分数和 token 双门槛选最终片段

最终装配可以采用一个简单的贪心策略:按 rerank_score 从高到低浏览候选,遇到过长片段先跳过;如果来源已经出现且还有其他来源可选,也先跳过;只有累计 token 不超过预算时才加入上下文。

def choose_context(ranked, tokenizer, context_budget=3000, per_chunk_limit=700):
    # 预留系统提示、历史消息和模型输出,检索片段不要吃满窗口。
    used_tokens = 0
    selected = []
    seen_sources = set()

    for chunk in ranked:
        # 先截断超长片段,避免一个 chunk 吞掉整个预算。
        text = chunk.text[:per_chunk_limit]
        token_count = len(tokenizer.encode(text, add_special_tokens=False))
        if token_count == 0 or used_tokens + token_count > context_budget:
            continue
        # 同一文档重复命中时,优先保留分数更高的第一段。
        if chunk.source in seen_sources:
            continue
        selected.append({"text": text, "source": chunk.source,
                         "score": chunk.rerank_score})
        seen_sources.add(chunk.source)
        used_tokens += token_count
    return selected, used_tokens

代码中的 `per_chunk_limit` 是字符截断示意,真正项目应使用同一个 tokenizer 计算截断位置,否则中文、代码和中英混排会产生偏差。若业务允许同一来源出现多段,可以把 `seen_sources` 换成来源配额,例如每个来源最多两段。

RAG rerank 分数、候选游标、token 计数器、单片段上限和来源去重共同装配提示词上下文的静态关系图
图2:最终片段同时受相关性约束和装配约束影响,token 计数器是上下文上限的实际闸门。

四个容易让结果变差的参数坑

  • 召回太窄:reranker 只能重排手里已有的候选,第一层没有召回的证据不会凭空出现。
  • 跨查询比较分数:不同问题的分布可能不同,固定写死“分数低于 0.7 就丢弃”需要先用业务数据校准。
  • 片段过长:一段包含多个主题时,重排分数可能不错,但会挤掉更短、更聚焦的证据。
  • 只看最终条数:5 个片段可能有 500 token,也可能有 5000 token;必须同时记录累计 token 和跳过原因。

排查时建议输出一条结构化日志:`retrieved=30 reranked=20 selected=6 used_tokens=2764 skipped_by_budget=8 skipped_by_source=3`。这条日志不能证明答案一定正确,但能快速说明问题发生在召回、重排还是上下文装配阶段。

RAG 上下文控制常见问题

reranker 能不能直接决定最终 top_n?

它可以提供排序依据,但最终 top_n 仍要经过 token 预算和片段长度约束。固定 top_n 适合做初始实验,不适合作为唯一闸门。

为什么把 top_k 从 10 调到 50 后反而变慢?

候选变多会增加 reranker 的 pair 推理量;可以先限制重排候选数,或批量推理,再观察召回收益是否值得这部分成本。

应该按 rerank 分数还是向量分数截断?

初筛用向量分数,最终排序通常用 rerank 分数;不要把两种分数当作同一量纲,更不要未经校准直接相加。

上下文不够时优先删哪类片段?

先删重复来源、低分且过长的片段,再检查是否保留了至少一个直接回答问题的高分证据;同时记录每次删减原因,方便回放。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>