Reranking چیست؟ آموزش افزایش دقت RAG با Cross-Encoder و مدل‌های Reranker

Reranking نتایج اولیه جستجو را با مدلی دقیق‌تر دوباره رتبه‌بندی می‌کند. در این آموزش، پیاده‌سازی Cross-Encoder، انتخاب مدل، ارزیابی و اتصال آن به RAG را بررسی می‌کنیم.

Share
Reranking چیست؟ آموزش افزایش دقت RAG با Cross-Encoder و مدل‌های Reranker

مقدمه

در بسیاری از سیستم‌های 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 ممکن است نتایج زیر را برگرداند:

  1. روش محاسبه هزینه API
  2. معرفی Token و Context Window
  3. تکنیک‌های کاهش مصرف Token
  4. مقایسه قیمت مدل‌های زبانی
  5. استفاده از Prompt Caching

هر پنج نتیجه با Query ارتباط دارند؛ اما نتیجه سوم احتمالاً باید در رتبه اول قرار بگیرد. یک Reranker ارتباط Query و هر Document را با دقت بیشتری بررسی می‌کند و ترتیب را تغییر می‌دهد:

  1. تکنیک‌های کاهش مصرف Token
  2. استفاده از Prompt Caching
  3. روش محاسبه هزینه API
  4. معرفی Token و Context Window
  5. مقایسه قیمت مدل‌های زبانی

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-EncoderCross-Encoder
ورودی مدلQuery و Document جداگانهQuery و Document هم‌زمان
خروجیEmbeddingامتیاز ارتباط
امکان ذخیره خروجی اسنادبلهخیر
سرعت جستجوبسیار بالاپایین‌تر
دقت رتبه‌بندیمناسبمعمولاً بالاتر
کاربرد اصلیCandidate GenerationReranking
مقیاس مناسبمیلیون‌ها سندده‌ها یا صدها 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 ساده می‌تواند مراحل زیر را اجرا کند:

  1. دریافت Query
  2. نرمال‌سازی Query
  3. بازیابی Candidateها
  4. حذف نتایج تکراری
  5. Reranking
  6. انتخاب اسناد نهایی
  7. ساخت Context
  8. ارسال Context به مدل زبانی
  9. تولید پاسخ همراه منابع

نمونه ساختار:

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 KFinal 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 و هزینه پروژه سازگار باشد.

برای مقایسه دو معماری، حداقل این حالت‌ها را آزمایش کنید:

  1. Vector_Search بدون Reranking
  2. BM25 بدون Reranking
  3. Hybrid_Search بدون Reranking
  4. Vector_Search همراه Cross-Encoder
  5. Hybrid_Search همراه Cross-Encoder
  6. 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 استفاده می‌کند.

برای شروع عملی:

  1. با BM25، Vector_Search یا Hybrid_Search حدود ۳۰ تا ۵۰ Candidate پیدا کنید.
  2. Candidateها را با یک Cross-Encoder چندزبانه Rerank کنید.
  3. نتایج تکراری را حذف کنید.
  4. بین ۳ تا ۸ Chunk برتر را وارد Context کنید.
  5. پاسخ را با یک مدل زبانی تولید کنید.
  6. Recall، MRR، nDCG، کیفیت پاسخ و Latency را اندازه‌گیری کنید.
  7. پارامترها را براساس داده واقعی پروژه تنظیم کنید.

برای دسترسی به API مدل‌های هوش مصنوعی و اتصال Backend خود به مدل‌های Chat و Embedding، می‌توانید از API هوش مصنوعی درواره با Base URL زیر استفاده کنید:

https://api.darvareh.ir/v1

مقالات مرتبط

Read more