Queues + AI Agents — תכנון תורים למשימות AI ארוכות

מאת צוות מדיה דיל · 09.08.2026 · Technology · 9 דק׳

משימת AI שלוקחת דקות ולא מילישניות דורשת תכנון אחר לגמרי מ-API רגיל. מדריך לתכנון תורים עבור Agents: Concurrency, Backoff, Priority ו-Dead Letter Queues.

קריאה ל-LLM שצריכה לעבד מסמך של עשרות עמודים, להריץ כמה כלים ברצף ולחכות לתשובות שלהם, יכולה לקחת בקלות דקה-שתיים. אם מנסים להריץ את זה בתוך request-response רגיל של שרת web, נתקלים מהר בקירות: timeout של ה-load balancer, חיבור HTTP שנשאר פתוח יותר מדי זמן, ולקוח שמקבל שגיאת 504 בזמן שהעיבוד עדיין רץ ברקע. תורים (Queues) הם התשובה הקלאסית לבעיה הזו, אבל תכנון תור למשימות AI שונה במובנים חשובים מתכנון תור למשימות רגילות - כי עלויות ה-compute גבוהות בהרבה, זמני העיבוד לא צפויים, וכשל בטיפול יכול לעלות כסף ממש (קריאה חוזרת ל-API יקר), לא רק זמן.

למה בכלל צריך תור בין הבקשה לסוכן

ההפרדה בין קליטת הבקשה לביצוע בפועל פותרת שלוש בעיות בבת אחת. ראשית, שחרור החיבור המיידי: השרת שמקבל את הבקשה מאשר קליטה תוך מילישניות ומחזיר task_id, בלי לחכות שהמודל יסיים. שנית, ספיגת עומסים (Buffering): אם מגיעות 500 בקשות בבת אחת, התור סופג אותן והעיבוד מתבצע בקצב שהמערכת יכולה לעמוד בו, במקום לקרוס תחת עומס. שלישית, הפרדת אחריות: הצד שמייצר את הבקשה (למשל טופס באתר) לא צריך לדעת כלום על מנגנון ה-retry או ה-timeout של הסוכן - זו אחריות של הצרכן בצד השני של התור.

מודל התור: Push, Pull או שילוב

עוד החלטה שקובעת את כל שאר הארכיטקטורה היא איך worker מקבל עבודה מהתור. במודל Pull, כל worker פונה יזום לתור ושואל "יש לי עבודה?" - פשוט לתפעול ומאפשר לכל worker לקצב את עצמו לפי היכולת האמיתית שלו באותו רגע. במודל Push, התור עצמו דוחף עבודה ל-worker ברגע שהוא זמין, מה שיכול לצמצם latency בין הופעת המשימה לתחילת הטיפול בה, אבל דורש מנגנון תיאום זהיר יותר כדי לא להציף worker שכבר עמוס. רוב תשתיות ה-messaging המודרניות תומכות בגרסה היברידית: ה-worker "מושך" עבודה, אבל התור מיידע אותו מיידית כשמשהו חדש נכנס, כך שאין המתנה מיותרת בין polling תכוף מדי לעיכוב מיותר.

Concurrency: כמה משימות מריצים במקביל

זו ההחלטה שהכי משפיעה על עלות וביצועים. מודל שרץ מול API חיצוני (כמו Claude או GPT) מוגבל ב-Rate Limits - בקשות לדקה, טוקנים לדקה. הגדרת concurrency גבוהה מדי גורמת לשגיאות 429 שחוזרות על עצמן; concurrency נמוכה מדי משאירה משימות ממתינות בתור זמן רב. הגישה הנכונה היא concurrency דינמית: מתחילים בערך שמרני, וקוראים ל-headers של תגובת ה-API (כמו x-ratelimit-remaining) כדי להתאים את הקצב בזמן אמת, לא לפי ניחוש קבוע מראש.

בנוסף, כדאי להפריד תורים לפי סוג עומס. משימה שדורשת מודל "כבד" (הסקה עמוקה, קונטקסט ארוך) ומשימה שדורשת מודל קל וזול לא צריכות לחלוק אותו pool של workers - אחרת משימות קלות נתקעות מאחורי משימות כבדות. הרעיון הזה מתחבר ישירות ל-AI Workload Scheduling, שמרחיב על תעדוף לפי עלות ו-GPU.

Priority Queues: לא כל המשימות שוות

ברוב המערכות בפועל יש הבדל ברור בין משימה שמשתמש ממתין לתשובה ממנה עכשיו (Interactive) לבין משימה שרצה ברקע בלי לחץ זמן (Batch). ערבוב שתי הקטגוריות באותו תור גורם למצב שבו בקשה דחופה של לקוח ממתינה מאחורי אצווה של 10,000 משימות ניתוח שנוצרו בלילה. הפתרון: תורים נפרדים לפי עדיפות, עם הקצאת workers יחסית - למשל 80% מהקיבולת לתור ה-Interactive ו-20% ל-Batch, כדי שגם עבודת הרקע לא תרעב לגמרי.

