You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

16 KiB

پایپ‌لاین بازیابی دو مرحله‌ای: Vector Search + Reranking

مقدمه

در سیستم‌های RAG (Retrieval-Augmented Generation)، کیفیت پاسخ مدل زبانی مستقیماً به کیفیت داده‌هایی بستگی دارد که به عنوان context به آن داده می‌شود. اگر داده‌های نامرتبط یا کم‌ارتباط به مدل برسند، پاسخ نهایی نیز ضعیف خواهد بود.

برای حل این مشکل، ما از یک پایپ‌لاین بازیابی دو مرحله‌ای استفاده می‌کنیم:

  1. مرحله اول - Vector Search: بازیابی N کاندید اولیه از وکتور دیتابیس بر اساس فاصله معنایی (Semantic Similarity)
  2. مرحله دوم - Reranking: ارزیابی دقیق‌تر کاندیدها توسط یک مدل Reranker و انتخاب بهترین‌ها

چرا فقط Vector Search کافی نیست؟

محدودیت‌های جستجوی وکتوری

جستجوی وکتوری (Vector Search) بر اساس فاصله بردارها (مثلاً Cosine Similarity) کار می‌کند. یعنی سوال کاربر و اسناد موجود در دیتابیس هر دو به بردار تبدیل می‌شوند و نزدیک‌ترین بردارها به عنوان نتیجه برگردانده می‌شوند.

اما این روش محدودیت‌هایی دارد:

محدودیت توضیح
دقت متوسط Embedding model ها معنای کلی متن را می‌گیرند، نه ارتباط دقیق سوال-جواب را
حساسیت به نحوه نوشتن دو جمله با معنای یکسان اما ساختار متفاوت ممکن است فاصله بیشتری داشته باشند
عدم درک سوال-جواب مدل embedding فقط شباهت معنایی می‌سنجد، نه اینکه آیا یک سند واقعاً پاسخ سوال را دارد
چالش‌های چند‌زبانه در متون فارسی/عربی/انگلیسی مخلوط، embedding ممکن است دقت کمتری داشته باشد

مثال عملی

فرض کنید کاربر می‌پرسد: "حکم روزه مسافر چیست؟"

Vector Search ممکن است این نتایج را برگرداند:

  1. سندی درباره احکام روزه مسافر (مرتبط)
  2. سندی درباره احکام نماز مسافر (شبیه اما نامرتبط)
  3. سندی درباره فضیلت روزه (کلمه روزه دارد اما پاسخ سوال نیست)
  4. سندی درباره احکام روزه بیمار (مشابه اما متفاوت)

Reranker می‌تواند تشخیص دهد که سند ۱ دقیقاً به سوال پاسخ می‌دهد و بقیه را در اولویت پایین‌تر قرار دهد.

معماری پایپ‌لاین

نمای کلی

graph TD
    A["سوال کاربر"] --> B["rag_injection_hook"]
    B --> C["build_rag_prompt"]
    
    subgraph "مرحله ۱: Vector Search"
        C --> D["Embedding سوال کاربر"]
        D --> E["جستجو در Qdrant"]
        E --> F["N=7 کاندید اولیه"]
    end
    
    subgraph "مرحله ۲: Reranking"
        F --> G["ارسال به Jina Reranker API"]
        G --> H["ارزیابی ارتباط هر کاندید با سوال"]
        H --> I["مرتب‌سازی بر اساس relevance_score"]
        I --> J["انتخاب top_n=3 نتیجه برتر"]
    end
    
    J --> K["ساخت context با منبع و امتیاز"]
    K --> L["تزریق به prompt کاربر"]
    L --> M["ارسال به LLM"]
    M --> N["پاسخ نهایی"]

جریان داده

سوال کاربر
    │
    ▼
┌─────────────────────────────┐
│  Pre-Hook: rag_injection_hook│  ← src/utils/hooks.py
│  ورودی کاربر را دریافت و    │
│  به build_rag_prompt پاس    │
│  می‌دهد                     │
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│  مرحله ۱: Vector Search     │  ← src/utils/search_knowledge.py
│  ─────────────────────────  │
│  • Embedding سوال با         │
│    EmbeddingFactory          │
│  • جستجو در Qdrant           │
│  • دریافت 7 کاندید اولیه    │
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│  مرحله ۲: Reranking         │  ← src/utils/reranker.py
│  ─────────────────────────  │
│  • ارسال سوال + 7 کاندید    │
│    به Jina Reranker API      │
│  • دریافت relevance_score   │
│    برای هر کاندید            │
│  • انتخاب 3 کاندید برتر     │
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│  ساخت Context نهایی         │
│  ─────────────────────────  │
│  • افزودن منبع و امتیاز     │
│    به هر سند                 │
│  • ترکیب با سوال کاربر      │
│  • تزریق به RunInput        │
└─────────────┬───────────────┘
              │
              ▼
        Agent (LLM)

