איך בונים Harness למשימות AI שנמשכות שעות או ימים
מאת צוות מדיה דיל · 12.08.2026 · Agent Infrastructure · 5 דק׳
משימת סוכן שנמשכת שעות לא יכולה לרוץ כמו קריאת API רגילה. כך בונים שכבת ביצוע שמחזיקה מעמד לאורך זמן בלי לאבד התקדמות.
סוכן שמקבל משימה כמו "עברי על כל 4,000 החשבוניות של הרבעון, זהי אנומליות והפיקי דוח" לא יכול לרוץ בתוך request-response רגיל של שרת HTTP. המשימה עשויה להימשך שעתיים, לכלול מאות קריאות כלי, ולהיתקל בדרך בבעיות שאין להן קשר ללוגיקה העסקית: השרת שמריץ אותה עובר deploy אמצע הדרך, חיבור הרשת לספק החיצוני מתנתק לרגע, או תהליך הרקע פשוט נהרג על ידי מנגנון ניהול משאבים שלא יודע שהוא באמצע עבודה קריטית. משימה קצרה שנכשלת — פשוט מריצים אותה שוב. משימה שרצה שעתיים ונכשלת בדקה ה-119 היא סיפור אחר לגמרי: אם אין דרך לחזור בדיוק לאיפה שהיא עצרה, כל ההשקעה החישובית והכספית שהושקעה בה הולכת לאיבוד, וצריך להתחיל מאפס. זה ההבדל בין Harness רגיל ל-Harness שנבנה במיוחד למשימות ארוכות טווח.
למה request-response לא מתאים למשימות ארוכות
הארכיטקטורה הסטנדרטית של שרת אינטרנט מניחה שכל בקשה מטופלת תוך שניות, ושאם התהליך שמטפל בה מת — פשוט שולחים בקשה חדשה. ההנחה הזו נשברת לגמרי כשמדובר במשימת סוכן ארוכה: אי אפשר להחזיק חיבור HTTP פתוח לשעתיים (טיימאאוטים בכל שכבה בדרך — load balancer, proxy, gateway — יחתכו אותו הרבה קודם), ואי אפשר פשוט "לנסות שוב" כי הניסיון החוזר מתחיל מהתחלה ומבזבז את כל מה שכבר הושג. הפתרון הארכיטקטוני הוא להפריד בין יצירת המשימה (בקשת API קצרה שמחזירה מיד task_id) לבין ביצוע המשימה, שרץ כתהליך רקע עצמאי לחלוטין, בלי תלות בחיבור שיצר אותו.
תהליך הרקע עצמו צריך לרוץ בתשתית שמיועדת לעבודה ארוכה — Worker Queue עם workers ייעודיים, או Orchestration Engine ייעודי (Temporal, AWS Step Functions ודומיהם) — ולא כ-handler בתוך אותו process שמשרת גם בקשות API רגילות. ערבוב בין השניים גורם לכך שמשימה ארוכה "תיתקע" ותחסום את השרת מלטפל בבקשות אחרות, או גרוע מכך — שהמנגנון שמנטר בריאות השרת יזהה תהליך "תקוע" ויהרוג אותו, בדיוק כשהוא באמצע עבודה לגיטימית.
Checkpointing: הלב של ה-Harness ארוך-הטווח
העיקרון המרכזי הוא לשמור מצב (state) אחרי כל צעד משמעותי, לא רק בסוף המשימה. "צעד משמעותי" מוגדר כל פעולה שיש לה תוצאה שאי אפשר לשחזר בזול — קריאת API יקרה, כתיבה לבסיס נתונים, עיבוד batch שלוקח דקות. אחרי כל צעד כזה, ה-Harness כותב checkpoint שמכיל את כל מה שצריך כדי להמשיך: אילו פריטים כבר טופלו, מה התוצאה שהתקבלה לכל אחד, ומה הצעד הבא בתור. אם התהליך נקטע, ריצה חדשה טוענת את ה-checkpoint האחרון וממשיכה בדיוק משם — במקום לעבד מחדש 3,999 חשבוניות שכבר טופלו רק כדי לגלות את האנומליה באלף הרביעית שוב.
async function processInvoice(taskId, invoiceId) {
const cp = await loadCheckpoint(taskId);
if (cp.processed.has(invoiceId)) return cp.results[invoiceId]; // כבר טופל
const result = await runAnomalyCheck(invoiceId);
cp.processed.add(invoiceId);
cp.results[invoiceId] = result;
await saveCheckpoint(taskId, cp); // נכתב אחרי כל פריט
return result;
}
הדוגמה הזו ממחישה עיקרון חשוב: הבדיקה "האם כבר טופל" חייבת להתבצע לפני ביצוע הפעולה, לא רק לפני שמירת ה-checkpoint. אחרת, קיים חלון זמן שבו הפעולה בוצעה בפועל אבל ה-checkpoint עדיין לא נשמר — ואם התהליך נקטע בדיוק שם, הריצה הבאה תבצע את אותה פעולה פעם שנייה. הנושא הזה — הבטחת ביצוע יחיד גם כשיש כשלים באמצע — מפורט לעומק במאמר נפרד על Idempotency בעולם הסוכנים.
מבנה נתונים ל-checkpoint: מה שומרים ומה לא
הפיתוי הראשוני הוא לשמור checkpoint שהוא פשוט "כל ה-context עד כה" — כל הודעות השיחה עם המודל, כל תוצאת כלי, הכול. זה עובד למשימות קצרות אבל נשבר במשימות ארוכות: context שגדל לאלפי הודעות הופך יקר לטעינה, יקר לשלוח שוב למודל בכל סבב, ובסופו של דבר חורג ממגבלת חלון ההקשר. הפתרון הנכון הוא checkpoint מובנה: לא תמלול מלא של השיחה, אלא מבנה נתונים ממוקד — אילו פריטים טופלו, מה המצב הנוכחי של כל אחד, ותקציר קצר (לא transcript מלא) של מה שנלמד עד כה, אם רלוונטי להמשך. הגישה הזו קרובה מאוד לעקרונות שמפורטים במאמר על Stateful Agents, אבל עם דגש נוסף על יעילות ל-Context Window כשמשימה כוללת אלפי צעדים.
מה קורה כשמשהו נכשל לגמרי
גם עם checkpointing מוקפד, יהיו פריטים שנכשלים באופן קבוע — נתון פגום, שגיאת הרשאות, תלות חיצונית שלא זמינה. Harness ארוך-טווח טוב לא מאפשר לכשל בפריט בודד לעצור את כל המשימה: הוא מדלג על הפריט, מסמן אותו לטיפול נפרד, וממשיך לפריט הבא. הפריטים שנכשלו חוזר ומצטברים ב-Dead Letter Queue ייעודי, לבדיקה ידנית או ריצה חוזרת ממוקדת אחרי שהבעיה תוקנה — בלי לחייב הרצה מחדש של כל 4,000 החשבוניות בגלל שלוש שנכשלו.
- Checkpoint אחרי כל צעד יקר לשחזור, לא רק בסוף המשימה
- בדיקת "כבר טופל" לפני ביצוע, לא רק לפני שמירה
- מבנה checkpoint ממוקד, לא transcript מלא של כל ההיסטוריה
- כשל בפריט בודד לא עוצר את כל המשימה — מדלגים וממשיכים
- Dead Letter Queue לפריטים שנכשלו שוב ושוב, לטיפול נפרד
עלות ותדירות Checkpointing — הטרייד-אוף המעשי
checkpoint אחרי כל פעולה בודדת נותן את ההגנה הכי טובה מפני איבוד עבודה, אבל יש לו מחיר: כתיבה תכופה לבסיס נתונים או לאחסון מתמיד מוסיפה latency לכל צעד, ובקנה מידה גדול (אלפי משימות מקבילות) יכולה להפוך לצוואר בקבוק בפני עצמה. הפתרון המעשי הוא checkpointing מדורג: פעולות זולות וקצרות נצברות בזיכרון ונכתבות ב-batch כל כמה עשרות שניות, בעוד פעולות יקרות במיוחד (תשלום, מחיקה, כל דבר בלתי הפיך) מקבלות checkpoint מיידי וסינכרוני משלהן. הטרייד-אוף הזה — כמה עבודה מוכנים להפסיד במקרה של קריסה, מול כמה latency מוכנים לשלם על כל צעד — הוא החלטה עסקית לא פחות מטכנית, ושווה לקבל אותה במודע ולא כברירת מחדל שאף אחד לא בדק.
נקודה אחרונה שכדאי לקחת בחשבון בשלב התכנון: משימות ארוכות זקוקות גם לנראות בזמן אמת, לא רק לשחזור אחרי כשל. משתמש שמפעיל משימה של שעתיים רוצה לדעת "כמה מתוך 4,000 החשבוניות כבר עובדו", לא רק לקבל תשובה סופית או שגיאה בסוף. מבנה ה-checkpoint שתואר למעלה מאפשר את זה כמעט בחינם: אם ה-checkpoint כבר שומר "אילו פריטים טופלו", חשיפת מספר זה ל-UI כאחוז התקדמות היא שאילתה פשוטה על אותו מבנה נתונים, בלי צורך במנגנון נפרד לדיווח סטטוס.
תגיות: Long-Running Agents · Checkpointing · Task Queue · Dead Letter Queue · Idempotency