Ettin Reranker 出了一整个家族,我帎你把选哪个说清楚
上周在优化我们 RAG pipeline 召回质量的时候发现 HuggingFace 上 Ettin 团队一口气放出了好几个不同尺寸的 Reranker 模型。从 33M 参数的小钢炮到 435M 的重型选手一整排摆在那里说实话第一眼我是懵的——到底该用哪个简单说结论如果你的 RAG 场景对延迟敏感P95 要求 50ms选 Ettin-Reranker-33M如果精度是第一优先级且硬件够用直接上 435M大多数生产场景 147M 是甜点位。下面是我实测完的数据和集成代码。先说结论模型参数量单条 Rerank 延迟A10GNDCG10BEIR 均值适用场景Ettin-Reranker-33M33M~8ms0.671高并发在线服务、端侧Ettin-Reranker-80M80M~18ms0.703中等流量 API 服务Ettin-Reranker-147M147M~32ms0.724生产级 RAG推荐Ettin-Reranker-435M435M~89ms0.741离线评估、精度优先延迟是我在 A10G 24GB 上用 batch_size1、sequence_length512 跑的实际你用 T4 会慢 2-3 倍。选型决策树graph TD A[你的 Reranker 需求] -- B{P95 延迟要求?} B --| 20ms| C[Ettin-33M] B --|20-50ms| D{精度要求?} B --| 50ms 可接受| E[Ettin-435M] D --|NDCG10 0.72| F[Ettin-147M ✅ 甜点] D --|0.70 左右够用| G[Ettin-80M] F -- H{GPU 显存?} H --| 4GB| I[退回 80M] H --| 4GB| J[上 147M]环境准备pip install transformers torch sentence-transformers # 如果要跑 ONNX 加速版 pip install optimum onnxruntime-gpu模型直接从 HuggingFace 拉# 以 147M 为例 huggingface-cli download ettin-ai/ettin-reranker-147m方案一直接用 sentence-transformers CrossEncoder最快上手的方式5 行代码搞定from sentence_transformers import CrossEncoder # 换成你要的尺寸 model CrossEncoder(ettin-ai/ettin-reranker-147m, max_length512) query 如何在 Python 中实现异步 HTTP 请求 documents [ Python 的 asyncio 库配合 aiohttp 可以实现高效的异步 HTTP 请求, requests 库是 Python 中最常用的同步 HTTP 客户端, JavaScript 的 fetch API 支持 Promise 风格的异步调用, Python 3.11 引入了 TaskGroup 来简化并发任务管理, ] # 返回相关性分数 scores model.predict([(query, doc) for doc in documents]) # 按分数排序 ranked sorted(zip(documents, scores), keylambda x: x[1], reverseTrue) for doc, score in ranked: print(f{score:.4f} | {doc[:50]})实测输出0.9847 | Python 的 asyncio 库配合 aiohttp 可以实现高效的异步 HTTP 请求 0.7123 | Python 3.11 引入了 TaskGroup 来简化并发任务管理 0.4521 | requests 库是 Python 中最常用的同步 HTTP 客户端 0.0834 | JavaScript 的 fetch API 支持 Promise 风格的异步调用区分度很明显第一名和最后一名差了 0.9实际 RAG 里做 top-k 截断时非常好用。方案二集成到 LLM RAG Pipeline实际生产里 Reranker 是夹在 Retriever 和 LLM 之间的。我的 pipeline 大概是这样向量检索召回 top-20 → Reranker 精排取 top-5 → 喂给 LLM 生成答案。from openai import OpenAI from sentence_transformers import CrossEncoder import numpy as np # Reranker 本地加载 reranker CrossEncoder(ettin-ai/ettin-reranker-147m, max_length512) # LLM 调用这里用聚合 APIOpenRouter 或 ofox.io 都行ofox 是 0% 加价对齐官方价格 llm_client OpenAI( api_keyyour-key, base_urlhttps://api.ofox.io/v1 ) def rag_pipeline(query: str, retrieved_docs: list[str], top_k: int 5): 完整的 retrieve → rerank → generate 流程 # Step 1: Rerank pairs [(query, doc) for doc in retrieved_docs] scores reranker.predict(pairs) # Step 2: 取 top-k top_indices np.argsort(scores)[::-1][:top_k] context_docs [retrieved_docs[i] for i in top_indices] # Step 3: 拼 context 调 LLM context \n---\n.join(context_docs) response llm_client.chat.completions.create( modelclaude-sonnet-4.6, messages[ {role: system, content: f根据以下参考资料回答问题\n{context}}, {role: user, content: query} ], temperature0.3 ) return response.choices[0].message.content, scores[top_indices]这套跑下来从用户提问到拿到答案Rerank 环节只占 30-40ms147M 在 A10G 上瓶颈反而在 LLM 生成那一步。方案三ONNX 加速部署延迟再砍一半如果你用 33M 或 80M 还嫌慢可以导出 ONNXfrom optimum.onnxruntime import ORTModelForSequenceClassification from transformers import AutoTokenizer model_id ettin-ai/ettin-reranker-80m tokenizer AutoTokenizer.from_pretrained(model_id) # 导出并加载 ONNX ort_model ORTModelForSequenceClassification.from_pretrained( model_id, exportTrue ) # 推理 inputs tokenizer( 异步编程, Python asyncio 教程, return_tensorspt, max_length512, truncationTrue ) outputs ort_model(**inputs) score outputs.logits[0].item() print(fScore: {score:.4f})ONNX 版在 CPU 上 33M 模型单条推理能做到 3-4ms比 PyTorch 快了一倍多。我在一台 4 核的云服务器上测的没 GPU 照样能跑。踩坑记录坑 1max_length 超了静默截断一开始我没注意文档长度有些 chunk 超过 512 token 被截断了导致排序结果很诡异——明明相关的文档排到了后面。后来加了个预处理把超长文档切成 overlap 片段再分别打分取最高def score_long_doc(query, doc, reranker, max_len512, stride256): 长文档分片打分取最大值 tokens tokenizer.encode(doc) if len(tokens) max_len: return reranker.predict([(query, doc)])[0] chunks [] for i in range(0, len(tokens), stride): chunk_tokens tokens[i:imax_len] chunks.append(tokenizer.decode(chunk_tokens)) scores reranker.predict([(query, c) for c in chunks]) return max(scores)坑 2batch_size 不是越大越好我一开始图省事把 20 条文档一次性丢进去batch_size20。在 T4 上直接 OOMtorch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 256.00 MiB435M 模型在 T4 16GB 上 batch_size 最多开到 8再多就炸。147M 可以开到 16。我最后的做法是动态调 batchimport torch def adaptive_batch_size(model_size_m: int, gpu_mem_gb: int) - int: 根据模型大小和显存粗略估算安全 batch_size if model_size_m 80: return min(32, gpu_mem_gb * 4) elif model_size_m 147: return min(16, gpu_mem_gb * 2) else: # 435M return min(8, gpu_mem_gb)坑 3跟 BGE-Reranker-v2 的分数范围不一样之前用 BGE-Reranker-v2-m3 的时候分数是 sigmoid 过的 0-1 区间。Ettin 家族的原始输出是 logits范围大概在 -10 到 15 之间。如果你有基于阈值截断的逻辑记得重新标定import torch.nn.functional as F # 如果你需要 0-1 范围的分数 normalized_scores F.sigmoid(torch.tensor(raw_scores)).numpy()各尺寸实际显存占用跑之前最好心里有数模型FP32 显存FP16 显存CPU 内存ONNX33M~200MB~130MB~140MB80M~450MB~280MB~320MB147M~800MB~500MB~580MB435M~2.1GB~1.3GB~1.7GB所以 33M 和 80M 完全可以跑在没有 GPU 的机器上ONNX CPU 就够了。小结折腾了两天把 Ettin 家族四个尺寸都跑了一遍结论就是147M 是大多数 RAG 场景的最优解。精度比 80M 高了 2 个点延迟还在 50ms 以内显存也不夸张。如果你的场景是高并发在线搜索QPS 10033M ONNX 是正解精度损失换来的延迟优势在用户体感上是值得的。435M 我目前只在离线评估和标注数据质量检查时用线上没必要。还有一点我不太确定——Ettin 团队说后续会出 fine-tune 脚本到时候在垂直领域数据上微调一下 80M可能比通用 147M 效果还好。等他们放出来我再测。