پیاده‌سازی

مرحله ۱: نقطه ورود - Pre-Hook

فایل: src/utils/hooks.py

@observe(name="rag_injection_hook")
def rag_injection_hook(run_input: RunInput, **kwargs):
    """
    Intercepts the user input and injects RAG context.
    """
    original_input = run_input.input_content
    run_input.input_content = build_rag_prompt(original_input)

این hook قبل از اجرای Agent فراخوانی می‌شود. ورودی خام کاربر را می‌گیرد و آن را از پایپ‌لاین RAG عبور می‌دهد. نتیجه (prompt غنی‌شده با context) جایگزین ورودی اصلی می‌شود.

نکته: تغییرات روی run_input به صورت in-place اعمال می‌شوند. نیازی به return نیست.

مرحله ۲: Vector Search - بازیابی کاندیدهای اولیه

فایل: src/utils/search_knowledge.py

def build_rag_prompt(user_question: str, embedder_model_name: str = None) -> str:
    global knowledge_base

    # Lazy initialization
    if knowledge_base is None:
        embed_factory = EmbeddingFactory()
        embedder = embed_factory.get_embedder(embedder_model_name)
        vector_db = get_qdrant_store(embedder=embedder)
        knowledge_base = Knowledge(vector_db=vector_db)

    # جستجوی اولیه: دریافت 7 کاندید نزدیک‌ترین
    initial_results = knowledge_base.search(query=user_question, max_results=7)

چرا max_results=7؟

عدد 7 یک تعادل بین دو نیاز است:

  • پوشش کافی: اگر تعداد کاندیدها خیلی کم باشد (مثلاً 3)، ممکن است بهترین نتیجه در بین آن‌ها نباشد
  • سرعت و هزینه: اگر تعداد خیلی زیاد باشد (مثلاً 50)، هزینه و زمان Reranking بالا می‌رود

با 7 کاندید اولیه، Reranker فضای کافی برای انتخاب 3 نتیجه واقعاً مرتبط دارد.

Lazy Initialization

Knowledge Base فقط یک‌بار مقداردهی می‌شود (در اولین درخواست) و سپس در یک متغیر global نگهداری می‌شود. این کار از ایجاد اتصال مجدد به Qdrant در هر درخواست جلوگیری می‌کند.

مرحله ۳: Reranking - فیلتر هوشمند نتایج

فایل: src/utils/reranker.py

def rerank_documents(query: str, documents: List[Any], top_n: int = 3) -> List[Any]:
    # 1. استخراج محتوای متنی اسناد
    doc_contents = [doc.content for doc in documents]

    # 2. ارسال به Jina Reranker API
    payload = {
        "model": "jina-reranker-v3",
        "query": query,
        "documents": doc_contents,
        "top_n": top_n
    }
    response = requests.post(url, headers=headers, json=payload)
    results = response.json()["results"]

    # 3. بازسازی لیست اسناد با امتیاز جدید
    reranked_docs = []
    for result in results:
        original_index = result["index"]
        relevance_score = result["relevance_score"]
        doc = documents[original_index]
        doc.meta_data["rerank_score"] = relevance_score
        reranked_docs.append(doc)

    return reranked_docs

مدل Reranker: jina-reranker-v3

از مدل Jina Reranker v3 استفاده می‌کنیم. دلایل انتخاب این مدل:

ویژگی توضیح
پشتیبانی چند‌زبانه عملکرد مناسب روی متون فارسی، عربی و انگلیسی
درک سوال-جواب بر خلاف embedding، ارتباط بین سوال و پاسخ را می‌فهمد (Cross-Encoder)
سرعت برای تعداد کم اسناد (7 کاندید) بسیار سریع است
API ساده فقط یک HTTP POST call نیاز دارد

تفاوت Reranker با Embedding

Embedding (Bi-Encoder) Reranker (Cross-Encoder)
ورودی هر متن جداگانه encode می‌شود سوال و سند با هم encode می‌شوند
خروجی بردار (vector) امتیاز ارتباط (relevance score)
سرعت سریع (مناسب جستجوی میلیون‌ها سند) کندتر (مناسب ده‌ها سند)
دقت متوسط بالا
کاربرد مرحله اول: غربال‌گری سریع مرحله دوم: انتخاب دقیق

به زبان ساده:

  • Embedding مثل خواندن سریع عنوان کتاب‌ها در کتابخانه و انتخاب چند کتاب مرتبط است
  • Reranker مثل ورق زدن آن چند کتاب و انتخاب بهترین‌ها بر اساس محتوای واقعی

نحوه کار Jina API

Jina یک لیست از اسناد و یک سوال دریافت می‌کند. برای هر سند، یک relevance_score (بین 0 تا 1) و index (شماره سند در لیست اصلی) برمی‌گرداند:

