RAG پیشرفته — Reranking، Hybrid و Query Expansion

قسمت ۹ ۲۲ دقیقه

یه مرور سریع

تو اپیزود قبلی یاد گرفتیم چطور RAG رو ارزیابی کنیم و شکست‌های رایجش رو تشخیص بدیم. حالا وقتشه بریم سراغ تکنیک‌هایی که کیفیت RAG رو به طرز چشمگیری بالا می‌برن. اینا تکنیک‌هایی هستن که فرق بین یه RAG ساده و یه RAG حرفه‌ای رو مشخص می‌کنن.

Reranking — مرحله دوم رتبه‌بندی

تو مرحله جستجو، یه لیست از تکه‌های «احتمالاً مرتبط» برمی‌گردونی. ولی مشکل اینه که رتبه‌بندی اولیه (بر اساس Cosine Similarity) همیشه دقیق نیست. شاید نتیجه سوم واقعاً مرتبط‌ترین باشه، نه نتیجه اول.

Reranking یعنی بعد از جستجوی اولیه، یه مدل هوشمندتر بیاد و نتایج رو دوباره رتبه‌بندی کنه.

Bi-Encoder vs Cross-Encoder

بذار فرق دو مدل رو با یه مثال توضیح بدم.

Bi-Encoder (همون مدل Embedding معمولی): مثل اینه که سؤال و هر تکه رو جداگانه بخونی و خلاصه کنی، بعد خلاصه‌ها رو مقایسه کنی. سریعه ولی ممکنه نکات ظریف ارتباط رو از دست بده.

Cross-Encoder: مثل اینه که سؤال و تکه رو کنار هم بذاری و با هم بخونی. آهسته‌تره ولی خیلی دقیق‌تره چون ارتباط مستقیم بین کلمات سؤال و تکه رو می‌بینه.

# Bi-Encoder (مرحله ۱ — جستجو)
query_vec = embed(query)        # سؤال رو جدا embed کن
doc_vecs = embed(documents)     # تکه‌ها رو جدا embed کن
scores = cosine_similarity(query_vec, doc_vecs)

# Cross-Encoder (مرحله ۲ — Reranking)
for doc in top_20_results:
    score = cross_encoder(query + " [SEP] " + doc)
    # سؤال و تکه رو با هم پردازش می‌کنه

عملی با Cohere Rerank

import cohere

co = cohere.Client("YOUR_API_KEY")

results = co.rerank(
    query="چطور رمز عبور رو عوض کنم؟",
    documents=initial_results,  # ۲۰ نتیجه اولیه
    top_n=5,                    # فقط ۵ تای بهترین رو برگردون
    model="rerank-multilingual-v3.0"
)

# حالا results دقیق‌تر مرتب شدن

نکته: Reranking روی ۲۰-۵۰ نتیجه اولیه انجام بده، نه روی کل دیتابیس. اگه روی همه تکه‌ها Rerank کنی، فایده سرعت ANN رو از دست می‌دی.

تأثیر Reranking

تحقیقات نشون می‌ده Reranking معمولاً ۵ تا ۱۵ درصد دقت بازیابی رو بالا می‌بره. مخصوصاً وقتی سؤالات پیچیده یا مبهم هستن.

Query Expansion — سؤال رو گسترش بده

گاهی سؤال کاربر خیلی کوتاه یا مبهمه. «مشکل شبکه» می‌تونه هزار تا معنی بده. Query Expansion یعنی سؤال رو بازنویسی یا گسترش بدی تا جستجوی بهتری داشته باشی.

روش ۱ — بازنویسی با LLM

rewrite_prompt = """
سؤال کاربر: {original_query}

این سؤال را به ۳ سؤال مشخص‌تر و دقیق‌تر بازنویسی کن 
که مفهوم اصلی را حفظ کنند.
"""

# ورودی: "مشکل شبکه"
# خروجی:
# 1. "علت قطعی اتصال شبکه وای‌فای چیست؟"
# 2. "چگونه مشکلات اتصال اینترنت را عیب‌یابی کنیم؟"
# 3. "تنظیمات شبکه برای رفع مشکل اتصال چیست؟"

حالا هر سه سؤال رو جستجو می‌کنی و نتایج رو ترکیب می‌کنی. شانس پیدا کردن تکه‌های مرتبط خیلی بیشتر می‌شه.

روش ۲ — Multi-Query Retrieval

مشابه روش قبلی، ولی سیستماتیک‌تر:

def multi_query_retrieve(original_query, n_queries=3):
    # مرحله ۱: تولید سؤالات مختلف
    queries = llm.generate_variations(original_query, n=n_queries)
    
    # مرحله ۲: جستجو برای هر سؤال
    all_results = []
    for q in queries:
        results = vector_search(q, top_k=10)
        all_results.extend(results)
    
    # مرحله ۳: حذف تکراری‌ها و رتبه‌بندی
    unique_results = deduplicate(all_results)
    return rerank(original_query, unique_results, top_n=5)

HyDE — فرضیه‌سازی قبل از جستجو

Hypothetical Document Embeddings یا HyDE یه ایده خلاقانه‌ست. به جای اینکه مستقیم سؤال رو Embed و جستجو کنی، اول از LLM بخوای یه «جواب فرضی» بنویسه. بعد اون جواب فرضی رو Embed و جستجو کن.

چرا این کار می‌کنه؟ چون بردار یه «جواب» به بردار «تکه‌های حاوی جواب» نزدیک‌تره تا بردار یه «سؤال».

def hyde_retrieval(question):
    # مرحله ۱: تولید جواب فرضی (بدون Context)
    hypothetical_answer = llm.generate(f"""
    به این سؤال یک پاسخ فرضی و کامل بنویس.
    نیازی نیست دقیق باشد، فقط باید شبیه جواب واقعی باشد.
    
    سؤال: {question}
    """)
    
    # مرحله ۲: Embed کردن جواب فرضی
    hyde_vector = embed(hypothetical_answer)
    
    # مرحله ۳: جستجو با بردار جواب فرضی
    results = vector_search(hyde_vector, top_k=10)
    
    return results

مثال:

  • سؤال: «چطور PDF آپلود کنم؟»
  • جواب فرضی: «برای آپلود فایل PDF، به بخش مدیریت فایل‌ها بروید، دکمه آپلود را بزنید، فایل PDF خود را انتخاب کنید و…»
  • حالا بردار این جواب فرضی رو جستجو می‌کنیم — احتمالاً تکه‌هایی پیدا می‌شن که واقعاً درباره آپلود PDF هستن

نکته: HyDE همیشه بهتر نیست. برای سؤالات ساده و مشخص ممکنه فرقی نکنه یا حتی بدتر بشه. برای سؤالات مبهم و پیچیده معمولاً خیلی مؤثره.

Parent-Child Chunking — تکه‌بندی والد-فرزند

تو اپیزود Chunking گفتیم تکه‌ها نباید خیلی بزرگ باشن (چون Embedding دقتش پایین میاد) و نباید خیلی کوچیک باشن (چون Context از دست میره). Parent-Child Chunking یه راه‌حل هوشمندانه برای این مشکله.

ایده: متن رو به دو سطح تکه‌بندی کن:

  • Child Chunks (فرزند): تکه‌های کوچیک (مثلاً ۲۰۰ توکن) — برای جستجو استفاده می‌شن
  • Parent Chunks (والد): تکه‌های بزرگ‌تر (مثلاً ۱۰۰۰ توکن) — برای Context به LLM فرستاده می‌شن
def parent_child_chunking(document):
    # مرحله ۱: تکه‌بندی بزرگ (والد)
    parent_chunks = split(document, chunk_size=1000)
    
    # مرحله ۲: هر والد رو به فرزندها تقسیم کن
    for parent in parent_chunks:
        children = split(parent, chunk_size=200)
        for child in children:
            # ذخیره فرزند با ارجاع به والد
            store(child, metadata={"parent_id": parent.id})
    
def retrieve(query):
    # مرحله ۱: جستجو بین فرزندها (تکه‌های کوچیک)
    matching_children = vector_search(query, top_k=5)
    
    # مرحله ۲: والدهای مربوطه رو برگردون
    parent_ids = set(c.metadata["parent_id"] for c in matching_children)
    parents = fetch_parents(parent_ids)
    
    return parents  # تکه‌های بزرگ‌تر با Context بیشتر

چرا خوبه؟ جستجو روی تکه‌های کوچیک دقیق‌تره (چون بردار تمرکز بیشتری داره). ولی Context بزرگ‌تر به LLM فرستاده می‌شه (چون اطلاعات بیشتری داره).

مثل اینه که تو فهرست کتاب دنبال یه عنوان بگردی (جستجوی دقیق)، ولی وقتی پیداش کردی کل فصل رو بخونی (Context کامل).

Contextual Compression — فشرده‌سازی هوشمند

بعضی وقت‌ها تکه‌ای که بازیابی می‌شه ۵۰۰ کلمه‌ست ولی فقط ۲ جمله‌اش واقعاً مرتبطه. بقیه‌اش نویز و فضای بیخودی از Context Window رو اشغال می‌کنه.

