1. 你将学到什么 #

重排是检索流水线里的「临门一脚」。前面的召回负责从海量文档里尽快捞出一堆可能相关的候选;重排负责在这堆候选里再认真读一遍,把真正能回答问题的排到最前面。

本文的学习顺序是:

  1. 先建立直觉:为什么「只召回」常常不够
  2. 再补前置:嵌入、双塔、交叉编码器分别是什么
  3. 然后看机制:重排模型到底怎么给一对「问题 + 文档」打分
  4. 接着动手:先写简版,再用真实 API,最后接到 LangChain
  5. 最后记坑:哪些做法看起来合理,实际会踩雷

2. 环境准备 #

下面这些东西准备好,后面的代码就能直接跑。

2.1. 需要安装的包 #

本文用到的核心库如下。在仓库根目录或任意干净虚拟环境里执行:

# 安装通义 SDK(直接调 TextReRank API 用)
pip install dashscope
# 安装环境变量加载工具(从 .env 读 API Key)
pip install python-dotenv
# 安装 LangChain 相关包(接到检索器时用)
pip install langchain-core langchain-community langchain-classic
# 安装 BM25 与中文分词(本文用 BM25 做粗召回,避免依赖向量库)
pip install rank-bm25 jieba

说明:

2.2. 需要配置的 API Key #

重排模型要花钱(或走免费额度),需要通义百炼的 Key。

在项目根目录的 .env 文件里写上:

DASHSCOPE_API_KEY=你的通义API密钥

申请入口见阿里云百炼控制台。没有 Key 时,第 5 节的简易重排器仍然可以完整运行;从第 6 节开始才需要 Key。

3. 为什么需要重排 #

只做召回、不做重排,系统经常会「找到了正确答案,却把它埋在第 3、第 4 名」。

3.1. 一个会失败的真实场景 #

假设用户问:

多少天内可以退货?

知识库里有这些片段(简化版):

编号 内容 是否正确答案
A 签收 7 日内可无理由退货,商品需保持完好。 ✅ 是
B 退款将在收到退货并检验合格后的 3 个工作日内原路退回。 ❌ 否(讲退款时效)
C 质量问题 15 日内可换货,需提供照片凭证。 ❌ 否(讲换货)
D 预售商品以商品页标注的发货时间为准。 ❌ 否(讲发货)

向量检索或混合检索常常会把 B、C、D 也捞进来,因为它们都带「退」「日」「货」这类相近语义。若最终只把 top-1 交给大模型,模型就可能根据「退款 3 个工作日」胡乱答成「3 天」,而真正的「7 日内」还躺在第 3 名。

召回的目标是「尽量别漏」;重排的目标是「把对的顶上去」。 两件事不能互相替代。

3.2. 通俗类比:海选与决赛 #

把整个检索想成招聘:

  1. 召回 = HR 用关键词海选
    条件写「Python、3 年经验」,机器很快筛出 100 份简历。速度快,但分不清「真·3 年」和「实习也算 3 年」。
  2. 重排 = 技术专家精读这 100 份
    专家一边对照岗位要求,一边读简历细节,最后排出前 5 名进入面试。

RAG 里也是一样:

全库(可能有几万、几十万条)
        │
        │  召回:快、粗、求「不漏」
        ▼
候选集(通常几十条)
        │
        │  重排:慢、细、求「排对」
        ▼
精排结果(通常 3~5 条)
        │
        ▼
送给大模型生成答案

3.3. 快与准为什么不能一步做完 #

如果对全库每一条都用「精读」模型打分,精度会很高,但速度和费用都会炸掉:10 万条文档就要跑 10 万次重排模型。

所以工业界几乎都用两段式:

阶段 典型技术 特点 目标
召回 BM25、向量检索、混合检索 快,可预计算,能扫全库 把正确答案捞进候选
重排 交叉编码器(本文主角) 慢,必须「问题+文档」现算 把正确答案排到最前

一句话记住:

重排不是搜索引擎,它是搜索结果的精修器。
候选里没有正确答案时,重排再强也救不回来。

4. 前置知识 #

下面这些词在重排文档里经常出现。不懂它们,后面读代码会很吃力,所以单独拆开讲。

4.1. 什么是「相关性分数」 #

相关性分数就是模型给出的一个数字,表示「这条文档和这个问题有多搭」。

开始最容易犯的错:

向量检索得分 0.82 > 重排得分 0.31
→ 就以为向量结果更好

