Idempotency בעולם הסוכנים — איך מונעים פעולה כפולה ותשלום כפול
מאת צוות מדיה דיל · 12.08.2026 · Agent Infrastructure · 5 דק׳
כשסוכן AI מנסה שוב אחרי timeout, איך יודעים אם הפעולה הקודמת בכלל התבצעה? בלי idempotency אמיתית, retry הוא לא רשת ביטחון — הוא סיכון בפני עצמו.
סוכן שמטפל בבקשות זיכוי כספי שלח בקשת חיוב-חוזר ל-Payment Gateway, קיבל timeout אחרי 30 שניות, והחליט — בהתאם ללוגיקת ה-retry שהוגדרה לו — לנסות שוב. הבעיה: הבקשה הראשונה כן הצליחה בפועל, רק שהתשובה לא הספיקה לחזור לפני שה-timeout נקטע. התוצאה: הלקוח קיבל זיכוי כפול. זה לא באג נדיר — זה התרחיש הכי צפוי בכל מערכת שמשלבת רשת לא אמינה עם לוגיקת retry, ובעולם הסוכנים זה חמור יותר מבעולם ה-API הרגיל, כי סוכן AI מקבל החלטות retry לא רק ברמת התשתית אלא גם ברמת ההיגיון: הוא עלול "להחליט" בעצמו לנסות שוב פעולה שנראתה לו כלא-מוצלחת, גם כשאף מנגנון retry טכני לא הפעיל אותו. הפתרון היחיד שעובד באמת הוא לא "לנסות למנוע כשלי רשת" — זה בלתי אפשרי — אלא לוודא שכל פעולה קריטית אידמפוטנטית: ביצוע חוזר שלה, מכל סיבה שהיא, לא גורם לתוצאה כפולה.
מה זה אומר בפועל: אידמפוטנטיות ברמת הפעולה
פעולה אידמפוטנטית היא כזו שביצוע שלה N פעמים נותן בדיוק את אותה תוצאה כמו ביצוע שלה פעם אחת. GET הוא אידמפוטנטי מטבעו — קריאה חוזרת לא משנה כלום. POST שיוצר רשומה חדשה, לעומת זאת, לא אידמפוטנטי מטבעו: קריאה כפולה יוצרת שתי רשומות. כדי להפוך פעולה כזו לאידמפוטנטית, מצרפים לה מזהה ייחודי — Idempotency Key — שנוצר לפני הניסיון הראשון ונשאר קבוע גם בכל ניסיון חוזר. השרת בצד המקבל בודק אם כבר טיפל במפתח הזה: אם כן, הוא מחזיר את התוצאה השמורה מהפעם הראשונה בלי לבצע את הפעולה שוב; אם לא, הוא מבצע ושומר את התוצאה תחת אותו מפתח.
const idempotencyKey = `refund:${orderId}:${attemptGroupId}`;
async function issueRefund(key, amount) {
const existing = await db.get(`idem:${key}`);
if (existing) return existing.result; // כבר בוצע, מחזירים את אותה תוצאה
const result = await paymentGateway.refund(amount, { idempotencyKey: key });
await db.set(`idem:${key}`, { result, ts: Date.now() });
return result;
}
הנקודה הקריטית: ה-Idempotency Key חייב להיווצר פעם אחת לכל כוונה לוגית, לא לכל ניסיון טכני. אם כל ניסיון retry יוצר מפתח חדש, כל ההגנה מתפוגגת — בדיוק כמו שלא הייתה קיימת בכלל.
למה סוכני AI מחמירים את הבעיה
ב-API רגיל, ה-retry logic נשלטת בקוד דטרמיניסטי שכתב מפתח — קל לוודא שהמפתח נוצר פעם אחת ונשמר לאורך כל ניסיונות ה-retry. בסוכן AI, יש שכבה נוספת של אי-ודאות: המודל עצמו עלול "להחליט" שהוא צריך לנסות שוב, בלי קשר ל-retry logic הטכני. לדוגמה, אם קריאת כלי נכשלת עם שגיאה עמומה, המודל עלול לפרש את זה כ"הפעולה לא בוצעה" ולנסות שוב מיוזמתו — למרות שבפועל היא כן בוצעה, וההודעה שחזרה הייתה רק שגיאת רשת בדרך חזרה. זו הסיבה שה-Harness חייב להיות מי שמנהל את ה-Idempotency Key, לא המודל: המפתח נוצר פעם אחת ברמת ה-Harness בתחילת הכוונה הלוגית ("בצע זיכוי להזמנה X"), ומועבר לכל קריאת כלי שקשורה לאותה כוונה — גם אם המודל עצמו "מחליט" לנסות שוב, ה-Harness מוודא שאותו מפתח משמש בכל ניסיון.
Idempotency בשרשראות רב-קפיצה
הבעיה מסתבכת עוד יותר כשהפעולה עוברת דרך כמה קפיצות — סוכן A קורא לסוכן B שמבצע בפועל. אם סוכן A מנסה שוב אחרי timeout, הוא חייב להעביר את אותו Idempotency Key לסוכן B בניסיון החוזר, אחרת סוכן B, שרואה כל קריאה כבקשה חדשה, יבצע את הפעולה פעמיים. הפרקטיקה הנכונה, שמפורטת גם במדריך שרשראות רב-קפיצה, היא שה-trace_id או מזהה משימה אחיד שמלווה את כל השרשרת משמש גם כבסיס ל-Idempotency Key בכל קפיצה — כך שכל קפיצה בשרשרת, בכל ניסיון חוזר, יודעת לזהות אם היא כבר טיפלה בבקשה הזו.
מתי retry לא מספיק, וצריך Dead Letter
Idempotency פותרת את בעיית הכפילות, אבל לא את בעיית הכשל המתמשך: אם פעולה נכשלת שוב ושוב מסיבה אמיתית (לא timeout חולף אלא, למשל, כרטיס אשראי לא תקף), retry אינסופי לא יעזור — הוא רק יבזבז משאבים. אחרי מספר ניסיונות מוגדר מראש, עם backoff אקספוננציאלי ביניהם, הפעולה צריכה לעבור ל-Dead Letter Queue לבדיקה ידנית — ולא להישאר בלולאת retry שקטה שאף אחד לא רואה. חשוב שגם ה-Dead Letter Queue עצמו יכבד את אותו Idempotency Key: אם מישהו מטפל ידנית בפעולה שנכשלה ומריץ אותה שוב, המפתח הקיים מבטיח שגם ההרצה הידנית לא תיצור כפילות עם ניסיון אוטומטי מוקדם יותר שבכל זאת הצליח.
- Idempotency Key נוצר פעם אחת לכל כוונה לוגית, לא לכל ניסיון טכני
- ה-Harness מנהל את המפתח, לא המודל — כדי שהחלטות "לנסות שוב" של המודל לא ישברו את ההגנה
- המפתח מועבר בשלמותו דרך כל קפיצה בשרשראות רב-קפיצה
- אחרי X ניסיונות — Dead Letter Queue, לא retry אינסופי
איפה שומרים את מפתחות ה-Idempotency, ולכמה זמן
מפתחות שמורים בדרך כלל בטבלה ייעודית או ב-Redis עם TTL — לא לנצח, כי מרחב המפתחות רק גדל, אבל גם לא קצר מדי, כי אם ה-TTL פג לפני שכל ניסיונות ה-retry האפשריים הסתיימו, ההגנה נעלמת בדיוק כשעדיין יכולה להיות תעבורה כפולה בדרך. כלל אצבע סביר הוא TTL שגדול משמעותית ממשך הזמן המקסימלי שבו retry עדיין יכול לקרות — אם מדיניות ה-retry מוגבלת לחלון של שעה, TTL של 24-48 שעות נותן שוליים בטוחים בלי לנפח את האחסון ללא הגבלה. בעולם עסקים שבו פעולות כספיות כרוכות גם בדרישות רגולטוריות, שווה לשקול לשמור את ה-Idempotency Key גם אחרי שה-TTL התפעולי פג, כחלק מה-Audit Trail — לא לצורך מניעת כפילות, אלא כהוכחה שהמערכת אכן טיפלה בכל פעולה פעם אחת בלבד.
מעבר לתכנון הנכון של האחסון, חשוב גם לבדוק ולוודא שההגנה אכן עובדת בפועל. הדרך היחידה לדעת בביטחון שמנגנון ה-Idempotency עובד היא לבדוק אותו במכוון, ולא להסתפק בכך שהוא "נראה נכון" בקוד. בדיקת אינטגרציה טובה יורה בכוונה שתי בקשות זהות (אותו Idempotency Key) במקביל ממש, ומוודאת שרק אחת מהן בפועל מבצעת את הפעולה בעוד השנייה מקבלת את התוצאה השמורה — תרחיש race condition שקל לפספס אם בודקים רק בקשות סדרתיות. שווה גם לבדוק את המקרה ההפוך: שתי בקשות עם Idempotency Key שונה אך לאותה פעולה עסקית בפועל (למשל בגלל באג שיצר מפתח שגוי) — כדי לוודא שהמערכת לפחות מתריעה כשמשהו נראה חשוד, גם אם היא לא יכולה למנוע את זה לגמרי ברמת הקוד בלבד. הטמעה נכונה של Idempotency היא חלק בלתי נפרד מכל תהליך הקשחה של מערכת סוכנים לקראת פרודקשן אמיתי, כפי שמפורט גם במדריך הטמעת מערכות AI בפרודקשן.
תגיות: Idempotency · Agent Harness · Retry Logic · Dead Letter Queue · Payment Processing · Multi-Hop Agents