فاز 11 · درس 7

راهنمای جامع RAG پیشرفته: جست‌وجوی ترکیبی، بازرتبه‌بندی و قطعه‌بندی هوشمند

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
یک قیف چندمرحله‌ای مدرن که اسناد خام را دریافت کرده و پس از فیلتر، تطبیق کلیدواژه‌ای و پالایش دقیق، مرتبط‌ترین برگه‌ها را دسته‌بندی می‌کند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم
  4. جست‌وجوی ترکیبی: معنایی + کلیدواژه‌ای
  5. ادغام متقابل رتبه‌ها (RRF)
  6. بازرتبه‌بندی
  7. تبدیل پرسمان
  8. قطعه‌بندی متن والد-فرزند
  9. فیلتر کردن بر اساس متادیتا
  10. ارزیابی
  11. پیاده‌سازی از صفر
  12. گام 1: پیاده‌سازی الگوریتم BM25
  13. گام 2: ادغام متقابل رتبه‌ها (RRF)
  14. گام 3: پایپ‌لاین جست‌وجوی ترکیبی
  15. گام 4: بازرتبه‌بندی ساده
  16. گام 5: پیاده‌سازی HyDE (امبدینگ فرضی سند)
  17. گام 6: قطعه‌بندی متن والد-فرزند
  18. گام 7: ارزیابی وفاداری
  19. استفاده از ابزارهای واقعی
  20. خروجی این درس
  21. تمرین‌ها
  22. اصطلاحات کلیدی
  23. برای مطالعه‌ی بیشتر

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

در RAG پیشرفته، با تلفیق جست‌وجوی کلیدواژه‌ای سنتی (مانند BM25) و جست‌وجوی برداری معنایی از طریق ادغام متقابل رتبه‌ها (RRF)، و سپس استفاده از مدل‌های بازرتبه‌بندی (Reranking)، دقت بازیابی را چندین برابر می‌کنید تا خروجی نهایی بدون توهم و کاملاً منطبق بر اسناد مرجع باشد.

اگر برای بخش بازرتبه‌بندی یا تولید پاسخ فرضی (HyDE) قصد استفاده از سرویس‌های خارجی مثل Cohere یا OpenAI را دارید، توجه داشته باشید که کارت‌های بانکی ایرانی برای پرداخت مستقیم قابل استفاده نیستند و باید از واسطه‌ها یا ارائه‌دهندگان داخلی بهره ببرید؛ همچنین مدل‌های سبکی مانند MiniLM یا bge-reranker به شکل کاملاً محلی و بدون نیاز به اینترنت نیز قابل اجرا هستند.

سیستم RAG پایه‌ای k قطعه‌ی دارای بیشترین شباهت را بازیابی می‌کند. این سازوکار برای پرسش‌های ساده جواب می‌دهد؛ اما در مواجهه با استدلال چندمرحله‌ای (multi-hop reasoning)، پرسش‌های مبهم و مجموعه‌داده‌های عظیم از پا درمی‌آید. تفاوت RAG پیشرفته با نوع پایه‌ای، تفاوت میان دموهایی است که روی 10 سند کار می‌کنند با سیستم‌هایی که پاسخ‌گوی 10 میلیون سند هستند.

مرتبط: بخش 23 از فاز 5 (استراتژی‌های قطعه‌بندی متن برای RAG) هر شش الگوریتم قطعه‌بندی متن — بازگشتی، معنایی، جمله‌ای، والد-فرزند، قطعه‌بندی متأخر و بازیابی بافت‌محور — را همراه با بنچمارک‌های Vectara/Anthropic پوشش می‌دهد. این درس بر پایه‌ی همان مباحث بنا شده است: جست‌وجوی ترکیبی، بازرتبه‌بندی و تبدیل پرسمان.

اهداف یادگیری

  • پیاده‌سازی استراتژی‌های پیشرفته‌ی قطعه‌بندی متن (معنایی، بازگشتی، والد-فرزند) به‌گونه‌ای که ساختار سند و بافت متن حفظ شود
  • ساخت یک پایپ‌لاین جست‌وجوی ترکیبی که تطبیق کلیدواژه‌ای BM25 را با جست‌وجوی برداری معنایی و یک مدل بازرتبه‌بندی کراس‌ان‌کودر ترکیب می‌کند
  • به‌کارگیری تکنیک‌های تبدیل پرسمان (HyDE، چندپرسمانی و عقب‌گرد) جهت بهبود بازیابی در پرسش‌های مبهم یا پیچیده
  • شناسایی و رفع خطاهای رایج در RAG: بازیابی قطعه‌ی اشتباه، نبود پاسخ در بافت متن و شکست در استدلال چندمرحله‌ای

صورت مسئله

شما در درس 06 یک پایپ‌لاین RAG پایه‌ای ساختید. آن سیستم برای پرسش‌های سرراست روی مجموعه‌ای کوچک از اسناد به‌خوبی کار می‌کند. حالا این سناریوها را امتحان کنید:

