10-使用Reranker提升RAG回答准确率

使用 Reranker(重排模型)提升 RAG 知识库回答准确率

系列:从零构建企业 RAG 知识库(第 10 篇)

1. 为什么召回后还要重排

第一阶段检索追求"别漏掉",通常快速取 20---100 个候选;生成模型上下文却只适合少量高质量证据。

text 复制代码
大范围召回(高 Recall) → Reranker 精排(高 Precision)
→ 去重与上下文预算 → 生成

Embedding 常将问题和文档分别编码,再比较向量;Cross-Encoder 类 Reranker 同时读取"问题 + 候选",能进行更细致的相关性判断,但计算更慢。

2. 重排接口必须保留候选身份

python 复制代码
from dataclasses import dataclass
from typing import Protocol


@dataclass(frozen=True)
class Candidate:
    chunk_id: str
    document_id: str
    tenant_id: str
    text: str
    retrieval_score: float


@dataclass(frozen=True)
class RerankedCandidate:
    candidate: Candidate
    rerank_score: float
    original_rank: int


class Reranker(Protocol):
    def score(self, query: str, texts: list[str]) -> list[float]:
        """返回与 texts 一一对应的相关性分数。"""

3. 安全的批量重排

python 复制代码
def rerank(
    query: str,
    candidates: list[Candidate],
    reranker: Reranker,
    top_n: int = 5,
) -> list[RerankedCandidate]:
    if not query.strip():
        raise ValueError("查询不能为空")
    if not 1 <= top_n <= 20:
        raise ValueError("top_n 必须在 1 到 20 之间")
    if not candidates:
        return []

    scores = reranker.score(query, [item.text for item in candidates])
    if len(scores) != len(candidates):
        raise RuntimeError("Reranker 分数数量与候选数量不一致")
    if any(not isinstance(score, (int, float)) for score in scores):
        raise RuntimeError("Reranker 返回了非数值分数")

    ranked = [
        RerankedCandidate(candidate, float(score), original_rank)
        for original_rank, (candidate, score) in enumerate(
            zip(candidates, scores),
            start=1,
        )
    ]
    ranked.sort(
        key=lambda item: (
            -item.rerank_score,
            item.original_rank,
            item.candidate.chunk_id,
        )
    )
    return ranked[:top_n]

原始排名作为同分排序依据,让结果稳定且可诊断。

4. 用确定性 Fake Reranker 测流程

python 复制代码
class KeywordReranker:
    """教学替身,只验证控制流,不代表真实重排模型。"""

    def score(self, query: str, texts: list[str]) -> list[float]:
        query_chars = set(query)
        return [
            float(len(query_chars & set(text))) / max(len(query_chars), 1)
            for text in texts
        ]


def test_relevant_candidate_moves_forward() -> None:
    candidates = [
        Candidate("c1", "d1", "t1", "退款政策总览", 0.95),
        Candidate("c2", "d2", "t1", "退款申请需要人工审核", 0.80),
    ]
    result = rerank(
        "退款申请如何审核",
        candidates,
        KeywordReranker(),
        top_n=1,
    )
    assert result[0].candidate.chunk_id == "c2"
    assert result[0].original_rank == 2

5. 真实 Cross-Encoder 的适配示意

python 复制代码
class SentenceTransformersReranker:
    """示意适配器;模型名称和许可证需按当前官方资料选择。"""

    def __init__(self, model) -> None:
        self.model = model

    def score(self, query: str, texts: list[str]) -> list[float]:
        pairs = [(query, text) for text in texts]
        raw_scores = self.model.predict(pairs)
        return [float(value) for value in raw_scores]


# 示例初始化方式需要安装并锁定 sentence-transformers:
# from sentence_transformers import CrossEncoder
# model = CrossEncoder("经过团队评测并批准的模型路径")
# reranker = SentenceTransformersReranker(model)

本文没有下载或运行具体重排模型,不声称任何模型在企业数据上的准确率。

6. 重排之后还要处理上下文

python 复制代码
def select_context(
    ranked: list[RerankedCandidate],
    max_chars: int,
    min_score: float,
) -> list[Candidate]:
    selected: list[Candidate] = []
    used = 0
    seen_documents: set[str] = set()

    for item in ranked:
        if item.rerank_score < min_score:
            continue
        size = len(item.candidate.text)
        if used + size > max_chars:
            continue
        # 示例限制每份文档一个 Chunk,避免近重复结果占满上下文
        if item.candidate.document_id in seen_documents:
            continue
        selected.append(item.candidate)
        seen_documents.add(item.candidate.document_id)
        used += size
    return selected

真实预算应使用目标生成模型 Tokenizer,而不是字符数。

7. 可复验的失败测试

python 复制代码
class BrokenReranker:
    def score(self, query: str, texts: list[str]) -> list[float]:
        return [1.0]  # 故意返回错误数量


def test_broken_reranker_is_rejected() -> None:
    candidates = [
        Candidate("c1", "d1", "t1", "A", 0.9),
        Candidate("c2", "d2", "t1", "B", 0.8),
    ]
    try:
        rerank("问题", candidates, BrokenReranker())
    except RuntimeError:
        pass
    else:
        raise AssertionError("错误的分数数量必须被拒绝")

8. 如何证明重排有效

固定第一阶段候选,比较重排前后:

  • MRR、NDCG@K 和 Precision@K;
  • 正确证据进入最终 Context 的比例;
  • 最终答案正确性与忠实度;
  • P95 延迟、吞吐和成本;
  • 长文、编号、否定条件和多语言切片。

如果第一阶段没有召回正确证据,Reranker 无法凭空找回它。

9. 对抗性审查

  • 权限过滤必须在重排前完成;
  • 限制候选数量和单条长度,避免资源耗尽;
  • 文档中的注入文本仍是不可信数据;
  • 记录召回分数、重排分数和模型版本;
  • 模型升级重新跑固定评测集;
  • Reranker 故障可降级到原始检索排序,但要标记降级状态。

10. 总结

Reranker 的价值是把"可能相关"的大候选集压缩成"更适合进入上下文"的小集合。它优化排序而不是创造证据,必须建立在高召回、权限正确和可评测的第一阶段检索之上。

相关推荐
皮皮虾❀1 小时前
钉钉AI服务商×教育行业:AI现代产业学院共建一体化实战——从“高校AI教学缺平台”到“校企共建AI产业学院培养数字化人才
人工智能·钉钉
kobe_OKOK_1 小时前
DRF接口幂等操作
python·django
火山引擎开发者社区2 小时前
【议程来了】火山引擎开发者社区技术日・NVIDIA 独家赞助-成都站
人工智能
三声三视2 小时前
从崩溃日志到 AI 技能:tri-god 蒸馏实战
人工智能·ai·aigc·agent
ZGIAI2 小时前
ZGI Workflow 并发控制:保护下游接口
人工智能·架构
richard_first2 小时前
Transformer 与大语言模型:第9章 LayerNorm 归一化层
人工智能·深度学习·机器学习·transformer
ZGIAI2 小时前
知识库有结果,不等于召回率合格
人工智能·架构
东小西2 小时前
【SAA实战】第 1 篇:ReactAgent 入门——先撸个"会调工具的助手"跑起来
java·人工智能·spring
廿士2 小时前
python脚本使用相关
python
兴通物联科技2 小时前
SMT PCB 微小 DataMatrix 码扫不动问题分析 兴通 XT8601B 600 万像素工业读码器落地实践
大数据·人工智能·单片机·嵌入式硬件·算法·计算机视觉