انتقل إلى المحتوى الرئيسي

الوحدة 6 — إعادة ترتيب المقاطع المسترجَعة

الوحدة السابقة أعطتنا استرجاعًا هجينًا بكفاءة معقولة. لكنّ أفضل عشرين مقطعًا حسب هذا الاسترجاع تحوي عادةً خمسة مفيدة و خمسة عشر ضجيجًا. إعادة الترتيب مرحلة صغيرة الكلفة قياسًا بالباقي، لكنّها تُحسِّن الجودة تحسينًا جوهريًّا، وتُصفّي القمّة قبل تمرير المقاطع إلى النموذج المولّد.

الفرق بين المشفِّر الثنائي والمشفِّر المتقاطع

في الوحدة 4 استعملنا نموذجًا يُنتج شعاعًا للسؤال منفصلًا عن شعاع المقطع، ثم قسنا القرب بجيب التمام. هذا مشفِّر ثنائي (bi-encoder) أو قطبان (two-tower). ميزته: يمكن حساب شعاع كلّ مقطع مرّة وتخزينه، والبحث لاحقًا يكلّف تضمين السؤال فقط.

المشفِّر المتقاطع (cross-encoder) يعمل بمنطق مختلف: يأخذ السؤال والمقطع معًا في إدخال واحد ويُنتج درجة صلة مباشرة. هذا يمنحه دقّة أعلى بكثير لأنّه يلاحظ التفاعل بين السؤال والمقطع كلمة بكلمة. ثمنه: لا يمكن حساب شيء مسبقًا، فيجب تشغيله على كلّ زوج (سؤال، مقطع) وقت الاستفسار.

المعيارBi-encoder (تضمين)Cross-encoder (إعادة ترتيب)
كلفة الفهرسةمرّة واحدة لكلّ مقطعلا فهرسة ممكنة
كلفة الاستفسارO(1) تضمين للسؤالO(k) استدعاء نموذج
الدقّةجيّدةممتازة
الاستعمالاسترجاع أوّليّ سريع من مئات الآلافترتيب دقيق لعشرات المرشّحين

المفتاح هنا: لا يعوض المشفِّرُ المتقاطع الأوّلَ ولا يُلغيه. يعمل الاثنان معًا في تسلسل: bi-encoder + BM25 يعطيان 20 – 50 مقطعًا (الاستدعاء)، ثم cross-encoder يعيد ترتيبها ويحتفظ بأفضل 3 – 5 (الدقّة).

نماذج إعادة ترتيب عملية

المرشّحون الأكثر استعمالًا اليوم:

  • BAAI/bge-reranker-v2-m3: مفتوح، متعدّد اللغات (يشمل العربية جيّدًا)، خفيف نسبيًّا (نصف مليار وسيط).
  • mixedbread-ai/mxbai-rerank-large-v1: مفتوح، جودة عالية.
  • cohere/rerank-3 (مغلق): جودة ممتازة، متاح كخدمة.

للفيل الأحمر، bge-reranker-v2-m3 نقطة انطلاق سليمة لأنّه يعمل محلّيًا ويحفظ السرّية.

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")

# انطلاقًا من عشرين مرشّحًا رجعوا من الاسترجاع الهجين
candidates = fused[:20]
pairs = [(question, chunks[c.id].text) for c in candidates]
scores = reranker.predict(pairs)

# إعادة ترتيب حسب الدرجة، والاحتفاظ بأفضل 5
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
top_k = [c for c, _ in ranked[:5]]

المفاضلة: الكلفة مقابل المكسب

نموذج إعادة الترتيب أبطأ من محرِّك بحث كلاسيكي. تشغيله على 20 مرشّحًا يستغرق نحو 100 – 300 ميلي‌ثانية على GPU متواضع، وأكثر على CPU. المكسب في المقابل حقيقي.

جدول مؤشّرات على الفيل الأحمر (أرقام توضيحية):

المرحلةRecall@5Precision@3Latence
كثيف فقط0.830.5540 ms
هجين (كثيف + BM25)0.900.6155 ms
هجين + reranker على 20 مرشّحًا0.920.81180 ms

القفزة على الدقّة (Precision@3) من 0.61 إلى 0.81 هي التي تُهمّ عمليًّا: هذا يعني أنّ 8 من كلّ 10 مقاطع تُمرَّر إلى النموذج المولِّد هي فعلًا مقاطع مفيدة، بدل 6 من 10. وقت الاستجابة الإضافي 125 ميلي‌ثانية مقبول جدًّا لتطبيق تفاعليّ.

كم مقطعًا نُبقي؟

مفاضلة أخرى: عدد kk المقاطع المُمرَّرة إلى النموذج بعد إعادة الترتيب.

  • قليل جدًّا (k=1k = 1 أو 22): خطر تفويت السياق الضروري، إجابات مقتضبة أو ناقصة.
  • قليل معقول (k=3k = 3 إلى 55): توازن سليم في أغلب الحالات. النموذج يجد ما يحتاج، والتعليمة تبقى قصيرة.
  • كثير (k=10k = 10 فأكثر): يزيد فرص وجود المقطع الصحيح، لكنّه يمدّد التعليمة (كلفة أعلى) ويُدخل ضجيجًا يشوّش النموذج، وقد يُفقده تركيزه (lost in the middle).