پرسمان مبهم: «درآمد فصل گذشته چقدر بود؟» جست‌وجوی معنایی قطعه‌هایی درباره‌ی استراتژی درآمد، پیش‌بینی‌های درآمد و دیدگاه مدیر مالی درباره‌ی رشد درآمد برمی‌گرداند. همه‌ی این‌ها از نظر معنایی به واژه‌ی «درآمد» شبیه‌اند، اما هیچ‌کدام عدد واقعی را ندارند. قطعه‌ی درست عبارت «47.2 میلیون دلار در فصل سوم 2025» را در بر دارد، اما به‌جای «درآمد» از واژه‌ی «عایدی» استفاده کرده است. مدل امبدینگ تصور می‌کند «استراتژی درآمد» به پرسمان نزدیک‌تر از «عایدی فصل سوم 47.2 میلیون دلار بود» است.

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

مسئله‌ی مجموعه‌داده‌های عظیم: شما 2 میلیون قطعه دارید. پاسخ درست در قطعه‌ی شماره‌ی 1,847,293 پنهان است. سیستم بازیابی شما در 5 نتیجه‌ی برتر، قطعه‌های شماره‌ی 14، 89,201، 1,200,000، 44 و 901,333 را بیرون می‌کشد؛ مواردی که در فضای امبدینگ نزدیک هستند، اما هیچ‌کدام پاسخ را در بر ندارند. در چنین مقیاسی، خطای ذاتی جست‌وجوی تقریبی نزدیک‌ترین همسایه آن‌قدر زیاد می‌شود که نتایج واقعاً مرتبط از میان k نتیجه‌ی برتر بیرون رانده می‌شوند.

دلیل شکست RAG پایه‌ای این است: شباهت برداری با مرتبط بودن یکسان نیست. یک قطعه می‌تواند از نظر معنایی به پرسمان شباهت داشته باشد، اما هیچ فایده‌ای برای پاسخ دادن به آن نرساند. RAG پیشرفته با چهار راهکار به این چالش می‌پردازد: جست‌وجوی ترکیبی (افزودن تطبیق کلیدواژه‌ای)، بازرتبه‌بندی (امتیازدهی دقیق‌تر به کاندیداها)، تبدیل پرسمان (اصلاح پرسمان پیش از جست‌وجو) و قطعه‌بندی متن بهتر (بازیابی با دانه‌بندی و جزئیات مناسب).

مفاهیم

جست‌وجوی ترکیبی: معنایی + کلیدواژه‌ای

جست‌وجوی معنایی (شباهت برداری) در درک مفهوم عالی عمل می‌کند. مثلاً عبارت «چگونه اشتراکم را لغو کنم؟» با «مراحل خاتمه دادن به طرح کاربری» تطبیق داده می‌شود؛ هرچند هیچ واژه‌ی مشترکی ندارند. اما این روش در تطبیق‌های دقیق کم می‌آورد. اگر یک مدل امبدینگ «کد خطای E-4021» را به چشم نویز ببیند، احتمالاً آن را با قطعه‌ای که دقیقاً حاوی «E-4021» است مطابقت نخواهد داد.

جست‌وجوی کلیدواژه‌ای (BM25) دقیقاً برعکس است و در تطبیق‌های مو‌به‌مو می‌درخشد؛ بنابراین «E-4021» را به‌طور کامل پیدا می‌کند. اما اگر سند عبارت «خاتمه دادن به طرح» را به کار برده باشد، برای عبارت «لغو اشتراک» هیچ نتیجه‌ای برنمی‌گرداند.

جست‌وجوی ترکیبی هر دو شیوه را اجرا کرده و سپس نتایج آن‌ها را ادغام می‌کند.

الگوریتم BM25 (مخفف Best Matching 25) رویکرد استاندارد در جست‌وجوی کلیدواژه‌ای است و از دهه‌ی 1990 میلادی تاکنون ساقه و ستون فقرات موتورهای جست‌وجو بوده است. فرمول آن بدین شرح است:

BM25(q, d) = sum over terms t in q:
    IDF(t) * (tf(t,d) * (k1 + 1)) / (tf(t,d) + k1 * (1 - b + b * |d| / avgdl))

که در آن tf(t,d) فراوانی عبارت t در سند d است، IDF(t) فراوانی معکوس سند، |d| طول سند، avgdl میانگین طول اسناد، k1 کنترل‌کننده‌ی اشباع فراوانی عبارت (پیش‌فرض 1.2) و b کنترل‌کننده‌ی نرمال‌سازی طول متن (پیش‌فرض 0.75) هستند.

به زبان ساده: هرچه اسناد حاوی عبارت‌های پرسمان (به‌ویژه عبارت‌های کم‌تکرارتر و نادر) باشند، BM25 امتیاز بالاتری به آن‌ها اختصاص می‌دهد؛ البته با بازده نزولی برای تکرار مکرر کلمات. سندی که واژه‌ی «درآمد» را 50 بار در خود دارد، الزماً 50 برابر مرتبط‌تر از سندی نیست که یک بار از آن استفاده کرده است.

ادغام متقابل رتبه‌ها (RRF)

