ביצוע עמיד (Durable Execution)

כלים וסוכנים

הגדרה

ביצוע עמיד (Durable Execution) הוא דפוס הנדסי שבו כל צעד במשימה ארוכה נרשם ביומן קבוע, כך שאם התהליך קורס באמצע הוא ממשיך מנקודת-העצירה במקום להתחיל מאפס.

למה משימה ארוכה נתקעת באמצע

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

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

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

יומן-האירועים וההרצה-החוזרת

הרעיון המרכזי פשוט, ומגיע מעולם שקדם ל-AI: במקום להחזיק את מצב-המשימה בזיכרון בלבד, המערכת רושמת כל אירוע משמעותי ביומן קבוע ששורד קריסה. חברת Temporal, שהפלטפורמה שלה בנויה סביב הדפוס הזה, מגדירה את היומן כ"רישום מלא ועמיד של כל מה שקרה במחזור-החיים של הרצת-תהליך". התיעוד של Azure Durable Functions מבית מיקרוסופט מתאר את אותו מנגנון בשם שהשתרש באנגלית, Event Sourcing: מצב-המשימה אינו נשמר כתמונת-מצב, אלא נגזר מרצף האירועים שנרשמו.

מכאן נובע הצעד השני, וזה החלק שמפתיע את מי ששומע עליו לראשונה. כשהתהליך עולה מחדש אחרי קריסה, המערכת אינה "קופצת" לנקודת-העצירה. היא מריצה את הקוד שוב מההתחלה — אבל בכל פעם שהקוד מבקש לבצע צעד שכבר רשום ביומן, הוא מקבל את התוצאה השמורה במקום לבצע אותו שוב. Temporal מנסחת זאת כך: "העובד משתמש ביומן-האירועים כדי להריץ מחדש את הקוד ולשחזר את מצב ההרצה לזה שהיה בדיוק לפני הקריסה". רק כשהקוד מגיע לצעד שאינו ביומן — הצעד שבו נעצר — הוא מתחיל לפעול באמת. השם המקצועי לתהליך הזה הוא "הרצה-חוזרת" (Replay), והתוצאה מבחינת המשתמש היא שהמשימה, בלשון אותו תיעוד, "ממשיכה בדיוק מהמקום שבו הפסיקה".

המחיר: הקוד חייב להתנהג אותו דבר בכל הרצה

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

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

הפתרון אינו לוותר על הדברים האלה אלא להזיז אותם: כל פעולה כזו נעשית ב"פעילות" (Activity) נפרדת, שהתוצאה שלה נרשמת ביומן — "ערכי-ההחזרה של פעילויות בטוחים תמיד להרצה-חוזרת, כי הם נשמרים בהיסטוריית-התזמור". מסגרות-העבודה מנסות לתפוס חלק מההפרות בעצמן, אבל רק חלק מוגדר מאוד: התיעוד של מיקרוסופט מתאר מנגנון-זיהוי אחד ויחיד — המסגרת "מנסה לזהות שימוש מקרי בתהליכונים שאינם תהליכון-המתזמר", וכשהיא מוצאת הפרה כזו היא זורקת חריגה בשם NonDeterministicOrchestrationException. הזיהוי הזה אינו נוגע לשאר האיסורים שנמנו כאן — שעון, אקראיות, משתני-סביבה או קריאות-רשת — ומיקרוסופט מצרפת לו אזהרה מפורשת: "התנהגות-הזיהוי הזו לא תתפוס את כל ההפרות, ואין להסתמך עליה".

חלוקת-התפקידים: מי מחליט ומי מבצע

מהאילוץ הזה נולדה חלוקה שחוזרת כמעט בכל מימוש של הדפוס. יש רכיב אחד — "מתזמר" (Orchestrator) — שכל תפקידו להחליט מה הצעד הבא, והוא כפוף לכללי-הדטרמיניזם. ויש רכיבים אחרים — "פעילויות" — שעושים את העבודה מול העולם: פונים לשירות חיצוני, כותבים לבסיס-נתונים, שולחים הודעה. עליהם אין את אותן מגבלות, ובלשון התיעוד: "ההגבלות האלה חלות רק על מתזמרים; לפעילויות אין הגבלות כאלה". כדאי לשים לב שהמילה "מתזמר" משמשת כאן במובן צר מהמקובל: זהו רכיב-קוד יחיד שמנהל את סדר-הצעדים בתוך תהליך אחד — לא תזמור-סוכנים, שבו הכוונה לתיאום בין כמה סוכנים נפרדים.

