דלג לתוכן הראשי
רמה: מתקדם עודכן: אוגוסט 2026

Reranking — RAG מדויק יותר

השדרוג הכי משתלם ל-RAG: שכבה שנייה שמדרגת מחדש את התוצאות ומעלה את הרלוונטי באמת לראש. שיפור דיוק דרמטי במאמץ קטן.

קודם: לוודא שזו בכלל הבעיה

Reranking מוצג בדרך כלל כשיפור שכדאי תמיד, וזו עצה שגורמת להרבה אנשים להוסיף שכבה שלא פותרת להם כלום.

ב-RAG יש שלוש נקודות כשל נפרדות, ורק אחת מהן היא מה ש-reranker מתקן:

  1. אינדוקס. הקטע הנכון בכלל לא קיים במאגר, או שהוא חתוך באמצע כך שהתשובה מפוצלת בין שני קטעים.
  2. שליפה. הקטע קיים, אבל לא חוזר בין המועמדים — או שהוא חוזר במקום ה-40 ולא נכנס להקשר.
  3. יצירה. הקטע הנכון הגיע למודל, והמודל התעלם ממנו או סילף אותו.

Reranker נוגע רק בשני, וגם שם רק בחלק ממנו — הוא מסדר מחדש מועמדים שכבר חזרו, והוא לא יכול להעלות קטע שהשליפה הרחבה פספסה לגמרי.

הבדיקה שמכריעה לוקחת עשר דקות: קח עשרים שאלות אמיתיות שאתה יודע מה התשובה להן ובאיזה קטע היא נמצאת. הרץ שליפה עם top_k=50, ובדוק כמה פעמים הקטע הנכון נמצא ברשימה בכלל.

hits = 0
for q, expected_chunk_id in eval_set:
    ids = [c.id for c in vector_search(q, top_k=50)]
    if expected_chunk_id in ids:
        hits += 1
print("recall@50: %d/%d" % (hits, len(eval_set)))
כשזו כן הבעיה

אז זה באמת השיפור המשתלם ביותר שאפשר לעשות ל-RAG — עלייה ניכרת בדיוק תמורת שכבה אחת ומעט קוד.

אחזור דו-שלבי

הרעיון פשוט: שני שלבים עם תפקידים הפוכים.

  1. שליפה רחבה. ה-Vector DB מחזיר הרבה מועמדים — 50, לפעמים 100. מהיר וזול, והמטרה שלו היא לא לפספס, לא לדייק.
  2. דירוג מחדש. מודל reranker בוחן כל מועמד מול השאילתה ומחזיר את המעטים שבאמת עונים. איטי בהרבה, ולכן רץ רק על מה שכבר סונן.

למודל השפה מזינים רק את התוצאה של השלב השני — הקשר קצר ומדויק. זה משפר גם את התשובה וגם את העלות, כי פחות טוקנים נכנסים. הרחבה: Context Engineering.

ההיגיון כאן זהה למיון בכל תחום אחר: מסנן זול ורחב, ואחריו מסנן יקר וצר. אף אחד לא היה מריץ את המודל היקר על מיליון מסמכים, ואף אחד לא היה מסתפק במסנן הגס.

Cross-encoder מול bi-encoder

ההבדל הטכני הוא מה שמסביר גם את הדיוק וגם את המחיר.

והמחיר: אי אפשר לחשב שום דבר מראש. כל צמד שאילתה-קטע דורש הרצה נפרדת. חמישים מועמדים פירושו חמישים הרצות, וזו הסיבה המדויקת שהשלב הזה בא רק אחרי סינון.

אותו הבדל מסביר גם את הכשל של שליפה וקטורית לבד: embedding לוכד "על מה הקטע מדבר", ולא "האם הוא עונה על השאלה הזאת". קטע שמזכיר את אותם מונחים בהקשר שונה לגמרי ידורג גבוה, וזה מקור נפוץ להזיות — המודל קיבל טקסט שנראה רלוונטי ואינו.

כמה מועמדים, וכמה להחזיר