فرض کنید دو فهرست رتبه‌بندی‌شده دارید: یکی از جست‌وجوی برداری و دیگری از BM25. چطور باید آن‌ها را با هم ترکیب کرد؟ ادغام متقابل رتبه‌ها (Reciprocal Rank Fusion یا RRF) روش استاندارد برای این کار است.

RRF_score(d) = sum over rankings R:
    1 / (k + rank_R(d))

در اینجا k یک عدد ثابت است (معمولاً 60) که مانع از غلبه‌ی کامل و انحصاری رتبه‌ی اول می‌شود.

سندی که در جست‌وجوی برداری رتبه‌ی 1 و در BM25 رتبه‌ی 5 را به دست آورده، چنین امتیازی می‌گیرد: 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318

سندی که در جست‌وجوی برداری رتبه‌ی 3 و در BM25 رتبه‌ی 2 را دارد: 1/(60+3) + 1/(60+2) = 0.0159 + 0.0161 = 0.0320

روش RRF تعادلی طبیعی میان این دو سیگنال برقرار می‌کند. سندی که در هر دو فهرست رتبه‌ی بالایی کسب کند، بالاترین امتیاز نهایی را می‌گیرد. سندی که در یک فهرست رتبه‌ی 1 را داشته باشد ولی در فهرست دیگر اصلاً نباشد، امتیازی متوسط دریافت خواهد کرد. از آنجا که این روش به‌جای امتیازهای خام از خودِ «رتبه‌ها» بهره می‌برد، بسیار پایدار و مقاوم است و اختلاف در شیوه‌ی توزیع امتیازات میان دو سیستم مشکلی ایجاد نمی‌کند.

بازرتبه‌بندی

بازیابی اولیه (چه برداری باشد، چه کلیدواژه‌ای یا ترکیبی) سرعت بالایی دارد اما دقت آن چندان زیاد نیست. این مرحله از بای‌ان‌کودرها (bi-encoders) بهره می‌برد: امبدینگِ پرسمان و اسناد کاملاً مستقل از هم محاسبه شده و سپس مقایسه می‌شوند. این امبدینگ‌ها یک بار ساخته و کش می‌شوند و بنابراین تا مقیاس چندین میلیون سند هم پاسخ‌گو هستند.

بازرتبه‌بندی از کراس‌ان‌کودرها (cross-encoders) استفاده می‌کند: پرسمان و سند کاندیدا هم‌زمان به یک مدل داده می‌شوند و مدل یک امتیاز مرتبط بودن تحویل می‌دهد. مدل هر دو متن را به شکل هم‌زمان و متقابل بررسی می‌کند و می‌تواند تعاملات ظریف و دقیق بین آن‌ها را دریابد. یک کراس‌ان‌کودر به‌خوبی می‌فهمد که پرسش «عایدی فصل سوم چقدر بود؟» ارتباط بسیار بالایی با قطعه‌ی «47.2 میلیون دلار در فصل سوم» دارد؛ حتی اگر یک بای‌ان‌کودر این ارتباط را متوجه نشده باشد.

نقطه‌ی ضعف اینجاست: کراس‌ان‌کودرها 100 تا 1000 برابر کندتر از بای‌ان‌کودرها هستند، چون جفتِ پرسمان-سند را به شکل مشترک پردازش می‌کنند. شما نمی‌توانید امتیازهای یک کراس‌ان‌کودر را از پیش برای یک میلیون سند محاسبه کنید. راهکار چیست؟ مجموعه‌ی بزرگ‌تری از کاندیداها را در گام اول بیرون بکشید (مثلاً 50 نتیجه‌ی اول از جست‌وجوی ترکیبی) و سپس با یک کراس‌ان‌کودر آن‌ها را بازرتبه‌بندی کنید تا به 5 نتیجه‌ی برتر و نهایی برسید.

پرس‌وجوجستجوی ترکیبی50 نامزد برتربازرتبه‌بند کراس‌ان‌کودر5 نتیجه نهایی برترساخت پرامپتتولید پاسخ

مدل‌های رایج بازرتبه‌بندی (در سال 2026):

  • Cohere Rerank 3.5: سرویس مدیریت‌شده به‌صورت API، چندزبانه، دارای بهترین میزان بهبود در بازخوانی (Recall) روی متون ترکیبی
  • Voyage rerank-2.5: سرویس مدیریت‌شده، کمترین میزان تاخیر در میان گزینه‌های میزبانی‌شده
  • Jina-Reranker-v2 Multilingual: مدل با وزن‌های باز (open-weight)، پشتیبانی از بیش از 100 زبان
  • bge-reranker-v2-m3: با وزن‌های باز، یک خط مبنای قوی
  • cross-encoder/ms-marco-MiniLM-L-6-v2: با وزن‌های باز، سبک و قابل اجرا روی CPU برای نمونه‌سازی اولیه
  • ColBERTv2 / Jina-ColBERT-v2: بازرتبه‌بندی‌کننده‌های چندبرداری با تعامل متأخر (late-interaction) — با هزینه‌ی محاسباتی O(tokens)O(\text{tokens}) به‌جای O(docs)O(\text{docs}) در زمان امتیازدهی

تبدیل پرسمان