ההפרדה הזו היא גם מה שמאפשר טיפול אוטומטי בכשלים. אם פעילות נכשלת, המערכת מנסה אותה שוב לפי מדיניות שהמפתח קבע — Temporal מציינת שלמפתח יש "שליטה מלאה על התדירות ועל מספר הפעמים שהניסיונות החוזרים האלה יתרחשו, לכל פעילות בנפרד". AWS Step Functions, השירות המקביל של אמזון, בונה את אותה יכולת לתוך שפת-התהליך עצמה, עם פקודות Retry ו-Catch: "אפשר לנסות שוב משימות שנכשלו, או לתפוס משימות שנכשלו ולהריץ אוטומטית צעדים חלופיים".

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

מה זה נותן לסוכן-AI

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

הראשונה היא אורך — וכמה בדיוק, זה כבר תלוי בשירות הקונקרטי ולא בדפוס עצמו. בשירות של אמזון, למשל, AWS Step Functions מגדיר שני סוגי-תהליך: "סטנדרטי", המיועד ל"תהליכים ארוכי-טווח וניתנים-לביקורת", שיכול לרוץ "עד שנה"; ו"אקספרס", לעומסי-אירועים גבוהים, שרץ "עד חמש דקות". ההבדל אינו רק באורך אלא גם בהבטחה: בתהליך סטנדרטי, בלשון התיעוד, "כל צעד בתהליך יתבצע בדיוק פעם אחת", ובאקספרס "צעד אחד או יותר עלול לרוץ יותר מפעם אחת".

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

השנייה היא המתנה. סוכן שנדרש לאישור אנושי לפני פעולה רגישה חייב לעצור, לפעמים לשעות. Step Functions מתאר את זה כדפוס-שימוש מפורש בשם "אדם-בלולאה", שבו התהליך ממתין לאסימון-חזרה מהאדם וממשיך רק אחריו. בביצוע עמיד המתנה כזו אינה מחזיקה תוכנית פתוחה בזיכרון; היא רק שורה ביומן.

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

מה ביצוע עמיד לא פותר

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

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

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

שלוש תגובות לתקלה שקוטעת משימה באמצע
ריצה רגילה, בלי הגנהניסיון-חוזר על המשימה כולהביצוע עמיד (Durable Execution)
מה קורה אחרי קריסה?ההתקדמות אובדת; מישהו צריך להתחיל את המשימה מחדשהמערכת מריצה את המשימה כולה שוב מההתחלההתהליך משוחזר מיומן-האירועים וממשיך מנקודת-העצירה
האם צעדים שכבר הצליחו מבוצעים שוב?כן — הכול מתחיל מאפסכן — כולל מיילים או חיובים שכבר יצאולא — תוצאה שנרשמה ביומן נקראת מהיומן במקום להתבצע שוב
מתאים למשימה שנמשכת ימים?לא — כל תקלה מאפסת את ההתקדמותפחות — כל ניסיון מתחיל מאפסכן — AWS Step Functions מגדיר תהליך "סטנדרטי" שרץ עד שנה, ובו כל צעד של התהליך מתבצע "בדיוק פעם אחת"; זו הבטחה על צעדי-התהליך, לא על פעולה חיצונית שכבר יצאה אל העולם
מה נדרש מהקוד?שום דבר מיוחדשום דבר מיוחדקוד-התזמור חייב להיות דטרמיניסטי: בלי שעון, בלי אקראיות ובלי קריאות-רשת ישירות

שאלות נפוצות ❓

מה זה בעצם "יומן-אירועים" בהקשר הזה?

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

למה אסור לקוד לשאול מה השעה?

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

זה אומר שהסוכן לא יטעה?

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

למה זה חשוב דווקא לסוכני-AI?

כי משימת-סוכן היא רצף ארוך של צעדים תלויי-עולם-חיצוני, ולעיתים כוללת המתנה לאישור אנושי. AWS Step Functions מתאר את ההמתנה הזו כדפוס מפורש בשם "אדם-בלולאה", ובביצוע עמיד היא לא עולה דבר בזמן שהיא נמשכת.