שני מספרים שכדאי לכוון במודע ולא להשאיר בברירת מחדל.

כמה לשלוף — ככל שיותר, כך קטן הסיכוי לפספס, וכך גדל החביון והעלות. ההתנהגות אינה ליניארית: המעבר מ-10 ל-50 משפר משמעותית, המעבר מ-50 ל-200 בדרך כלל כמעט לא. נקודת פתיחה סבירה היא 50, ואת המספר הנכון אצלך מוצאים בבדיקת ה-recall מהסעיף הראשון — תעלה עד שהוא מפסיק להשתפר, ואז תעצור.

כמה להעביר למודל — כאן הנטייה היא להעביר יותר "ליתר ביטחון", וזו טעות. יותר קטעים פירושם יותר רעש, והמודל נוטה להתפזר. שלושה עד חמישה קטעים טובים מנצחים עשרה בינוניים כמעט תמיד.

ויש גם אפשרות שכדאי להכיר: סף ציון במקום מספר קבוע. אם ה-reranker נותן ציון נמוך לכל המועמדים, זה אות ששאלת משהו שאין עליו תשובה במאגר — וזה בדיוק המקרה שבו עדיף שהמערכת תגיד "לא מצאתי" במקום לענות מקטעים לא רלוונטיים.

ranked = reranker.rank(query, docs)
good = [r for r in ranked if r.score > 0.3][:5]

if not good:
    return "לא מצאתי מידע רלוונטי במסמכים."

שלוש השורות האחרונות משפרות את אמון המשתמש יותר מכל שיפור בדיוק, כי הן מונעות את התשובה הבטוחה והשגויה.

ערכת הבדיקה: איך בונים אותה

כל מה שכתוב כאן נשען על אותה ערכה קטנה, ובלעדיה אין דרך לדעת אם משהו עזר. הבנייה שלה היא שעה, פעם אחת.

  1. אסוף שאלות אמיתיות. מה שמשתמשים באמת שאלו, לא מה שנחמד לבדוק. אם המערכת עוד לא בשימוש — קח את השאלות שאתה נשאל בעל פה.
  2. לכל שאלה, סמן את הקטע שעונה עליה. זו העבודה. אתה פותח את המסמכים ומוצא איפה התשובה, ורושם את המזהה.
  3. הכלל שקובע — כלול שאלות שאין להן תשובה. חמש מתוך עשרים. אלה הבודקות אם המערכת יודעת לומר "לא מצאתי", וזו התכונה שהכי משפיעה על אמון.
  4. הוסף שאלות שנוסחו רע. קצרות, מעורפלות, עם שגיאות כתיב, מעורבות עברית-אנגלית. ככה משתמשים באמת כותבים.

ומה שהופך אותה לנכס לאורך זמן: כל תלונה מוסיפה שאלה. אחרי חצי שנה יש לך אוסף של בדיוק המקרים שמפילים אותך, וכל שינוי עתידי נבדק מולם. זה גם מה שמאפשר לשדרג מודל בלי לפחד — מריצים, משווים, ויודעים.

חיפוש היברידי

שיפור משלים ולעיתים קרובות משמעותי יותר מה-reranker עצמו: לשלב חיפוש סמנטי עם חיפוש מילולי.

איך משלבים: מריצים את שניהם, ואז מאחדים את שתי הרשימות. השיטה הנפוצה נקראת Reciprocal Rank Fusion, והיא פשוטה להפתיע — כל מסמך מקבל ציון לפי המיקום שלו בכל רשימה, בלי צורך לנרמל ציונים שאינם ברי השוואה:

def rrf(lists, k=60):
    scores = {}
    for ranked in lists:                 # כל רשימה מדורגת
        for rank, doc_id in enumerate(ranked, start=1):
            scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)

merged = rrf([semantic_ids, keyword_ids])[:50]
top = reranker.rank(query, fetch(merged))[:5]

היתרון של השיטה הזאת הוא שהיא לא דורשת לכייל כלום — ציוני דמיון וקטורי וציוני BM25 נמצאים בסולמות שונים לגמרי, ואיחוד לפי דירוג עוקף את הבעיה.