گاهی اوقات گره کار در شیوه‌ی بازیابی نیست، بلکه به خود پرسمان مربوط می‌شود. پرسشی مثل «اون قضیه‌ی تغییر سیاست جدید چی بود؟» یک عبارت جست‌وجوی فاجعه‌بار است! هیچ واژه‌ی مشخصی در آن نیست، امبدینگِ حاصل از آن گنگ خواهد بود و هیچ سیستم بازیابی‌ای نمی‌تواند از دل آن به سند درست برسد.

بازنویسی پرسمان (Query Rewriting): بازنویسی و تبدیل پرسش کاربر به یک عبارت جست‌وجوی کارآمدتر. این کار را می‌توان با یک LLM انجام داد:

User: "What was that thing about the new policy change?"
Rewritten: "Recent policy changes and updates"

روش HyDE (امبدینگ فرضی سند): به‌جای جست‌وجو بر اساس خود پرسمان، یک پاسخ فرضی برای آن تولید کنید، از آن امبدینگ بگیرید و سپس اسناد واقعیِ مشابه با آن پاسخ را جست‌وجو کنید.

Query: "What is the refund policy for enterprise?"
Hypothetical answer: "Enterprise customers are eligible for a full refund
within 60 days of purchase. Refunds are pro-rated based on the remaining
subscription period and processed within 5-7 business days."

پاسخ فرضی را امبد کرده و در پی اسناد واقعی مشابه با آن بگردید. شهود پشت این رویکرد روشن است: در فضای امبدینگ، یک پاسخ فرضی بسیار نزدیک‌تر از خود سؤال به پاسخ واقعی جای می‌گیرد. ساختار زبانی پرسش‌ها و پاسخ‌ها متفاوت است. با ایجاد یک پاسخ فرضی، فاصله‌ی میان «فضای پرسش» و «فضای پاسخ» در دنیای امبدینگ‌ها را پر می‌کنید.

روش HyDE یک فراخوانی LLM به فرایند بازیابی اضافه می‌کند که تاخیر سیستم را بین 500 تا 2000 میلی‌ثانیه افزایش می‌دهد. با این حال، اگر کیفیت بازیابی روی پرسمان‌های خام ضعیف باشد، کاملاً ارزشش را خواهد داشت.

قطعه‌بندی متن والد-فرزند

قطعه‌بندی استاندارد متن همواره ما را با یک دوراهی مواجه می‌کند: قطعه‌های کوچک برای بازیابی دقیق، یا قطعه‌های بزرگ برای انتقال بافت کافی به مدل. تکنیک والد-فرزند این تعارض را برطرف می‌کند.

قطعه‌های کوچک (مثلاً 128 توکن) را برای بازیابی ایندکس کنید. زمانی که یکی از این قطعه‌های کوچک پیدا شد، قطعه‌ی والد آن (مثلاً 512 توکن) را برگردانید و به پرامپت تحویل دهید. قطعه‌ی کوچک با دقت بالا با پرسمان تطبیق می‌یابد و قطعه‌ی والد بافت لازم را برای یک پاسخ‌دهی بی‌نقص به LLM می‌رساند.

تطابق با فرزندبازگرداندن والدتکه والد (512 توکن)بخش کامل درباره سیاست بازپرداختتکه فرزند (128 توکن)طرح استاندارد: بازپرداخت 30 روزهتکه فرزند (128 توکن)سازمانی: 60 روزه به تناسبتکه فرزند (128 توکن)زمان پردازش: 5-7 روزتکه فرزند (128 توکن)نحوه ثبت درخواستپرس‌وجو: بازپرداخت سازمانی؟

پرسمان «بازپرداخت اشتراک سازمانی؟» دقیقاً با قطعه‌ی فرزند C2 مطابقت پیدا می‌کند. اما در نهایت قطعه‌ی والد P به پرامپت ارسال می‌شود؛ قطعه‌ای که بافت‌های پیرامونی مانند زمان پردازش و مراحل ارسال درخواست را هم در خود دارد.

فیلتر کردن بر اساس متادیتا

پیش از اجرای جست‌وجوی برداری، دامنه‌ی اسناد را با متادیتا محدود کنید: تاریخ، منبع، دسته‌بندی، نویسنده یا زبان. این کار فضای جست‌وجو را کوچک کرده و جلو نتایج بی‌ربط را می‌گیرد.

برای نمونه، پرسمان «ماه گذشته چه تغییری در خط‌مشی امنیتی ایجاد شد؟» باید صرفاً در اسناد 30 روز اخیر و در دسته‌بندی امنیتی جست‌وجو شود. بدون فیلتر متادیتا، کل اسناد را می‌گردید و چه بسا سندی امنیتی از 2 سال پیش را صرفاً به‌خاطر شباهت معنایی بالا بیرون بکشید.

سیستم‌های RAG در محیط عملیاتی، متادیتا را در کنار هر قطعه نگه می‌دارند: سند مبدأ، تاریخ ایجاد، دسته‌بندی، نویسنده و نسخه‌ی سند. پایگاه‌های داده‌ی برداری امکان فیلتر کردن اولیه بر اساس متادیتا را قبل از اجرای جست‌وجوی شباهت فراهم می‌کنند؛ قابلیتی که برای حفظ کارایی در مقیاس‌های بزرگ حیاتی است.

