ZenML RAG 流水线中的重排序实现:rerankers 包接入与 Top-20→Top-5 策略全解
【免费下载链接】zenmlZenML 🙏: One AI Platform from Pipelines to Agents. https://zenml.io.项目地址: https://gitcode.com/GitHub_Trending/ze/zenml
本文基于 ZenML LLM Ops 指南中的《Implementing Reranking in ZenML》一章,讲解如何在已有的 RAG 推理流水线中接入文档重排序(reranking)能力:从rerankers包的 API 语义、rerank_documents辅助函数的完整实现,到"先取 Top-20、再重排取 Top-5"的工程策略,帮助读者在自己的 RAG 系统中落地一条可评估、可开关的重排序链路。读完后你将掌握:如何在流水线中可选地插入 reranker、如何保留文档原始元数据(如 URL)、以及如何为后续的检索评估留出对比入口。
重排序在 RAG 流水线中的位置
ZenML 官方 LLM Ops 指南的重排序章节建立在一条已经跑通的 RAG 流水线之上:数据摄取与预处理、嵌入(embeddings)生成、向量检索这些步骤已经就位,并且已经配置了基础的检索评估指标。重排序是在这条链路之上"可选叠加"的一环——它读取初检(initial retrieval)步骤返回的文档集合,针对原始查询对这些文档重新排序,把最相关的文档推到最前面,从而让下游 LLM 拿到质量更高的上下文。
关于 reranker 的类型(cross-encoder、bi-encoder、轻量级模型)及其收益的完整背景,可参考同目录下概念性文档 Understanding Reranking。
从源码结构看,ZenML 框架核心(src/zenml)中并没有内建专门的 reranking 模块——重排序是 RAG 应用层的组件,它作为一个普通 Python 依赖在 pipeline step 内部被调用,再通过 ZenML 的@step/@pipeline机制编排。这意味着接入 reranker 的自由度很高:换模型、换供应商、加缓存,都只需要改 step 内部的实现,不影响框架层。
为什么选用 rerankers 包
原文档选择rerankers作为重排序实现层(包名rerankers,此处不附外部链接),给出的理由值得借鉴:
- 低技术债:它是一个轻量依赖,接口统一,不需要针对每种模型单独写适配代码;
- 覆盖主流模型类型:该包提供了 Hugging Face Hub 上的开源 reranker 模型、API 驱动的商用模型(如 Jina、Cohere 等)的统一入口,也允许通过抽象基类
Reranker自定义实现; - 输入输出契约简单:reranker 接收一个查询和一组待重排文档,输出按重排分数排序的文档列表。
一个最小的用法示例如下(cross-encoder模型):
from rerankers import Reranker ranker = Reranker('cross-encoder') texts = [ "I like to play soccer", "I like to play football", "War and Peace is a great book" "I love dogs", "Ginger cats aren't very smart", "I like to play basketball", ] results = ranker.rank(query="What's your favorite sport?", docs=texts)调用后得到的RankedResults对象形如:
RankedResults( results=[ Result(doc_id=5, text='I like to play basketball', score=-0.46533203125, rank=1), Result(doc_id=0, text='I like to play soccer', score=-0.7353515625, rank=2), Result(doc_id=1, text='I like to play football', score=-0.9677734375, rank=3), Result(doc_id=2, text='War and Peace is a great book', score=-5.40234375, rank=4), Result(doc_id=3, text='I love dogs', score=-5.5859375, rank=5), Result(doc_id=4, text="Ginger cats aren't very smart", score=-5.94921875, rank=6) ], query="What's your favorite sport?", has_scores=True )从这个输出可以直接看出 reranker 的工作语义:结果按分数升序排列(rank=1 为最相关),每条Result携带doc_id(指向输入docs列表的下标)、重排后的text、score与最终rank。示例中与"运动"相关的三条文档排到了最前面,与查询无关的动物、书籍类文档沉底——这正是重排序希望达到的效果。
除了cross-encoder,rerankers包还支持从 Hugging Face Hub 加载其它开源 reranker 模型、使用 API 驱动的服务端模型,或自行继承Reranker抽象类定义模型,具体配置方式以该包官方文档为准。
实现 rerank_documents 辅助函数
在 ZenML 的 RAG 流水线里,重排序被封装成一个可以按需调用的辅助函数。完整实现如下:
def rerank_documents( query: str, documents: List[Tuple], reranker_model: str = "flashrank" ) -> List[Tuple[str, str]]: """Reranks the given documents based on the given query.""" ranker = Reranker(reranker_model) docs_texts = [f"{doc[0]} PARENT SECTION: {doc[2]}" for doc in documents] results = ranker.rank(query=query, docs=docs_texts) # pair the texts with the original urls in `documents` # `documents` is a tuple of (content, url) # we want the urls to be returned reranked_documents_and_urls = [] for result in results.results: # content is a `rerankers` Result object index_val = result.doc_id doc_text = result.text doc_url = documents[index_val][1] reranked_documents_and_urls.append((doc_text, doc_url)) return reranked_documents_and_urls这段代码里有几个工程要点值得逐条拆解:
- 模型可配置,默认
flashrank。参数reranker_model的默认值是"flashrank",原文档说明这是开发阶段实测后选定的默认值。由于Reranker(model_name)的构造器接受字符串标识,切到cross-encoder、flashrank-fast或任何 Hugging Face Hub 上的模型只需要改这一个参数,函数签名与调用方均无需变动。 - 送入 reranker 的文本拼接了元数据:
docs_texts = [f"{doc[0]} PARENT SECTION: {doc[2]}" for doc in documents]。这里把文档正文(doc[0])与其所属父章节(doc[2])拼在一起参与打分——章节标题往往携带语义信息(例如文档实际来自 "feature-stores" 章节),有助于 reranker 更准确地判断相关性。 - 用
doc_id找回原始元数据。重排后只保留了 text 和分数,但业务上还需要每条文档的 URL 作为答案引用。rerankers的Result.doc_id就是输入docs列表中的下标,因此documents[result.doc_id][1]即可把重排后的文本与原始 URL 重新配对。这一步是重排序链路里最容易被忽略的细节:一旦丢失了这个映射,就无法把重排结果追溯回原始文档。 - 返回值契约稳定:函数返回
List[Tuple[str, str]],每个元素是(重排后的文档文本,原始 URL),调用方不需要关心rerankers内部的RankedResults结构。
在检索入口集成重排序:Top-20 检索、Top-5 返回
重排序真正发挥价值的前提是"宽进严出":初检阶段多取一些候选,给 reranker 足够大的重排空间,但最终只把最精华的少数文档交给 LLM。原文档中的检索函数query_similar_docs完整体现了这一策略:
def query_similar_docs( question: str, url_ending: str, use_reranking: bool = False, returned_sample_size: int = 5, ) -> Tuple[str, str, List[str]]: """Query similar documents for a given question and URL ending.""" embedded_question = get_embeddings(question) db_conn = get_db_conn() num_docs = 20 if use_reranking else returned_sample_size # get (content, url) tuples for the top n similar documents top_similar_docs = get_topn_similar_docs( embedded_question, db_conn, n=num_docs, include_metadata=True ) if use_reranking: reranked_docs_and_urls = rerank_documents(question, top_similar_docs)[ :returned_sample_size ] urls = [doc[1] for doc in reranked_docs_and_urls] else: urls = [doc[1] for doc in top_similar_docs] # Unpacking URLs return (question, url_ending, urls)逐点看这个实现:
use_reranking开关决定候选集大小。num_docs = 20 if use_reranking else returned_sample_size:开启重排时从 PostgreSQL 向量库取 Top-20 候选;不开启时直接取 Top-5。这条分支是后文做"重排前后对比评估"的关键——同一个函数、同一套评估问题,只切换这个布尔开关,就能得到两条可比的检索基线。- 截断发生在重排之后。
rerank_documents(..., top_similar_docs)[:returned_sample_size]先对 20 条候选整体重排,再切片取前 5 条。也就是说最终返回给下游的永远是 Top-5,但"选哪 5 条"由 reranker 的分数说了算,而不是由向量相似度说了算。 - 元数据随取随带。
get_topn_similar_docs(..., include_metadata=True)保证取回的每条文档是包含 content、url、父章节等信息的元组,这正是rerank_documents中doc[2]拼接和doc[1]取 URL 能够成立的前提。 - 返回值三元组
(question, url_ending, urls)中保留了url_ending,它是评估阶段判断"期望文档是否被检索到"的对照基准(见下文)。
需要强调的运行前提:该函数依赖get_db_conn()返回的 PostgreSQL 连接以及已入库的嵌入数据,因此必须先完成指南前文所述的嵌入生成与向量入库步骤,重排序链路才有数据可排。
与评估链路打通:为重排效果留好对照面
原文档在集成完重排后指出:现在可以评估 reranker 的性能,观察它对检索质量的影响。配套的评估实现(对比use_reranking=True/False两条基线的失败率、用 ZenML Dashboard 可视化对比结果)在下一章 Evaluating Reranking Performance 中完整展开,其数据基础正是本节query_similar_docs暴露的use_reranking参数。
从本节的代码结构可以推断出重排效果评估的可行路径:
- 同一问题集、同一函数、仅切换开关——
perform_retrieval_evaluation(sample_size, use_reranking)对use_reranking的两次调用天然构成对照实验,排除了数据集、模型版本等混杂变量; - 以 URL 命中作为判定标准——
query_similar_docs返回的urls与url_ending做包含判断,命中即通过。重排的价值最终体现为:重排后 Top-5 里期望文档 URL 的出现率是否上升; - 失败样例可追溯——评估日志会打印每条失败问题的期望 URL 与实际取回 URL,便于人工判断是 reranker 排序偏差,还是嵌入检索本身召回不足(指南中后续章节正是基于这类观察,决定转向"微调嵌入模型"的方向)。
完整代码位置与实践建议
本文档中的rerank_documents与query_similar_docs出自 ZenML 官方的 LLM Complete Guide 示例项目(zenml-projects仓库中的llm-complete-guide目录,重排相关实现集中于steps/eval_retrieval.py)。该示例仓库不在本仓库内,建议直接克隆后对照本文代码阅读,重点看 step 如何声明输入输出、评估 step 如何消费重排开关参数。
落地到自建 RAG 系统时,可以按本文结构复用三条经验:
- 把 reranker 封装成薄函数,模型名做成参数而不是硬编码,保留从
flashrank切到cross-encoder或 API 模型的能力; - 重排前多取候选(如 4~5 倍于最终条数),重排后严格截断,让 reranker 在更大的候选池里做精排;
- 用
doc_id(输入下标)建立重排结果与原始元数据的映射,保证 URL 等引用信息在重排后不丢失,并为下游评估提供可判定的输出契约。
最后提醒:重排是"锦上添花"而非必选项。如果向量检索本身召回质量不高(评估中不带重排的基线分数也偏低),优先方向是改进嵌入模型或检索配置,而不是依赖 reranker 弥补召回缺陷——这一点在 评估重排性能 一节的实验结论中得到了验证。
【免费下载链接】zenmlZenML 🙏: One AI Platform from Pipelines to Agents. https://zenml.io.项目地址: https://gitcode.com/GitHub_Trending/ze/zenml
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考