דילוג לתוכן הראשי
פרויקט אישי

פלטפורמת RAG רב-לשונית ובלתי תלויה ב-framework

פרויקט אישי: להתייחס לתשתית retrieval כבעיית מחזור חיים ולא כדמו

מקור
פרויקט אישילצפייה בקוד ב-GitHub
Tech Stack
Python · FastAPI · PostgreSQL · Qdrant · Ollama · LangChain · Docker · Alembic
RAG, AI Engineering, פיתוח Backend, אמינות,

אמל״ק

הבעיה
דמואים של 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) אחד.

החלטות מרכזיות

  • Qdrant כאינדקס נגזר ולא כמקור סמכות. כש-PostgreSQL מחזיק את מצב מחזור החיים והאינדקס הווקטורי ניתן לבנייה מחדש, אינדוקס מחדש או התאוששות מאינדקס פגום הם פעולה בטוחה ומוגדרת היטב, ולא סיכון לאובדן נתונים.
  • שני מנועי 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 מופיעות אחרי השאילתה המוצלחת הראשונה: מה קורה כשמסמכים משתנים, כשמודלים משתנים או כשהאינדקס הנגזר סוטה ממקור האמת. תכנון לצורכי המקרים האלה מההתחלה, גם בפרויקט של אדם אחד, אילץ החלטות ארכיטקטורה אמיתיות במקום קיצורי דרך של דמו.