זיכרון בסוכני AI
בלי זיכרון, סוכן שוכח הכל בכל שיחה. עם זיכרון נכון, הוא זוכר העדפות, למד מאינטראקציות קודמות, ומרגיש אישי. כך בונים אותו.
"זיכרון" הוא ארבע בעיות שונות
רוב הפרויקטים שנתקעים בזיכרון של סוכנים נתקעים באותו מקום: קראו לארבעה דברים שונים באותו שם, בנו פתרון אחד, וגילו שהוא פותר רק אחד מהם.
- מצב השיחה. מה נאמר עד עכשיו בשיחה הנוכחית. בעיה של חלון הקשר.
- עובדות על המשתמש או הישות. שם, חברה, מסלול, העדפות, אזור זמן. בעיה של בסיס נתונים.
- מצב המשימה. מה כבר בוצע, מה ממתין, מה נכשל. בעיה של ניהול תהליך.
- מה שנלמד. העדפות סגנון, דרכי עבודה, מה עבד בפעם הקודמת. הקשה מכולן, והיחידה שאין לה פתרון מוסכם.
וההרגל הנפוץ הוא לבנות מסד וקטורי ולקרוא לו "זיכרון". זה פותר חלק מהשני, גרוע, ולא נוגע בשלושת האחרים. עובדות על משתמש הן נתונים מובנים — שם, מסלול, תאריך — והשליפה שלהן היא מדויקת ולא סמנטית. טבלה פשוטה עושה את זה טוב יותר מכל הטמעה.
האם אני שולף לפי ערך מדויק או לפי משמעות? הראשון — טבלה. השני — ורק השני — מצדיק מסד וקטורי.
החלון אינו זיכרון
נקודה בסיסית שקל לפספס: המודל לא זוכר כלום בין קריאה לקריאה. כל קריאה מתחילה מאפס, וכל מה שהוא "יודע" על השיחה הוא מה שנשלח אליו באותה בקשה. חלון ההקשר הוא לא זיכרון אלא שולחן עבודה שמתנקה בכל פעם.
התיקון האינטואיטיבי — לשלוח את כל התמליל בכל בקשה — עובד לעשר הודעות ונשבר אחר כך, משתי סיבות שאינן קשורות זו לזו:
- עלות. אתה משלם על כל ההיסטוריה בכל תור. שיחה של חמישים הודעות משלמת על ההודעה הראשונה חמישים פעם.
- איכות. ככל שההקשר ארוך יותר, כך יורדת תשומת הלב של המודל למה שנמצא באמצע שלו. הקצוות נשמרים טוב יותר מהמרכז, ולכן דווקא הפרט החשוב מאמצע השיחה הוא זה שייעלם.
ובעברית יש כאן מכפיל שכדאי לדעת עליו: טקסט עברי צורך יותר טוקנים מאותו טקסט באנגלית, כי הטוקנייזרים אומנו בעיקר על אנגלית ומפרקים מילה עברית לרסיסים. אותה שיחה בעברית מגיעה לתקרת ההקשר מוקדם יותר ועולה יותר — פירוט במדריך HuggingFace.
ניהול השיחה: שלוש אסטרטגיות
לניהול מצב השיחה יש שלוש גישות בסיסיות, ולכל אחת כשל אופייני.
- תמליל מלא. הכי פשוט, הכי מדויק, ולא ניתן להרחבה. מתאים לשיחות קצרות בהגדרה — תמיכה, טופס, אשף.
- חלון גולש. רק N ההודעות האחרונות. זול וקבוע, והכשל שלו חד: מה שנפל מהחלון נמחק לחלוטין. המשתמש אמר את שמו בהודעה הראשונה, ובהודעה העשרים הסוכן שואל אותו מי הוא.
- סיכום מתגלגל. מסכמים את מה שיצא מהחלון ומצרפים את הסיכום. מחזיק שיחות ארוכות, והכשל שלו ערמומי יותר: סיכום מוחק בדיוק את מה שצריך — מספרים, מזהים, שמות, החלטות מדויקות. "דיברנו על אפשרויות תמחור" הוא סיכום נאמן וחסר תועלת.
מה שעובד בפועל: היברידי
המבנה שמחזיק משלב את שלושתם, וכל רכיב בו עונה על כשל של אחר:
messages = [
{"role": "system", "content": SYSTEM},
# 1. עובדות נעוצות — לא נמחקות לעולם
{"role": "system", "content": render_facts(user_facts)},
# 2. סיכום של כל מה שלפני החלון
{"role": "system", "content": rolling_summary},
# 3. N התורים האחרונים, מילה במילה
*recent_turns[-8:],
{"role": "user", "content": new_message},
]
החלק הראשון הוא זה שפותר את הבעיה האמיתית. עובדות קשיחות לא עוברות דרך הסיכום — הן נשלפות מהטבלה ומורכבות מחדש בכל בקשה, ולכן הן לא נשחקות. הסיכום מטפל בהקשר הרך, והתורים האחרונים שומרים על הדיוק במקום שבו הוא הכי נחוץ.
וכשמסכמים — תנחה את המסכם במפורש מה לשמור. סיכום חופשי יזרוק את המספרים.
סכם את חלק השיחה הזה בקצרה.
חובה לשמור מילה במילה:
שמות, מספרים, סכומים, תאריכים,
מזהי הזמנה, והחלטות שהתקבלו.
אל תסכם החלטה — צטט אותה.
עובדות: הטבלה מנצחת
כאן נמצא ההבדל הגדול ביותר בין מערכת שעובדת לאחת שמתנהגת מוזר.
עובדה על משתמש היא נתון: plan = "pro", timezone = "Asia/Jerusalem", prefers_whatsapp = true. השליפה שלה היא לפי מפתח, לא לפי דמיון. מסד וקטורי יחזיר לך "את שלוש העובדות הכי דומות לשאלה", וזו לא השאלה — אתה רוצה את המסלול של המשתמש הזה, בוודאות, תמיד.
המבנה המינימלי שעובד:
CREATE TABLE user_facts (
user_id TEXT NOT NULL,
key TEXT NOT NULL, -- "plan", "timezone", "prefers_channel"
value TEXT NOT NULL,
source TEXT, -- "user_said" | "system" | "inferred"
updated_at TIMESTAMP NOT NULL,
PRIMARY KEY (user_id, key)
);
שלוש העמודות האחרונות הן מה שהופך את זה לשימושי ולא רק לאחסון. source מאפשר לתעדף — מה שהמשתמש אמר במפורש גובר על מה שהמודל הסיק. updated_at מאפשר להכריע בסתירות. וה-PRIMARY KEY על הצמד מבטיח שעובדה חדשה דורסת את הישנה במקום להצטבר לצידה — וזו בדיוק הבעיה בסעיף על ריקבון.
כמה עובדות באמת להזריק
שאלה מעשית שעולה מיד אחרי שהטבלה קיימת: אם למשתמש יש ארבעים עובדות שמורות, כמה מהן נכנסות לכל בקשה.
התשובה הנאיבית — כולן — נשברת משתי סיבות מוכרות: זה עולה, וזה מדלל. ארבעים עובדות שרובן לא רלוונטיות לשאלה הנוכחית מקשות על המודל למצוא את השתיים שכן.
מה שעובד הוא חלוקה לשתי שכבות: גרעין קבוע — חמש-שש עובדות שרלוונטיות תמיד (שם, מסלול, שפה, אזור זמן), שנכנסות בכל בקשה; ושכבה מותנית שנשלפת לפי הנושא. אם השאלה על חיוב, נכנסות העובדות הפיננסיות; אם היא על תמיכה טכנית, נכנסות אלה.
הניתוב עצמו לא חייב מודל. תיוג פשוט של כל עובדה בקטגוריה, והתאמה לפי מילות מפתח בשאלה, מכסה את רוב המקרים ועולה אפס. זה גם צפוי יותר — וזיכרון שמתנהג צפוי שווה יותר מזיכרון חכם שלפעמים מפתיע.
מתי כן מסד וקטורי
יש מקרה אחד שבו הוא הכלי הנכון, והוא לא "זיכרון" במובן הרגיל: כשמה שנשמר הוא טקסט לא מובנה, והשליפה היא לפי משמעות.
- שיחות קודמות. "על מה דיברנו בפעם שעסקנו בהחזרות" — שאלה סמנטית על חומר לא מובנה. זה בדיוק המקרה.
- מסמכים של המשתמש. קבצים שהעלה, הערות שכתב.
- דוגמאות לפתרונות קודמים. "מה עשינו בפעם הקודמת שהלקוח ביקש X".
וגם כאן, אותו כלל שמפורט במדריך ה-reranking: אותו מודל הטמעה לאינדוקס ולשאילתה, ובעברית — מודל רב-לשוני בלבד. מודל שאומן לאנגלית יחזיר תוצאות גרועות בלי שום שגיאה שתרמז על כך. עוד על השכבה הזאת במסדי נתונים וקטוריים ובEmbeddings.
הצד הקשה הוא הכתיבה, לא הקריאה
כל ההסברים מתמקדים בשליפה, והבעיה האמיתית היא מי מחליט שמשהו שווה לזכור. שלוש גישות, ולכל אחת מחיר:
- מפורש. המשתמש אומר "תזכור ש...". מדויק לחלוטין וכמעט אף אחד לא משתמש בזה. שווה לתמוך, ולא לסמוך על זה כמנגנון יחיד.
- חוקים. אירועים מוגדרים כותבים עובדות — נרשם למסלול, שינה אזור זמן, סגר עסקה. אמין, צפוי, ומכסה רק את מה שחשבת עליו מראש. זו הגישה שכדאי להתחיל ממנה.
- חילוץ במודל. אחרי כל תור, מודל מחפש מה כדאי לזכור. מכסה הכל, וגם מייצר את כל הבעיות: הוא זוכר דברים שנאמרו בדרך אגב, מנסח אותם מחדש בטעות, וצובר זבל בקצב יציב.
אם בכל זאת מחלצים במודל, שתי הגבלות מונעות את רוב הנזק: סכימה סגורה — רשימה מוגדרת של מפתחות שמותר לכתוב, ולא טקסט חופשי; וסף — רק מה שנאמר במפורש, לא מה שהוסק.
מהשיחה הזאת, חלץ עובדות על המשתמש.
מותר לכתוב רק את המפתחות הבאים:
plan, timezone, prefers_channel,
company_size, industry
חוקים:
- רק מה שהמשתמש אמר במפורש.
- אל תסיק ואל תנחש.
- אם אין עובדה חדשה — החזר רשימה ריקה.
- לכל עובדה, צטט את המשפט שממנו לקחת.
הדרישה לציטוט היא שעושה את רוב העבודה: היא הופכת המצאה לבלתי אפשרית כמעט, והיא גם מה שמאפשר לך לבדוק את המנגנון בלי לקרוא שיחות שלמות.
זיכרון שהתיישן
הבעיה שכמעט אף אחד לא מטפל בה, והיא זו שגורמת לסוכן להישמע טיפש אחרי חצי שנה.
משתמש אמר בינואר שהוא מעדיף מייל. במרץ הוא אמר וואטסאפ. אם שתי העובדות יושבות במאגר, השליפה תחזיר את שתיהן — והמודל יבחר אחת, לפעמים את הישנה. הסוכן פונה בערוץ שהמשתמש נטש, והמשתמש מסיק שהמערכת לא מקשיבה.
שלושה מנגנונים, לפי סדר החשיבות:
- לדרוס, לא לצבור. מפתח אחד לכל עובדה, וכתיבה חדשה מחליפה. זה מה שה-
PRIMARY KEYלמעלה עושה, וזה פותר את רוב המקרים בלי שום לוגיקה נוספת. - לפתור סתירות בכתיבה. לא בקריאה. כשמגיעה עובדה שסותרת קיימת — להכריע מיד לפי מקור ותאריך, ולא להשאיר את ההכרעה למודל בזמן אמת.
- תפוגה לפי סוג. "אזור זמן" נכון שנים; "עובד כרגע על פרויקט X" נכון שבועות. עובדה בלי תאריך תפוגה מובלע היא עובדה שתשקר לך בסוף.
ועוד אחד שנשמע טריוויאלי ומתגלה כקריטי: שמור מתי נשמעה העובדה, לא רק מתי נכתבה. משתמש שמספר בשיחה על החלטה מלפני שנה יוצר רשומה חדשה עם תוכן ישן, והבחנה בין השניים מונעת מהחדש לדרוס את הנכון.
הסוג הרביעי: מה שנלמד
העדפות סגנון, דרכי עבודה, מה עבד בפעם הקודמת. זה מה שאנשים מתכוונים אליו כשהם אומרים "סוכן שלומד אותי", וזו גם הקטגוריה היחידה מהארבע שאין לה פתרון מוסכם.
הקושי אינו טכני אלא הגדרתי: אי אפשר לדעת אם משהו היה העדפה או מקרה. משתמש שביקש פעמיים תשובה קצרה — האם הוא מעדיף קצר, או שבמקרה שתי השאלות היו פשוטות? מערכת שמסיקה מהר מדי הופכת מעצבנת, ואחת שמסיקה לאט מדי לא נראית כלומדת בכלל.
מה שכן עובד, ובלי שום קסם:
- לשאול. אחרי כמה אינטראקציות: "שמתי לב שאתה מעדיף תשובות קצרות — שאמשיך ככה?" זה הופך הסקה מפוקפקת לעובדה מפורשת, ומעביר את ההחלטה למי שיודע.
- רשימה סגורה של העדפות. אורך תשובה, שפה, רמת פירוט, טון. ארבע-חמש, לא טקסט חופשי. מה שאפשר לנסח כשדה — אפשר לנהל.
- ברירת מחדל שניתנת לביטול. העדפה שנלמדה צריכה להיות ניתנת לעקיפה בבקשה אחת, ובלי להיכתב מחדש בעקבותיה.
ומה שלא עובד: לתת למודל לכתוב לעצמו "הנחיות שנלמדו" בטקסט חופשי ולצרף אותן לכל בקשה. זה נשמע אלגנטי, וזה מצטבר לפסקה ארוכה של הוראות סותרות שאף אחד לא קרא — ואחרי חודש אי אפשר להסביר למה הסוכן מתנהג כמו שהוא מתנהג.
העצה המעשית: אל תתחיל מכאן. שלושת הסוגים הראשונים מחזירים כמעט את כל התועלת, והם ניתנים לניפוי. הרביעי הוא אופטימיזציה שמגיעה אחרי שהשאר עובד — ובהרבה מוצרים היא לא מגיעה בכלל, וזה בסדר.
מצב המשימה: לא מודל, מכונת מצבים
הסוג השלישי מהרשימה בפתיחה, ובו הטעות הכי יקרה: לנסות לזכור אותו בתוך השיחה.
סוכן שמבצע תהליך רב-שלבי — לבדוק מלאי, לחייב, לשלוח אישור — צריך לדעת מה כבר קרה. אם הידיעה הזאת חיה בתמליל, אז סיכום, תקלה או ניתוק מייצרים מצב שבו הסוכן מחייב פעמיים.
הפתרון הוא לא זיכרון טוב יותר אלא להוציא את זה מהמודל לגמרי:
run = {
"id": "run_8812",
"steps": {
"check_stock": {"status": "done", "at": "..."},
"charge": {"status": "done", "ref": "ch_44"},
"notify": {"status": "pending"},
},
}
המודל מקבל את המבנה הזה כקלט ומחליט מה הצעד הבא; הוא לא מחזיק אותו. וכל פעולה שנוגעת בכסף או בעולם החיצון צריכה מפתח ייחודיות — מזהה שמונע ביצוע כפול גם אם הסוכן ינסה שוב. זו לא המלצה אלא תנאי, וההסבר המלא במדריך הסוכנים.
עברית: שלושה פרטים
- חילוץ ישויות חלש יותר. שמות פרטיים ישראליים, שמות חברות ומונחים מקומיים מזוהים פחות טוב. אם אתה מחלץ שם לקוח לתוך המאגר, שווה לבדוק את זה במפורש על עשרים דוגמאות אמיתיות ולא להניח.
- טריגר מפורש צריך ניסוח עברי. אם אתה תומך ב"תזכור ש...", תזהה גם "תרשום לך", "שים לב ש", "לעתיד". משתמשים לא אומרים את אותו דבר פעמיים.
- נרמול לפני השוואה. ערך שנשמר כ"וואטסאפ" וערך שנשמר כ"WhatsApp" הם שתי עובדות שונות במאגר. תנרמל בכתיבה — רשימת ערכים סגורה, לא טקסט חופשי.
לבנות או להשתמש בספרייה
יש היום ספריות ושירותים שמציעים "זיכרון לסוכנים" כרכיב מוכן. שווה לדעת מה הם באמת נותנים לפני שמכניסים תלות.
מה שהם עושים טוב: את שכבת הדבק — לשמור, לשלוף, לסכם, לנהל חלון. זו עבודה אמיתית וזה חוסך ימים.
מה שהם לא עושים: להחליט בשבילך מה שווה לזכור, איך לפתור סתירות, ומה מתיישן. אלה החלטות שתלויות במוצר שלך, והן — כפי שמפורט למעלה — הצד הקשה.
הכלל שאני עובד לפיו: עובדות מובנות ומצב משימה — בונים לבד, כי זו טבלה ואין בה שום מורכבות; שליפה סמנטית מהיסטוריה — שווה ספרייה, כי שם יש עבודה אמיתית. וכמו בכל תלות, שתי שאלות לפני: מה יוצא איתך אם תעזוב, והאם אפשר לקרוא את מה שנשמר בלי הכלי.
ברגע שיש זיכרון, יש מידע אישי
זה נשמע כמו נושא לעורכי דין והוא בעיקר דרישת הנדסה. מהרגע שהמערכת שומרת עובדות על אנשים, שלושה דברים חייבים להיות אפשריים בקוד:
- למחוק הכל למשתמש אחד. ואם הזיכרון מפוזר בין טבלה, מסד וקטורי וסיכומים — צריך שיהיה מזהה שמקשר בין שלושתם. מי שלא תכנן את זה מראש מגלה שאי אפשר.
- להראות מה שמור. "מה אתה יודע עליי" היא שאלה לגיטימית, וגם דרך מצוינת לדבג.
- לתחום שמירה. סיכומי שיחות מלפני שנתיים כמעט תמיד לא מועילים, ותמיד מהווים סיכון.
ונקודה שקל לפספס: סיכום הוא גם מידע אישי. מחיקת השיחות בלי מחיקת הסיכומים שנגזרו מהן משאירה את המידע במערכת.
ואותו דבר נכון להטמעות. וקטור שנגזר ממשפט שכתב משתמש הוא ייצוג של אותו משפט, ומחיקה שמנקה את הטקסט ומשאירה את הווקטור באינדקס לא הסירה את המידע — היא רק הפכה אותו לקשה יותר לקריאה. תכנן את המחיקה כך שהיא נוגעת בכל שלוש השכבות: הטבלה, הסיכומים והאינדקס.
כשהסוכן זוכר משהו מוזר
התקלה הנפוצה ביותר בייצור היא לא שהסוכן שכח אלא שהוא זוכר משהו שגוי ומתעקש עליו. וזה כמעט בלתי אפשר לניפוי אם לא תכננת לזה.
מה שצריך לתעד בכל קריאה כדי שתקלה כזאת תהיה פתירה:
- אילו עובדות נשלפו והוזרקו — לא רק מזהה המשתמש, אלא הערכים עצמם כפי שנשלחו.
- הסיכום שהיה בתוקף באותו רגע.
- מתי כל עובדה נכתבה, ומאיזה מקור.
עם שלושת אלה, תלונה של משתמש הופכת לשאילתה של שתי דקות. בלעדיהם היא הופכת לניחוש — ומפתחים נוטים אז להאשים את המודל, בזמן שבמאגר יושבת עובדה שגויה משבוע שעבר.
ושווה להוסיף מנגנון אחד שמחזיר הרבה: דרך למשתמש לתקן. "לא, אני כבר לא עובד שם" צריך להגיע לטבלה, לא רק לתמליל. סוכן שאי אפשר לתקן אותו הוא סוכן שהמשתמש יפסיק לסמוך עליו אחרי הטעות השנייה.
איך יודעים שהזיכרון עוזר
"עכשיו הוא זוכר" זו לא מדידה, והיא גם לא תחזיק כשתשנה משהו בעוד חודש.
מה שכן אפשר למדוד:
- שאלות חוזרות. כמה פעמים הסוכן שואל משהו שכבר נאמר לו. המדד הישיר ביותר, וקל לספור אותו.
- השלמת משימה בשיחה ארוכה. אחוז השיחות שהגיעו לסיום מוצלח אחרי יותר מעשרה תורים. שם זיכרון גרוע נשבר.
- סתירות. כמה פעמים הסוכן אמר משהו שסותר עובדה שמורה. זה מה שתופס זיכרון שהתיישן.
- עלות לשיחה. כי כל שיפור בזיכרון שולח יותר טוקנים, וצריך לדעת כמה.
ובנייה של ערכת בדיקה עם שיחות רב-תוריות אמיתיות היא מה שמאפשר להשוות גרסאות — אותה גישה שמפורטת בEvals.
מתי לא צריך זיכרון בכלל
- כשהמשימה מסתיימת בשיחה. טופס, שאלה, חישוב. סוכן חסר מצב פשוט יותר, זול יותר, וצפוי יותר — וזה שילוב שקשה לנצח.
- כשהעובדות כבר קיימות במערכת שלך. אם ה-CRM יודע מי הלקוח, אל תשמור את זה שוב. תשלוף משם בזמן הבקשה, וכך גם לא תצטרך לסנכרן.
- לפני שיש משתמשים. זיכרון הוא אופטימיזציה של חוויה. בלי שיחות אמיתיות אין דרך לדעת מה בכלל שווה לזכור.
- כשהסיכון עולה על התועלת. בתחומים רגישים, מערכת שלא שומרת כלום היא לפעמים הבחירה הנכונה — ואפשר לומר את זה ללקוח כיתרון.
טעויות שחוזרות
- מסד וקטורי לעובדות מובנות. שליפה מקורבת במקום מדויקת, ואי-ודאות בלי סיבה.
- סיכום שמוחק מספרים. ואז הסוכן מדבר על "האפשרויות שדנו בהן" בלי לדעת אילו.
- לצבור עובדות במקום לדרוס. המקור העיקרי לתשובות שסותרות את עצמן.
- חילוץ חופשי במודל. מאגר שמתמלא בזבל שאף אחד לא ניקה.
- מצב משימה בתוך השיחה. הדרך המהירה ביותר לחייב פעמיים.
- בלי מנגנון מחיקה. ואז הבקשה הראשונה למחוק משתמש הופכת לפרויקט.
- מודל הטמעה שאומן לאנגלית. בעברית, תוצאות גרועות בלי שום שגיאה.
- לבנות זיכרון לפני שיש שיחות. אופטימיזציה לבעיה שעוד לא ראית.
הצעד הבא
זיכרון הוא חלק מבניית סוכנים ומניהול הקשר. העמק בשניהם.