القاعدة الشائعة: k=3k = 3 إلى 55 للأسئلة الحرفية، و**k=5k = 5 إلى 88** للأسئلة الاستنتاجية التي تحتاج تركيبًا. القياس على مجموعة الأسئلة الموصوفة (الوحدة 8) هو الفاصل الحقيقي.

تنويع المصادر: MMR

مشكلة شائعة: أفضل خمسة مقاطع قد تكون متكرّرة، إذ يقع نصّ مشابه في ثلاث مذكّرات مختلفة. النموذج يقرأ الشيء نفسه ثلاث مرّات، ولا يعرف عن الأسئلة الأخرى شيئًا.

خوارزمية MMR (Maximal Marginal Relevance) توازن بين الصلة بالسؤال والتنويع بين المختارين:

MMR=argmaxdiRS[λsim(q,di)(1λ)maxdjSsim(di,dj)]\text{MMR} = \arg\max_{d_i \in R \setminus S} \left[ \lambda \cdot \text{sim}(q, d_i) - (1 - \lambda) \cdot \max_{d_j \in S} \text{sim}(d_i, d_j) \right]

مع λ[0,1]\lambda \in [0, 1]: قريب من 1 يفضّل الصلة، وقريب من 0 يفضّل التنويع. القيمة الشائعة λ=0,7\lambda = 0{,}7.

def mmr(query_vec, candidate_vecs, candidate_ids, k=5, lam=0.7):
selected = []
selected_ids = []
remaining = list(range(len(candidate_vecs)))

while remaining and len(selected) < k:
best_i, best_score = None, -1e9
for i in remaining:
sim_q = query_vec @ candidate_vecs[i]
sim_s = max((candidate_vecs[i] @ candidate_vecs[j] for j in selected), default=0)
score = lam * sim_q - (1 - lam) * sim_s
if score > best_score:
best_i, best_score = i, score
selected.append(best_i)
selected_ids.append(candidate_ids[best_i])
remaining.remove(best_i)

return selected_ids

يفيد MMR خصوصًا حين يتعامل المشروع مع مجموعة تحوي نسخًا متعدّدة من الوثائق نفسها (نسخة 2023 ونسخة 2024 من الإجراء نفسه): بلا MMR ستستحوذ النسختان على القمّة، ويضيع مقطع من وثيقة أخرى مفيد لجواب شامل.

متى نتخطّى إعادة الترتيب

الجواب البارد: نادرًا. لكن الاعتراف بالقيود مفيد:

  • زمن استجابة حرج (أقلّ من 50 ميلي‌ثانية): إعادة الترتيب لا تناسب.
  • مشروع صغير جدًّا (بضع عشرات من المقاطع): البحث الأوّلي يعطي فعلًا أفضل مقطع في الرتبة الأولى.
  • موازنة الكلفة: لكلّ استفسار استدعاءات إضافية للنموذج، ولو كان محلّيًّا فهو موارد.
إعادة ترتيب على نموذج غير مناسب للغة

ms-marco-MiniLM نموذج شهير للإنجليزية، ويعطي نتائج رديئة جدًّا على العربية. اختيار نموذج متعدّد اللغات أو مخصَّص للعربية إلزامي، ونسيان هذا يعني أنّ إعادة الترتيب تُدهور النتائج بدل أن تحسِّنها.

الفحص السريع بعد إضافة reranker

احتفظ بجدول قبل/بعد على 20 سؤالًا موصوفًا. في السطر: السؤال. في العمودين: أفضل خمسة مقاطع قبل وبعد. مرّ عليها بعينك. إن كنت لا ترى فرقًا واضحًا لصالح «بعد»، فالنموذج غير مناسب أو الاسترجاع الأوّلي رديء جدًّا (المقاطع الصحيحة ليست حتّى في العشرين الأولى، فلا شيء يمكن أن يصنعه المُرتِّب).

في الخلاصة

  • المشفِّر المتقاطع أدقّ من الثنائي لأنّه يرى السؤال والمقطع معًا، لكنّه يعمل وقت الاستفسار فقط ولا يُفهرَس.
  • سلسلة العمل: استرجاع هجين على 20 – 50 مرشّحًاإعادة ترتيب على مجموعهمالاحتفاظ بـk=3k = 3 إلى 55.
  • المكسب على الدقّة (Precision@k) هو الأبرز، وهو ما يُشعر به المستخدم في جودة الإجابات.
  • MMR يُنوّع المصادر ويتفادى الانحصار في نسخ متكرّرة من الوثيقة نفسها.

الوحدة التالية: بناء التعليمة النهائية التي ستُمرَّر إلى النموذج المولّد، مع فرض الاستشهاد بالمصادر والامتناع عند غياب الأدلّة.