上周在优化我们 RAG pipeline 召回质量的时候,发现 HuggingFace 上 Ettin 团队一口气放出了好几个不同尺寸的 Reranker 模型。从 33M 参数的小钢炮到 435M 的重型选手,一整排摆在那里,说实话第一眼我是懵的——到底该用哪个?

简单说结论:如果你的 RAG 场景对延迟敏感(P95 要求 < 50ms),选 Ettin-Reranker-33M;如果精度是第一优先级且硬件够用,直接上 435M;大多数生产场景 147M 是甜点位。下面是我实测完的数据和集成代码。

先说结论

模型参数量单条 Rerank 延迟(A10G)NDCG@10(BEIR 均值)适用场景
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_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 MiB

435M 模型在 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 效果还好。等他们放出来我再测。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