RAG מתקדם
— מ"בערך" למדויק
RAG בסיסי (embed → חפש → הדבק) עובד בהדגמה ונכשל בפרודקשן: מחזיר קטעים לא רלוונטיים, מפספס תשובות קיימות, וגורם להזיות. במדריך: chunking חכם, hybrid search, reranking, metadata filtering ו-GraphRAG — הטכניקות שהופכות אחזור "בערך" למערכת מדויקת שאפשר לסמוך עליה.
למה RAG בסיסי נכשל בפרודקשן
הזרימה הבסיסית מוכרת: מפצלים מסמכים לקטעים, ממירים לווקטורים, שומרים, ובזמן שאלה שולפים את הדומים ומדביקים לפרומפט. זה עובד — עד שהדאטה אמיתי.
שלוש בעיות שוברות אותה, וכל טכניקה בדף הזה מטפלת באחת מהן:
- חיפוש סמנטי לבדו מפספס מונחים מדויקים. ווקטורים מבינים משמעות ולא תמיד תופסים מספר קטלוגי, שם קובץ או קוד שגיאה. "שגיאה E-4032" מחזירה תוצאות כלליות על שגיאות.
- הקטע הנכון קיים אבל לא בחמישייה הראשונה. הוא דורג במקום החמישה־עשר, והמודל רואה חמישה.
- חלוקה גרועה קורעת את ההקשר. חיתוך עיוור באמצע משפט, או טבלה שנפרדה מהכותרת שלה. הקטע קיים וחסר משמעות.
אבל לפני שניגשים לטכניקות, יש שלב אחד שמדלגים עליו כמעט תמיד — והוא זה שקובע אם מה שתעשו אחריו יעזור.
אי אפשר לשפר מה שאי אפשר לייחס
"ה-RAG לא מדויק" אינו אבחנה. זו תוצאה של שרשרת בת ארבעה שלבים, וכל אחד מהם נכשל אחרת ודורש תיקון אחר. הטעות היקרה ביותר בתחום היא להתחיל להחליף טכניקות לפני שיודעים איזה שלב נכשל.
סדר הבדיקה, מהזול לנדיר — וברוב המקרים זה נגמר בשניים הראשונים:
- האם הקטע הנכון קיים במאגר בכלל? חפשו אותו בטקסט חופשי, לא דרך הווקטורים. לעתים קרובות מתברר שהוא מעולם לא נכנס — קובץ שנכשל בעיבוד, PDF סרוק, מסמך שלא הועלה. שום דירוג מחדש לא ימצא מה שאין.
- איך הוא נראה אחרי החלוקה? הדפיסו את הקטע כפי שנשמר. כאן מתגלים חיתוכים באמצע משפט וטבלאות שהפכו לרצף מספרים.
- באיזה מקום הוא דורג? לא בחמישייה — בעשירייה? בעשרים? "לא נמצא כלל" ו"נמצא במקום השביעי" הן שתי בעיות שונות לגמרי שנראות זהות מבחוץ, ורק לשנייה יש פתרון זול.
- ואם הוא היה בהקשר והתשובה עדיין שגויה — רק אז זו בעיית ניסוח, וזה השלב האחרון שכדאי לחשוד בו.
המכשיר שמאפשר את כל זה הוא קטן: עשרים עד חמישים שאלות אמיתיות, ולצד כל אחת הקטע שאמור לענות עליה. זה לוקח שעתיים לבנות, הוא לא דורש לשפוט תשובות אלא רק לבדוק אם הקטע נשלף, והוא הופך כל שינוי בהמשך הדף הזה ממשהו שמרגיש נכון למשהו שאפשר למדוד.
כמה מהשאלות הקטע הנכון הופיע בחמש הראשונות, ובאיזה מקום ממוצע. הראשון אומר אם המערכת עובדת; השני אומר אם הבעיה בשליפה או בדירוג.
חלוקה: התקרה של כל המערכת
איכות החלוקה קובעת את תקרת האיכות של כל מה שמעליה. קטע טוב הוא יחידת מידע שאפשר להבין בפני עצמה — כי זה בדיוק מה שהמודל יקבל, בלי מה שהיה לפניה ואחריה.
- לחתוך לפי מבנה ולא לפי אורך. כותרות, סעיפים, פסקאות. טבלאות ורשימות נשארות שלמות. ספירת תווים היא קירוב עצלן שנכשל בדיוק במסמכים החשובים.
- קטע אחד, נושא אחד. הכלל היחיד שבאמת חשוב. אם קטע עונה על שתי שאלות שונות — הוא צריך להיות שניים. וקטור באורך קבוע שמתאר ארבעה נושאים הוא ממוצע מטושטש שאינו קרוב לאף אחד מהם.
- חפיפה קטנה. כמה עשרות מילים, כדי שמשפט שנחתך בגבול לא יאבד את ההקשר שלו.
- להוסיף את הכותרת לתוך הקטע. קטע שמתחיל ב"סעיף 4.2 — ביטול מנוי" נשלף טוב יותר מאותו קטע בלעדיה, כי ההקשר נכנס לייצוג עצמו.
- העשרה בהקשר. הרחבה של הקודם: לפני ההטמעה, להוסיף לכל קטע משפט שמסביר מאיפה הוא — "מתוך מדיניות ההחזרות, סעיף חריגים". זה עולה ריצה חד-פעמית על המאגר, והוא אחד השיפורים החזקים ביותר ביחס למאמץ.
ובעברית יש כאן מלכודת שקטה: ספירת תווים מטעה. אותו מספר תווים בעברית ובאנגלית אינו אותו מספר טוקנים, כך שחלוקה לפי תווים מייצרת קטעים לא אחידים בלי שמישהו יבחין. חלקו לפי מבנה, ובדקו את הגודל בטוקנים.
מה שמסמכים אמיתיים עושים לחלוקה
אלגוריתם החלוקה נראה פשוט עד שפוגשים את המסמכים שבאמת נמצאים בארגון. ארבעה מקרים שמפילים חלוקה נאיבית, וכולם שכיחים:
- PDF שהוא בעצם תמונה. חוזה סרוק, חשבונית שצולמה. אין בו טקסט לחלץ, ומה שנכנס למאגר הוא קטע ריק — ואז "המערכת לא מוצאת" בלי שום סימן לכך שהמסמך מעולם לא נקרא. בדקו שאפשר לסמן ולהעתיק טקסט לפני שמאשימים את השליפה.
- טבלאות. חלוקה שמפרידה שורה מהכותרות שלה יוצרת קטע של מספרים חסרי משמעות. טבלה נשארת שלמה, ואם היא גדולה — כל שורה מקבלת את הכותרות מוצמדות אליה.
- מסמכים היררכיים. תקנון או מדיניות שבהם סעיף 4.2.1 חסר משמעות בלי 4.2. זה בדיוק המקרה שבו הוספת שרשרת הכותרות לתוך הקטע משנה תוצאה.
- מצגות וקבצי אקסל. הטקסט בהם מקוטע מטבעו. לרוב עדיף להמיר אותם ידנית למסמך מסודר מאשר להזין כמו שהם ולקוות.
והכלל שמסכם: איכות המאגר נקבעת בשלב ההכנה, לפני שנגעתם באלגוריתם. שעה של סידור מסמכים שווה יותר משבוע של כוונון פרמטרים.
חיפוש משולב — ההחזר הגבוה ביותר למאמץ
אם עושים דבר אחד מהדף הזה, זה הדבר. חיפוש וקטורי וחיפוש מילולי נכשלים במקומות הפוכים, ושילובם מכסה את שניהם.
הווקטורי תופס משמעות ופרפרזה — "איך מבטלים מנוי" מוצא גם "הפסקת שירות" — ונחלש על מזהים. המילולי עושה בדיוק ההפך: מק״טים, קודי שגיאה, שמות מוצר ומספרי גרסה. מריצים את שניהם, ממזגים את הדירוגים, ומעבירים את המועמדים הלאה.
ובעברית זה חשוב פעמיים, משתי סיבות מנוגדות:
- החיפוש המילולי חלש יותר בעברית בגלל אותיות השימוש — "מערכת", "המערכת", "במערכת" ו"שהמערכת" הן ארבע מילים שונות למנוע מילולי. בלי נרמול לשורש, החצי הזה של השילוב מאבד הרבה מכוחו.
- ודווקא המק״טים והשמות הלועזיים כתובים באנגלית בתוך הטקסט העברי — וזה בדיוק מה שהחיפוש המילולי כן תופס מצוין והווקטורי מפספס.
כלומר: בעברית השילוב שווה יותר, ודורש קצת יותר עבודה כדי לעבוד.
דירוג מחדש — שלב הדיוק
השליפה מהירה וגסה; הדירוג מחדש איטי ומדויק. הדפוס הוא לשלוף רחב ולהעביר צר: עשרים עד חמישים מועמדים מהשליפה — שם עדיף לטעות לכיוון של יותר, כי מה שלא נשלף אבוד — ואז מודל דירוג שקורא כל מועמד מול השאלה ומעביר שלושה עד חמישה.
למה זה עובד: חיפוש וקטורי מודד דמיון, והשאלה שאתם באמת שואלים היא רלוונטיות. "המוצר כולל אחריות" ו"המוצר אינו כולל אחריות" הם שכנים קרובים מאוד במרחב — אותו נושא, אותן מילים — ורק אחד מהם עונה. מודל דירוג רואה את השאלה ואת הקטע יחד ויכול להבחין.
המחיר הוא השהיה ועלות, והוא אמיתי. שתי דרכים להקטין אותו: לדרג רק כשמספר המועמדים גדול, ולשמור תוצאות לשאלות חוזרות — שבמערכת שירות חוזרות הרבה יותר משנדמה.
איך זה נראה בקוד
הפייפליין כולו, בצורתו המינימלית — כדי שיהיה ברור שאין כאן קסם ושכל שלב ניתן למדידה בנפרד:
def answer(question, user):
# 1. סינון בשאילתה עצמה, לא אחריה
scope = {"tenant": user.tenant_id}
# 2. שתי שליפות, רחב
vec = vector_search(embed(question), scope, k=30)
lex = keyword_search(question, scope, k=30)
cands = merge_ranks(vec, lex) # מיזוג דירוגים
# 3. דירוג מחדש, צר
top = rerank(question, cands)[:4]
if not top:
return {"found": False}
# 4. ניסוח, עם דרישת ציטוט
out = call_model(
system=GROUNDED_PROMPT, # "ענה רק מהמקורות; אם אין — אמור שאין"
context=top,
question=question,
schema=ANSWER_SCHEMA, # answer, source_quote, source_id, found
)
# 5. בדיקה מכנית: הציטוט באמת מופיע במקור?
if out["found"] and out["source_quote"] not in joined(top):
out["needs_review"] = True
log(question=question, retrieved=[c.id for c in cands[:10]],
sent=[c.id for c in top], output=out) # 6
return out
שלושה פרטים נושאים כאן את רוב המשקל. ה-scope נכנס לשליפה ולא מסנן אחריה. הבדיקה בשלב 5 תופסת תשובה שנשמעת מצוין ומצטטת משהו שלא נאמר — בשורה אחת של קוד, בלי מודל נוסף. והתיעוד מפריד בין מה שנשלף למה שנשלח, וזו בדיוק ההפרדה שמאפשרת לענות מאוחר יותר על השאלה אם הבעיה הייתה בשליפה או בדירוג.
לטפל בשאלה, לא רק במסמכים
כמעט כל המאמץ בתחום מושקע בצד המסמכים, ויש אי-התאמה מובנית בצד השני: השאלה קצרה והקטע ארוך, והם כתובים אחרת. "איך מבטלים?" הוא שלוש מילים בגוף ראשון; הקטע שעונה עליו הוא פסקה רשמית בגוף שלישי.
שלוש טכניקות, לפי סדר העלות:
- נרמול והרחבה. לנסח את השאלה מחדש בשתיים-שלוש גרסאות ולחפש בכולן. עולה קריאה נוספת, ומשתלם במיוחד בשאלות קצרות ובעברית מדוברת.
- פירוק שאלה מורכבת. "מה מדיניות ההחזרות ומה קורה אם המוצר נפתח?" הן שתי שאלות; חיפוש אחד עליהן מחזיר תשובה בינונית לשתיהן. לפצל, לחפש בנפרד, לאחד.
- שאלות בצד המסמך. לייצר לכל קטע שתיים-שלוש שאלות שהוא עונה עליהן ולהטמיע גם אותן. עבודה חד-פעמית שמקרבת את המאגר לצורת השאילתה.
ובעברית יש כאן תוספת: כתיב חסר ומלא וטעויות הקלדה. "תוכנה" מול "תכנה", "מיליון" מול "מליון". אלה חלק מהקלט האמיתי, והם צריכים להופיע גם במערך הבדיקה — אחרת מודדים מערכת על שאלות מנוסחות יפה יותר ממה שמגיע בפועל.
סינון, הרשאות, וגרף
סינון לפי מטא-נתונים הוא הדרך הזולה ביותר לשפר דיוק: לצמצם את מרחב החיפוש לפני שמחפשים. תאריך, סוג מסמך, מחלקה, לקוח.
ונקודה שהיא לא אופטימיזציה אלא תנאי: סינון הרשאות שייך לשאילתה, לא לשלב שאחריה. מערכת רב-לקוחית שמחפשת בכל המאגר ואז מסננת היא גם איטית וגם דולפת — ותמיד יופיעו בה תשובות ריקות בלי סיבה גלויה, כי חמש התוצאות הטובות שייכות למישהו אחר.
ולגבי GraphRAG — ייצוג הידע כגרף של ישויות וקשרים: הוא פותר סוג שאלות שחיפוש רגיל לא פותר, כאלה שדורשות לחבר מידע ממקומות שאינם דומים זה לזה ("אילו לקוחות שקנו X פנו גם על Y"). הוא גם יקר להקמה ולתחזוקה, ובמאגר שמשתנה הוא דורש עדכון מתמיד. הוא נכנס אחרי שכל השאר עובד ועדיין חסר, לא כשדרוג ראשון.
שלב הניסוח, וההבדל בין תשובה לציטוט
הקטע הנכון הגיע להקשר וזה עדיין לא נגמר. שלושה דברים בשלב הניסוח שמשנים את התוצאה יותר מכל טכניקת שליפה נוספת:
לדרוש ציטוט. לבקש שהתשובה תצרף את המשפט מהמקור שעליו היא נשענת — ואז לבדוק בקוד שהמשפט הזה אכן מופיע בקטע שנשלף. אם לא, התשובה חשודה. זו בדיקה מכנית וזולה שתופסת בדיוק את סוג הטעות שמדדי שליפה לא תופסים.
לאפשר "אין לי מידע". מערכת שחייבת לענות תמיד תמציא כשאין. הרשות לומר שאין תשובה במקורות היא לא ויתור — היא מה שהופך שיעור ה"לא יודע" למדד שימושי: עלייה בו מצביעה על תוכן חסר או על שליפה שנשברה, וזו התרעה מוקדמת שמגיעה בחינם.
לומר מאיפה. תשובה שמפנה למסמך ולסעיף מאפשרת למשתמש לבדוק, ומורידה דרמטית את הנזק מתשובה שגויה. בעסק, זה גם ההבדל בין מערכת שאנשים סומכים עליה לבין מערכת שהם מאמתים ידנית ממילא.
כשהמודל מקבל שני קטעים סותרים
מצב שקורה הרבה יותר ממה שמדברים עליו: השליפה עבדה, שני הקטעים רלוונטיים — והם אומרים דברים הפוכים. מדיניות שהתעדכנה, נוהל שהוחלף, מסמך ישן לצד חדש.
מה שהמודל עושה בלי הנחיה הוא כמעט הגרוע ביותר: הוא בוחר אחד מהם, בדרך כלל את זה שנוסח בביטחון רב יותר, ועונה כאילו אין סתירה. המשתמש מקבל תשובה חד-משמעית ואין לו דרך לדעת שהייתה מחלוקת.
שלושה תיקונים, לפי סדר היעילות:
- למנוע מלכתחילה. רוב הסתירות הן גרסאות ישנות שלא נמחקו. עדכון ברמת המסמך פותר את הרוב המכריע.
- להעדיף לפי תאריך. אם לכל קטע יש תאריך עדכון, אפשר להטות את הדירוג לטובת החדש — ולומר למודל במפורש שכשיש סתירה, המאוחר גובר.
- לבקש שיאמר. הנחיה שמורה לציין סתירה במפורש במקום לבחור בשקט. זה מייצר תשובה פחות חלקה ומידע הרבה יותר שימושי — וזה גם מאתר עבורכם תוכן שדורש סידור.
מאגר שמתעדכן
רוב ההסברים מניחים מאגר סטטי. בפועל מסמכים משתנים ונמחקים, ושלוש תקלות חוזרות:
- מחיקה שלא הגיעה למאגר. המסמך הוסר, הקטעים נשארו, והמערכת עונה לפי מדיניות שבוטלה. זו המביכה שבהן, והיא נפוצה.
- שתי גרסאות שחיות יחד. הקטעים החדשים נוספו בלי שהישנים הוסרו. עכשיו יש במאגר שתי תשובות סותרות, ומי שנשלף הוא מי שדומה יותר — לא מי שנכון.
- עדכון חלקי שנעצר באמצע. חצי מהמסמך בגרסה אחת וחצי באחרת.
הפתרון לשלושתם זהה: לקשור כל קטע למזהה המסמך ולעדכן ברמת המסמך — למחוק את כל קטעיו ולהכניס את החדשים כיחידה אחת. ולצד זה, בדיקה תקופתית שסופרת כמה מסמכים קיימים במקור וכמה מיוצגים במאגר; פער בין המספרים הוא ממצא ולא רעש.
ופרט שחוסך הרבה במאגרים גדולים: לשמור גיבוב של הטקסט לצד כל קטע, כדי שעדכון מסמך יטמיע מחדש רק את מה שבאמת השתנה.
בעברית, מה שבאמת שונה
ריכוז של מה שפזור בדף, כי זה החלק שאין לו מקבילה בתיעוד האנגלי ושמפיל מערכות עבריות:
- אותיות שימוש שוברות חיפוש מילולי. ה, ב, ל, מ, ש, ו — "מערכת" ו"במערכת" הן שתי מילים שונות למנוע. בלי נרמול לשורש, חצי מהחיפוש המשולב מאבד מכוחו.
- כתיב חסר ומלא. אותה מילה, שתי כתיבות, שתי נקודות במרחב. שייך גם למאגר וגם למערך הבדיקה.
- ספירת תווים אינה ספירת טוקנים. חלוקה לפי תווים מייצרת קטעים לא אחידים, והערכת עלות יוצאת נמוכה מדי.
- ערבוב אנגלית בתוך עברית. מק״טים, שמות מוצר ומונחים מקצועיים. מודל הטמעה שמטפל בכל שפה בנפרד מתקשה כאן.
- סימני כיווניות נסתרים. טקסט שהועתק מ-PDF עברי נושא איתו תווים בלתי נראים שנכנסים למאגר ומשפיעים על ההשוואה בלי שרואים אותם. שווה לנקות בשלב ההכנה.
והמבחן שמסכם את כולם, ולוקח חצי שעה: עשרים שאלות אמיתיות בעברית, עשרים קטעים שעונים עליהן, ובדיקה אם המערכת מזווגת נכון. זה שווה יותר מכל השוואת מודלים שתקראו.
מה זה עולה
שתי עלויות שונות באופיין, ונוח לבלבל ביניהן. הטמעת המאגר היא חד-פעמית — משולמת שוב רק בהחלפת מודל או בעדכון תוכן. כל שאילתה משלמת הטמעה של השאלה, ואם יש דירוג מחדש — גם אותו, על כל מועמד.
שלוש נקודות מעשיות: מטמון על שאלות חוזרות — במערכות שירות אותן שאלות חוזרות בלי סוף; לדרג רק כשצריך — לא על כל שאילתה; ולזכור שבעברית ההערכה יוצאת נמוכה מדי, כי אותו תוכן נשבר ליותר טוקנים.
ומעל הכול, המדד שמנהלים לפיו: עלות לשאלה שנענתה נכון. מערכת זולה שעונה לא נכון היא לא זולה.
מה למדוד בייצור
מערך השאלות מודד מול מה שידעתם לשאול בזמן הבנייה. מה שמשתמשים שואלים בפועל זז, ולכן צריך גם אותות מהייצור — ושלושה מהם לא דורשים שום סימון ידני:
- שיעור "אין לי מידע". עלייה פתאומית בו אומרת כמעט תמיד שהשליפה נשברה, לא שהתוכן נעלם. זו ההתרעה המוקדמת הזולה ביותר שיש.
- שאלה שחוזרת בניסוח אחר באותה שיחה. הסימן הברור ביותר לכך שהתשובה הראשונה לא הספיקה — ואף אחד לא צריך לדווח עליו.
- ניצולת הקטעים שנשלחו. מתוך הקטעים שהגיעו למודל, כמה באמת השפיעו על התשובה. שיעור נמוך עקבי אומר ששולחים יותר מדי, וזה גם מייקר וגם מדלל.
ומה שמחבר בין השניים: כל דפוס שמתגלה בייצור חוזר למערך כמקרה בדיקה. זה מה שהופך את המערך למשהו שמשתפר עם הזמן במקום להתיישן — ובפרט, כל תלונה על תשובה שגויה נכנסת אליו לפני שמתקנים, אחרת אין דרך לדעת שהתיקון עבד.
הפייפליין המלא
איך הכול מתחבר, ובאיזה סדר לבנות. כל שלב הוא תוספת, ואין טעם להוסיף אותו לפני שהמדידה מראה שהוא נחוץ:
- חלוקה לפי מבנה עם כותרות, חפיפה ומטא-נתונים. זה הבסיס, וזה גם מה שקובע את התקרה.
- הטמעה ואחסון, עם שיוך ומזהה מסמך לכל קטע.
- סינון לפי הרשאה ומטא-נתונים — בשאילתה עצמה.
- חיפוש משולב, וקטורי ומילולי, ומיזוג הדירוגים.
- דירוג מחדש של המועמדים לשלושה עד חמישה.
- ניסוח עם ציטוט ועם רשות לומר שאין מידע.
- תיעוד של מה נשלף, באיזה דירוג, ומה נשלח למודל.
והשלב האחרון אינו האחרון בחשיבות. בלי לדעת מה נשלף ובאיזה סדר, כל תקלה עתידית תהיה חקירה מאפס.
מתי הבעיה אינה RAG
- כשהמידע לא קיים במסמכים. אז זו בעיית תוכן. שום טכניקה לא תמציא מה שאין.
- כשהמסמכים סותרים או מיושנים. RAG ימצא את הסתירות מהר מאוד ויציג אותן כתשובה. סדרו את התוכן קודם.
- כשהמאגר קטן. חמישים קטעים נכנסים ישירות להקשר. כל התשתית הזו מיותרת.
- כשהתוכן משתנה כל שעה. מלאי, מחירים, סטטוס — אלה שאילתה למסד נתונים, לא מאגר להטמעה מחדש.
- כשהשאלה דורשת חישוב. "כמה לקוחות" היא שאילתה, לא שליפה סמנטית.
טעויות שחוזרות
- להחליף טכניקות בלי לדעת איזה שלב נכשל. הטעות היקרה ביותר בתחום.
- לחתוך לפי מספר תווים. שובר משפטים, ובעברית גם מייצר קטעים לא אחידים.
- להטמיע עמוד שלם כיחידה. ייצוג מטושטש שלא קרוב לאף שאלה.
- לוותר על חיפוש מילולי. מק״טים, קודי שגיאה ושמות ייעלמו.
- לסנן הרשאות אחרי החיפוש. איטי, דולף, ומחזיר ריק בלי סיבה גלויה.
- להעביר עשרים קטעים למודל. הקשר מדולל ועלות כפולה בלי שיפור מקביל.
- לא לאפשר "אין לי מידע". מערכת שחייבת לענות תמציא.
- להוסיף GraphRAG כשדרוג ראשון. יקר, ומטפל בבעיה שכנראה אין לכם.
- מחיקות שלא מסונכרנות. המערכת עונה לפי מסמך שכבר לא קיים.
- לבנות הכול ואז למדוד. המערך של עשרים שאלות נבנה ראשון, לא אחרון.