Guide · 使用教程
FlashRank Python 教程:候选文档重排、中文评估与延迟测试
固定真实多语言 ONNX 模型,验证候选来源与 token 长度,记录本地延迟,并保留中文退款问题排序失败的例子。
这篇教程会完成什么
你会在 Python 中给一组中文候选片段重排,保留来源信息,拒绝超长输入,并分别测量初始化和推理耗时。示例使用公开的合成退款文本,不连接向量数据库、不调用付费模型 API,也不自动执行退款或其他业务操作。
先明确一个实际结果:这次多语言模型把“关闭自动续费不会自动退款”的片段放在“七天内且未使用可申请退款”的直接规则之前。程序成功返回,不代表答案证据排对了。下面会保留这个现象,说明工程校验与质量验收应该分开。
资料与本地验证日期为 2026-10-21,固定 FlashRank 0.2.10、Python 3.11,使用 ms-marco-MultiBERT-L-12 的 ONNX 路径。本文没有统一中文检索基准,四条合成文本不能证明模型在真实业务中的好坏,也不能给出生产 p95。
开始前需要准备
准备可运行 Python 3.11 的环境、能安装依赖及下载模型的网络,以及持久可写的模型缓存目录。其他 Python 与操作系统组合先检查 ONNX Runtime 是否提供对应安装包。本次默认 Python 3.14 没有成功解析到所需 ONNX Runtime 包,改用 3.11 后完成安装;这不是对所有平台的兼容性判断。
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install 'flashrank==0.2.10'
python -m pip freeze > requirements.lock.txt
首次模型初始化可能下载权重,之后才有条件验证缓存下的本地运行。不要把库代码 Apache-2.0 直接套到权重上:prithivida/flashrank 文件仓库标注 CC-BY-SA-4.0,并说明署名与回链要求,具体上游模型也应核对。缓存应来自可信来源,生产环境固定文件版本并校验完整性,不临时加载不明模型包。
第一步:先确认候选已被召回
重排只处理传入的候选,不负责遍历整个知识库。这里直接构造四条片段,模拟检索器已经找出的结果。真实应用要先根据认证身份执行访问控制,限制来源和版本,之后才把允许的候选送进重排。
本文函数中的租户一致性检查只是额外防错:tenant 必须来自服务端可信会话,不能相信请求里随便提交的租户名;候选的 meta 也必须来自可信数据库记录。相同字段不等于用户具有访问权。示例不实现登录、数据库过滤或完整授权。
第二步:保存完整脚本
将下面代码保存为 rerank_demo.py。它只支持本例的 pairwise ONNX 模型;先复制模型 tokenizer 并关闭截断,用查询与候选的组合输入检查真实 token 长度。候选上限 30、返回上限 10 是本例的应用限制,不是 FlashRank 的官方通用限额。
FlashRank 当前 pairwise 实现会添加分数并改变候选顺序,因此传入深复制,避免污染调用方的数据。返回后检查 ID、原文和来源未变,并把数值分数转为普通 Python 浮点数以便 JSON 序列化。
"""FlashRank 0.2.10 pairwise/ONNX example; synthetic, public demo data only."""
import copy
import json
import math
import os
import time
from importlib.metadata import version
from flashrank import Ranker, RerankRequest
from tokenizers import Tokenizer
def checked_rerank(ranker, query, candidates, tenant, top_k=2):
if not isinstance(query, str) or not query.strip():
raise ValueError('query must be nonempty')
if not isinstance(tenant, str) or not tenant.strip():
raise ValueError('tenant must come from authenticated server context')
if type(top_k) is not int or not 1 <= top_k <= 10:
raise ValueError('top_k must be between 1 and 10')
if not 1 <= len(candidates) <= 30:
raise ValueError('candidate count must be between 1 and 30')
if ranker.llm_model is not None:
raise ValueError('this example supports only pairwise ONNX models')
# Clone the actual model tokenizer and disable its automatic truncation.
counter = Tokenizer.from_str(ranker.tokenizer.to_str())
counter.no_truncation()
counter.no_padding()
limit = ranker.tokenizer.truncation['max_length']
originals = {}
for p in candidates:
if not isinstance(p, dict):
raise ValueError('invalid candidate')
cid, text, meta = p.get('id'), p.get('text'), p.get('meta')
if not isinstance(cid, str) or not cid or cid in originals:
raise ValueError('candidate IDs must be nonempty and unique')
if not isinstance(text, str) or not text.strip():
raise ValueError('candidate text must be nonempty')
if not isinstance(meta, dict) or meta.get('tenant_id') != tenant:
raise ValueError('tenant mismatch; reject before model execution')
if not meta.get('source') or not meta.get('version'):
raise ValueError('source and version required')
if len(counter.encode(query, text).ids) > limit:
raise ValueError('query-document pair exceeds model token budget')
originals[cid] = p
started = time.perf_counter()
ranked = ranker.rerank(RerankRequest(query=query, passages=copy.deepcopy(candidates)))
elapsed_ms = (time.perf_counter() - started) * 1000
if len(ranked) != len(candidates) or {p['id'] for p in ranked} != set(originals):
raise RuntimeError('unexpected output candidate set')
for p in ranked:
original = originals[p['id']]
if p['text'] != original['text'] or p['meta'] != original['meta']:
raise RuntimeError('source data changed')
p['score'] = float(p['score'])
if not math.isfinite(p['score']):
raise RuntimeError('non-finite relevance score')
if any(a['score'] < b['score'] for a, b in zip(ranked, ranked[1:])):
raise RuntimeError('unexpected score ordering')
return ranked[:top_k], elapsed_ms
def fixtures():
texts = [
('refund', '退款规则:购买后七天内且未使用服务可申请退款。'),
('password', '登录失败时,可以通过注册邮箱重置密码。'),
('billing', '发票将在付款成功后发送到客户邮箱。'),
('cancel', '关闭自动续费可避免下期扣款,但不会自动退还已付费用。'),
]
return [dict(id=cid, text=text, meta=dict(tenant_id='demo', source='public-demo-policy', version='v1')) for cid, text in texts]
if __name__ == '__main__':
model = os.environ.get('FLASHRANK_MODEL', 'ms-marco-MultiBERT-L-12')
start = time.perf_counter()
ranker = Ranker(model_name=model, cache_dir=os.environ.get('FLASHRANK_CACHE', './model-cache'), max_length=512)
init_ms = (time.perf_counter() - start) * 1000
candidates = fixtures()
before = copy.deepcopy(candidates)
query = '购买后什么情况下可以退款?'
rows, first_ms = checked_rerank(ranker, query, candidates, 'demo')
timings = [checked_rerank(ranker, query, candidates, 'demo')[1] for _ in range(5)]
assert candidates == before
print(json.dumps(dict(flashrank=version('flashrank'), model=model, initialization_ms=round(init_ms, 2), first_rerank_ms=round(first_ms, 2), warm_rerank_ms=[round(t, 2) for t in timings], results=rows, input_unchanged=True), ensure_ascii=False, indent=2))
第三步:准备缓存并运行
export FLASHRANK_CACHE="$PWD/model-cache"
python rerank_demo.py > result.json
运行过程可能显示下载日志。检查 result.json 中的版本、模型名、来源字段、输出顺序和时间。第一次初始化包含权重准备与模型加载,不能当成纯推理耗时;已经缓存后的初始化也不能和首次下载混在一起平均。
本次运行中,最高的候选 ID 是 cancel,直接描述退款条件的 refund 在其后。问题问的是“购买后什么情况下可以退款?”,所以第一名没有完整回答条件。两者分数的差距也不意味着前者在事实层面更可靠。不能将高相关性分数当作答案正确概率。
这里保留 top 2 让两条都可见,不是建议遇到失败就无限扩大上下文。正确做法是整理更多问题与参考证据,检查否定句、近似规则和不同模型的表现;如果没有达到验收标准,保留原流程或换方案。把题目改到模型能排对再发布,会掩盖真实问题。
第四步:解释延迟和校验结果
脚本输出三个不同口径:initialization_ms 是进程中创建模型对象的时间;first_rerank_ms 是第一次候选重排;warm_rerank_ms 是随后五次重复调用。后两个计时只包住库的重排调用,未包含应用前面的输入检查、检索、网络、排队和最终回答,因此不是端到端响应时间。
本次还在准备好缓存后,阻断 Python socket 外连,运行九项集成检查:真实模型输出与 top_k、输入未被修改、空问题拒绝、空候选拒绝、重复 ID 拒绝、租户不一致拒绝、非法 top_k 拒绝、超长组合输入拒绝,以及缺少来源拒绝。检查通过说明本例的数据约束成立,没有证明中文质量提高、完整访问控制成立,或者所有运行路径都不会联网。
候选数量、文本长度、CPU 并发和模型线程配置变化都会影响时间。生产评估要在目标硬件上使用足够样本,记录吞吐、内存、错误率和尾延迟。五次调用只便于初步观察热运行,不报告 p95 或供应商排名。
第五步:建立质量验收
把本地示例扩展成独立问题集时,每题保存允许访问的候选 ID、内容版本、参考证据和是否无答案。至少覆盖退款条件、关闭续费、发票、过期条款、产品编号与相似但不回答问题的干扰片段。
先跑原检索顺序,再跑重排,比较正确证据进入最终 top_k 的比例。若参考证据原本就在第一条,重排反而挪走,应计为退化。若参考证据没进入候选,应记录为召回阶段问题,同时保留在整体系统评估的分母里,不能直接丢弃失败题目。
调参和最终验收问题分开。针对同一小批题目反复换模型、top_k 与长度,最后再用这些题宣称改进,会高估效果。还应评价最后生成的回答是否遗漏例外、是否引用正确、无证据时是否拒答;只有排序分数不够。
常见错误
模型总是截掉后面的条款
检查查询与文档的组合 token 数,使用与选定重排模型相同的 tokenizer。脚本会拒绝超长候选,避免悄悄丢掉尾部条件。超限时调整上游切分,保留来源位置,或者明确设计截断策略;不要把 max_length 随意调到模型支持范围外。
第一次很慢,以后变快
先看是否正在下载权重,再看模型初始化。将对象放在可复用的服务生命周期中,准备持久缓存。不要每个请求重新创建模型,也不要只测热运行后就承诺所有用户请求都这么快。
正确证据根本不在候选中
优先查解析、分块、索引更新、过滤条件和召回。FlashRank 不会补回没有提供的内容。需要了解分工,可读 FlashRank vs Chonkie 和 Chonkie 分块教程。
得分很高,但答案仍然错误
模型给的是相关性信号。过期条款、相互矛盾的版本和提示注入文本也可能与问题高度相关。先检查可信来源与版本,再核验答案;任何退款、发信、删除或账户修改都走独立审批,不让候选文本成为操作授权。
本地库没有 API 费,为什么仍有成本
软件许可费 $0 不包含计算、缓存、下载、部署、监控和维护。更大的模型会改变资源需求。若改用外部 API,还要核算输入处理量、重试和其他服务费用;不要为了回退而静默外发原本要求本地处理的客户内容。
什么时候该换别的方案
中文验收持续不达标时,可以比较其他模型或托管服务,但保持同一套候选与独立问题集。没有模型运维资源、且允许外发查询与候选时,可参考 FlashRank vs Voyage AI;需要整体选型,可看 RAG 重排工具推荐。
中国大陆网络、模型下载、账户与部署平台差异,需要在目标环境确认。这次测试只覆盖当前机器和指定的缓存 ONNX 模型,没有承诺跨地区可达性、企业服务等级或其他 listwise 路径行为。
官方资料
常见问题
- 本例调用付费模型 API 吗?
- 没有。使用本地 ONNX 模型和合成文本,第一次准备权重可能联网,最终集成检查阻断 socket 外连。
- 为什么推荐 Python 3.11?
- 本次默认 Python 3.14 未找到所需 ONNX Runtime 安装包,3.11 环境完成了安装与运行;其他平台应独立检查兼容性。
- 本例中文问题排序正确吗?
- 没有完全正确。取消续费片段排在直接退款规则前面,所以教程保留失败,强调接口成功不等于质量提升。
- tenant_id 一致就证明没有越权吗?
- 不能。函数只做防错检查,可信身份、数据库过滤和完整授权仍由应用负责。
- 九项检查通过说明什么?
- 说明真实模型接口、输入不变与指定坏数据拒绝约束成立,不证明检索质量提升或生产尾延迟达标。
- 如何处理超过 token 预算的候选?
- 先明确拒绝、重新切分或可审计的截断策略,并保留来源关系;本例选择拒绝,不静默丢掉尾部。