ارزیابی

یک سیستم RAG ساخته‌اید؛ چطور بفهمید عملکردش خوب است یا نه؟ با این سه معیار:

ارتباط بازیابی (Recall@k): به ازای مجموعه‌ای از پرسش‌های آزمایشی که اسناد مرتبط با آن‌ها از پیش مشخص است، چه درصدی از این اسناد مرتبط در میان k نتیجه‌ی اول حضور دارند؟ اگر پاسخ یک پرسش در قطعه‌ی 47 باشد، آیا قطعه‌ی 47 جزو 5 نتیجه‌ی اول می‌آید؟

وفاداری (Faithfulness): آیا پاسخ تولیدشده واقعاً بر پایه‌ی اسناد بازیابی‌شده شکل گرفته است؟ اگر قطعه‌های بازیابی‌شده بگویند «مهلت بازپرداخت وجه 60 روز است» اما مدل بگوید «مهلت بازپرداخت 90 روز است»، دچار افت وفاداری شده‌اید. در این حالت مدل با وجود داشتن بافت درست، دچار توهم شده است.

صحت پاسخ: آیا پاسخ نهایی با پاسخ موردانتظار تطابق دارد؟ این یک معیار سرتاسری (end-to-end) است که کیفیت بازیابی و کیفیت تولید متن را به شکل هم‌زمان می‌سنجد.

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

چارچوب ارزیابیپرسش‌های آزمون+ پاسخ‌های مورد انتظار+ شناسه اسناد مرتبطارزیابی بازیابیRecall@k: آیا اسناد درستبازیابی شدند؟ارزیابی وفاداریآیا پاسخ مستند بهاسناد بازیابی‌شده است؟ارزیابی صحتآیا پاسخ با پاسخمورد انتظار مطابقت دارد؟
RAG مبتنی بر ایجنتبازیابی ← استدلال ← اقدام ← تکرار
AGENTIC RAGBasic RAG retrieves once and answers. Agentic RAG loops: retrieve candidates, reason over whether they actually answer the query, then act — rewrite the query, rerank, or retrieve again. The orbiting token is one query cycling the loop until the context is good enough to answer, which is what multi-hop questions need.بازیابیاستدلالاقدام / بازبینیمجموعه‌دادهایجنتیک RAG: تکرار حلقه تا مستند شدن پاسخ به شواهد
روش Basic RAG یک‌بار بازیابی کرده و پاسخ می‌دهد. اما Agentic RAG وارد حلقه می‌شود: کاندیداها را بازیابی می‌کند، استدلال می‌کند که آیا واقعاً پاسخ پرس‌وجو را می‌دهند یا خیر، و سپس اقدام می‌کند — بازنویسی پرس‌وجو، بازرتبه‌بندی یا بازیابی مجدد. توکن در حال چرخش همان پرس‌وجویی است که تا رسیدن به بافت مناسب برای پاسخ‌دهی در این حلقه می‌چرخد؛ یعنی همان چیزی که پرسش‌های چندگامی (multi-hop) به آن نیاز دارند.

پیاده‌سازی از صفر

گام 1: پیاده‌سازی الگوریتم BM25

import math
from collections import Counter

class BM25:
    def __init__(self, k1=1.2, b=0.75):
        self.k1 = k1
        self.b = b
        self.docs = []
        self.doc_lengths = []
        self.avg_dl = 0
        self.doc_freqs = {}
        self.n_docs = 0

    def index(self, documents):
        self.docs = documents
        self.n_docs = len(documents)
        self.doc_lengths = []
        self.doc_freqs = {}

        for doc in documents:
            words = doc.lower().split()
            self.doc_lengths.append(len(words))
            unique_words = set(words)
            for word in unique_words:
                self.doc_freqs[word] = self.doc_freqs.get(word, 0) + 1

        self.avg_dl = sum(self.doc_lengths) / self.n_docs if self.n_docs else 1

    def score(self, query, doc_idx):
        query_words = query.lower().split()
        doc_words = self.docs[doc_idx].lower().split()
        doc_len = self.doc_lengths[doc_idx]
        word_counts = Counter(doc_words)
        score = 0.0

        for term in query_words:
            if term not in word_counts:
                continue
            tf = word_counts[term]
            df = self.doc_freqs.get(term, 0)
            idf = math.log((self.n_docs - df + 0.5) / (df + 0.5) + 1)
            numerator = tf * (self.k1 + 1)
            denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avg_dl)
            score += idf * numerator / denominator

        return score

    def search(self, query, top_k=10):
        scores = [(i, self.score(query, i)) for i in range(self.n_docs)]
        scores.sort(key=lambda x: x[1], reverse=True)
        return scores[:top_k]

گام 2: ادغام متقابل رتبه‌ها (RRF)

def reciprocal_rank_fusion(ranked_lists, k=60):
    scores = {}
    for ranked_list in ranked_lists:
        for rank, (doc_id, _) in enumerate(ranked_list):
            if doc_id not in scores:
                scores[doc_id] = 0.0
            scores[doc_id] += 1.0 / (k + rank + 1)
    fused = sorted(scores.items(), key=lambda x: x[1], reverse=True)
    return fused