{
  "results": [
    { "index": 2, "relevance_score": 0.92 },
    { "index": 0, "relevance_score": 0.78 },
    { "index": 5, "relevance_score": 0.65 }
  ]
}

سپس ما با استفاده از index، سند اصلی را از لیست اولیه پیدا کرده و relevance_score را در meta_data آن ذخیره می‌کنیم.

مرحله ۴: ساخت Context نهایی

پس از reranking، اسناد برتر به همراه اطلاعات منبع و امتیاز ارتباط به یک context تبدیل می‌شوند:

context_parts = []
for doc in relevant_docs:
    meta = getattr(doc, "meta_data", {}) or {}
    source = meta.get('source', 'Unknown')
    score = meta.get('rerank_score', 0)
    content = f"[Source: {source} | Relevance: {score:.2f}]\n{doc.content}"
    context_parts.append(content)
context_str = "\n\n".join(context_parts)

هر سند در context نهایی شامل:

  • Source: منبع سند (مثلاً نام کتاب یا فایل)
  • Relevance: امتیاز ارتباط از Reranker (0 تا 1)
  • Content: محتوای اصلی سند

مرحله ۵: Prompt نهایی

final_prompt = (
    "Here is the context from the database:\n"
    "---------------------\n"
    f"{context_str}\n"
    "---------------------\n"
    f"User Question: {user_question}"
)

این prompt به LLM داده می‌شود. مدل با دیدن context مرتبط و سوال کاربر، پاسخ مناسب تولید می‌کند.

مدیریت خطا و Fallback

سطح ۱: خطا در Reranker

اگر Jina API در دسترس نباشد یا خطا دهد، سیستم به ترتیب اصلی Vector Search بازمی‌گردد:

except Exception as e:
    print(f"❌ Reranking failed: {e}. Falling back to vector search order.")
    return documents[:top_n]

سطح ۲: عدم وجود API Key

اگر JINA_API_KEY تنظیم نشده باشد، بدون reranking ادامه می‌دهد:

api_key = os.getenv("JINA_API_KEY")
if not api_key:
    print("⚠️ JINA_API_KEY not found. Returning original order.")
    return documents[:top_n]

سطح ۳: خطا در Knowledge Base

اگر کل Knowledge Base در دسترس نباشد، به مدل اطلاع داده می‌شود که پاسخ ندهد:

except Exception as e:
    context_str = "Knowledge base temporarily unavailable. dont answer the question ..."

خلاصه زنجیره Fallback

Reranking موفق → 3 سند با بالاترین ارتباط
        │ (خطا)
        ▼
Fallback → 3 سند اول از Vector Search (بدون reranking)
        │ (خطا)
        ▼
Fallback → پیام عدم دسترسی به Knowledge Base

تنظیمات و پارامترها

متغیرهای محیطی

متغیر توضیح الزامی
JINA_API_KEY کلید API برای Jina Reranker بله (بدون آن فقط Vector Search)
QDRANT_URL آدرس سرور Qdrant بله

پارامترهای قابل تنظیم

پارامتر مقدار فعلی محل تعریف توضیح
max_results 7 search_knowledge.py تعداد کاندیدهای اولیه از Vector Search
top_n 3 search_knowledge.pyrerank_documents() تعداد نتایج نهایی پس از Reranking
model jina-reranker-v3 reranker.py مدل Reranker مورد استفاده

توصیه برای تنظیم پارامترها

  • نسبت max_results به top_n باید حداقل 2:1 باشد تا Reranker فضای کافی برای انتخاب داشته باشد
  • افزایش max_results دقت را بالا می‌برد اما سرعت و هزینه API را افزایش می‌دهد
  • top_n=3 برای اکثر سناریوها مناسب است؛ تعداد بیشتر ممکن است context را شلوغ کند

فایل‌های مرتبط

فایل مسئولیت
src/utils/hooks.py Pre-Hook برای تزریق RAG به ورودی Agent
src/utils/search_knowledge.py پایپ‌لاین اصلی RAG: جستجو، reranking و ساخت prompt
src/utils/reranker.py ارتباط با Jina Reranker API و مرتب‌سازی نتایج
src/knowledge/embedding_factory.py ساخت و مدیریت مدل‌های Embedding
src/knowledge/vector_store.py اتصال به Qdrant و مدیریت collection

نتیجه‌گیری

پایپ‌لاین دو مرحله‌ای Vector Search + Reranking بهترین تعادل بین سرعت و دقت را فراهم می‌کند:

  • مرحله اول (Vector Search) سریع است و از بین میلیون‌ها سند، تعداد محدودی کاندید مرتبط انتخاب می‌کند
  • مرحله دوم (Reranking) دقیق است و با درک عمیق‌تر ارتباط سوال-جواب، بهترین اسناد را فیلتر می‌کند

این ترکیب باعث می‌شود context ارسالی به LLM با کیفیت بالا باشد و در نتیجه پاسخ‌های دقیق‌تر و مرتبط‌تری تولید شود.