Reranking چیست؟ آموزش افزایش دقت RAG با Cross-Encoder و مدلهای Reranker
Reranking نتایج اولیه جستجو را با مدلی دقیقتر دوباره رتبهبندی میکند. در این آموزش، پیادهسازی Cross-Encoder، انتخاب مدل، ارزیابی و اتصال آن به RAG را بررسی میکنیم.
مقدمه
در بسیاری از سیستمهای RAG یا Retrieval-Augmented Generation، مشکل اصلی مدل زبانی نیست؛ مشکل از اسنادی شروع میشود که در اختیار مدل قرار میگیرند.
ممکن است Vector_Search دهها سند نسبتاً مرتبط پیدا کند، اما مرتبطترین بخش را در رتبه پنجم یا دهم قرار دهد. اگر فقط سه نتیجه اول وارد Context شوند، مدل زبانی هرگز سند اصلی را نمیبیند و پاسخی ناقص یا اشتباه تولید میکند.
Reranking یا «رتبهبندی مجدد» برای حل همین مسئله استفاده میشود.
در این معماری، ابتدا یک موتور سریع مانند BM25، Vector_Search یا Hybrid_Search تعداد نسبتاً زیادی Candidate پیدا میکند. سپس یک مدل دقیقتر، ارتباط واقعی هر Candidate با Query را محاسبه و نتایج را دوباره مرتب میکند.
جریان کلی سیستم به این شکل است:
User Query
|
v
Fast Retrieval
BM25 / Vector Search / Hybrid Search
|
v
Top 20 to 100 Candidates
|
v
Reranker
Cross-Encoder / Late Interaction / Rerank API
|
v
Top 3 to 10 Documents
|
v
Context Builder
|
v
LLM
|
v
Final Answer
Reranking معمولاً یکی از مؤثرترین روشها برای افزایش دقت RAG، موتور جستجوی معنایی، جستجوی محصول و سیستم پرسشوپاسخ سازمانی است.
Reranking چیست؟
Reranking فرایندی است که در آن نتایج تولیدشده توسط مرحله Retrieval با استفاده از یک مدل یا الگوریتم دقیقتر دوباره امتیازدهی و مرتب میشوند.
فرض کنید کاربر این عبارت را جستجو میکند:
چگونه مصرف توکن API هوش مصنوعی را کاهش دهیم؟
مرحله Retrieval ممکن است نتایج زیر را برگرداند:
- روش محاسبه هزینه API
- معرفی Token و Context Window
- تکنیکهای کاهش مصرف Token
- مقایسه قیمت مدلهای زبانی
- استفاده از Prompt Caching
هر پنج نتیجه با Query ارتباط دارند؛ اما نتیجه سوم احتمالاً باید در رتبه اول قرار بگیرد. یک Reranker ارتباط Query و هر Document را با دقت بیشتری بررسی میکند و ترتیب را تغییر میدهد:
- تکنیکهای کاهش مصرف Token
- استفاده از Prompt Caching
- روش محاسبه هزینه API
- معرفی Token و Context Window
- مقایسه قیمت مدلهای زبانی
Reranking سند جدیدی پیدا نمیکند. وظیفه آن مرتبکردن بهتر اسنادی است که Retrieval اولیه پیدا کرده است.
به همین دلیل، کیفیت نهایی به دو شرط وابسته است:
- مرحله Retrieval باید اسناد مرتبط را در Candidateها پیدا کند.
- مرحله Reranking باید مرتبطترین Candidateها را به ابتدای فهرست منتقل کند.
اگر سند صحیح در خروجی Retrieval اولیه وجود نداشته باشد، Reranker نمیتواند آن را بازیابی کند.
چرا Vector_Search بهتنهایی کافی نیست؟
در Vector_Search، معمولاً Query و هر Document به یک بردار ثابت تبدیل میشوند. سپس شباهت میان بردار Query و بردار Document محاسبه میشود.
این معماری Bi-Encoder نام دارد:
Query --------> Encoder --------> Query Vector
|
| Similarity
|
Document -----> Encoder --------> Document Vector
مزیت مهم Bi-Encoder این است که Embedding اسناد از قبل محاسبه و در Vector Database ذخیره میشود. هنگام جستجو فقط Embedding مربوط به Query ساخته خواهد شد؛ بنابراین جستجو بسیار سریع و مقیاسپذیر است.
اما فشردهکردن یک سند در یک بردار ثابت باعث از دست رفتن بخشی از اطلاعات میشود. یک سند ممکن است چند موضوع، چند موجودیت و چند پاسخ مختلف داشته باشد، درحالیکه تمام این اطلاعات باید در یک بردار خلاصه شوند.
Vector_Search ممکن است در شرایط زیر دچار خطا شود:
- Query شامل عدد، نام محصول یا اصطلاح دقیق باشد.
- دو سند از نظر موضوع کلی مشابه اما از نظر پاسخ متفاوت باشند.
- Query فارسی و سند ترکیبی از فارسی و انگلیسی باشد.
- سند طولانی و دارای چند موضوع باشد.
- تفاوت میان اسناد به یک عبارت کوتاه وابسته باشد.
- کاربر از کلمات متفاوتی برای بیان مفهوم استفاده کرده باشد.
- اسناد تکراری یا بسیار مشابه در مجموعه وجود داشته باشند.
هدف Retriever اولیه معمولاً رسیدن به Recall بالا است؛ یعنی سند مرتبط را از دست ندهد. هدف Reranker افزایش Precision و قراردادن بهترین سند در رتبههای اول است.
معماری دو مرحلهای Retrieval و Reranking
یک Pipeline استاندارد از دو مرحله اصلی تشکیل میشود.
مرحله اول: Candidate Generation
در این مرحله تعداد زیادی نتیجه با سرعت بالا پیدا میشوند. برای مثال:
- BM25 برای تطبیق واژگانی
- Dense Vector_Search برای شباهت معنایی
- Sparse Retrieval
- Hybrid_Search برای ترکیب BM25 و Vector_Search
- فیلترهای Metadata
- ترکیب چند Retriever
خروجی میتواند بین ۲۰ تا ۱۰۰ Candidate باشد.
مرحله دوم: Reranking
Reranker تمام Candidateها را همراه Query دریافت میکند و برای هر جفت Query و Document یک امتیاز Relevance تولید میکند.
فرمول ساده امتیازدهی، بهشکل سازگار با Ghost:
rerank_score = reranker(query, document)
سپس اسناد براساس rerank_score مرتب میشوند و فقط چند نتیجه برتر وارد Context مدل زبانی خواهند شد.
candidate_k = 50
final_k = 5
در این مثال، Retriever پنجاه Candidate پیدا میکند و Reranker پنج نتیجه نهایی را انتخاب میکند.
Cross-Encoder چیست؟
Cross-Encoder یکی از رایجترین معماریهای Reranking است. برخلاف Bi-Encoder، Query و Document جداگانه پردازش نمیشوند؛ بلکه بهصورت یک ورودی مشترک وارد مدل خواهند شد.
[Query, Document] -----> Cross-Encoder -----> Relevance Score
مدل میتواند Attention را بین تمام Tokenهای Query و Document برقرار کند. در نتیجه، ارتباط دقیق کلمات، عبارتها، اعداد و موجودیتها بهتر تشخیص داده میشود.
برای هر سند باید یک بار مدل اجرا شود:
(query, document_1) -> score_1
(query, document_2) -> score_2
(query, document_3) -> score_3
این روش معمولاً از مقایسه ساده Embeddingها دقیقتر، اما پرهزینهتر است.
مستندات رسمی Sentence Transformers نیز Cross-Encoderها را بهعنوان مدلهایی برای محاسبه مستقیم امتیاز شباهت و Reranking معرفی میکند.
تفاوت Bi-Encoder و Cross-Encoder
| ویژگی | Bi-Encoder | Cross-Encoder |
|---|---|---|
| ورودی مدل | Query و Document جداگانه | Query و Document همزمان |
| خروجی | Embedding | امتیاز ارتباط |
| امکان ذخیره خروجی اسناد | بله | خیر |
| سرعت جستجو | بسیار بالا | پایینتر |
| دقت رتبهبندی | مناسب | معمولاً بالاتر |
| کاربرد اصلی | Candidate Generation | Reranking |
| مقیاس مناسب | میلیونها سند | دهها یا صدها Candidate |
| تعامل Tokenهای Query و Document | مستقیم نیست | مستقیم است |
استفاده از Cross-Encoder روی کل دیتابیس منطقی نیست؛ زیرا برای هر Query باید تمام اسناد دوباره پردازش شوند. معماری درست این است که ابتدا Bi-Encoder یا BM25 دامنه جستجو را کوچک کند و سپس Cross-Encoder فقط Candidateها را بررسی کند.
پیادهسازی Reranking با Python و Sentence Transformers
برای اجرای یک Cross-Encoder محلی میتوان از کتابخانه sentence-transformers استفاده کرد:
pip install -U sentence-transformers torch
برای متون فارسی بهتر است مدلی انتخاب شود که پشتیبانی Multilingual آن صریحاً در Model Card ذکر شده باشد. مدل BAAI/bge-reranker-v2-m3 یک Reranker چندزبانه است و در Model Card رسمی آن به قابلیت Multilingual اشاره شده است.
نمونه ساده:
from sentence_transformers import CrossEncoder
model = CrossEncoder(
"BAAI/bge-reranker-v2-m3",
max_length=512
)
query = "چگونه هزینه استفاده از API هوش مصنوعی را کاهش دهیم؟"
documents = [
"Context Window مشخص میکند مدل چه تعداد توکن را پردازش میکند.",
"برای کاهش هزینه API میتوان از مدل کوچکتر، کش، خلاصهسازی تاریخچه و محدودکردن خروجی استفاده کرد.",
"Embedding نمایش عددی متن است و در جستجوی معنایی استفاده میشود.",
"مدلهای زبانی براساس تعداد توکنهای ورودی و خروجی قیمتگذاری میشوند."
]
pairs = [(query, document) for document in documents]
scores = model.predict(pairs)
ranked_results = sorted(
zip(documents, scores),
key=lambda item: float(item[1]),
reverse=True
)
for rank, (document, score) in enumerate(ranked_results, start=1):
print(rank, float(score), document)
نکته مهم این است که مقدار Score میان مدلهای مختلف لزوماً قابل مقایسه نیست. برای مثال، نباید نتیجه گرفت امتیاز 4.2 در یک مدل دقیقاً معادل 0.87 در مدل دیگر است.
آنچه معمولاً اهمیت دارد ترتیب نسبی Candidateها در یک Query مشخص است.
پیادهسازی یک تابع Reranker قابل استفاده در پروژه
در پروژه واقعی بهتر است ID، Metadata و محتوای هر سند حفظ شود:
from typing import Any
from sentence_transformers import CrossEncoder
class LocalReranker:
def __init__(
self,
model_name: str = "BAAI/bge-reranker-v2-m3",
max_length: int = 512
):
self.model = CrossEncoder(
model_name,
max_length=max_length
)
def rerank(
self,
query: str,
documents: list[dict[str, Any]],
top_n: int = 5
) -> list[dict[str, Any]]:
if not documents:
return []
pairs = [
(query, document["content"])
for document in documents
]
scores = self.model.predict(
pairs,
batch_size=16,
show_progress_bar=False
)
results = []
for document, score in zip(documents, scores):
result = document.copy()
result["rerank_score"] = float(score)
results.append(result)
results.sort(
key=lambda item: item["rerank_score"],
reverse=True
)
return results[:top_n]
استفاده از کلاس:
candidates = [
{
"id": "doc-101",
"title": "کاهش هزینه API",
"content": "با انتخاب مدل مناسب، کش و محدودکردن خروجی میتوان هزینه API را کاهش داد.",
"retrieval_score": 0.72
},
{
"id": "doc-102",
"title": "آشنایی با Embedding",
"content": "Embedding روشی برای تبدیل متن به بردار عددی است.",
"retrieval_score": 0.81
},
{
"id": "doc-103",
"title": "محاسبه Token",
"content": "هزینه بسیاری از مدلها براساس توکن ورودی و خروجی محاسبه میشود.",
"retrieval_score": 0.78
}
]
reranker = LocalReranker()
results = reranker.rerank(
query="چگونه هزینه API هوش مصنوعی را کمتر کنم؟",
documents=candidates,
top_n=2
)
for result in results:
print(result["id"], result["rerank_score"])
در اینجا retrieval_score و rerank_score دو مفهوم متفاوتاند. در بسیاری از سیستمها پس از Reranking، ترتیب نهایی صرفاً براساس امتیاز Reranker تعیین میشود.
ساخت Pipeline کامل RAG با Reranking
یک Pipeline ساده میتواند مراحل زیر را اجرا کند:
- دریافت Query
- نرمالسازی Query
- بازیابی Candidateها
- حذف نتایج تکراری
- Reranking
- انتخاب اسناد نهایی
- ساخت Context
- ارسال Context به مدل زبانی
- تولید پاسخ همراه منابع
نمونه ساختار:
def answer_question(query: str) -> str:
candidates = hybrid_retrieve(
query=query,
limit=50
)
candidates = deduplicate_documents(candidates)
ranked_documents = reranker.rerank(
query=query,
documents=candidates,
top_n=5
)
context = build_context(ranked_documents)
answer = generate_answer(
query=query,
context=context
)
return answer
توابع Retrieval به Vector Database یا موتور جستجوی پروژه وابستهاند؛ اما Reranker میتواند مستقل از نوع Retriever کار کند.
اتصال مرحله تولید پاسخ به API درواره
میتوان مرحله Retrieval و Reranking را در Backend اجرا کرد و سپس اسناد منتخب را برای تولید پاسخ به یک مدل زبانی از طریق API درواره فرستاد.
کلید API باید فقط در Backend و متغیر محیطی نگهداری شود.
pip install -U openai
متغیرهای محیطی:
export DARVAREH_API_KEY="YOUR_API_KEY"
export DARVAREH_CHAT_MODEL="YOUR_CHAT_MODEL_ID"
نمونه کد:
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DARVAREH_API_KEY"],
base_url="https://api.darvareh.ir/v1"
)
def build_context(documents: list[dict]) -> str:
sections = []
for index, document in enumerate(documents, start=1):
sections.append(
f"""[منبع {index}]
عنوان: {document.get("title", "بدون عنوان")}
شناسه: {document["id"]}
محتوا:
{document["content"]}
"""
)
return "\n\n".join(sections)
def generate_answer(query: str, documents: list[dict]) -> str:
context = build_context(documents)
response = client.chat.completions.create(
model=os.environ["DARVAREH_CHAT_MODEL"],
temperature=0.1,
messages=[
{
"role": "system",
"content": (
"فقط براساس منابع ارائهشده پاسخ بده. "
"اگر پاسخ در منابع وجود ندارد، صریحاً اعلام کن. "
"در پایان هر ادعا شماره منبع مرتبط را ذکر کن."
)
},
{
"role": "user",
"content": f"""پرسش:
{query}
منابع بازیابیشده:
{context}
"""
}
]
)
return response.choices[0].message.content
در این معماری، درواره API موردنیاز برای دسترسی به مدل زبانی را فراهم میکند. Retrieval، Vector Database و Reranker اجزایی هستند که در Backend پروژه مدیریت میشوند.
برای دریافت API و استفاده در پروژههای هوش مصنوعی میتوانید وارد درواره شوید.
استفاده از Embedding API درواره در مرحله Retrieval
برای ساخت Vector_Search باید اسناد و Query با یک Embedding Model یکسان پردازش شوند.
export DARVAREH_EMBEDDING_MODEL="YOUR_EMBEDDING_MODEL_ID"
نمونه تابع:
def create_embedding(text: str) -> list[float]:
response = client.embeddings.create(
model=os.environ["DARVAREH_EMBEDDING_MODEL"],
input=text
)
return response.data[0].embedding
در زمان Indexing:
document_vector = create_embedding(document_text)
vector_store.upsert(
id=document_id,
vector=document_vector,
payload={
"title": document_title,
"content": document_text
}
)
در زمان Query:
query_vector = create_embedding(user_query)
candidates = vector_store.search(
vector=query_vector,
limit=50
)
سپس Candidateها به Reranker داده میشوند:
ranked_documents = reranker.rerank(
query=user_query,
documents=candidates,
top_n=5
)
شناسه مدلهای Chat و Embedding باید از مدلهای موجود در حساب یا مستندات فعلی سرویس انتخاب شوند. استفاده از Placeholder در کد باعث میشود مقاله به یک Model ID متغیر وابسته نباشد.
Reranking بعد از Hybrid_Search
یکی از بهترین معماریها برای RAG حرفهای، ترکیب سه سیگنال است:
- BM25 برای تطبیق کلمات و عبارتهای دقیق
- Vector_Search برای ارتباط معنایی
- Reranker برای رتبهبندی دقیق Candidateها
جریان پیشنهادی:
Query
|
+----> BM25 --------+
| |
+----> Vector ------+----> Fusion ----> Top 50
|
v
Reranker
|
v
Top 5
در مرحله Fusion میتوان از Reciprocal Rank Fusion یا RRF استفاده کرد. فرمول Ghost-compatible آن:
RRF_score(document) =
sum of [1 / (k + rank_in_each_result_list)]
در این فرمول:
rank_in_each_result_listرتبه سند در هر Retriever است.kیک ثابت برای کاهش تأثیر اختلاف رتبههای بسیار بالا است.- امتیاز RRF فقط برای ساخت Candidate List استفاده میشود.
- Reranker ترتیب نهایی Candidateها را تعیین میکند.
نمونه ساده RRF:
from collections import defaultdict
def reciprocal_rank_fusion(
result_lists: list[list[dict]],
k: int = 60
) -> list[dict]:
scores = defaultdict(float)
documents = {}
for result_list in result_lists:
for rank, document in enumerate(result_list, start=1):
document_id = document["id"]
scores[document_id] += 1.0 / (k + rank)
documents[document_id] = document
fused = []
for document_id, score in scores.items():
item = documents[document_id].copy()
item["fusion_score"] = score
fused.append(item)
return sorted(
fused,
key=lambda item: item["fusion_score"],
reverse=True
)
پس از Fusion:
fused_candidates = reciprocal_rank_fusion(
[bm25_results, vector_results]
)[:50]
final_results = reranker.rerank(
query=query,
documents=fused_candidates,
top_n=5
)
انتخاب Candidate K و Final K
دو پارامتر مهم در این معماری عبارتاند از:
candidate_k = تعداد نتایج ورودی Reranker
final_k = تعداد نتایج خروجی Reranker
اگر candidate_k بسیار کوچک باشد، احتمال دارد Retriever سند مرتبط را پیدا کرده باشد اما آن سند خارج از محدوده Reranking قرار بگیرد.
اگر candidate_k بیشازحد بزرگ باشد:
- Latency افزایش پیدا میکند.
- مصرف CPU یا GPU بیشتر میشود.
- هزینه Rerank API افزایش پیدا میکند.
- پردازش اسناد طولانی سنگینتر خواهد شد.
مقادیر اولیه مناسب برای آزمایش:
| نوع سیستم | Candidate K | Final K |
|---|---|---|
| FAQ کوچک | ۱۰ تا ۲۰ | ۳ تا ۵ |
| RAG عمومی | ۳۰ تا ۵۰ | ۴ تا ۸ |
| Hybrid Search | ۴۰ تا ۱۰۰ | ۵ تا ۱۰ |
| جستجوی محصول | ۵۰ تا ۲۰۰ | ۱۰ تا ۳۰ |
| اسناد بسیار طولانی | ۲۰ تا ۵۰ | ۳ تا ۶ |
این اعداد قانون ثابت نیستند. مقدار نهایی باید با Dataset واقعی پروژه و اندازهگیری Recall، nDCG، Latency و کیفیت پاسخ تعیین شود.
Cross-Encoder با اسناد طولانی چه میکند؟
Cross-Encoderها Context Window محدودی دارند. اگر مجموع Tokenهای Query و Document از محدودیت مدل بیشتر شود، بخشی از Document قطع یا Truncate خواهد شد.
این موضوع ممکن است باعث حذف همان بخشی شود که پاسخ را در خود دارد.
راهکارهای مناسب عبارتاند از:
Chunking قبل از Indexing
بهجای ذخیره کل صفحه، آن را به Chunkهای معنادار تقسیم کنید. هر Chunk باید:
- تا حد امکان یک موضوع مشخص داشته باشد.
- عنوان و مسیر بخش را حفظ کند.
- بیشازحد کوتاه نباشد.
- از محدودیت ورودی Reranker کوچکتر باشد.
افزودن عنوان به محتوای Chunk
عنوان صفحه و Heading والد میتوانند سیگنال مهمی به Reranker بدهند:
def prepare_rerank_text(document: dict) -> str:
return (
f"عنوان: {document['title']}\n"
f"بخش: {document.get('section', '')}\n"
f"متن: {document['content']}"
)
Parent-Child Retrieval
Chunk کوچک برای جستجو و Reranking استفاده میشود، اما پس از انتخاب، بخش بزرگتر یا Parent Document برای ساخت Context بازیابی خواهد شد.
Sliding Window
برای اسناد بسیار طولانی میتوان چند Window از سند ساخت و بیشترین امتیاز را بهعنوان امتیاز سند در نظر گرفت:
document_score = maximum score among all document windows
این روش دقت را بالا میبرد، اما تعداد Pairهای ورودی مدل و Latency را افزایش میدهد.
Reranking برای متن فارسی
مدلی که روی English Retrieval عملکرد خوبی دارد الزاماً برای فارسی مناسب نیست. برای انتخاب Reranker فارسی باید چند موضوع بررسی شود.
پشتیبانی واقعی از فارسی
برچسب Multilingual نقطه شروع مناسبی است، اما بهتنهایی کافی نیست. مدل باید روی Queryهای واقعی فارسی ارزیابی شود.
Dataset ارزیابی بهتر است موارد زیر را داشته باشد:
- فارسی رسمی
- فارسی محاورهای
- کلمات فنی انگلیسی در متن فارسی
- شکل فارسی و انگلیسی یک کلمه
- نیمفاصله
- اعداد فارسی و انگلیسی
- نام محصولات و Model IDها
- غلطهای تایپی رایج
برای مثال، هر دو شکل زیر ممکن است توسط کاربران جستجو شوند:
آموزش پرامپت نویسی
آموزش Prompt نویسی
یا:
کاهش مصرف توکن
کاهش مصرف Token
محتوا و Metadata بهتر است هر دو شکل طبیعی اصطلاحات مهم را پوشش دهند، بدون اینکه متن به Keyword Stuffing تبدیل شود.
نرمالسازی کنترلشده
نرمالسازی Query و Document باید با احتیاط انجام شود:
import re
def normalize_persian(text: str) -> str:
text = text.replace("ي", "ی")
text = text.replace("ك", "ک")
text = text.replace("\u200c", " ")
text = re.sub(r"\s+", " ", text)
return text.strip()
حذف کامل علائم، اعداد یا کاراکترهای فنی همیشه مناسب نیست. عباراتی مانند C++، .NET، GPT-4، Node.js و Python 3.12 نباید در فرایند نرمالسازی تخریب شوند.
ارزیابی روی Queryهای واقعی
بهترین Dataset از Search_Log، پرسشهای پشتیبانی، Queryهای کاربران و سؤالات واقعی محصول ساخته میشود. Dataset ترجمهشده از انگلیسی معمولاً تمام پیچیدگیهای جستجوی فارسی را منعکس نمیکند.
ColBERT و Late Interaction چیست؟
Cross-Encoder دقیق است، اما باید Query و هر Document را بهصورت مشترک پردازش کند. Late Interaction تلاش میکند تعادلی میان دقت و سرعت ایجاد کند.
در مدلهایی مانند ColBERT:
- Query به چند بردار در سطح Token تبدیل میشود.
- Document نیز به چند بردار Token-level تبدیل میشود.
- بردارهای Document را میتوان از قبل محاسبه کرد.
- تعامل دقیقتر Query و Document هنگام جستجو انجام میشود.
فرمول سادهشده MaxSim بهصورت متن سازگار با Ghost:
ColBERT_score(query, document) =
for each query token:
find the maximum similarity with all document tokens
then sum those maximum similarities
مقاله اصلی ColBERT این معماری را برای حفظ تعامل ریزدانه Query و Document، همراه با امکان پیشمحاسبه نمایش اسناد معرفی کرده است.
Qdrant نیز در مستندات خود از Multi-Vector و Late Interaction و اجرای Reranking با ColBERT پشتیبانی میکند.
مقایسه Cross-Encoder، ColBERT و LLM Reranker
| روش | دقت | سرعت | پیچیدگی اجرا | کاربرد مناسب |
|---|---|---|---|---|
| Cross-Encoder | بالا | متوسط تا پایین | کم تا متوسط | RAG و جستجوی چندمرحلهای |
| ColBERT | بالا | معمولاً سریعتر در مقیاس | بیشتر | Retrieval و Reranking بزرگمقیاس |
| LLM Reranker | بالقوه بسیار بالا | پایین | متوسط | Queryهای پیچیده و Candidateهای محدود |
| Rule-based Reranker | وابسته به قواعد | بسیار بالا | کم | سیگنالهای تجاری و Metadata |
| Rerank API | بالا | وابسته به شبکه و سرویس | کم | پیادهسازی سریع و مدیریتشده |
چه زمانی Cross-Encoder مناسب است؟
- تعداد Candidateها محدود است.
- دقت مهمتر از چند ده میلیثانیه Latency است.
- مدل مناسب زبان پروژه وجود دارد.
- امکان اجرای مدل روی CPU یا GPU فراهم است.
چه زمانی ColBERT مناسب است؟
- حجم داده زیاد است.
- Token-level Matching اهمیت دارد.
- زیرساخت Multi-Vector در دسترس است.
- به تعادل بهتر میان دقت و سرعت نیاز دارید.
چه زمانی LLM Reranker مناسب است؟
- ارتباط Query و Document به استدلال پیچیده نیاز دارد.
- تعداد Candidateها بسیار محدود است.
- Latency و هزینه بیشتر قابل قبول است.
- معیار ارتباط را بتوان بهصورت دقیق در Prompt تعریف کرد.
برای اغلب پروژهها، Cross-Encoder نقطه شروع عملیتری است.
ترکیب امتیاز Retrieval و Reranking
در سادهترین حالت، ترتیب نهایی فقط براساس rerank_score تعیین میشود. بااینحال، گاهی Metadata و امتیاز اولیه نیز اهمیت دارند.
یک امتیاز ترکیبی میتواند به این صورت تعریف شود:
final_score =
alpha * normalized_rerank_score
+ beta * normalized_retrieval_score
+ gamma * metadata_score
برای مثال:
alpha = 0.80
beta = 0.15
gamma = 0.05
قبل از ترکیب، Scoreها باید Normalized شوند؛ زیرا دامنه Cross-Encoder، BM25 و Vector Similarity یکسان نیست.
نمونه Min-Max Normalization:
normalized_score =
(score - minimum_score)
/ (maximum_score - minimum_score)
پیادهسازی:
def min_max_normalize(values: list[float]) -> list[float]:
minimum = min(values)
maximum = max(values)
if maximum == minimum:
return [1.0 for _ in values]
return [
(value - minimum) / (maximum - minimum)
for value in values
]
ترکیب Score نباید صرفاً براساس حدس انجام شود. وزنها باید روی Validation Set تنظیم شوند.
استفاده از Metadata در رتبهبندی نهایی
ارتباط معنایی تنها معیار مناسب برای همه سیستمها نیست. گاهی باید سیگنالهای دیگری نیز اعمال شوند:
- تازگی محتوا
- نوع سند
- زبان
- دستهبندی
- موجودبودن محصول
- اولویت منبع
- سطح دسترسی کاربر
- کیفیت یا کاملبودن سند
نمونه اعمال Boost برای تازگی:
def calculate_final_score(
rerank_score: float,
freshness_score: float,
source_quality_score: float
) -> float:
return (
0.85 * rerank_score
+ 0.10 * freshness_score
+ 0.05 * source_quality_score
)
فیلترهای قطعی بهتر است قبل از Reranking اجرا شوند. برای مثال، اگر کاربر فقط اسناد یک محصول را میخواهد، ابتدا براساس Product ID فیلتر کنید و سپس نتایج باقیمانده را Rerank کنید.
حذف نتایج تکراری
Reranker ممکن است چند Chunk بسیار مشابه از یک صفحه را در رتبههای اول قرار دهد. این وضعیت باعث مصرف بیهوده Context Window میشود.
یک روش ساده، محدودکردن تعداد Chunk از هر سند والد است:
def limit_chunks_per_parent(
documents: list[dict],
max_per_parent: int = 2
) -> list[dict]:
counts = {}
selected = []
for document in documents:
parent_id = document["parent_id"]
current_count = counts.get(parent_id, 0)
if current_count >= max_per_parent:
continue
selected.append(document)
counts[parent_id] = current_count + 1
return selected
روش پیشرفتهتر استفاده از MMR یا Maximum Marginal Relevance است که میان ارتباط و تنوع تعادل ایجاد میکند:
MMR_score =
lambda * relevance_to_query
- (1 - lambda) * similarity_to_selected_documents
این فرمول نیز بهصورت متن ساده نوشته شده و در Ghost بدون MathJax نمایش داده میشود.
ارزیابی Reranker
نباید کیفیت Reranker را فقط با مشاهده چند Query ارزیابی کرد. به یک Dataset برچسبخورده نیاز داریم.
هر رکورد Dataset میتواند چنین ساختاری داشته باشد:
{
"query": "چگونه مصرف توکن را کاهش دهیم؟",
"documents": [
{
"id": "doc-1",
"relevance": 3
},
{
"id": "doc-2",
"relevance": 1
},
{
"id": "doc-3",
"relevance": 0
}
]
}
یک مقیاس ساده Relevance:
0: نامرتبط1: ارتباط کم2: مرتبط3: پاسخ مستقیم و کامل
Recall@K
Recall@K بررسی میکند چند درصد اسناد مرتبط در K نتیجه اول بازیابی شدهاند.
Recall@K =
number of relevant documents found in top K
/ total number of relevant documents
Recall بیشتر برای ارزیابی Retriever اولیه اهمیت دارد. اگر Recall مرحله اول پایین باشد، Reranking نمیتواند مشکل را حل کند.
Precision@K
Precision@K نشان میدهد چه سهمی از K نتیجه اول واقعاً مرتبط هستند.
Precision@K =
number of relevant documents in top K
/ K
MRR
Mean Reciprocal Rank به رتبه اولین پاسخ مرتبط اهمیت میدهد:
reciprocal_rank =
1 / rank of the first relevant document
MRR =
average reciprocal_rank across all queries
اگر اولین نتیجه مرتبط در رتبه اول باشد، امتیاز آن Query برابر 1 است. اگر در رتبه پنجم باشد، امتیاز 0.2 خواهد بود.
nDCG
nDCG زمانی مفید است که اسناد درجات مختلفی از ارتباط داشته باشند.
فرمول متنی و سازگار با Ghost:
DCG@K =
sum for positions 1 through K of:
(2^relevance - 1) / log2(position + 1)
nDCG@K =
DCG@K / ideal_DCG@K
nDCG به سند بسیار مرتبطی که در رتبه پایین قرار گرفته جریمه بیشتری میدهد.
ارزیابی End-to-End
بهبود Retrieval Metric لزوماً به معنای بهبود پاسخ نهایی نیست. علاوه بر Reranking باید معیارهای زیر نیز اندازهگیری شوند:
- Correctness پاسخ
- Faithfulness نسبت به منابع
- کاملبودن پاسخ
- کیفیت Citation
- درصد پاسخهای بدون پشتوانه
- Latency کل
- تعداد Token ورودی
- هزینه هر Query
طراحی آزمایش برای انتخاب تنظیمات مناسب
برای تنظیم candidate_k و final_k میتوان Grid_Search انجام داد:
candidate_values = [20, 50, 100]
final_values = [3, 5, 8]
for candidate_k in candidate_values:
for final_k in final_values:
metrics = evaluate_pipeline(
candidate_k=candidate_k,
final_k=final_k
)
print({
"candidate_k": candidate_k,
"final_k": final_k,
"mrr": metrics["mrr"],
"ndcg_at_5": metrics["ndcg_at_5"],
"latency_ms": metrics["latency_ms"]
})
نتیجه بهتر همیشه بیشترین nDCG نیست. تنظیم مناسب باید با محدودیت Latency و هزینه پروژه سازگار باشد.
برای مقایسه دو معماری، حداقل این حالتها را آزمایش کنید:
- Vector_Search بدون Reranking
- BM25 بدون Reranking
- Hybrid_Search بدون Reranking
- Vector_Search همراه Cross-Encoder
- Hybrid_Search همراه Cross-Encoder
- Hybrid_Search همراه Late Interaction
بهینهسازی سرعت Reranking
استفاده از Batch
Pairها را بهصورت Batch به مدل ارسال کنید:
scores = model.predict(
pairs,
batch_size=32,
show_progress_bar=False
)
Batch بزرگتر همیشه سریعتر نیست. مقدار مناسب به حافظه GPU، طول اسناد و مدل وابسته است.
محدودکردن طول ورودی
اگر Chunkها بیشازحد طولانی باشند، هزینه Reranking بالا میرود. اندازه Chunk باید با نوع محتوا و محدودیت مدل تنظیم شود.
کاهش Candidateها با Retrieval بهتر
Hybrid_Search، Metadata Filtering و Query Routing میتوانند Candidateهای نامرتبط را قبل از Reranker حذف کنند.
اجرای مدل هنگام شروع برنامه
مدل را برای هر Request دوباره Load نکنید:
# Correct: load once during application startup
reranker = LocalReranker()
Warm-up
پس از Loadشدن مدل، یک Batch کوچک آزمایشی اجرا کنید تا زمان اولین Query واقعی کاهش پیدا کند.
reranker.rerank(
query="آزمایش",
documents=[
{
"id": "warmup",
"content": "متن آزمایشی"
}
],
top_n=1
)
استفاده از GPU، ONNX یا Quantization
بسته به مدل و زیرساخت میتوان از روشهای زیر استفاده کرد:
- CUDA
- Mixed Precision
- ONNX Runtime
- OpenVINO
- Quantization
- مدل کوچکتر
- سرویس مستقل Reranking
هر بهینهسازی باید دوباره از نظر کیفیت ارزیابی شود؛ زیرا Quantization یا کاهش طول ورودی ممکن است ترتیب نتایج را تغییر دهد.
معماری سرویس Reranking در Production
در پروژههای بزرگ بهتر است Reranker یک سرویس مستقل باشد:
Application Backend
|
v
Retrieval Service
|
v
Reranking Service
|
v
Context Builder
|
v
Darvareh AI API
نمونه Contract برای سرویس:
{
"query": "آموزش کاهش هزینه API",
"top_n": 5,
"documents": [
{
"id": "doc-1",
"text": "..."
},
{
"id": "doc-2",
"text": "..."
}
]
}
خروجی:
{
"results": [
{
"id": "doc-2",
"score": 0.932,
"original_index": 1
},
{
"id": "doc-1",
"score": 0.817,
"original_index": 0
}
]
}
وجود original_index یا id ضروری است؛ زیرا باید بتوان نتیجه Reranker را به Metadata اصلی Document متصل کرد.
پیادهسازی API ساده با FastAPI
pip install fastapi uvicorn sentence-transformers
کد سرویس:
from contextlib import asynccontextmanager
from fastapi import FastAPI
from pydantic import BaseModel, Field
from sentence_transformers import CrossEncoder
reranker_model = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global reranker_model
reranker_model = CrossEncoder(
"BAAI/bge-reranker-v2-m3",
max_length=512
)
yield
app = FastAPI(lifespan=lifespan)
class Document(BaseModel):
id: str
text: str
class RerankRequest(BaseModel):
query: str = Field(min_length=1)
documents: list[Document]
top_n: int = Field(default=5, ge=1, le=100)
@app.post("/rerank")
def rerank(request: RerankRequest):
pairs = [
(request.query, document.text)
for document in request.documents
]
if not pairs:
return {"results": []}
scores = reranker_model.predict(
pairs,
batch_size=16,
show_progress_bar=False
)
results = [
{
"id": document.id,
"score": float(score),
"original_index": index
}
for index, (document, score) in enumerate(
zip(request.documents, scores)
)
]
results.sort(
key=lambda item: item["score"],
reverse=True
)
return {
"results": results[:request.top_n]
}
اجرای سرویس:
uvicorn app:app --host 0.0.0.0 --port 8000
نمونه درخواست:
curl -X POST "http://localhost:8000/rerank" \
-H "Content-Type: application/json" \
-d '{
"query": "چگونه هزینه API را کاهش دهیم؟",
"top_n": 2,
"documents": [
{
"id": "doc-1",
"text": "Embedding برای جستجوی معنایی استفاده میشود."
},
{
"id": "doc-2",
"text": "کش، کاهش تاریخچه و انتخاب مدل کوچکتر هزینه API را کاهش میدهد."
}
]
}'
مانیتورینگ Reranking
در محیط Production این شاخصها را ثبت کنید:
- تعداد Candidateها
- تعداد نتایج نهایی
- طول متوسط Document
- زمان Tokenization
- زمان Inference
- Latency صدکهای P50، P95 و P99
- مصرف CPU و GPU
- خطای Out of Memory
- نرخ Truncation
- تغییر رتبه نتایج
- نسبت Queryهای بدون نتیجه مناسب
- کیفیت پاسخ نهایی
- مدل و نسخه Reranker
نمونه Log ساختاریافته:
{
"event": "rerank_completed",
"query_id": "q-8912",
"model": "BAAI/bge-reranker-v2-m3",
"candidate_count": 50,
"selected_count": 5,
"latency_ms": 83,
"max_length": 512,
"device": "cuda"
}
نام و نسخه مدل باید ثبت شود تا در زمان تغییر مدل بتوان Regression را تشخیص داد.
خطاهای رایج در استفاده از Reranker
اجرای Reranker روی تمام اسناد
Cross-Encoder برای رتبهبندی Candidateهای محدود طراحی شده است. اجرای آن روی میلیونها سند هزینه پردازشی بسیار زیادی دارد.
Candidate K بسیار کوچک
اگر فقط پنج نتیجه به Reranker بدهید، امکان جبران خطاهای Retriever محدود میشود.
استفاده از مدل انگلیسی برای محتوای فارسی
عملکرد خوب روی Benchmarkهای انگلیسی تضمینی برای عملکرد مناسب روی Queryهای فارسی نیست.
ارزیابی فقط با چند مثال دستی
برای مقایسه مدلها به Dataset ثابت و معیارهای قابل اندازهگیری نیاز دارید.
ارسال کل صفحات طولانی
صفحه ممکن است Truncate شود و پاسخ اصلی از ورودی مدل حذف شود. Chunking مناسب ضروری است.
استفاده مستقیم از Score بهعنوان Probability
خروجی بسیاری از Rerankerها Logit یا امتیاز نسبی است، نه احتمال کالیبرهشده.
ترکیب Scoreهای خام
امتیاز BM25، Cosine Similarity و Cross-Encoder دامنه یکسانی ندارند. قبل از ترکیب باید Normalization یا یک مدل Learning-to-Rank مناسب استفاده شود.
نادیدهگرفتن تنوع نتایج
پنج Chunk تقریباً یکسان از یک صفحه، Context مفیدی ایجاد نمیکنند. Deduplication و محدودیت تعداد Chunk هر Parent ضروری است.
ارزیابی Reranker بدون بررسی Retriever
ممکن است مشکل واقعی Recall پایین مرحله Retrieval باشد. در این شرایط تعویض Reranker بهتنهایی نتیجه مطلوبی ندارد.
چکلیست پیادهسازی Reranking
پیش از انتشار سیستم بررسی کنید:
- Retriever اولیه Recall مناسبی دارد.
- Queryهای فارسی واقعی در Dataset ارزیابی وجود دارند.
- شکل فارسی و انگلیسی اصطلاحات فنی پوشش داده شده است.
- Candidate K با آزمایش انتخاب شده است.
- Final K با ظرفیت Context مدل هماهنگ است.
- اسناد طولانی بهدرستی Chunk شدهاند.
- عنوان و Metadata ضروری همراه Chunk ارسال میشوند.
- نتایج تکراری حذف میشوند.
- Score خام مدل بهاشتباه Probability تلقی نمیشود.
- Latency مرحله Reranking جداگانه ثبت میشود.
- نسخه مدل در Logها وجود دارد.
- کیفیت Retrieval و پاسخ نهایی جداگانه سنجیده میشوند.
- API Key درواره فقط در Backend نگهداری میشود.
- Model IDها از تنظیمات محیطی خوانده میشوند.
- مسیر Base URL دقیقاً
https://api.darvareh.ir/v1است.
پرسشهای متداول
Reranker چیست؟
Reranker مدلی است که Query و مجموعهای از نتایج اولیه را دریافت میکند، ارتباط هر نتیجه با Query را میسنجد و ترتیب نتایج را اصلاح میکند.
آیا Reranking جایگزین Vector_Search است؟
خیر. Vector_Search معمولاً Candidateها را سریع پیدا میکند و Reranker همان Candidateها را با دقت بیشتری مرتب میکند. این دو مکمل یکدیگرند.
Cross-Encoder چه تفاوتی با Embedding Model دارد؟
Embedding Model برای Query و Document بردارهای جداگانه تولید میکند. Cross-Encoder هر جفت Query و Document را همزمان پردازش و مستقیماً یک امتیاز ارتباط تولید میکند.
آیا Reranking دقت RAG را افزایش میدهد؟
اگر Retriever سند مرتبط را در Candidate List قرار داده باشد، Reranking میتواند احتمال ورود بهترین اسناد به Context را افزایش دهد. میزان بهبود باید روی Dataset واقعی اندازهگیری شود.
چند نتیجه باید به Reranker داده شود؟
برای شروع، ۳۰ تا ۵۰ Candidate مقدار مناسبی است؛ اما مقدار نهایی به Latency، طول اسناد، مدل و Dataset پروژه بستگی دارد.
چند سند باید وارد Context مدل زبانی شود؟
معمولاً بین ۳ تا ۱۰ Chunk انتخاب میشود. ارسال اسناد بیشتر همیشه بهتر نیست و میتواند نویز و مصرف Token را افزایش دهد.
بهترین Reranker برای فارسی کدام است؟
یک پاسخ ثابت برای همه پروژهها وجود ندارد. باید چند مدل Multilingual را روی Queryها و اسناد واقعی فارسی مقایسه کنید. BAAI/bge-reranker-v2-m3 یکی از گزینههای قابل آزمایش است، نه انتخاب تضمینی برای تمام کاربردها.
آیا میتوان Reranker را روی CPU اجرا کرد؟
بله، اما Latency به اندازه مدل، طول اسناد، Batch Size و توان CPU بستگی دارد. برای ترافیک بالا ممکن است GPU، ONNX، Quantization یا مدل سبکتر لازم باشد.
آیا Score بالاتر همیشه به معنی سند مرتبطتر است؟
درون خروجی یک Query و مدل مشخص معمولاً Score بالاتر برای مرتبسازی استفاده میشود. بااینحال، Score مدلهای مختلف یا حتی Queryهای متفاوت لزوماً قابل مقایسه نیست.
ColBERT بهتر است یا Cross-Encoder؟
Cross-Encoder پیادهسازی سادهتری دارد و برای Reranking تعداد محدودی Candidate مناسب است. ColBERT با Late Interaction میتواند در مقیاس بزرگ تعادل بهتری میان سرعت و دقت ایجاد کند، اما زیرساخت پیچیدهتری میخواهد.
آیا استفاده از Reranker هزینه LLM را کاهش میدهد؟
بهصورت غیرمستقیم ممکن است کاهش دهد. با انتخاب اسناد دقیقتر میتوان Context کوتاهتر و کمنویزتری ساخت؛ در نتیجه تعداد Tokenهای ورودی مدل زبانی کاهش پیدا میکند.
جمعبندی
Reranking لایهای میان Retrieval و تولید پاسخ است که نتایج اولیه را با دقت بیشتری مرتب میکند. یک معماری قدرتمند RAG معمولاً از Retriever سریع برای حفظ Recall و Reranker دقیق برای افزایش Precision استفاده میکند.
برای شروع عملی:
- با BM25، Vector_Search یا Hybrid_Search حدود ۳۰ تا ۵۰ Candidate پیدا کنید.
- Candidateها را با یک Cross-Encoder چندزبانه Rerank کنید.
- نتایج تکراری را حذف کنید.
- بین ۳ تا ۸ Chunk برتر را وارد Context کنید.
- پاسخ را با یک مدل زبانی تولید کنید.
- Recall، MRR، nDCG، کیفیت پاسخ و Latency را اندازهگیری کنید.
- پارامترها را براساس داده واقعی پروژه تنظیم کنید.
برای دسترسی به API مدلهای هوش مصنوعی و اتصال Backend خود به مدلهای Chat و Embedding، میتوانید از API هوش مصنوعی درواره با Base URL زیر استفاده کنید:
https://api.darvareh.ir/v1
مقالات مرتبط
- آموزش Hybrid Search؛ ترکیب BM25 و Vector Search در RAG
- RAG چیست؟ آموزش Retrieval-Augmented Generation
- جستجوی معنایی یا Semantic Search چیست؟
- Vector Database چیست؟ راهنمای پایگاه داده برداری
- Embedding چیست؟ راهنمای کامل امبدینگ
- Chunking چیست؟ آموزش تقسیم متن برای RAG
- ساخت RAG با LangChain، LlamaIndex و API درواره
- ارزیابی مدل هوش مصنوعی و Evals
- آموزش LlamaIndex برای ساخت برنامههای RAG