گام 3: پایپ‌لاین جست‌وجوی ترکیبی

def hybrid_search(query, chunks, vector_embeddings, vocab, idf, bm25_index, top_k=5, fusion_k=60):
    query_emb = tfidf_embed(query, vocab, idf)
    vector_results = search(query_emb, vector_embeddings, top_k=top_k * 3)
    bm25_results = bm25_index.search(query, top_k=top_k * 3)
    fused = reciprocal_rank_fusion([vector_results, bm25_results], k=fusion_k)
    return fused[:top_k]

گام 4: بازرتبه‌بندی ساده

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

def rerank(query, candidates, chunks):
    query_words = set(query.lower().split())
    stop_words = {"the", "a", "an", "is", "are", "was", "were", "what", "how",
                  "why", "when", "where", "do", "does", "for", "of", "in", "to",
                  "and", "or", "on", "at", "by", "it", "its", "this", "that",
                  "with", "from", "be", "has", "have", "had", "not", "but"}
    query_terms = query_words - stop_words

    scored = []
    for doc_id, initial_score in candidates:
        chunk = chunks[doc_id].lower()
        chunk_words = set(chunk.split())

        term_overlap = len(query_terms & chunk_words)

        query_bigrams = set()
        q_list = [w for w in query.lower().split() if w not in stop_words]
        for i in range(len(q_list) - 1):
            query_bigrams.add(q_list[i] + " " + q_list[i + 1])
        bigram_matches = sum(1 for bg in query_bigrams if bg in chunk)

        position_boost = 0
        for term in query_terms:
            pos = chunk.find(term)
            if pos != -1 and pos < len(chunk) // 3:
                position_boost += 0.5

        rerank_score = (
            term_overlap * 1.0
            + bigram_matches * 2.0
            + position_boost
            + initial_score * 5.0
        )
        scored.append((doc_id, rerank_score))

    scored.sort(key=lambda x: x[1], reverse=True)
    return scored

گام 5: پیاده‌سازی HyDE (امبدینگ فرضی سند)

def hyde_generate_hypothesis(query):
    templates = {
        "what": "The answer to '{query}' is as follows: Based on our documentation, {topic} involves specific policies and procedures that define how the process works.",
        "how": "To address '{query}': The process involves several steps. First, you need to initiate the request. Then, the system processes it according to the defined rules.",
        "default": "Regarding '{query}': Our records indicate specific details and policies related to this topic that provide a comprehensive answer."
    }
    query_lower = query.lower()
    if query_lower.startswith("what"):
        template = templates["what"]
    elif query_lower.startswith("how"):
        template = templates["how"]
    else:
        template = templates["default"]

    topic_words = [w for w in query.lower().split()
                   if w not in {"what", "is", "the", "how", "do", "does", "a", "an",
                                "for", "of", "to", "in", "on", "at", "by", "and", "or"}]
    topic = " ".join(topic_words) if topic_words else "this topic"

    return template.format(query=query, topic=topic)


def hyde_search(query, chunks, vector_embeddings, vocab, idf, top_k=5):
    hypothesis = hyde_generate_hypothesis(query)
    hypothesis_emb = tfidf_embed(hypothesis, vocab, idf)
    results = search(hypothesis_emb, vector_embeddings, top_k)
    return results, hypothesis

گام 6: قطعه‌بندی متن والد-فرزند

def create_parent_child_chunks(text, parent_size=200, child_size=50):
    words = text.split()
    parents = []
    children = []
    child_to_parent = {}

    parent_idx = 0
    start = 0
    while start < len(words):
        parent_end = min(start + parent_size, len(words))
        parent_text = " ".join(words[start:parent_end])
        parents.append(parent_text)

        child_start = start
        while child_start < parent_end:
            child_end = min(child_start + child_size, parent_end)
            child_text = " ".join(words[child_start:child_end])
            child_idx = len(children)
            children.append(child_text)
            child_to_parent[child_idx] = parent_idx
            child_start += child_size

        parent_idx += 1
        start += parent_size

    return parents, children, child_to_parent

گام 7: ارزیابی وفاداری

def evaluate_faithfulness(answer, retrieved_chunks):
    answer_sentences = [s.strip() for s in answer.split(".") if len(s.strip()) > 10]
    if not answer_sentences:
        return 1.0, []

    grounded = 0
    ungrounded = []
    context = " ".join(retrieved_chunks).lower()

    for sentence in answer_sentences:
        words = set(sentence.lower().split())
        stop_words = {"the", "a", "an", "is", "are", "was", "were", "and", "or",
                      "to", "of", "in", "for", "on", "at", "by", "it", "this", "that"}
        content_words = words - stop_words
        if not content_words:
            grounded += 1
            continue

        matched = sum(1 for w in content_words if w in context)
        ratio = matched / len(content_words) if content_words else 0

        if ratio >= 0.5:
            grounded += 1
        else:
            ungrounded.append(sentence)

    score = grounded / len(answer_sentences) if answer_sentences else 1.0
    return score, ungrounded