queues:
  interactive:
    priority: high
    max_concurrency: 40
    timeout_seconds: 30
  batch:
    priority: low
    max_concurrency: 10
    timeout_seconds: 600
  retry_dlq:
    priority: medium
    max_attempts: 3
    backoff: exponential

Retry ו-Backoff: איך נופלים בלי לשבור הכל

כישלונות עם LLM הם כמעט תמיד עניין של "מתי", לא "אם" - rate limit זמני, timeout ברשת, או תגובה לא תקינה מהמודל. Exponential Backoff עם jitter (עיכוב אקראי קטן שנוסף לזמן ההמתנה) הוא הסטנדרט: ניסיון ראשון נכשל, ממתינים שנייה, נכשל שוב, ממתינים 2-4 שניות, וכן הלאה, כדי לא להפציץ שירות שכבר עמוס. חשוב להבחין בין שגיאות שכדאי לנסות שוב (429, 503, timeout) לבין שגיאות שלא (400 עם פרומפט לא תקין - retry לא יתקן את זה, רק יבזבז כסף שוב ושוב).

מספר הניסיונות צריך להיות סופי ומוגדר. אחרי שהוא נגמר, המשימה עוברת ל-Dead Letter Queue (DLQ) - תור נפרד שמכיל משימות שנכשלו סופית, לבדיקה ידנית או אוטומטית מאוחר יותר. בלי DLQ, משימות כושלות או נעלמות בשקט (אובדן מידע) או חוזרות לתור הראשי לנצח וחוסמות אותו. הנושא הרחב יותר של אסטרטגיות retry לסוכנים מפורט ב-Agent Retry Architecture.

Idempotency ו-Exactly-Once (שלא באמת קיים)

חשוב להבין: כמעט אף מערכת תורים לא מבטיחה באמת "Exactly-Once Delivery". ההבטחה המעשית היא At-Least-Once, וזו אחריות הצרכן להפוך את הטיפול לאידמפוטנטי - שהרצה כפולה של אותה משימה לא תגרום לתופעת לוואי כפולה (למשל, לא לחייב כרטיס אשראי פעמיים או לשלוח שני מיילים זהים). המנגנון הנפוץ הוא שמירת task_id שכבר טופל בזיכרון persistent עם TTL, ובדיקה לפני כל ביצוע.

Visibility Timeout ו-Worker Crashes

כשעובד (Worker) מושך משימה מהתור, היא לא נמחקת מיד - היא הופכת ל"בלתי נראית" לזמן מוגדר (Visibility Timeout), ונמחקת רק לאחר אישור סיום. אם ה-worker קורס באמצע עיבוד ארוך, המשימה חוזרת אוטומטית לתור אחרי שפג ה-timeout, ומעובדת ע"י worker אחר. הבעיה: אם ה-timeout קצר מדי ביחס לזמן העיבוד האמיתי של סוכן AI (שיכול לקחת דקות), המשימה "תחזור" בזמן שהיא עדיין בטיפול, ותרוץ פעמיים במקביל. הפתרון הוא Heartbeat - ה-worker מאריך את ה-visibility timeout כל עוד הוא עדיין עובד, כמו "לחיצת דופק" תקופתית.

דוגמה מהשטח: עיבוד מסמכים לניתוח משפטי

מערכת שמקבלת חוזים להעלאה, מריצה עליהם ניתוח AI (חילוץ סעיפים, זיהוי סיכונים, השוואה לתבנית) ומחזירה דוח. העלאת קובץ יוצרת משימה בתור interactive עם timeout קצר יחסית לשלב הראשוני (חילוץ טקסט), ומייצרת בתורה משימת batch לניתוח העמוק שיכול לקחת דקות. המשתמש מקבל אישור מיידי שהקובץ התקבל, ועדכון (בזמן אמת דרך WebSocket או polling קל) כשהניתוח מוכן. אם הניתוח נכשל שלוש פעמים - למשל בגלל קובץ סרוק שדורש OCR - המשימה עוברת ל-DLQ ומופעל תהליך נפרד עם כלים אחרים.

Batching: קיבוץ משימות דומות להרצה יעילה יותר

כשיש נפח גבוה של משימות דומות - למשל סיווג של אלפי פניות תמיכה - הרצת כל משימה כקריאה נפרדת למודל מבזבזת overhead משמעותי (זמן רשת, טעינת הקשר) על כל בקשה בנפרד. Micro-Batching אוסף כמה משימות מהתור לחלון זמן קצר (למשל 200 מילישניות או עד גודל אצווה מוגדר, לפי מה שקורה קודם) ומריץ אותן יחד בקריאה אחת למודל שתומך בכך, מה שמעלה תפוקה כוללת במחיר עיכוב שולי לכל משימה בודדת. זהו טרייד-אוף קלאסי בין latency לתפוקה (Throughput) - מתאים מצוין למשימות batch שאין להן דרישת זמן-אמת קשיחה, ופחות מתאים למשימות interactive שבהן כל מילישנייה נספרת.