这是错的。0.82 和 0.31 来自两套打分体系,没有可比性。线上系统在重排之后,应完全信任重排后的名次,丢掉第一阶段的分数。

4.2. 什么是文本嵌入(Embedding) #

嵌入就是把一段文字变成一串数字(向量),让计算机能用「距离远近」表达「意思远近」。

直觉:

"我喜欢编程"  →  [0.12, -0.05, 0.88, ...]
"我爱写代码"  →  [0.11, -0.04, 0.85, ...]   ← 两个向量很近
"今天天气很好" →  [0.70,  0.22, -0.31, ...]  ← 和上面两个很远

召回阶段大量使用嵌入:文档可以提前算好向量存进库,查询时只算问题向量,再找最近邻。这就是它快的原因。

4.3. 什么是双塔模型(Bi-Encoder) #

双塔模型 = 两个塔分别编码,再比向量。

问题 ──► 编码器 A ──► 问题向量 ──┐
                              ├─► 余弦相似度 / 点积 ──► 分数
文档 ──► 编码器 B ──► 文档向量 ──┘

特点:

类比:先各自写一份摘要,再拿两份摘要比对。摘要写得再好,也可能丢掉「否定词」「具体数字」这类细节。

4.4. 什么是交叉编码器(Cross-Encoder) #

交叉编码器 = 把问题和文档拼成一段,一起送进模型。

[问题] + [分隔符] + [文档]
        │
        ▼
   同一个 Transformer
   (内部做全文注意力交互)
        │
        ▼
   输出一个相关性分数

特点:

类比:把岗位要求和简历摊在一张桌上,逐句对照,而不是先各自写摘要再比对。

4.5. 双塔 vs 交叉:一张对照表 #

对比项 双塔 Bi-Encoder 交叉 Cross-Encoder
输入方式 问题、文档各自编码 问题 + 文档拼在一起
能否预计算文档 能 不能
速度 快 慢
精度 一般 更高
典型用途 召回 重排

RAG 里常见组合:双塔(或 BM25)负责召回,交叉编码器负责重排。

4.6. 什么是「文档压缩器」(Compressor) #

在 LangChain 里,重排组件常被归类为 Document Compressor(文档压缩器)。

名字有点怪,含义其实很朴素:

输入一批文档,输出更少(或更精炼)的文档。

重排就是一种压缩:

同一位置还可以放「用 LLM 裁剪无关句子」之类的组件。本文只关心重排这一种。

4.7. 什么是 Pointwise 打分 #

重排最主流的打分方式叫 Pointwise(点式):

还有 Listwise(一次看整个列表再排序)等更复杂玩法。开始阶段掌握 Pointwise 就够了,也是通义 gte-rerank-v2 实际在做的事。

5. 重排模型到底怎么工作 #

有了前置概念,可以把重排的内部步骤说清楚。这一节偏「理解」,下一节开始写代码。

5.1. 三步流水线 #

交叉编码器重排通常分三步:

  1. 拼接输入
    把问题和文档拼成一条序列,中间用特殊分隔符隔开,例如:
    [CLS] 多少天内可以退货? [SEP] 签收 7 日内可无理由退货…… [SEP]
  2. 深度交互
    Transformer 的注意力机制让问题中的每个词都能关注文档中的每个词。于是模型能发现:「问题里的『多少天』」对应「文档里的『7 日内』」。
  3. 输出分数
    模型把交互后的表示送进一个小分类头,吐出一个相关性分数;对所有候选重复后排序,再截断为 top_n。

5.2. 为什么比向量检索更准 #

向量检索编码文档时,还不知道用户会问什么,只能把整句压成一个「通用坐标」。细节(否定词、具体天数、退货 vs 退款)容易糊在一起。

重排是拿着问题去读文档,所以能区分:

5.3. 为什么必须先召回再重排 #

因为第 2 步要对每一个候选跑一遍模型:

所以正确姿势永远是:

全库 ──召回──► 少量候选 ──重排──► top_n

6. 先写一个「简易重排器」 #

正式调 API 之前,先用纯 Python 模拟一遍「粗召回 → 打分 → 排序 → 截断」。这样你能看清重排的流程,不被 SDK 细节干扰。

6.1. 这一节在演示什么 #

简易版用「问题和文档的公共字数」当假分数。它当然不如神经网络聪明,但流程和真重排一模一样:

  1. 准备问题与候选文档
  2. 对每个候选打一个分数
  3. 按分数降序排序
  4. 只保留前 top_n 条

