למה סוכן AI מבצע אותה פעולה פעמיים, ואיך מונעים את זה
# למה סוכן AI מבצע אותה פעולה פעמיים, ואיך מונעים את זה
פעולה עסקית צריכה לקבל זהות לפני שהיא מקבלת כפתור "נסה שוב".
הכלל הזה נשמע טכני, אבל הכשל שהוא מונע עסקי מאוד. סוכן AI יוצר משימה ב־CRM, שולח בקשה למערכת ומחכה לתשובה. החיבור מתנתק רגע אחרי שהמערכת ביצעה את הפעולה. הסוכן אינו יודע אם הפעולה הצליחה, ולכן הוא מנסה שוב. עכשיו יש שתי משימות, שתי הזמנות או שתי הודעות שנראות כמעט זהות.
הבעיה אינה שהסוכן "התבלבל". מבחינתו, ניסיון חוזר הוא תגובה סבירה לחוסר ודאות. הבעיה היא שהמערכת לא יודעת לזהות ששני הניסיונות מבטאים אותה כוונה עסקית.
התזה של המאמר הזה היא פשוטה: לא מונעים כפילויות באמצעות בקשה מהסוכן להיות זהיר. מונעים אותן באמצעות מזהה קבוע לפעולה, רישום מצב והפרדה בין הכנת פעולה לבין הביצוע שלה.
## הכפילות נוצרת דווקא אחרי שהפעולה הצליחה
קל לדמיין שכפילות מתחילה משתי לחיצות או משתי הודעות זהות. במערכות מחוברות היא מופיעה לא פעם ברגע הרבה יותר מתעתע: הפעולה הצליחה, אבל האישור על ההצלחה לא חזר.
נניח שסוכן מכין הזמנת ספק. הוא שולח למערכת הרכש הוראה ליצור הזמנה. המערכת יוצרת אותה, אך התשובה נקטעת בדרך. מבחינת מערכת הרכש יש הזמנה חדשה. מבחינת הסוכן יש שגיאת תקשורת. אם הניסיון הבא יישלח כבקשה חדשה, תיווצר הזמנה נוספת.
אותו דפוס מופיע גם בלי תקלה דרמטית. תהליך יכול להיעצר בגלל מגבלת זמן, לעלות מחדש ולהמשיך מהשלב האחרון שנרשם אצלו. אם הרישום אומר "עמדתי ליצור משימה" ולא "המשימה נוצרה ומספרה 1842", הריצה החדשה עלולה לבצע שוב את מה שכבר קרה.
כאן כדאי להפריד בין שני דברים שבשיחה רגילה נשמעים דומים:
* ניסיון הוא הפעלה טכנית אחת של פעולה.
* כוונה היא התוצאה העסקית שרצינו להשיג.
לאותה כוונה יכולים להיות כמה ניסיונות. המערכת צריכה לאפשר את הניסיונות בלי לייצר כמה תוצאות.
## הודעה דומה אינה הוכחה שזו אותה כוונה
פתרון מפתה הוא לחפש רשומה דומה לפני יצירה. אם כבר קיימת משימה לאותו לקוח ובאותו נושא, לא יוצרים עוד אחת. זה עוזר במקרים מסוימים, אבל הוא אינו בסיס אמין.
שתי בקשות שונות יכולות להיראות זהות. לקוח יכול לבקש שתי אספקות של אותו מוצר לאותו אתר, או לדווח פעמיים על תקלות נפרדות עם אותו ניסוח. מנגד, אותה בקשה יכולה להגיע בשני נוסחים שונים, פעם ממייל ופעם מטופס פנימי. דמיון טקסטואלי אינו יודע להכריע אם מדובר בכפילות או באירוע חדש.
גם שעה, שם לקוח וסכום אינם תמיד מזהה מספיק. אם מנהלת מאשרת מחדש הצעה מתוקנת באותו סכום, זו עשויה להיות כוונה חדשה. אם הסוכן מנסה שוב לשלוח את אותה גרסה אחרי ניתוק, זו אותה כוונה בדיוק.
לכן המזהה צריך להיוולד מהאירוע העסקי, לא מהניחוש של המודל. לדוגמה: מזהה בקשה פנימית יחד עם סוג הפעולה וגרסת המסמך. "בקשה 731, יצירת הצעת מחיר, גרסה 4" מתארת כוונה אחת. כל ניסיון לבצע אותה נושא את אותו מזהה.
## מפתח פעולה הופך ניסיון חוזר לבטוח
בעולם התוכנה קוראים לעיקרון הזה idempotency. הרעיון פשוט גם בלי המונח: אם שולחים שוב את אותה פעולה עם אותו מפתח, המערכת מחזירה את התוצאה שכבר נוצרה במקום לבצע אותה פעם נוספת.
Stripe מתעדת שימוש במפתח כזה כדי לאפשר ניסיון חוזר של בקשות יצירה ועדכון בלי ליצור פעולה כפולה. AWS מתארת את אותו עיקרון במערכות שבהן תהליך מורכב מפעיל כמה שירותים, וחלק מהקריאות עשויות להיכשל בדרך. אלה דוגמאות טכניות, אבל ההחלטה העסקית קודמת למימוש: צריך להגדיר מה נחשב "אותה פעולה".
מפתח טוב מחובר ליחידת העבודה. הוא אינו מספר אקראי חדש בכל ניסיון, כי אז כל ניסיון נראה כמו כוונה חדשה. הוא גם לא נשען רק על שם הלקוח, כי לקוח רשאי לבצע יותר מפעולה אחת.
בתהליך של פתיחת משימת גבייה, למשל, אפשר לקשור את המפתח לחשבונית, לסוג המשימה ולמחזור הגבייה. אם הסוכן ניסה שוב אחרי תקלה, המפתח נשאר. אם החשבונית נכנסה למחזור גבייה חדש לאחר החלטה עסקית, נוצר מפתח חדש.
מערכת היעד לא תמיד תומכת במפתח כזה. במקרה הזה אפשר להקים רישום פעולה לפני הקריאה החיצונית. הרישום מחזיק את המפתח, את המצב ואת מזהה התוצאה במערכת היעד. זה פחות אלגנטי מתמיכה מובנית, אך עדיין נותן לתהליך זיכרון עובדתי שאינו תלוי בזיכרון השיחה של הסוכן.
## ארבעה מצבים במקום תשובת כן או לא
רישום פעולה חלש שומר רק "בוצע" או "לא בוצע". התקלה מעניינת נמצאת באמצע. לכן יחידת עבודה צריכה לעבור בין מצבים שמסבירים מה ידוע כרגע.
מצב מוכן אומר שכל הנתונים נבדקו והפעולה טרם נשלחה. מצב בביצוע אומר שהבקשה יצאה, אבל אין עדיין תוצאה מוסמכת. מצב הושלם מחזיק גם את מזהה הרשומה שנוצרה. מצב לא ידוע אומר שייתכן שהפעולה בוצעה, אך האישור לא חזר.
המצב האחרון חשוב במיוחד. "לא ידוע" אינו "נכשל". אם הופכים כל חוסר תשובה לכישלון, הסוכן ישלח מיד ניסיון חדש. במקום זאת, הוא צריך לבצע בירור: לחפש לפי מפתח הפעולה, לבדוק אם מערכת היעד מחזירה מזהה, או להעביר את המקרה לבדיקה אם אין דרך אמינה לברר.
ההבחנה הזאת משנה גם את הממשק לאדם. במקום התראה כללית על תקלה, אפשר להציג: "נשלחה בקשה ליצירת הזמנה 731, לא התקבל אישור, לא בוצע ניסיון נוסף". עכשיו יש לאדם החלטה ברורה. הוא אינו צריך לנחש אם למחוק כפילות שכבר נוצרה.
## שליחה היא השלב האחרון, לא הראשון
כדי לצמצם את שטח הכשל, מכינים את הפעולה לפני שמבצעים אותה. הסוכן אוסף את הנתונים, בודק שדות חובה, מציג את היעד ומייצר את מפתח הפעולה. רק אחרי שהחבילה יציבה עוברים לפעולה החיצונית.
ניקח הודעת עדכון ללקוח. ההכנה כוללת בחירת נמען, נוסח, מסמך מצורף וסיבת השליחה. הביצוע הוא הקריאה ששולחת בפועל. אם הנוסח משתנה אחרי ניסיון שנכשל, כבר לא ברור אם זה ניסיון חוזר או הודעה חדשה. לכן שינוי בתוכן מחייב החלטה: האם מבטלים את הכוונה הקודמת ופותחים חדשה, או ממשיכים עם אותה חבילה בדיוק.
הפרדה דומה נדרשת ביצירת רשומה ב־CRM. קודם מגדירים מי הלקוח, מה סוג הרשומה ומה המקור. אחר כך יוצרים. לאחר ההצלחה שומרים מיד את מזהה הרשומה. עדכון עתידי פונה לאותו מזהה, ולא מבצע שוב חיפוש רך לפי שם וטלפון בתקווה למצוא את מה שנוצר.
המודל יכול לעזור בהכנת הנתונים ובהבנת ההקשר. הוא לא צריך להמציא בכל ריצה מחדש את זהות הפעולה. הזהות היא חלק מחוזה התהליך.
## בדיקת הכשל צריכה לנתק את התשובה בכוונה
בדיקה רגילה מפעילה תהליך ורואה שהרשומה נוצרה. היא לא בודקת את הרגע שמייצר כפילויות. כדי לבדוק ניסיון חוזר, צריך לגרום למערכת לאבד את האישור אחרי הביצוע.
בסביבת בדיקה מריצים פעולה אחת, מאפשרים למערכת היעד לבצע אותה, ואז מדמים ניתוק לפני שהתוצאה חוזרת לסוכן. לאחר מכן מפעילים שוב את אותה יחידת עבודה. התוצאה הנכונה היא רשומה אחת, עם אותו מזהה פעולה, ויומן שמראה שני ניסיונות.
כדאי לבדוק גם מצב הפוך: שתי כוונות שונות עם פרטים זהים. אם מנגנון מניעת הכפילויות חוסם את השנייה רק מפני שהלקוח והסכום זהים, הוא אגרסיבי מדי. מנגנון טוב מונע ביצוע כפול בלי למחוק עבודה חוקית.
עוד בדיקה נוגעת לזמן. בקשה ישנה יכולה להגיע באיחור אחרי שכבר נוצרה כוונה חדשה. AWS מצביעה על ההבדל בין ניסיון מאוחר של אותה בקשה לבין שימוש חוזר במזהה עבור כוונה אחרת. מבחינת העסק, המשמעות היא שלא ממחזרים מפתחות רק כי עבר זמן נוח. מחזור החיים שלהם צריך להתאים למחזור החיים של הפעולה.
## המדד שמגלה אם מנגנון ההגנה באמת עובד
ספירת כפילויות שמצא הצוות היא מדד מאוחר. עד שמישהו רואה שתי הזמנות, הנזק כבר נכנס לתהליך. עדיף למדוד את המצבים שהיו עלולים ליצור כפילות.
רושמים כמה ניסיונות חוזרים התרחשו, בכמה מהם הוחזרה תוצאה קיימת, כמה פעולות נשארו במצב לא ידוע וכמה פעמים אדם נדרש להכריע. לצד זה בודקים חסימות שווא: מקרים שבהם פעולה חדשה נעצרה מפני שנראתה דומה לקודמת.
המטרה אינה להגיע לאפס ניסיונות חוזרים. רשתות נקטעות ושירותים מגיבים לאט. המטרה היא שניסיון חוזר לא ישנה את התוצאה העסקית. פעולה אחת צריכה להישאר פעולה אחת גם כשהדרך אליה כללה שתי קריאות.
לפני שמחברים סוכן AI ל־CRM, למייל או למערכת הזמנות, בחרו פעולה אחת ושאלו מה יקרה אם היא תצליח אבל התשובה לא תחזור. אם אין תשובה ברורה, התהליך עדיין לא מוכן להרשאת כתיבה.
ב[פגישת גילוי אוטומציה](/automation-discovery) אפשר להביא פעולה אחת שאתם מתכננים להעביר לסוכן. נמפה לה מזהה, מצבי ביניים ומבחן ניתוק לפני שהיא מקבלת גישה למערכת חיה.
### מקורות
* [Stripe: בקשות עם מפתח idempotency](https://docs.stripe.com/api/idempotent_requests)
* [AWS Builders' Library: ניסיונות חוזרים בטוחים עם API idempotent](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/)