Contextual Compression یعنی بعد از بازیابی، هر تکه رو فشرده کنی و فقط بخش‌های مرتبط رو نگه داری.

def contextual_compression(query, retrieved_chunks):
    compressed = []
    for chunk in retrieved_chunks:
        result = llm.generate(f"""
        سؤال: {query}
        متن: {chunk}
        
        فقط بخش‌هایی از متن را استخراج کن که مستقیماً 
        به سؤال مرتبط هستند. اگر هیچ بخشی مرتبط نیست، 
        «نامرتبط» بنویس.
        """)
        
        if result != "نامرتبط":
            compressed.append(result)
    
    return compressed

مزایا:

  • Context Window کمتر مصرف می‌شه
  • LLM روی اطلاعات مرتبط‌تر تمرکز می‌کنه
  • مشکل Lost in the Middle کمتر می‌شه

معایب:

  • یه فراخوانی LLM اضافی برای هر تکه لازمه
  • تأخیر بیشتر و هزینه بیشتر

ترکیب تکنیک‌ها — پایپلاین پیشرفته

حالا بذار همه تکنیک‌ها رو کنار هم بذاریم و یه پایپلاین کامل بسازیم:

def advanced_rag_pipeline(user_query):
    # مرحله ۱: Query Expansion
    expanded_queries = expand_query(user_query, n=3)
    
    # مرحله ۲: Multi-Query Retrieval
    all_chunks = []
    for q in expanded_queries:
        # جستجوی ترکیبی (Hybrid Search)
        vector_results = vector_search(q, top_k=15)
        keyword_results = bm25_search(q, top_k=15)
        merged = rrf_merge(vector_results, keyword_results)
        all_chunks.extend(merged)
    
    # مرحله ۳: حذف تکراری
    unique_chunks = deduplicate(all_chunks)
    
    # مرحله ۴: Reranking
    reranked = cross_encoder_rerank(
        query=user_query,
        documents=unique_chunks,
        top_n=8
    )
    
    # مرحله ۵: Contextual Compression
    compressed = contextual_compression(user_query, reranked)
    
    # مرحله ۶: تولید جواب
    answer = llm.generate(
        system_prompt=rag_system_prompt,
        context=compressed,
        question=user_query
    )
    
    return answer

هشدار: لازم نیست همه تکنیک‌ها رو استفاده کنی! شروع کن با RAG ساده، بعد بر اساس نتایج ارزیابی (اپیزود قبلی) ببین کجا ضعف داری و تکنیک مناسب رو اضافه کن.

کی از کدوم تکنیک استفاده کنم؟

Reranking: تقریباً همیشه مفیده. اگه فقط یه تکنیک اضافه می‌کنی، این باشه.

Query Expansion: وقتی کاربرا سؤالات کوتاه یا مبهم می‌پرسن.

HyDE: وقتی سؤالات خیلی متفاوت از متن مستندات هستن (مثلاً سؤال عامیانه، مستندات رسمی).

Parent-Child Chunking: وقتی مستنداتت ساختاریافته هستن (مثل مقالات با عنوان و زیرعنوان).

Contextual Compression: وقتی Context Window محدوده یا تکه‌هات خیلی بلندن.

جمع‌بندی

تو این اپیزود یاد گرفتی که:

  • Reranking با Cross-Encoder نتایج جستجو رو خیلی دقیق‌تر می‌کنه
  • Query Expansion و Multi-Query Retrieval سؤالات مبهم رو بهتر مدیریت می‌کنن
  • HyDE با تولید جواب فرضی، جستجو رو بهبود می‌ده
  • Parent-Child Chunking دقت جستجو و غنای Context رو همزمان بالا می‌بره
  • Contextual Compression نویز رو حذف می‌کنه و Context Window رو بهینه می‌کنه
  • لازم نیست همه تکنیک‌ها رو استفاده کنی — بر اساس نیازت انتخاب کن

حالا یه RAG قدرتمند داری که خوب جستجو می‌کنه، خوب جواب می‌ده، و می‌دونی چطور ارزیابیش کنی. ولی یه سؤال بزرگ مونده: چطور این رو ببری تو پروداکشن و واقعاً روی سرور اجرا کنی؟ اپیزود بعدی و آخرین اپیزود این سری، درباره RAG در پروداکشن هست — از چالش‌های مقیاس‌پذیری تا امنیت و هزینه.

نظرات

هنوز نظری ثبت نشده. اولین نفر باشید!

نظر خود را بنویسید