Embedding 与 Reranker 入门:先找得到,再排得准
正在读取学习记录…
Embedding 模型把查询和资料变成可比较的数字表示,常用于从大量资料里先找候选;Reranker 再把查询与少量候选放在一起细看,重新排序。可以把它想成先从书架挑几本可能相关的书,再逐本翻看哪本更合用。
这里先用一张人工分数表走完“找候选 → 重排 → 取证据”的正常路径。之后再看报销查询里的两种问题:正确规定根本没被找出,或者找出了却被排到后面。先读过向量与相似度会更顺;这些手工分数不是模型实测效果。
0. 这两个模型在 Agent 中分别做什么
找一本能回答“报销要哪些材料”的书,可以分两步:先从书架上挑出几本报销相关的书,再逐本翻看哪本写到了所需材料。检索系统也有这两步:Embedding 将查询和文档分别转成向量,用向量比较来挑候选;Reranker 接收“查询 + 候选文档”,给相关性打分并重新排列。
先记住这两个动作:找出候选,排好候选。排在前面的资料接着交给生成模型,用于组织回答。下一节会用具体数量看清两步如何连接。
阅读时查名词:资料集合、文本块与候选数量
| 名词 | 第一次阅读时先这样理解 | 暂时不要误认为 |
|---|---|---|
| 语料库 | 允许检索的一组原始文档及版本 | 模型参数里自带的全部知识 |
| 文本块 chunk | 从文档切出的可定位片段 | 随便截取的一段字符 |
| Embedding | 把输入编码为向量的模型或过程 | 正确答案评分器 |
| 召回 | 从大范围取候选 | 最终回答 |
| Reranker | 对现有候选重新评分排序 | 能找回候选集之外的文档 |
| Top-K | 最多保留 K 个候选 | 质量已经合格的证明 |