def evaluate_retrieval_recall(queries_with_relevant, retrieval_fn, k=5):
    total_recall = 0.0
    results = []

    for query, relevant_indices in queries_with_relevant:
        retrieved = retrieval_fn(query, k)
        retrieved_indices = set(idx for idx, _ in retrieved)
        relevant_set = set(relevant_indices)
        hits = len(retrieved_indices & relevant_set)
        recall = hits / len(relevant_set) if relevant_set else 1.0
        total_recall += recall
        results.append({
            "query": query,
            "recall": recall,
            "hits": hits,
            "total_relevant": len(relevant_set)
        })

    avg_recall = total_recall / len(queries_with_relevant) if queries_with_relevant else 0
    return avg_recall, results

استفاده از ابزارهای واقعی

پیاده‌سازی با یک کراس‌ان‌کودر واقعی برای بازرتبه‌بندی:

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")

def rerank_with_cross_encoder(query, candidates, chunks, top_k=5):
    pairs = [(query, chunks[doc_id]) for doc_id, _ in candidates]
    scores = reranker.predict(pairs)
    scored = list(zip([doc_id for doc_id, _ in candidates], scores))
    scored.sort(key=lambda x: x[1], reverse=True)
    return scored[:top_k]

استفاده از سرویس بازرتبه‌بندی مدیریت‌شده‌ی Cohere:

import cohere

co = cohere.Client()

def rerank_with_cohere(query, candidates, chunks, top_k=5):
    docs = [chunks[doc_id] for doc_id, _ in candidates]
    response = co.rerank(
        model="rerank-english-v3.0",
        query=query,
        documents=docs,
        top_n=top_k
    )
    return [(candidates[r.index][0], r.relevance_score) for r in response.results]

اجرای HyDE با یک LLM واقعی:

import anthropic

client = anthropic.Anthropic()

def hyde_with_llm(query):
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=256,
        messages=[{
            "role": "user",
            "content": f"Write a short paragraph that would be a good answer to this question. Do not say you don't know. Just write what the answer would look like.\n\nQuestion: {query}"
        }]
    )
    return response.content[0].text

پیاده‌سازی جست‌وجوی ترکیبی عملیاتی با استفاده از Weaviate:

import weaviate

client = weaviate.connect_to_local()

collection = client.collections.get("Documents")
response = collection.query.hybrid(
    query="enterprise refund policy",
    alpha=0.5,
    limit=10
)

پارامتر alpha توازن را تنظیم می‌کند: مقدار 0.0 یعنی جست‌وجوی کلیدواژه‌ای خالص (BM25)، مقدار 1.0 یعنی برداری خالص، و 0.5 یعنی وزن یکسان برای هر دو. اکثر سیستم‌های محیط تولید از مقادیری بین 0.3 و 0.7 استفاده می‌کنند.

خروجی این درس

این درس فایل‌های زیر را تولید می‌کند:

  • outputs/prompt-advanced-rag-debugger.md -- پرامپتی برای شناسایی، عیب‌یابی و اصلاح خطاهای کیفی RAG
  • outputs/skill-advanced-rag.md -- یک Skill برای پیاده‌سازی RAG عملیاتی با جست‌وجوی ترکیبی و بازرتبه‌بندی

تمرین‌ها

  1. سه روش BM25، جست‌وجوی برداری و جست‌وجوی ترکیبی را روی اسناد نمونه با هم مقایسه کنید. برای هر یک از 5 پرسمان تستی، یادداشت کنید کدام روش قطعه‌ی مرتبط‌تر را در رتبه‌ی اول می‌آورد. جست‌وجوی ترکیبی باید حداقل در 3 مورد از 5 پرسمان برنده شود.

  2. یک فیلتر متادیتا بسازید. یک فیلد «دسته‌بندی» (category) با مقادیر security، billing، api و product به هر سند اضافه کنید. قبل از اجرای جست‌وجوی برداری، دامنه‌ی قطعه‌ها را تنها به دسته‌بندی مدنظر محدود کنید. کارکرد آن را با پرسمان «What encryption is used?» بسنجید و مطمئن شوید فقط قطعه‌های دسته‌ی security را جست‌وجو می‌کند.

  3. با بهره‌گیری از تابع تولید متن ساده‌ی درس 06، یک پایپ‌لاین کامل HyDE پیاده کنید. کیفیت بازیابی (میزان مرتبط بودن در 3 نتیجه‌ی برتر) را در تمام 5 پرسمان نمونه، میان جست‌وجوی مستقیم با پرسمان و جست‌وجو از طریق HyDE مقایسه کنید. روش HyDE باید در پرسمان‌های مبهم نتایج بهتری به دست دهد.

  4. استراتژی قطعه‌بندی متن والد-فرزند را روی اسناد نمونه پیاده‌سازی کنید؛ با child_size=30 و parent_size=100. جست‌وجو را با قطعه‌های فرزند انجام دهید اما قطعه‌های والد را به پرامپت بفرستید. پاسخ‌های حاصل را با روش قطعه‌بندی استاندارد با chunk_size=50 مقایسه کنید.

  5. یک مجموعه‌داده‌ی ارزیابی بسازید: 10 سؤال همراه با قطعه‌های حاوی پاسخ آن‌ها. مقادیر Recall@3، Recall@5 و Recall@10 را برای این حالت‌ها بسنجید: (الف) فقط جست‌وجوی برداری، (ب) فقط BM25، (ج) جست‌وجوی ترکیبی، (د) ترکیبی + بازرتبه‌بندی. نتایج را در قالب نمودار بیاورید و ببینید بازرتبه‌بندی بیشترین فایده را در کدام بخش داشته است.

