Context Graphs — חלופה חכמה יותר לאחסון שיחות כרשימה
מאת צוות מדיה דיל · 12.08.2026 · AI Retrieval · 5 דק׳
אחסון היסטוריית שיחה כרשימת הודעות שטוחה נשבר בשיחות ארוכות. Context Graph מייצג עובדות וישויות כגרף שאפשר לשלוף ממנו רק את מה שרלוונטי לתור הנוכחי.
סוכן שירות לקוחות שניהל שיחה של 80 הודעות עם משתמש שהזמין החלפת מוצר, ביטל אותה, הזמין הזמנה חדשה, ואז שאל "מה קורה עם ההחזר הכספי" — ענה בביטחון על ההזמנה הלא נכונה. הסיבה לא הייתה מודל חלש; היא הייתה שהשיחה כולה, כל 80 ההודעות, הוזרמה לחלון ההקשר כרשימה כרונולוגית שטוחה, ומודל השפה פשוט "איבד" את העובדה שההזמנה הראשונה בוטלה — תופעה מוכרת בשם lost in the middle, שבה מידע שנמצא באמצע קונטקסט ארוך מקבל פחות משקל תשומת לב ממידע שבתחילתו או בסופו. הפתרון לא היה חלון הקשר גדול יותר. הוא היה מבנה נתונים אחר לגמרי.
למה רשימה שטוחה נשברת בקנה מידה
אחסון היסטוריית שיחה כ-array של הודעות ({role, content, timestamp}) הוא הפתרון הכי אינטואיטיבי, וגם הכי נפוץ בכל SDK של LLM. הבעיה היא שהוא לא מבחין בין מידע שעדיין נכון לבין מידע שהוחלף. אם משתמש אמר "אני גר בתל אביב" בהודעה 3 ואז "עברתי לחיפה" בהודעה 47, שתי העובדות נשארות בהיסטוריה בו-זמנית, ומודל השפה צריך "לנחש" איזו מהן רלוונטית עכשיו — משימה שקשה יותר ככל שהשיחה מתארכת. בנוסף, ברשימה שטוחה כל תור חדש מגדיל את עלות ה-tokens באופן ליניארי, גם אם 90% מההיסטוריה כבר לא רלוונטי לשאלה הנוכחית. Truncation נאיבי (שמירת רק N ההודעות האחרונות) פותר את בעיית האורך אבל מוחק מידע שעדיין קריטי — אם ההזמנה הוזכרה בהודעה 5 ונשאלת עליה שאלה בהודעה 60, חיתוך ל-20 הודעות אחרונות פשוט מאבד אותה.
עקרון ה-Context Graph
הגישה החלופית: במקום לאחסן את השיחה כרצף הודעות, מחלצים ממנה ישויות (Entities — לקוח, הזמנה, מוצר) ועובדות (Facts — טענות על הישויות, כמו "הזמנה #4521 בוטלה") ומאחסנים אותן כגרף: צמתים לישויות, קשתות לקשרים ולעובדות ביניהן. כשמתקבל תור חדש בשיחה, שכבת חילוץ (בדרך כלל LLM עצמו, בקריאה נפרדת וזולה) מזהה אילו ישויות ועובדות חדשות הוצגו, ומעדכנת את הגרף — כולל סימון עובדות ישנות כלא-תקפות כשהן מוחלפות, במקום למחוק אותן. כשמגיעה שאלה חדשה, המערכת לא שולפת "את כל השיחה" אלא מריצה שאילתת גרף ממוקדת: איזה תת-גרף רלוונטי לישויות שמוזכרות בשאלה הנוכחית, ומזרימה רק אותו למודל.
זה משנה איכותית את אופי ה-retrieval: במקום context שגדל ליניארית עם אורך השיחה, יש context שגדל עם מספר הישויות הרלוונטיות לשאלה הנוכחית — שבדרך כלל נשאר קטן וקבוע גם בשיחות ארוכות מאוד. בתרחיש שפתחנו איתו, שאלה על "ההחזר הכספי" הייתה מפעילה שאילתת גרף שמתמקדת בישות ההזמנה הפעילה בלבד, כשהקשת ל"בוטלה" של ההזמנה הראשונה כבר מסמנת אותה כלא-רלוונטית להקשר הנוכחי.
מבנה נתונים: מה שם בצומת ומה בקשת
ברוב המימושים המעשיים, כל צומת מייצג ישות עם מזהה יציב (customer_id, order_id) ותכונות עדכניות; כל קשת מייצגת עובדה עם timestamp, מקור (איזו הודעה בשיחה יצרה אותה), וסטטוס תקפות. שדה הסטטוס הוא הקריטי ביותר — הוא מה שמאפשר להבחין בין "מה שהיה נכון פעם" ל"מה שנכון עכשיו" בלי לאבד את ההיסטוריה (שעדיין שימושית, למשל לצורך ביקורת). זה בעצם גרסה קטנה של בעיה רחבה יותר — ניהול עובדות שמשתנות לאורך זמן — שמטופלת בהרחבה במדריך Temporal Knowledge.
מבחינה טכנית, אין צורך במסד גרף ייעודי בשלב הראשוני — טבלת ישויות וטבלת קשתות ב-PostgreSQL, עם אינדקס על entity_id ו-valid_until, מספיקות היטב לרוב יישומי הצ'אטבוט. מסד גרף כמו Neo4j מצדיק את עצמו כשהטראברסלים נהיים עמוקים ומרובי-קפיצות, כפי שמוסבר במדריך ארכיטקטורת גרפי ידע.
חילוץ ישויות: מתי להריץ ואיך לא להאט את השיחה
שאלה מעשית קריטית היא מתי מריצים את שלב החילוץ. אם מריצים אותו סינכרונית לפני כל תגובה, המשתמש מחכה לשתי קריאות LLM ברצף — חילוץ ואז generation — מה שמכפיל את זמן התגובה הנתפס. הפתרון הנפוץ הוא הרצה אסינכרונית: התשובה למשתמש נשלחת מיד על סמך מצב הגרף הקיים, ועדכון הגרף מהתור הנוכחי רץ ברקע ומוכן לשלב הבא. זה מציג פשרה קטנה — התור הנוכחי עצמו לא זמין לשליפה עד לתום עיבוד הרקע — אבל בפועל זו כמעט תמיד הבחירה הנכונה, כי המידע החדש ביותר גם ככה מגיע ישירות בפרומפט של התור הנוכחי, ולא דרך הגרף.
שאלה נוספת היא רזולוציית ישויות (entity resolution): כשמשתמש כותב "ההזמנה שלי" בתור אחד ו"מספר 4521" בתור אחר, שני האזכורים צריכים להתמזג לאותו צומת בגרף, לא להיווצר כשתי ישויות נפרדות. פתרון פשוט וסביר הוא לשמור context window קצר של הישויות שהוזכרו לאחרונה, ולתת ל-LLM שמבצע את החילוץ גישה אליו כדי שיוכל לפתור אזכורים עקיפים (co-reference) נכון.
שאילתת גרף בפועל: מה מוזרם לפרומפט
כשמגיעה שאלה חדשה, שכבת ה-retrieval לא שולפת את כל הגרף אלא מבצעת שאילתה ממוקדת: זיהוי הישויות שמוזכרות (או נרמזות) בשאלה הנוכחית, ואז טראברסל של קפיצה או שתיים ממנה — הישות עצמה, הקשרים הישירים שלה, והעובדות התקפות (לא הישנות) שקשורות אליה. במקום להזרים למודל "כל מה שנאמר על ההזמנה הזו אי פעם", מזרימים רק "מה שנכון עכשיו על ההזמנה הזו, ומה שקשור אליה ישירות" — בדרך כלל כמה עשרות טוקנים במקום אלפים. זה בעצם מה שהופך את הגישה למדרגה (scalable) גם בשיחות של מאות תורות: גודל הפרומפט לא תלוי באורך השיחה אלא במורכבות תת-הגרף הרלוונטי לרגע הנוכחי, שנשאר בדרך כלל קטן ויציב.
שווה לציין שהמעבר הזה גם משנה את אופי הבדיקות (testing) של הסוכן: במקום לבדוק "האם התשובה נכונה בהינתן כל ההיסטוריה", בודקים בנפרד את איכות שלב החילוץ (האם הישויות והעובדות הנכונות זוהו בכל תור) ואת איכות שלב השאילתה (האם תת-הגרף הרלוונטי אכן נבחר נכון) — שני מרכיבים שקל הרבה יותר לבדוק ולתקן בנפרד מאשר תשובה סופית אחת שתלויה בכל ההקשר בבת אחת.
מתי זה שווה את המורכבות הנוספת
Context Graph מוסיף שלב עיבוד (חילוץ ישויות מכל תור) שלא קיים בגישת הרשימה השטוחה — עלות אמיתית בזמן ריצה ובעלות. הוא משתלם כשהשיחות ארוכות באופן קבוע (עשרות תורות ומעלה), כשיש הרבה עובדות שמתעדכנות תוך כדי שיחה (סטטוסים, כתובות, החלטות), וכשדיוק בהקשר קריטי יותר מפשטות המימוש — כמו בתמיכת לקוחות, ייעוץ רפואי או פיננסי. עבור צ'אטבוט FAQ פשוט עם שיחות קצרות של 3-5 תורות, זו overengineering מיותרת — רשימה שטוחה עם חיתוך פשוט עובדת מצוין. הגישה הזו למעשה מרחיבה את אותם עקרונות שמתוארים ב-GraphRAG (GraphRAG) מתחום מסמכים סטטיים לתחום הקשר שיחה דינמי, שם המבנה משתנה בכל תור.
תגיות: Context Graph · Conversation Memory · Lost in the Middle · AI Agents · Knowledge Graph · Entity Extraction