1. 为什么要拆成两个阶段
把允许搜索的资料集合叫作语料库。假设里面有 N 段文本:先从中找出 K 个可能相关的候选,这一步叫召回;再细排这些候选,取前 M 段用于回答。K 是召回数量,M 是最终保留数量,通常 M 不大于 K。例如从 1000 段资料里先找 20 段,再挑 3 段,比每次都逐段细读 1000 段更容易控制计算量。
N 很小时,可以直接对全部段落重排;资料较多时,两阶段能减少需要细读的候选。K 太小则可能漏掉答案,因此要同时比较耗时和召回结果。Sentence Transformers 的官方教程同时展示了两阶段检索和小规模文档直接重排。检索与重排教程
| 阶段 | 输入 | 输出 | 首先检查什么 |
|---|---|---|---|
| 准备索引 | 文档、切分规则、版本和访问权限 | 可检索的文本块及元数据 | 目标规定是否真的入库 |
| 召回 | 查询、允许访问的文档范围 | K 个候选及召回分数 | 相关证据是否进入候选 |
| 重排 | 查询与每个候选文本 | 新的相关性分数与顺序 | 已召回的正确证据是否靠前 |
| 组装上下文 | 候选、Token 预算、来源约束 | 送给模型的证据片段 | 关键限定条件是否被截断 |
| 生成与核验 | 问题、证据 | 回答与引用 | 引用是否支持具体结论 |
从候选到答案还要经过两步:选出完整证据片段,再据此生成并核对回答。重排负责顺序,制度版本和引用内容则在后面分别核对。
2. Embedding:提前把文档放进可比较的空间
Embedding 模型把文本编码成向量。离线阶段,为每段文档算一次向量并保存;查询到来时,计算查询向量,再寻找接近它的文档向量。这样通常不必在每次查询时重新编码全部文档。
“查询”和“文档”不一定使用完全相同的前缀或提示。有些检索模型针对两类输入要求不同模板。关键是遵守该模型训练时的输入约定,并让查询、文档表示处在兼容的检索空间。只检查维度相同不够:两个模型都输出 768 维,也不意味着可以互相比较。
3. Reranker:让查询与候选一起参与判断
常见的交叉编码器(Cross-Encoder)把“查询、候选文本”作为一对输入,让二者在模型内部共同参与计算,输出相关性分数。这和“分别算两个向量,再求余弦”不是同一件事。
因此,更换查询通常需要重新评估每个查询—候选对;不能像独立文档向量一样,提前为文档存一个对所有问题通用的重排分数。批处理可以提高吞吐,但没有消除候选对的计算。实际成本还取决于文本长度、模型、硬件和批大小。
| 比较点 | 常见双编码召回 | 常见交叉编码重排 |
|---|---|---|
| 文本输入 | 查询和文档分别编码 | 查询和文档成对编码 |
| 文档计算的复用 | 文档向量可以提前缓存 | 同一文档面对不同查询通常要重新评分 |
| 使用范围 | 大范围候选筛选 | 已有候选的细排 |
| 分数含义 | 指定空间和度量下的匹配程度 | 特定模型的相关性评分 |
| 主要风险 | 漏召回、空间不匹配、近似检索误差 | 候选外证据无法补回、截断、排序偏差 |
进阶:其他检索表示与分数变换
稀疏检索常按词项匹配,多向量检索为一段文本保留多个表示,LLM 重排则让语言模型比较候选。研究这些变体时,分别检查表示和评分方法。
如何理解 0.9 的相似分数
不同模型可能输出原始分数,也可能经过 Sigmoid 变换。Sigmoid 是把任意实数映射到0到1的数学函数;它改变数值范围,业务正确率则需要带参考答案的数据检验。BGE Reranker 模型卡展示了两种输出方式。
同样不能直接把余弦 0.81 与重排分数 4.2 相加,便宣称得到了“综合可信度”。如果需要融合,必须明确分数处理和参数,用独立验证集检查;不要凭数字大小猜含义。
4. 手算一次:召回决定了重排能看到什么
把查询明确为:“出差报销没有纸质发票时,可以用什么凭证,需要谁审批?”四份短资料如下:
- A:出差申请需填写目的地和出行日期,由直属主管确认。
- B:无纸质发票时,可提交电子发票和付款凭证。
- C:住宿费用按城市等级设置每日金额上限。
- D:使用替代凭证报销,需直属主管批准并由财务复核。
B 回答凭证,D 回答审批;两份合起来回答完整问题。A、C 虽然也讲差旅,却没有这两个答案。相关标签先按资料内容判断,再看下面的分数。
下面两列分数都是人为指定。重排分数也不是事实正确率;故意把 B、D 放得较高,是为了观察“有能力把相关内容排前面,但看不到漏掉文档”的情形。
| 文档 | 内容摘要 | 相关标签 | 召回分数 | 假设的重排分数 |
|---|---|---|---|---|
| A | 出差申请流程 | 否 | 0.93 | 0.10 |
| B | 无纸质发票时可接受的凭证 | 是 | 0.89 | 0.95 |
| C | 住宿报销金额上限 | 否 | 0.87 | 0.15 |
| D | 替代凭证适用的审批条件 | 是 | 0.83 | 0.90 |
当 K=2,候选只有 A、B。重排后变成 B、A;D 根本没有输入重排器,不能凭它本来可能得到 0.90 就突然出现在结果中。当 K=4,四段都进入候选,重排后的前两段才是 B、D。
用检索指标把差异写清楚:
- 候选 Recall@K:候选中相关文档数量 ÷ 语料中标注相关文档数量。本例 K=2 时为 1/2,K=4 时为 2/2。
- 最终 Precision@2:保留的两段中相关文档数量 ÷ 2。K=2 时为 1/2,K=4 时为 2/2。
- 本例最终 Recall@2:最终两段中相关文档数量 ÷ 2,也分别为 1/2 和 2/2。分母相同是因为本例恰有两段相关文档,不是两个指标天然相同。
这只验证了手工数据的逻辑,不能推导出“把 K 调成 4 就能达到 100%”。真实语料的相关文档集合往往还标得不全;此时分母本身就需要说明。
自己运行这组计算
把下方两个完整文件保存为 candidate_boundary.py 和 test_candidate_boundary.py。程序使用上表的固定分数,依次执行候选截取、重排和指标计算。
保存为 candidate_boundary.py:
"""用固定分数观察候选召回与重排的分工。"""
# 每行依次是资料编号、召回分数、重排分数。
rows = [
("A", 0.93, 0.10),
("B", 0.89, 0.95),
("C", 0.87, 0.15),
("D", 0.83, 0.90),
]
# 人工参考答案:B 和 D 是相关资料。
relevant = {"B", "D"}
def evaluate(k):
if type(k) is not int or k <= 0:
raise ValueError("k must be a positive integer")
# 先按召回分数取前 k 篇资料。
candidates = sorted(rows, key=lambda row: (-row[1], row[0]))[:k]
# 重排只对刚才选中的候选重新排序。
ranked = sorted(candidates, key=lambda row: (-row[2], row[0]))
top = [row[0] for row in ranked[:2]]
candidate_ids = {row[0] for row in candidates}
# 统计相关资料有多少进入候选,以及前两名有多少相关。
candidate_recall = len(candidate_ids & relevant) / len(relevant)
precision = len(set(top) & relevant) / len(top)
assert set(top) <= candidate_ids
return {"k": k, "top": top, "candidate_recall": candidate_recall, "precision": precision}
if __name__ == "__main__":
for k in (2, 4):
result = evaluate(k)
print(k, result["top"], result["candidate_recall"], result["precision"])完整代码:test_candidate_boundary.py
# 核对Embedding 与 Reranker 入门的正常结果和错误分支;断言失败时会显示对应的测试。
import unittest
from candidate_boundary import evaluate
class CandidateBoundaryTest(unittest.TestCase):
def test_small_candidate_set_cannot_recover_missing_document(self):
result = evaluate(2)
self.assertEqual(result["top"], ["B", "A"])
self.assertEqual(result["candidate_recall"], 0.5)
self.assertEqual(result["precision"], 0.5)
def test_larger_candidate_set_exposes_both_relevant_documents(self):
result = evaluate(4)
self.assertEqual(result["top"], ["B", "D"])
self.assertEqual(result["candidate_recall"], 1.0)
self.assertEqual(result["precision"], 1.0)
def test_non_positive_or_boolean_k_is_rejected(self):
for value in [0, -1, True, 1.5]:
with self.subTest(value=value), self.assertRaises(ValueError):
evaluate(value)
if __name__ == "__main__":
unittest.main(verbosity=2)在空文件夹中保存源码后,在终端中先确认路径和文件名,再执行:
pwd
ls
python --version
python -B -X utf8 candidate_boundary.py
python -B -X utf8 -m unittest -v test_candidate_boundary.py这两个文件使用标准库。如果得到 can't open file,检查终端当前目录和实际文件名。
# 按固定输入观察Embedding 与 Reranker 入门的处理过程和返回结果。
rows = [
("A", 0.93, 0.10),
("B", 0.89, 0.95),
("C", 0.87, 0.15),
("D", 0.83, 0.90),
]
relevant = {"B", "D"}
for k in (2, 4):
candidates = sorted(rows, key=lambda row: (-row[1], row[0]))[:k]
ranked = sorted(candidates, key=lambda row: (-row[2], row[0]))
top = [row[0] for row in ranked[:2]]
candidate_ids = {row[0] for row in candidates}
candidate_recall = len(candidate_ids & relevant) / len(relevant)
precision = len(set(top) & relevant) / len(top)
print(k, top, candidate_recall, precision)
assert set(top) <= candidate_ids对照手算,预期打印 2 ['B', 'A'] 0.5 0.5 和 4 ['B', 'D'] 1.0 1.0。第二排序键是文档 ID,作用是在分数相同时保持确定顺序,不是新的相关性判断。
跟着核心源码理解候选边界
rows 每一行依次保存文档 ID、人工设定的召回分数和人工设定的重排分数;relevant 是独立写下的相关文档集合。第一次 sorted 按召回分数降序取前 K,第二次只在这 K 行中按重排分数再排序:
candidates = sorted(rows, key=lambda row: (-row[1], row[0]))[:k]
ranked = sorted(candidates, key=lambda row: (-row[2], row[0]))set 表示不重复的名单。candidate_ids & relevant 取两份名单的交集,即“同时在候选里、也在相关名单里”的文档:K=2时只有B,除以相关名单的两篇就是0.5。set(top) <= candidate_ids 检查 top 的每一篇都属于候选名单,这里的 <= 表示集合包含关系。
负号把高分排在前面;同分时再比较文档 ID。[:k] 保留前k篇:D在K=2时已经被切掉,重排只能比较眼前的A、B,无法把D变回来。
这里用固定分数,单独观察候选范围如何限制重排结果。先只改 K;如果同时换模型、分块和 K,就很难判断哪一步改变了结果。
三项单元测试分别固定 K=2、K=4 和非法 K。前两项防止后续修改悄悄改变候选边界,第三项拒绝 0、负数、布尔值和浮点数。修改候选截取或排序代码时,这三项检查会帮助发现结果变化。
代码依赖这个固定夹具:ID 唯一、相关集合非空、候选不少于两条。扩展成通用函数时必须处理空集合;没有相关标签时不要除以零,更不要自动填成 100%。题库会要求你明确这些边界。
进阶:为索引保留原文、版本与访问范围
一个文本块应当随身带着什么
假设入库的片段编号是 travel-2026-17,只保存向量还不够。至少要能找回原文、文档标题、章节、版本、生效日期和访问控制信息。否则搜索结果虽然有高分,却无法回答“引用的是哪版制度”。
| 字段 | 示例 | 为什么要保留 |
|---|---|---|
| chunk_id | travel-2026-17 | 去重、定位和追踪同一片段 |
| document_version | travel-policy-2026-08 | 防止新旧制度混用 |
| text | 完整规定及例外条款 | 重排和最终引用都需要原文 |
| source | 内部文档路径及章节 | 让读者核对依据 |
| embedding_revision | 模型与编码配置版本 | 判断索引是否需要重建 |
| access_scope | 已授权部门范围 | 限制候选和外部模型可见内容 |
向量相似不等于含义完全一致。“无发票允许报销”和“无发票不允许报销”共享许多词,业务结论却相反。因此领域术语、否定、数字、时间和例外条件都应该出现在测试查询中。
5. 从手算走到真实检索实验
候选截取和重排顺序已经看清了。接入真实模型时,把固定分数表替换成模型计算的分数,再用同一份问题集比较效果。
一个可复核实验可以分三组进行:
| 组别 | 固定项 | 改变项 | 能回答的问题 |
|---|---|---|---|
| 召回比较 | 语料版本、查询、标签、K | 词法检索与向量检索 | 谁更容易找全所需证据 |
| 重排比较 | 完全相同的候选列表、M | 不重排与指定重排模型 | 候选相同时排序是否改善 |
| 端到端比较 | 问题集与证据判定标准 | 完整检索配置 | 最终回答是否更可靠,代价如何 |
比较两种重排器时,保持切分、Embedding 和 K 相同,把变化集中在重排器上,就容易解释差异来自哪里。记录模型完整标识与 revision、库版本、设备、输入截断长度、批大小、查询模板、语料校验值及原始排序结果。模型下载和计算也有资源成本,应提前指定磁盘缓存目录,不在入门代码里隐式下载大型权重。
查询集应包含简称、错别字、否定、跨段条件、过期制度、查无此据和越权请求。调参数用验证集,最后再看保留测试集;如果看过测试集后反复调整阈值,它就不能继续充当未见测试集。
算出“找回了多少相关资料”和“返回的资料里有多少真正相关”之后,再看结果出现在第几位。例如用户通常只看前三条,第十条才出现正确政策也会影响体验。评估报告还应列出样本数量、标签怎么定、失败案例、响应时间和运行条件;一张“准确率提高”的图不足以说明原因。需要更细的排名指标时,再按任务选择 MRR 或 nDCG 并核对各自的计算方式。
6. 找不到答案时,按这张表排查
| 症状 | 优先查什么 | 下一步操作 |
|---|---|---|
| 目标规定不在任何结果中 | 入库是否成功、版本和权限、切分是否丢掉原文 | 按文档 ID 查找原文与文本块 |
| 原文在库,但不在前 K | 查询模板、关键词基线、向量空间、K、索引配置 | 对同一查询打印召回名单 |
| 在候选中,重排后靠后 | 查询—文档对、输入截断、领域适配、分数方向 | 核对传入文本与模型分数定义 |
| 排在前面却没有进入回答 | 上下文预算、去重、片段拼接和最终请求 | 查看真正发给模型的资料 |
| 引用了过期或越权内容 | 生效日期和授权过滤、缓存隔离 | 在检索和缓存读取时筛选可用内容 |
| 文档相关但结论错误 | 否定、条件、引用范围及生成过程 | 对照原文条件核对回答 |
权限过滤应发生在未授权内容进入后续可见范围之前,尤其不能先把内容发给外部重排服务,再在页面上隐藏。缓存也需要考虑授权范围,否则可能把别人的候选结果复用过来。
重排服务超时怎么办?可以评估退回原召回顺序,但应明确记录降级状态,不把原分数冒充重排分数。在高风险场景,停止回答、提示证据不足可能比悄悄降级更合适。策略取决于实际后果,需要测试,不是一条通用默认值。
7. 用开源工具实现召回和重排
把刚才的手工分数换成模型计算,可以从以下工具的教程和模型卡入手。先看一个完整的两阶段检索例子,再了解具体编码器和重排器的输入输出。
| 顺序 | 项目或资料 | 建议阅读内容 | 对应的知识点 |
|---|---|---|---|
| 1 | Sentence Transformers | 官方 Retrieve & Re-Rank 教程 | 输入输出、候选范围、双编码与交叉编码的差别 |
| 2 | FlagEmbedding | Reranker 使用入口与评估资料 | 如何构造查询—文档对,哪些配置影响分数 |
| 3 | BGE Reranker 模型卡 | Usage、分数映射与模型限制 | 原始分数与映射分数的区别,自己的任务需要验证什么 |
先看模型卡的任务和使用说明:编码器输入一段文字、输出向量;重排器输入查询与文档对、输出相关性分数。按目标选择对应接口。FlagEmbedding 提供编码器和重排器实现,可以按相应模型卡继续研究。
8. 练习与实践作品
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 召回和重排有什么区别?
粗检索选出20段候选后,reranker对它们重新评分。哪项说法正确?
对应知识点:L0-embeddings
简答题 · 重排分数是正确概率吗?
某重排模型把原始分数经过 Sigmoid 变成0.9。产品想展示“该回答90%正确”。说明这个结论缺少什么,并解释为什么不能直接把它与余弦分数相加。
对应知识点:L0-embeddings
代码题 · 验证候选集的上限
使用正文A/B/C/D夹具,将候选截取和重排封装为函数。验证K=2和K=4的输出分别为[B,A]和[B,D];加入空候选、空相关集合和同分测试,明确未定义指标如何表示。
对应知识点:L0-embeddings
场景题 · 召回增加,延迟也增加
K从20调到100后候选召回率提高,但重排延迟超标。你会怎样设计对照实验,区分K、模型与文本截断的影响?如果重排超时,如何选择和记录降级?
对应知识点:L0-embeddings
先沿用四条资料与两组固定分数,把候选数量 K 从2改为4,比较 D 是否进入候选。再把 K 设为3:前三名仍是 A、B、C,D 还是不在重排范围里。这个中间值可以帮助你理解,改善召回依赖“正确资料进入”,不只是数字变大。
把正确文档移出候选集合,再运行重排:它会出现在最终结果里吗?随后增加候选数量,观察它是否重新进入比较。能解释这两次差异,就理解了“先找候选、再排顺序”的分工。
9. 总结与讨论
Embedding 召回解决大范围筛选,常见 Cross-Encoder 在候选内部重新判断相关性。先问正确证据有没有进入候选,再问顺序是否合理,最后问上下文与引用是否保留了条件。这样才能把“回答不好”拆成可检查的环节。
试着给同学解释:如果增加 K 提高了召回,却使重排延迟超出要求,你会先改变什么?把语料规模、查询类型、候选数量和延迟测量条件写在答案旁。只说“换更强的模型”,别人无法复现你的判断。练习请使用教学数据,不放公司原文或个人信息。
这里最重要的反例是文档 D:它是正确资料,却在 K=2 时没进候选集。重排器只能比较眼前这两篇,无法把 D 从库里“变回来”。把 K 改成 4,在固定手工分数下,候选召回从 0.5 变成 1。
留笔记时请画两张短名单,标上每篇的 ID、人工标签、召回顺序和重排顺序。这样一眼就能看出错在“没找着”还是“排得靠后”。两阶段方法可参照 Sentence Transformers Retrieve & Re-Rank。下一篇读模型 API 配置。
参考资料
- Sentence Transformers:Retrieve & Re-Rank:双阶段检索的官方教程。
- FlagEmbedding:BGE 系列训练、推理和评估实现入口。
- BGE Reranker v2 M3 模型卡:输入形式、分数处理和示例限制。
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。