פלטפורמת RAG רב-לשונית ובלתי תלויה ב-framework
פרויקט אישי: להתייחס לתשתית retrieval כבעיית מחזור חיים ולא כדמו
- מקור
- פרויקט אישילצפייה בקוד ב-GitHub
- Tech Stack
- Python · FastAPI · PostgreSQL · Qdrant · Ollama · LangChain · Docker · Alembic
אמל״ק
תקציר ב-30 שניות- הבעיה
- דמואים של retrieval נעצרים ב-upload ובשאילתה. רציתי לבדוק מה נדרש כדי להפעיל תשתית retrieval: מחיקה, החלפת מודל embedding, drift של האינדקס והחלפת framework התזמור.
- מה בניתי
- כמתכנן ומממש יחיד בניתי את הארכיטקטורה, ה-backend, אסטרטגיית הבדיקות (יחידה, אינטגרציה ו-end-to-end עם Testcontainers) והתיעוד של backend מסוג RAG, local-first ורב-לשוני.
- תוצאה וראיות
- ה-backend תומך ב-ingestion אסינכרוני, בצ'אט עם ציון מקורות, במחיקה, באינדוקס מחדש ללא השבתה ובדוחות reconciliation. הוא מאומת בסטים של בדיקות יחידה, אינטגרציה, מיגרציה ו-end-to-end. זהו פרויקט אישי.
זהו פרויקט Backend אישי ויזום, לא ניסיון מקצועי בפרודקשן. אין לו תעבורת פרודקשן, אין לו משתמשים ואין לו frontend. הוא שלם מבחינת שלב מחזור החיים של ה-backend, וולידציה של ה-UI נדחתה במכוון.
הבעיה
רוב המדריכים ל-RAG נעצרים ב"מעלים מסמך ושואלים שאלה". אני רציתי לבדוק את החלקים שהופכים לחובה ברגע שמתייחסים לתשתית retrieval כמשהו שבאמת מפעילים: מה קורה כשמסמך נמחק, כשמודל ה-embedding מוחלף, כשהאינדקס ומקור האמת לא מסכימים, או כשרוצים להחליף את ה-framework של התזמור בלי לכתוב מחדש את ה-API.
הקשר
פרויקט Backend יזום, שנבנה ושופר לבד, בלי חברה ובלי פריסת פרודקשן מאחוריו. הוא רץ כולו על תשתית מקומית: FastAPI, PostgreSQL, Redis, Qdrant ו-Ollama להרצה מקומית של מודלי LLM ו-embedding. לא נדרשות קריאות ל-API חיצוני.
אילוצים
- רב-לשוניות מההתחלה: תוכן ושאילתות בעברית ובאנגלית, כולל retrieval חוצה שפות, ולא אנגלית עם תרגום שהודבק מעל.
- בלי תלות בענן: כל ה-stack היה צריך לרוץ מקומית באמצעות Docker Compose.
- בלי נעילה ל-framework: הלוגיקה של ה-retrieval וה-generation לא אמורה להיות בלתי נפרדת מספריית תזמור מסוימת.
- מכיוון שפיתחתי את הפרויקט לבדי, היה צריך לסדר את ההיקף בכוונה: הפרויקט מחולק במפורש לשלבים, וה-backend של מחזור החיים הוקפא עד לעבודה עתידית על UI ו-E2E, ולא נבנה כמאמץ אחד בלי גבול.
התפקיד שלי
מתכנן ומממש יחיד: ארכיטקטורה, מימוש ה-backend, אסטרטגיית בדיקות (יחידה, אינטגרציה ו-end-to-end של ה-backend מבוסס Testcontainers) ותיעוד.
ארכיטקטורה וגישה
שלוש מערכות אחסון עם תפקידים נפרדים ומפורשים: PostgreSQL כסמכות למחזור החיים (מקור האמת למצב המסמכים ולמטא-דאטה), אחסון אובייקטים כסמכות לתוכן המקורי, ו-Qdrant כאינדקס וקטורי נגזר שאפשר לבנות מחדש, שאינו נחשב מערכת הרשומות המחייבת. מעליהן יושבת שכבת הפשטה ל-providers של embeddings, LLM וגישה ל-vector store, וכן הפשטה בשם RagEngine שמאפשרת למתזמר מותאם אישית ולמנוע אופציונלי מבוסס LangChain לחלוק API ציבורי אחד וחוזה streaming (SSE) אחד.
החלטות מרכזיות
- שני מנועי RAG להחלפה זה בזה מאחורי חוזה אחד. מתזמר מותאם אישית ומנוע מבוסס LangChain מממשים את אותו ממשק
RagEngine, כך שבחירת ה-framework ניתנת להחלפה ואינה נושאת משקל מבני. זה אומת בבדיקות שוויון התנהגותי על פני תרחישים רב-לשוניים. - גרסאות מפורשות ל-embeddings ולאוספי וקטורים. תצורת ה-embedding ואוספי הוקטורים מנוהלים בגרסאות מפורשות, כך ששינוי במודל ה-embedding או ב-chunking לא יכול לערבב בשקט וקטורים שאינם תואמים. זה מניח בסיס לאינדוקס מחדש בטוח.
- הפשטת providers ולא קיבוע של ספק LLM אחד. שכבת ה-embedding וה-LLM וה-vector store בנויה כך שאפשר להוסיף בהמשך providers נוספים באמצעות stubs, בלי לגעת ב-pipeline של ה-ingestion או ה-retrieval.
- מוכנות לפריסה כשלב מרכזי (שלב 2.10). ולידציית תצורה בגישת fail-fast, backoff אקספוננציאלי מוגבל עם jitter בקריאות ל-providers, לוגים מובנים בפורמט JSON עם מזהי קורלציה לבקשות וכיבוי שיתופי של workers נחשבים חלק מהמערכת ולא תוספת שמדביקים לפני השקה היפותטית.
שיקולים ופשרות
בניית שכבת מחזור החיים (תצפית, התאוששות, מחיקה, ניטרול כפילויות, שדרוג והתאמה) לפני שהיה frontend אומרת שהפרויקט נשאר Backend בלבד זמן ארוך יותר מגישה שמתחילה בדמו. זו פשרה מכוונת, שמתועדת בהחלטת הקפאת ה-backend של הפרויקט עצמו, ובה בחרתי בבסיס נכון על פני UI שאפשר להראות מהר. אי-תלות ב-framework באמצעות חוזה RagEngine משותף גם אומרת יותר הפשטה ממה שמימוש מבוסס framework יחיד היה צריך. זה מוצדק כאן, כי ההשוואה בין מנוע מותאם אישית למנוע מבוסס LangChain הייתה חלק מהמטרה.
תוצאות ואימות
Backend שתומך בהעלאת מסמכים וב-ingestion אסינכרוני של כמה סוגי קבצים, בצ'אט streaming עם ציון מקורות, בניטרול כפילויות בהעלאה על בסיס hash, במחיקה אסינכרונית מלאה של מסמכים, באינדוקס מחדש בשיטת build-ahead, ללא השבתה, ובדוחות reconciliation וביקורת לקריאה בלבד על פני שלוש מערכות האחסון. הוא מאומת בסטים של בדיקות יחידה, אינטגרציה, מיגרציה ו-end-to-end של ה-backend, ומוגן ב-hook מסוג pre-commit בשם make verify.
מה למדתי
הבעיות ההנדסיות המעניינות במערכות RAG מופיעות אחרי השאילתה המוצלחת הראשונה: מה קורה כשמסמכים משתנים, כשמודלים משתנים או כשהאינדקס הנגזר סוטה ממקור האמת. תכנון לצורכי המקרים האלה מההתחלה, גם בפרויקט של אדם אחד, אילץ החלטות ארכיטקטורה אמיתיות במקום קיצורי דרך של דמו.