اصطلاحات کلیدی

اصطلاح بر زبان‌ها در واقعیت
BM25 «جست‌وجوی کلیدواژه‌ای» یک الگوریتم رتبه‌بندی احتمالاتی که اسناد را بر اساس فراوانی عبارات، فراوانی معکوس در اسناد و نرمال‌سازی طول سند امتیازدهی می‌کند
جست‌وجوی ترکیبی (Hybrid search) «بهترین از هر دو جهان» اجرای هم‌زمان جست‌وجوی معنایی (برداری) و کلیدواژه‌ای (BM25) و ادغام نتایج با روش‌های ترکیب رتبه
ادغام متقابل رتبه‌ها (Reciprocal Rank Fusion) «ادغام لیست‌های رتبه‌بندی» ترکیب چند فهرست رتبه‌بندی‌شده از طریق جمع زدن مقدار 1/(k + rank) برای هر سند در تمام لیست‌ها
بازرتبه‌بندی (Reranking) «امتیازدهی دور دوم» بهره‌گیری از یک مدل کراس‌ان‌کودر سنگین‌تر برای امتیازدهی مجدد به کاندیداهای به‌دست‌آمده از بازیابی اولیه
کراس‌ان‌کودر (Cross-encoder) «مدل مشترک پرسمان-سند» مدلی که پرسمان و سند را در قالب یک ورودی واحد می‌گیرد و امتیاز ارتباط می‌دهد؛ دقیق‌تر از بای‌ان‌کودرهاست اما برای جست‌وجو در کل اسناد بیش از حد کند است
بای‌ان‌کودر (Bi-encoder) «مدل امبدینگ مستقل» مدلی که از پرسمان و اسناد به شکل مجزا امبدینگ می‌گیرد؛ بسیار سریع است چون امبدینگ‌ها از قبل محاسبه می‌شوند، اما دقتی کمتر از کراس‌ان‌کودر دارد
روش HyDE «جست‌وجو با پاسخ ساختگی» تولید یک پاسخ فرضی برای پرسمان، امبد کردن آن، و جست‌وجو برای یافتن اسناد واقعیِ مشابه با آن
قطعه‌بندی متن والد-فرزند (Parent-child chunking) «جست‌وجوی ریز، بافت درشت» ایندکس قطعه‌های ریز برای بازیابی دقیق، اما ارائه‌ی قطعه‌ی والدِ بزرگ‌تر به مدل برای ایجاد بافت متنی مناسب
فیلتر کردن بر اساس متادیتا (Metadata filtering) «محدودسازی پیش از جست‌وجو» پالایش اسناد بر اساس ویژگی‌ها (تاریخ، منبع، دسته‌بندی) پیش از جست‌وجوی برداری برای کاستن از فضای جست‌وجو
وفاداری (Faithfulness) «عدم انحراف از متن» این‌که پاسخ تولیدشده واقعاً مستند به اسناد بازیابی‌شده باشد، نه برآمده از توهم ناشی از داده‌های پیش‌آموزش مدل

برای مطالعه‌ی بیشتر

سؤالات متداول

چرا جست‌وجوی برداری ساده برای RAG کافی نیست؟

چون شباهت برداری با مرتبط بودن یکسان نیست؛ جست‌وجوی برداری برای عبارات دقیق (مانند کد خطا یا شناسه) عملکرد ضعیفی دارد و ممکن است قطعه‌های هم‌معنا ولی فاقد پاسخ واقعی را بازیابی کند.

الگوریتم RRF چگونه نتایج جست‌وجوی متنی و برداری را ادغام می‌کند؟

ادغام متقابل رتبه‌ها (RRF) بر اساس رتبه‌ی اسناد در هر لیست عمل می‌کند و با فرمول مجموع 1 بر روی (k + rank)، اسنادی که در هر دو روش رتبه‌ی خوبی گرفته‌اند را بدون وابستگی به تفاوت توزیع امتیازها به بالای لیست می‌آورد.

تفاوت کراس‌ان‌کودر و بای‌ان‌کودر در بازرتبه‌بندی چیست؟

بای‌ان‌کودر پرسمان و سند را جداگانه به بردار تبدیل می‌کند و بسیار سریع است، اما کراس‌ان‌کودر پرسمان و سند را هم‌زمان تحلیل کرده و دقت بسیار بالاتری در محاسبه‌ی ارتباط دارد، هرچند کندتر است.

استراتژی قطعه‌بندی والد-فرزند چه مشکلی را حل می‌کند؟

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

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.