这一节不需要 API Key,适合验证环境与思路。

6.2. 完整可运行代码 #

把下面整段保存为 toy_rerank.py,直接运行:

# -*- coding: utf-8 -*-
# 上面这行告诉 Python:本文件按 UTF-8 编码读取,避免中文注释乱码

# 定义用户问题:后面所有候选都围绕这个问题打分
query = "多少天内可以退货?"

# 定义候选文档列表:模拟「粗召回」已经捞出来的几条
# 每条是一个字典,含编号 id 和正文 text,方便打印时对照
candidates = [
    # 正确答案:明确写了「7 日内可无理由退货」
    {"id": "A", "text": "签收 7 日内可无理由退货,商品需保持完好。"},
    # 干扰项:也有「退」「日」,但讲的是退款时效,不是退货期限
    {"id": "B", "text": "退款将在收到退货并检验合格后的 3 个工作日内原路退回。"},
    # 干扰项:讲换货,不是退货
    {"id": "C", "text": "质量问题 15 日内可换货,需提供照片凭证。"},
    # 干扰项:讲发货,和退货几乎无关
    {"id": "D", "text": "预售商品以商品页标注的发货时间为准。"},
]


# 定义一个极其简单的打分函数:公共字符越多,分数越高
def toy_score(question: str, document: str) -> float:
    # 把问题转成字符集合,去掉空白,避免空格干扰
    q_chars = set(question.replace(" ", ""))
    # 把文档转成字符集合,同样去掉空白
    d_chars = set(document.replace(" ", ""))
    # 若问题为空,直接返回 0,避免除零
    if not q_chars:
        # 没有有效字符时无法计算重合度
        return 0.0
    # 计算公共字符个数,再除以问题字符数,得到 0~1 的粗略相关性
    return len(q_chars & d_chars) / len(q_chars)


# 定义重排主函数:打分 → 排序 → 截断
def toy_rerank(question: str, docs: list[dict], top_n: int = 3) -> list[dict]:
    # 准备一个空列表,用来收集带分数的结果
    scored = []
    # 遍历每一个候选文档
    for doc in docs:
        # 取出正文
        text = doc["text"]
        # 调用简易打分函数,得到一个浮点分数
        score = toy_score(question, text)
        # 把原文档和分数打包成新字典,后面排序用
        scored.append({"id": doc["id"], "text": text, "score": score})
    # 按 score 从高到低排序;reverse=True 表示降序
    scored.sort(key=lambda item: item["score"], reverse=True)
    # 只保留前 top_n 条,模拟真实重排的截断行为
    return scored[:top_n]


# 调用简易重排,保留前 3 名
ranked = toy_rerank(query, candidates, top_n=3)

# 打印标题,方便在终端里辨认这一段输出
print("问题:", query)
# 打印结果说明
print("重排结果(分数从高到低):")
# 用 enumerate 从 1 开始编号,打印名次、编号、分数、正文
for i, item in enumerate(ranked, start=1):
    # 分数保留 4 位小数,和后面真实 API 的展示风格保持一致
    print(f"  {i}. score={item['score']:.4f} [{item['id']}] {item['text']}")

6.3. 你应该看到什么 #

输出大致如下(具体分数取决于字符重合,重点看流程是否跑通):

问题: 多少天内可以退货?
重排结果(分数从高到低):
  1. score=0.xxxx [A] 签收 7 日内可无理由退货,商品需保持完好。
  2. score=0.xxxx [B] 退款将在收到退货并检验合格后的 3 个工作日内原路退回。
  3. score=0.xxxx [C] 质量问题 15 日内可换货,需提供照片凭证。

简易版可能不够聪明,甚至偶尔把干扰项排得很靠前——这没关系。你要带走的是流程:

打分函数可以换,流水线不变。
下一节只是把 toy_score 换成真正的神经网络 API。

7. 用通义 API 做真实重排 #

这一节调用阿里云通义的 TextReRank,模型用当前可用的 gte-rerank-v2。

7.1. API 在做什么 #

你把「一个问题 + 一批文档」发给服务端,它返回每条文档的:

然后你按分数排序(SDK 通常已经排好),取前 top_n 条即可。

7.2. 完整可运行代码 #

把下面整段保存为 dashscope_rerank_demo.py。确保项目根目录 .env 里已有 DASHSCOPE_API_KEY。

