רוב אבות-הטיפוס של RAG נראים אותו דבר מתחת למכסה המנוע. מישהו מזין PDF לתוך צינור עיבוד (pipeline), מחלק את הטקסט למקטעים (chunks) מסודרים של 512 טוקנים, דוחף אותם למסד נתונים וקטורי, ומכריז על העבודה כגמורה. עבור הדגמה ראשונית במסדרון, זה יכול להיראות מרשים. בסביבת ייצור, זה קורס.

מקטע קבוע לא מתחשב במה שהוא חותך. הוא עלול לחתוך חוזה משפטי באמצע סעיף שיפוי. הוא עלול לדחוף חמישה נקודות קצה (endpoints) של API לא קשורות לאותו חלון הקשר (context window) ולהציף את המודל ברעש. הוא יאלץ אתכם לשלוף יותר מקטעים ממה שנדרש, מה שינפח את השיהוי (latency) וישרוף טוקנים. התוצאה היא תשובות חלקיות, הזיות (hallucinations) ומשתמשים מתוסכלים.

פרקנו את שכבת השליפה (retrieval layer) שלנו עד היסודות ובנינו אותה מחדש. התוצאה הייתה מערכת שהגיעה ל-95 אחוז Recall תוך הפחתת השיהוי ב-40 אחוזים. הנה בדיוק איך עשינו זאת.

למה מקטעים קבועים נכשלים בסביבת ייצור

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

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

תיעוד API סובל מבעיה הפוכה. מקטע של חמש מאות טוקנים עלול לבלוע מודול שלם: כותרות אימות (authentication headers), קודי שגיאה, מגבלות קצב (rate limits) וסכימות של webhooks. כאשר מפתח שואל כיצד לטפל ב-AUTH_4027, השליפה מציפה תערובת של פונקציות לא קשורות. למודל אין ברירה אלא למצע אותן לדייסה גנרית.

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

התאמת המקטע למסמך

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

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

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

כרטיסי תמיכה (Support tickets) הם רועשים ולא ליניאריים. שרשור יכול להתחיל בדיווח על באג, להציג פתרון זמני (workaround) ולהסתיים בהערת הסלמה פנימית. חלוקה סמנטית (semantic chunking) מזהה שינויים בנושא על ידי מדידת דמיון ה-embedding בין משפטים. אנו מאפשרים הפסקות רק בגבולות נושאיים טבעיים, כך ששיחה על כשלי התחברות תישאר נפרדת מהמשך שיחה על מחזורי חיוב.

וויקיים (Wikis) היו הקשים ביותר. הם נמרחים, מקושרים זה לזה ומאורגנים בצורה רופפת. השתמשנו ב-agentic chunking, שבו LLM קל-משקל קורא דף ומחליט על ההפסקות על בסיס לכידות נושאית. זה עולה מעט יותר בזמן ההזנה (ingestion), אך המקטעים המתקבלים הם עצמאיים ומוכנים לשליפה. דף על שיטות עבודה מומלצות לפריסה מתחלק ליחידות לוגיות: בדיקות טרום-תעופה, נהלי rollback והגדרת ניטור, במקום בלוקים שרירותיים של טקסט.

שליפה היברידית: מילות מפתח ווקטורים יחד

חיפוש וקטורי צפוף (dense vector search) מבין משמעות. הוא גרוע מאוד במציאת מחרוזות מדויקות. אם משתמש מחפש קוד שגיאה מדויק כמו AUTH_4027 או שם לקוח כמו "Stark Industries", ייצוגי ה-embedding עלולים להחמיץ את המטרה מכיוון שהם מותאמים לקרבה מושגית, ולא לדיוק ברמת התווים.

לחיפוש מילות מפתח טהור באמצעות BM25 יש את הפגם ההפוך. הוא ימצא את AUTH_4027 בצורה מושלמת, אך הוא יפספס את הגשר המושגי בין "authorization failure" לבין "login denied".

אנחנו מריצים את שניהם במקביל. BM25 וחיפוש וקטורי (vector search) פועלים באופן עצמאי על אותו קורפוס. רשימות התוצאות שלהם ממוזגות באמצעות Reciprocal Rank Fusion, שמסדר מחדש את המועמדים על ידי איזון הדירוגים המיקומיים שלהם. אין צורך במשקלים מכוילים. פשוט מקבלים את הדיוק של התאמה מדויקת (exact match) ואת האינטואיציה של חיפוש סמנטי (semantic search) ברשימה מדורגת אחת.

לאחר מכן, אנחנו מוסיפים cross-encoder reranker. זהו מודל נפרד שמעריך כל passage אל מול השאילתה המקורית, ומפיק אות רלוונטיות עדין בהרבה מכל retriever בנפרד. זה מוסיף כ-50 מילישניות של latency. זה מגדיל את ה-recall ב-15 אחוזים. אם איכות התשובה חשובה לכם, הפשרה הזו היא בלתי ניתנת למשא ומתן.

הרחבת שאילתות (Query Expansion): לתקן את החיפוש לפני שהוא מתחיל

שאילתות גרועות הן הסוד המלוכלך של כל מערכת שליפה (retrieval system). משתמשים לא כותבים כמו מרחב ה-embedding שלכם. הם מקלידים "it broke". הם מדביקים stack traces מקוטעים. הם משתמשים בז'רגון פנימי שהאינדקס שלכם מעולם לא ראה.

אנחנו משנים את השאילתה עוד לפני שהיא נוגעת באינדקס. ראשית, אנחנו מרחיבים שאילתה בודדת לשלושה עד חמישה מונחי חיפוש מגוונים. אם המקור הוא "payment failed", אנחנו מחפשים גם את "transaction error", "billing declined", ו-"charge unsuccessful