ניטור עומק תור ו-Autoscaling של Workers

מספר ה-workers שמעבדים את התור לא צריך להיות קבוע. כשעומק התור גדל מעבר לסף מוגדר, כדאי להוסיף workers נוספים אוטומטית; כשהוא מתרוקן, לצמצם בחזרה כדי לחסוך עלות. המדד המרכזי כאן הוא לא רק עומק התור עצמו אלא קצב הגדילה שלו - תור שגדל לאט יכול להיפתר עם תוספת קלה, בעוד תור שגדל במהירות מצביע על בעיה עמוקה יותר (worker תקוע, rate limit חיצוני שהושג, או עלייה אמיתית בביקוש שדורשת תגובה מיידית). הקשר בין ניטור זה לתזמון קיבולת עומסים מפורט ב-AI Workload Scheduling.

תורים מרובי-אזורים (Multi-Region): כשהעולם שלכם גלובלי

מוצר עם משתמשים בכמה יבשות מתמודד עם שאלה נוספת: האם יש תור אחד גלובלי, או תור נפרד לכל אזור? תור גלובלי יחיד מפשט את הניהול, אבל מכריח כל בקשה לעבור latency רשת בין-יבשתי לפני שהיא בכלל נכנסת לעיבוד - בזבוז של עשרות עד מאות מילישניות שהיה אפשר לחסוך. תורים אזוריים נפרדים מקצרים latency משמעותית, אבל דורשים החלטה ברורה על מה קורה כשמשאב (למשל מפתח API עם rate limit גלובלי משותף) חייב להיות מתואם בין האזורים. הגישה הנפוצה בפועל: תור אזורי לכל בקשה, עם שכבת תיאום קלה ומרוכזת רק לגבי המשאבים שבאמת דורשים סנכרון גלובלי, כמו תקציב כולל או rate limit של ספק חיצוני. חשוב גם לוודא שנתוני מצב שהמשימה תלויה בהם (למשל היסטוריית משתמש) זמינים באזור שבו היא מתבצעת, אחרת חוסכים ב-latency של התור אבל מפסידים אותו בחזרה בקריאת הנתונים.

שקיפות למשתמש: מה מציגים כשמשימה עדיין בתור

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

טעויות נפוצות

  • תור אחד לכל סוגי המשימות - מערבב עדיפויות ומודלים שונים ויוצר head-of-line blocking.
  • Timeout קבוע שלא מתחשב בסוג המשימה - משימה שדורשת מודל "חושב" ארוך יותר צריכה timeout שונה ממשימת סיווג פשוטה.
  • שכחת Idempotency - הנחה סמויה ש"זה יעבוד פעם אחת" עד שקורה כשל רשת שגורם לחיוב כפול.
  • אין ניטור על עומק תור - בלי alert על גדילת התור מעבר לסף, מגלים בעיה רק כשלקוחות מתלוננים. מתחבר ל-AI Infrastructure Monitoring.

שאלות נפוצות

מתי כדאי להשתמש בתור במקום בהפעלה ישירה של הסוכן?

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

איזה תור טכנולוגי מתאים ל-AI Agents?

תלוי בקנה המידה: פתרונות מנוהלים כמו SQS מתאימים לרוב המקרים בזכות פשטות ואמינות; Redis-based queues מתאימים כשצריך latency נמוך מאוד; Kafka מתאים כשצריך גם היסטוריית אירועים ולא רק תור עבודה - הרחבה ב-Kafka + AI.

איך מונעים שמשימה יקרה תרוץ שוב ושוב בגלל באג ב-retry?

הגבלת max_attempts קשיחה, הבחנה בין שגיאות בנות-retry לשגיאות שאינן, ו-DLQ שמפסיק את המחזור ומאפשר בדיקה ידנית לפני שממשיכים.

האם צריך תור נפרד לכל סוכן במערכת מרובת-סוכנים?

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

סיכום

תכנון תור עבור AI Agents דורש חשיבה שמעבר ל"תור FIFO פשוט" - concurrency דינמית, priority, idempotency ו-DLQ הם לא nice-to-have אלא תנאי לפרודקשן יציב. אם אתם בונים מערכת AI שצריכה לעמוד בעומס אמיתי, בתשתית פרודקשן של מדיה דיל יש לנו ניסיון מעשי בדיוק בזה - אפשר לדבר איתנו בוואטסאפ.

תגיות: Message Queue · AI Agents · Concurrency · Retry · Dead Letter Queue · Idempotency · Backoff

← חזרה לבלוג · צור קשר