# -*- coding: utf-8 -*-
# 指定源文件编码,保证中文注释正常

# 导入标准库:用于判断 HTTP 状态是否成功
from http import HTTPStatus

# 从 .env 文件加载环境变量(包括 DASHSCOPE_API_KEY)
from dotenv import load_dotenv

# 导入通义重排接口
from dashscope import TextReRank

# 执行加载:把 .env 中的键值写进操作系统环境变量
load_dotenv()

# 定义用户问题
query = "多少天内可以退货?"

# 定义候选文档:顺序就是 index=0,1,2,3
documents = [
    # index=0:正确答案
    "签收 7 日内可无理由退货,商品需保持完好,包装与配件齐全。",
    # index=1:退款时效干扰项
    "退款将在收到退货并检验合格后的 3 个工作日内原路退回。",
    # index=2:换货干扰项
    "质量问题 15 日内可换货,需提供照片凭证并联系客服登记。",
    # index=3:发货干扰项
    "预售商品以商品页标注的发货时间为准,如遇缺货会主动联系并可全额退款。",
]

# 调用通义重排 API
# model:必须用仍在服务的 gte-rerank-v2(旧名 gte-rerank 会 403)
# query:用户问题
# documents:候选正文列表
# top_n:最多返回几条;这里取 3
# return_documents:False 表示结果里不回传正文,我们本地用 index 回查即可
response = TextReRank.call(
    model="gte-rerank-v2",
    query=query,
    documents=documents,
    top_n=3,
    return_documents=False,
)

# 检查 HTTP 状态码,不是 200 就打印错误信息并退出
if response.status_code != HTTPStatus.OK:
    # 打印状态码与错误消息,方便排查 Key、权限、模型名问题
    print("重排失败:", response.status_code, getattr(response, "message", response))
    # 失败时直接结束脚本,避免后面访问空结果
    raise SystemExit(1)

# 从响应里取出 results 列表
results = response.output.results

# 打印问题,作为输出标题
print("问题:", query)
# 打印结果说明
print("真实重排结果:")
# 遍历返回的每一条精排结果
for rank, item in enumerate(results, start=1):
    # 取出该结果对应的原始下标
    index = int(item.index)
    # 取出相关性分数
    score = float(item.relevance_score)
    # 用下标回到本地 documents 列表,取出正文
    text = documents[index]
    # 按名次打印:分数、原始下标、正文
    print(f"  {rank}. score={score:.4f} [index={index}] {text}")

7.3. 预期结果长什么样 #

正确时,第 1 名应是「签收 7 日内可无理由退货…」,分数明显高于干扰项。示意:

问题: 多少天内可以退货?
真实重排结果:
  1. score=0.32xx [index=0] 签收 7 日内可无理由退货……
  2. score=0.19xx [index=1] 退款将在收到退货并检验合格后……
  3. score=0.17xx [index=?] ……

注意两点:

  1. 分数小数末位可能轻微跳动(比如 0.3256 / 0.3252),排名通常稳定。不要写 if score == 0.3256。
  2. 若报错里出现 403 或 results 为空,优先检查:Key 是否有效、模型名是否写成了已停用的 gte-rerank。

7.4. 和简易版对照着看 #

步骤 简易版 真实 API
打分 toy_score 数公共字符 TextReRank.call 神经网络打分
排序 list.sort 服务端已按分数排好
截断 [:top_n] 请求参数 top_n
输出字段 自定义 score relevance_score + index

流程相同,只是打分器从「数字符」换成了「交叉编码器」。

8. 接到 LangChain:粗召回 + 重排 #

真实项目里,候选文档通常来自检索器,而不是手写列表。这一节用:

8.1. 流水线长什么样 #

用户问题
   │
   ▼
BM25 粗召回(例如取 5 条)
   │
   ▼
DashScopeRerank(精排后保留 top_n=3)
   │
   ▼
最终 3 条 Document(metadata 里带 relevance_score)

对外你只调用一次 retriever.invoke(问题),内部自动走完两段。

8.2. 一个必须绕开的坑:模型名被覆盖 #

LangChain 的 DashScopeRerank 在构造时有个校验器,会把你传入的模型名强行改回旧的 gte-rerank。而旧模型现在会返回 403,表现成很难懂的:

AttributeError: 'NoneType' object has no attribute 'results'

正确写法:

  1. 先 DashScopeRerank(top_n=3) 构造出来
  2. 再执行 reranker.model = "gte-rerank-v2"