עברית: איפה זה נשבר

שני דברים שספציפיים לעברית ומשנים את התכנון.

החיפוש המילולי כמעט לא עובד בלי טיפול

בעברית מילות יחס וקישור נדבקות למילה: מערכת, המערכת, במערכת, שהמערכת, ומהמערכת. עבור BM25 אלה חמש מילים שונות לחלוטין. משתמש שיחפש "מערכת" לא ימצא מסמך שכתוב בו "במערכת", וזה קורה כל הזמן.

מה שצריך: נרמול לפני האינדוקס וגם לפני השאילתה — הסרת אותיות שימוש נפוצות, איחוד אותיות סופיות, והורדת ניקוד אם יש. גם טיפול פשוט משפר דרמטית. מנועי חיפוש רציניים מציעים מנתח מורפולוגי לעברית, ואם אתה משתמש באחד — שווה להפעיל אותו במפורש, כי הוא לא ברירת מחדל.

בלי זה, החצי המילולי של החיפוש ההיברידי לא תורם כמעט כלום, ואתה משלם על מורכבות בלי לקבל את היתרון.

Reranker רב-לשוני, ולא כל אחד

חלק מה-rerankers אומנו על אנגלית בלבד. הם יחזירו ציונים גם על עברית — פשוט ציונים גרועים, בלי שום שגיאה שתרמז לך על כך. תבדוק במפורש שהמודל תומך רב-לשונית, ועדיף לאמת את זה בעצמך על עשרים שאלות בעברית ולא להסתמך על הצהרה.

וכפי שנכתב במדריך לHuggingFace: אותו כלל תקף גם למודל ההטמעה. אותו מודל לאינדוקס ולשאילתה, ואם החלפת — צריך לבנות את האינדקס מחדש.

איך זה נראה בקוד

יש שתי אפשרויות: שירות מנוהל שמקבל שאילתה ורשימת מסמכים ומחזיר אותם מדורגים, או מודל פתוח שרץ אצלך — למשל ממשפחת ה-BGE, דרך HuggingFace.

# שלב 1 — שליפה רחבה
candidates = vector_search(query, top_k=50)

# שלב 2 — דירוג מחדש
ranked = reranker.rank(
    query=query,
    documents=[c.text for c in candidates],
)

# שלב 3 — סף, לא רק top-k
top = [r for r in ranked if r.score > 0.3][:5]

context = "\n\n".join(t.text for t in top)

ומה שכדאי להוסיף כבר עכשיו ולא אחר כך: לתעד את הציונים. כשמשתמש יתלונן על תשובה, ההבדל בין "אין לי מושג" לבין "הקטע הנכון היה במקום השביעי עם ציון 0.28" הוא כל ההבדל בין ניחוש לתיקון.

חיתוך: מה שקורה לפני הכל

הסעיף על החלופות הזכיר את זה בקצרה, ושווה להרחיב — כי זו הבעיה שהכי הרבה מערכות סובלות ממנה ושהכי קל לזהות לאחר מעשה.

חיתוך לפי מספר תווים קבוע הוא ברירת המחדל בכל כלי, והוא לא מודע למבנה. התוצאה: קטע נגמר באמצע טבלה, כותרת מנותקת מהתוכן שמתחתיה, ותשובה שמפוצלת בין שני קטעים כך שאף אחד מהם לא מספיק.

ובעברית יש הערה: אל תחתוך באמצע מילה, וזה נשמע מובן מאליו עד שרואים כלי שחותך לפי בייטים ולא לפי תווים. עברית ב-UTF-8 תופסת שני בייטים לתו, וחיתוך לפי בייטים מייצר תו שבור בקצה כל קטע.

ציטוט מקורות — למה זה שייך לכאן

נראה כמו נושא נפרד והוא בעצם המשך ישיר: ברגע שיש דירוג עם ציונים, אפשר לתת למשתמש לראות מאיפה התשובה הגיעה — וזה משנה את המערכת יותר מכל שיפור בדיוק.

