Ettin Reranker 出了一整个家族,我帎你把选哪个说清楚
上周在优化我们 RAG pipeline 召回质量的时候,发现 HuggingFace 上 Ettin 团队一口气放出了好几个不同尺寸的 Reranker 模型。从 33M 参数的小钢炮到 435M 的重型选手,一整排摆在那里,说实话第一眼我是懵的——到底该用哪个?
简单说结论:如果你的 RAG 场景对延迟敏感(P95 要求 < 50ms),选 Ettin-Reranker-33M;如果精度是第一优先级且硬件够用,直接上 435M;大多数生产场景 147M 是甜点位。下面是我实测完的数据和集成代码。
先说结论
| 模型 | 参数量 | 单条 Rerank 延迟(A10G) | NDCG@10(BEIR 均值) | 适用场景 |
|---|---|---|---|---|
| Ettin-Reranker-33M | 33M | ~8ms | 0.671 | 高并发在线服务、端侧 |
| Ettin-Reranker-80M | 80M | ~18ms | 0.703 | 中等流量 API 服务 |
| Ettin-Reranker-147M | 147M | ~32ms | 0.724 | 生产级 RAG(推荐) |
| Ettin-Reranker-435M | 435M | ~89ms | 0.741 | 离线评估、精度优先 |
延迟是我在 A10G 24GB 上用 batch_size=1、sequence_length=512 跑的,实际你用 T4 会慢 2-3 倍。
选型决策树
graph TD A[你的 Reranker 需求] --> B{P95 延迟要求?} B -->|< 20ms| C[Ettin-33M] B -->|20-50ms| D{精度要求?} B -->|> 50ms 可接受| E[Ettin-435M] D -->|NDCG@10 > 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_length=512) 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), key=lambda x: x[1], reverse=True) 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_length=512) # LLM 调用(这里用聚合 API,OpenRouter 或 ofox.io 都行,ofox 是 0% 加价对齐官方价格) llm_client = OpenAI( api_key="your-key", base_url="https://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( model="claude-sonnet-4.6", messages=[ {"role": "system", "content": f"根据以下参考资料回答问题:\n{context}"}, {"role": "user", "content": query} ], temperature=0.3 ) return response.choices[0].message.content, scores[top_indices]这套跑下来,从用户提问到拿到答案,Rerank 环节只占 30-40ms(147M 在 A10G 上),瓶颈反而在 LLM 生成那一步。
方案三:ONNX 加速部署(延迟再砍一半)
如果你用 33M 或 80M 还嫌慢,可以导出 ONNX:
from 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, export=True ) # 推理 inputs = tokenizer( "异步编程", "Python asyncio 教程", return_tensors="pt", max_length=512, truncation=True ) outputs = ort_model(**inputs) score = outputs.logits[0].item() print(f"Score: {score:.4f}")ONNX 版在 CPU 上 33M 模型单条推理能做到 3-4ms,比 PyTorch 快了一倍多。我在一台 4 核的云服务器上测的,没 GPU 照样能跑。
踩坑记录
坑 1:max_length 超了静默截断
一开始我没注意文档长度,有些 chunk 超过 512 token 被截断了,导致排序结果很诡异——明明相关的文档排到了后面。后来加了个预处理,把超长文档切成 overlap 片段再分别打分取最高:
def score_long_doc(query, doc, reranker, max_len=512, stride=256): """长文档分片打分取最大值""" 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:i+max_len] chunks.append(tokenizer.decode(chunk_tokens)) scores = reranker.predict([(query, c) for c in chunks]) return max(scores)坑 2:batch_size 不是越大越好
我一开始图省事把 20 条文档一次性丢进去,batch_size=20。在 T4 上直接 OOM:
torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 256.00 MiB435M 模型在 T4 16GB 上 batch_size 最多开到 8,再多就炸。147M 可以开到 16。我最后的做法是动态调 batch:
import 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 内存(ONNX) |
|---|---|---|---|
| 33M | ~200MB | ~130MB | ~140MB |
| 80M | ~450MB | ~280MB | ~320MB |
| 147M | ~800MB | ~500MB | ~580MB |
| 435M | ~2.1GB | ~1.3GB | ~1.7GB |
所以 33M 和 80M 完全可以跑在没有 GPU 的机器上,ONNX + CPU 就够了。
小结
折腾了两天把 Ettin 家族四个尺寸都跑了一遍,结论就是:147M 是大多数 RAG 场景的最优解。精度比 80M 高了 2 个点,延迟还在 50ms 以内,显存也不夸张。
如果你的场景是高并发在线搜索(QPS > 100),33M + ONNX 是正解,精度损失换来的延迟优势在用户体感上是值得的。435M 我目前只在离线评估和标注数据质量检查时用,线上没必要。
还有一点我不太确定——Ettin 团队说后续会出 fine-tune 脚本,到时候在垂直领域数据上微调一下 80M,可能比通用 147M 效果还好。等他们放出来我再测。