原因:校验器只在创建对象时跑一遍;创建后再赋值,走普通属性路径,不会被改回去。

8.3. 完整可运行代码 #

保存为 langchain_rerank_demo.py 后运行:

# -*- coding: utf-8 -*-
# 指定 UTF-8,避免中文注释出问题

# 导入中文分词库:BM25 处理中文前必须先切词
import jieba

# 从 .env 加载 DASHSCOPE_API_KEY
from dotenv import load_dotenv

# 导入「压缩检索器」:负责把粗召回和重排串起来
from langchain_classic.retrievers import ContextualCompressionRetriever

# 导入通义重排的 LangChain 封装(本质是文档压缩器)
from langchain_community.document_compressors import DashScopeRerank

# 导入 BM25 检索器,用作粗召回
from langchain_community.retrievers import BM25Retriever

# 导入 Document 数据结构:page_content 存正文,metadata 存附加信息
from langchain_core.documents import Document

# 加载环境变量
load_dotenv()

# 准备一份迷你知识库:每条都带 chunk_id,方便对照名次
chunks = [
    # 正确答案
    Document(
        page_content="签收 7 日内可无理由退货,商品需保持完好,包装与配件齐全。",
        metadata={"chunk_id": "as-0001"},
    ),
    # 换货干扰项
    Document(
        page_content="质量问题 15 日内可换货,需提供照片凭证并联系客服登记。",
        metadata={"chunk_id": "as-0002"},
    ),
    # 跨境退货运费干扰项
    Document(
        page_content="跨境订单以页面公示为准,退回运费由买家承担。",
        metadata={"chunk_id": "as-0003"},
    ),
    # 退款时效干扰项(最强干扰之一)
    Document(
        page_content="退款将在收到退货并检验合格后的 3 个工作日内原路退回。",
        metadata={"chunk_id": "as-0004"},
    ),
    # 发货时效
    Document(
        page_content="一般订单 48 小时内发出,节假日顺延。",
        metadata={"chunk_id": "as-0005"},
    ),
    # 预售发货干扰项
    Document(
        page_content="预售商品以商品页标注的发货时间为准,如遇缺货会主动联系并可全额退款。",
        metadata={"chunk_id": "as-0006"},
    ),
]


# 定义中文分词函数:去掉空白 token,避免空字符串进入 BM25
def cn_tokenize(text: str) -> list[str]:
    # jieba.lcut 把句子切成词列表
    words = jieba.lcut(text)
    # 用列表推导式过滤纯空白
    return [w for w in words if w.strip()]


# 用全部语料构建 BM25 检索器
# k=5 表示粗召回最多返回 5 条
# preprocess_func 必须传入中文分词,否则默认按空格切,中文会整句当一个词
bm25 = BM25Retriever.from_documents(
    chunks,
    k=5,
    preprocess_func=cn_tokenize,
)

# 构造重排器:top_n=3 表示精排后只留 3 条给下游
reranker = DashScopeRerank(top_n=3)

# 打印构造后的默认模型名,你会看到它被改成了旧的 gte-rerank
print("构造后的模型名:", reranker.model)

# 构造完成后再改模型名,绕开校验器的强制覆盖
reranker.model = "gte-rerank-v2"

# 再打印一次,确认已经改成可用模型
print("修正后的模型名:", reranker.model)

# 把「BM25 粗召回」和「重排压缩器」包装成一个检索器
retriever = ContextualCompressionRetriever(
    # base_compressor:负责精排/压缩
    base_compressor=reranker,
    # base_retriever:负责先捞候选
    base_retriever=bm25,
)

# 定义要测试的问题
question = "多少天内可以退货?"

# 先看「不做重排」时 BM25 的原始顺序,便于对比
print("\n【仅 BM25 粗召回】")
# 直接调用 BM25,得到未精排的候选
bm25_docs = bm25.invoke(question)
# 逐条打印 chunk_id 与正文
for i, doc in enumerate(bm25_docs, start=1):
    # metadata 里的 chunk_id 是我们建语料时写入的
    print(f"  {i}. [{doc.metadata['chunk_id']}] {doc.page_content}")