שלוש סיבות מעשיות:

מבחינת מימוש זה זול: הקטעים כבר מגיעים עם מזהים מהשלב הקודם. מה שצריך זה למספר אותם בהקשר שנשלח למודל, ולבקש שיפנה אליהם.

context = "\n\n".join(
    "[%d] %s" % (i, t.text) for i, t in enumerate(top, 1))

SYSTEM = """ענה רק על סמך הקטעים המצורפים.
בסוף כל טענה ציין את מספר הקטע: [1], [2].
אם התשובה לא נמצאת בקטעים — אמור זאת."""

ואז המספרים ממופים חזרה לקישורים בממשק. ההוראה האחרונה חשובה לא פחות מהראשונה — היא הופכת את "לא מצאתי" לתשובה לגיטימית במקום למצב שהמודל מנסה להימנע ממנו.

חביון ועלות

Reranking מוסיף שלב, והשלב הזה לא חינם — לא בזמן ולא בכסף. מה שקובע אם זה מקובל הוא איפה זה יושב:

ופרט מעשי: מטמון. אם אותן שאילתות חוזרות — וברוב המערכות הן כן — שמירת התוצאה המדורגת חוסכת את השלב כולו. זה השיפור הזול ביותר ברשימה.

שווה גם לנרמל את מפתח המטמון: להוריד רווחים מיותרים, להאחיד אותיות ולהסיר סימני פיסוק לפני שמשתמשים בשאילתה כמפתח. בעברית זה משנה במיוחד, כי אותה שאלה נכתבת בכמה צורות — עם ובלי ה"א הידיעה, עם ובלי סימן שאלה — ובלי נרמול כל וריאציה מייצרת חישוב חדש. חמש שורות קוד שמכפילות את שיעור הפגיעות במטמון.

איך יודעים שזה עזר

"התשובות נראות טובות יותר" הוא לא מדד, והוא גם לא יחזיק כשתשנה משהו אחר בעוד חודש.

מה שצריך זה אותה ערכה מהסעיף הראשון — עשרים עד חמישים שאלות עם הקטע הנכון מסומן — ושני מספרים:

תמדוד לפני ואחרי. ואם השיפור זניח — זה מידע שווה, כי הוא אומר שהבעיה במקום אחר ושחסכת לעצמך שכבה שצריך לתחזק.

ושווה למדוד גם את הדבר ההפוך: כמה פעמים ה-reranker הוריד קטע נכון שהיה גבוה. זה קורה — במיוחד עם מודל שלא מתאים לשפה או לתחום — וזה הכשל היחיד כאן שגורם נזק ולא רק לא עוזר. אם המספר הזה גדול מאפס בערכה שלך, זה סימן להחליף מודל ולא לכוונן פרמטרים.

מה עוד לנסות לפני, או במקום

שלושה שיפורים שלעיתים קרובות תורמים יותר מ-reranker ועולים פחות:

שכתוב שאילתה

הטכניקה שהכי מוזנחת ביחס לתועלת. משתמשים כותבים שאילתות קצרות ועמומות — "החזרות?", "כמה זה עולה" — ומצפים שהמערכת תבין. שליפה וקטורית על שתי מילים מחזירה כמעט רעש.

שלוש גרסאות, מהזולה ליקרה:

המקרה השלישי הוא הנפוץ ביותר וגם המפתיע ביותר כשמגלים אותו: מערכת שעובדת מצוין על השאלה הראשונה ומתפרקת על השנייה, כי היא שולפת לפי הטקסט של השאלה בלבד.

ובעברית שווה להוסיף להנחיית השכתוב לשמור מונחים לועזיים כמו שהם — משתמש שכתב "deploy" לא צריך שהשכתוב יהפוך אותו ל"פריסה", כי במסמכים כתוב deploy.

מתי לא

טעויות שחוזרות

הצעד הבא

חזק את שאר שרשרת ה-RAG — הטמעות, מסד וקטורי והנדסת הקשר.