# 再看「粗召回 + 重排」后的结果
print("\n【BM25 + 重排】")
# invoke 一次即可走完两段流水线
rerank_docs = retriever.invoke(question)
# 逐条打印重排分数、编号、正文
for i, doc in enumerate(rerank_docs, start=1):
    # relevance_score 是重排器写入 metadata 的字段;普通 BM25 结果没有它
    score = doc.metadata["relevance_score"]
    # chunk_id 用于人工核对是不是正确答案 as-0001
    chunk_id = doc.metadata["chunk_id"]
    # 打印名次、分数、编号、正文
    print(f"  {i}. {score:.4f} [{chunk_id}] {doc.page_content}")

8.4. 如何读输出 #

重点对比两段:

  1. 仅 BM25:正确答案 as-0001 可能不在第 1(甚至较靠后)
  2. BM25 + 重排:as-0001 应升到第 1,且 relevance_score 明显高于第 2、第 3

示意:

构造后的模型名: gte-rerank
修正后的模型名: gte-rerank-v2

【仅 BM25 粗召回】
  1. [as-0004] 退款将在……
  2. [as-0002] 质量问题 15 日内……
  3. [as-0001] 签收 7 日内……

【BM25 + 重排】
  1. 0.32xx [as-0001] 签收 7 日内可无理由退货……
  2. 0.19xx [as-0004] 退款将在……
  3. 0.17xx [as-0006] 预售商品……

这就是重排的价值:同样的候选集合,换一种「会读内容」的打分方式,名次被纠正了。

8.5. 边界行为 #

情况 实际行为
top_n 大于候选条数 有多少返回多少,不报错、不补齐
粗召回得到 0 条 直接返回空列表,不会调用重排 API(省钱)
同一问题连问多次 排名通常稳定,分数小数末位可能微抖

9. 重排之后,分数还能做什么 #

很多人第一反应是:既然有了 relevance_score,那就设个阈值,低于 0.2 就拒答。

9.1. 为什么不建议用重排分做拒答阈值 #

重排分数回答的是:

在当前这批候选里,谁相对更相关?

它不保证跨问题可比。常见现象:

所以:

9.2. 重排分适合干什么 #

更合适的用途:

  1. 展示给开发者看:调试时打印分数,观察区分度
  2. 截断条数:已经通过 top_n 完成
  3. 人工抽检:分数接近时,人工看是不是同族干扰项太多

记住口诀:

重排分用来排序,不用来判生死。

10. 开始常见坑 #

这一节集中列出自学时最容易踩的雷,建议收藏。

10.1. 坑:指望重排拯救糟糕的召回 #

如果正确答案根本没进候选集,重排只能把「一堆错的」排得整整齐齐。

检查顺序应是:

  1. 正确答案是否在粗召回结果里?
  2. 在的话,重排有没有把它顶到前面?
  3. 都不对,再查语料切分、同义词、权限过滤等问题

10.2. 坑:把召回分和重排分直接比较 #

两套分数量纲不同。重排完成后,下游只看重排后的顺序和 relevance_score。

10.3. 坑:模型名写成旧的 gte-rerank #

旧模型会 403。用 LangChain 封装时务必:

# 先构造重排器,只关心 top_n
reranker = DashScopeRerank(top_n=3)
# 再强制改成仍在服务的模型名
reranker.model = "gte-rerank-v2"

10.4. 坑:对中文 BM25 忘记分词 #

BM25 默认常按空格切词。中文句子没有空格时,整句会被当成一个词,所有文档得分可能变成 0,而且常常不报错。

本文里用 jieba.lcut 就是为了避开这个静默失败。

10.5. 坑:把重排分数当成精确常量 #

不要写:

# 错误示范:把某一次调用的分数当成永恒真理
if score == 0.3256:
    pass

小数末位有噪声。展示时保留 2~4 位即可。

10.6. 坑:候选太多导致又慢又贵 #

交叉编码器要对每个候选跑一遍。实践里常见配置:

开始先从「召回 5~10、重排留 3」开始,够用了。

11. 小结 #

把整篇压成几句:

  1. 召回求不漏,重排求排对。
  2. 双塔适合海选,交叉编码器适合决赛。
  3. 重排不能扫全库,只能精修候选。
  4. 通义侧优先用 gte-rerank-v2;LangChain 封装要构造后再改 model。
  5. 重排分用来排序,不适合单独当拒答阈值。
  6. 候选里没有正确答案时,先修召回,再谈重排。

推荐的开始默认组合:

BM25(或 向量 / 混合)粗召回
        +
gte-rerank-v2 精排 top_n=3
        +
大模型基于这 3 条生成答案