تصور کنید یک عامل هوش مصنوعی را برای مدیریت گزارشات هفتگی شرکتتان به کار گرفتهاید، اما یک اختلال کوچک در شبکه باعث میشود مدل یک گزارش را سه بار تولید و ارسال کند. این اتفاق زمانی رخ میدهد که مدل پاسخی از ابزار دریافت نمیکند و بدون داشتن راهکاری برای تأیید وضعیت، دستور را دوباره ارسال میکند. در واقع، یک قطع اتصال (Timeout) در یک عامل خودمختار به معنای شکست عملیات نیست؛ بلکه صرفاً به این معناست که تأییدیه دریافت نشده است. اگر عاملی تابعی مانند create_report_job("weekly") را فراخوانی کند و پاسخی دریافت نکند، تلاش مجدد کورکورانه میتواند منجر به ایجاد ورودیهای تکراری در پایگاهداده شود، در حالی که انجام ندادن هیچ اقدامی ممکن است باعث شود یک تسک حیاتی هرگز اجرا نشود.
این چالش دقیقاً در نقطهای رخ میدهد که توسعهدهندگان از ساخت چتباتهای ساده به سمت عاملهای هوش مصنوعی (AI Agents) — شبیه به کارمندانی که نه تنها حرف میزنند، بلکه ابزارها را برای تغییر وضعیت دنیای واقعی مدیریت میکنند — حرکت میکنند. در این سیستمها، فاصله بین ثبت یک داده در پایگاهداده (Database Commit) و دریافت تأییدیه شبکه (Network Acknowledgment)، منبع اصلی ایجاد «کارهای شبحوار» و فساد دادهها است. برای اکثر توسعهدهندگان، تفاوت بین یک سیستم قابلاعتماد و سیستمی که بهطور تصادفی حجم کاری را دوبرابر میکند، در مدیریت همین لحظات نهفته است. این نوع ناپایداریها اغلب منجر به شکستهای خاموشی در عاملها میشود که در مانیتورینگهای سنتی دیده نمیشوند و تشخیص آنها را دشوار میکند.
به نقل از یک آزمایش فنی منتشر شده در dev.to در تاریخ ۱۳ سپتامبر، راهکار این مشکل حفظ هویت «قصد عملیاتی» است، نه هویت «تلاش برای اجرا». این هدف از طریق مکانیزمی به نام کلید عملیاتی (Operation Key) محقق میشود. یک قطع اتصال (Timeout) بهتنهایی نمیتواند بگوید که آیا عملیات ثبت شده است یا خیر؛ بنابراین شما به سرویسی نیاز دارید که این هویت را بهصورت سختگیرانه اجرا کند.
همانطور که در تحلیلهای قبلی ما دربارهی پایداری سیستمهای عاملمحور اشاره کردیم، حذف احتمالات از لایهی تصمیمگیری مدل و انتقال آنها به لایهی زیرساخت، تنها راه رسیدن به دقت صنعتی است. این رویکرد برای جلوگیری از اشتباهات رایجی در اتوماسیون که جریانهای کاری تولیدی را مختل میکنند، ضروری است.
مکانیزم رسید (Receipt Mechanism)
معماری پیشنهادی از یک پایگاهداده SQLite برای ردیابی همزمانِ کار و رسید مربوط به آن در یک تراکنش واحد استفاده میکند. این فرآیند به شرح زیر است:
- اجراکننده (Runner) یک کلید عملیاتی منحصربهفرد برای تسک مورد نظر تولید میکند.
- کارگر (Worker) پیش از ایجاد کار جدید، جدول
receiptsرا برای آن کلید خاص بررسی میکند. - اگر کلید وجود داشته باشد و محتوا (Payload) یکسان باشد، کارگر بهجای ایجاد کار جدید، شناسه کار (Job ID) موجود را بازمیگرداند.
- اگر کلید وجود داشته باشد اما محتوا متفاوت باشد، سیستم درخواست را به دلیل تضاد (Conflict) رد میکند.
- اگر کلید وجود نداشته باشد، یک کار جدید ایجاد شده و رسید آن ثبت میگردد.
جزئیات پیادهسازی فنی
برای تضمین اجرای این قوانین، این آزمایش محدودیتهای خاص پایگاهداده و منطق تراکنشی را پیاده کرده است:
- طراحی طرحواره (Schema): جدول
jobsنوع گزارش را ذخیره میکند، در حالی که جدولreceiptsاز کلید عملیاتی به عنوان کلید اصلی (PRIMARY KEY) و ازjob_idبه عنوان یک فیلد منحصربهفرد (UNIQUE) استفاده میکند. - تراکنشهای اتمیک (Atomic Transactions): ثبت رسید و ثبت کار در یک تراکنش مشترک قرار دارند. این امر از ایجاد شکافی جلوگیری میکند که در آن یک اثر راه دور (Remote Effect) بدون داشتن سابقه محلی رخ دهد.
- قفلگذاری نوشتن (Write Locking): سیستم از دستور
BEGIN IMMEDIATEبرای شروع تراکنش نوشتن در SQLite پیش از جستجوی رسید استفاده میکند. این کار تضمین میکند که SQLite تنها به یک نویسنده همزمان اجازه دسترسی دهد، هرچند ممکن است در شرایط ترافیک بالا منجر به خطاهایSQLITE_BUSYشود. - مدیریت تضاد: اگر درخواستی با کلید موجود اما محتوای گزارش متفاوت برسد، سیستم خطای
ValueError("key_payload_conflict")صادر کرده و تراکنش را بازمیگرداند (Rollback).
شبیهسازی مرزهای شکست
برای اثبات این مدل، از یک اسکریپت پایتون (retry-lab.py) استفاده شده که با دستور os._exit()، پردازش را در دو نقطه بحرانی متوقف میکند تا کرشهای ناگهانی کارگر بدون اجرای مراحل پاکسازی پایتون شبیهسازی شوند:
۱. پیش از ثبت (Exit Code 17): پردازش قبل از نهایی شدن نوشتن در پایگاهداده متوقف میشود. در این حالت، تلاش مجدد با همان کلید، بهدرستی اولین و تنها کار را ایجاد میکند (تعداد کارها: ۰ $\rightarrow$ ۱).
۲. پس از ثبت، پیش از تأییدیه (Exit Code 18): پایگاهداده کار را ذخیره میکند، اما پردازش پیش از اطلاع دادن به عامل درباره موفقیت عملیات، کرش میکند. تلاش مجدد با همان کلید، صرفاً شناسه کار موجود را برمیگرداند و از تکرار جلوگیری میکند (تعداد کارها: ۱ $\rightarrow$ ۱).
تحلیل موارد اجرا شده
این آزمایش پنج سناریوی خاص را برای اعتبارسنجی منطق تلاش مجدد (Retry) بررسی کرد:
- کلید یکسان، گزارش یکسان (توقف پیش از ثبت): منجر به ایجاد ۱ کار شد. تلاش مجدد با موفقیت جای خالی را پر کرد.
- کلید یکسان، گزارش یکسان (توقف پس از ثبت): تعداد کارها ۱ باقی ماند. تلاش مجدد نتیجه را بازپخش کرد بدون اینکه کار تکراری بسازد.
- کلید جدید، گزارش یکسان (توقف پس از ثبت): منجر به ۲ کار شد. این تایید میکند که یک کلید جدید نشاندهنده یک قصد عملیاتی مجزا است.
- کلید یکسان، گزارش متفاوت (توقف پس از ثبت): تعداد کارها ۱ باقی ماند و درخواست به دلیل تضاد محتوا رد شد.
- کار دوم عمدی با کلید جدید: منجر به ۲ کار شد که عملکرد عادی برای درخواستهای متمایز است.
تفکیک «قصد» از «محتوا»
این آزمایش بر یک تمایز حیاتی تأکید میکند: پارامترهای یکسان همیشه به معنای قصد تکراری نیستند. اگر کاربر واقعاً دو گزارش هفتگی یکسان بخواهد، استفاده از هشِ محتوا (Payload Hash) به عنوان کلید، بهاشتباه درخواست دوم را مسدود میکند. بنابراین، کلید عملیاتی باید شناسهای مجزا برای «قصد» (Intent) انجام یک عمل باشد، نه صرفاً خلاصهای از دادههای ارسالی.
ماتریس تصمیمگیری برای تلاش مجدد
توسعهدهندگان میتوانند از منطق زیر برای تصمیمگیری پس از یک Timeout استفاده کنند:
- تلاش مجدد با همان کلید: اگر سرویس از قراردادهای بازپخش (Replay Contracts) پشتیبانی میکند و کلید هنوز معتبر است. کلید و درخواست را حفظ کنید.
- تلاش جدید: تنها زمانی توجیه میشود که شواهد قطعی و معتبر نشان دهد عملیات اول هرگز اعمال نشده و در آینده نیز اعمال نخواهد شد.
- تطبیق از طریق مسیر وضعیت: اگر تنها یک Timeout یا پاسخ گمشده وجود دارد و تضمینی برای بازپخش نیست، عامل باید وضعیت را از یک مسیر معتبر استعلام کند. هرگز «عدم اثر» را از روی Timeout استنتاج نکنید.
- استفاده از معناشناسی Idempotent: اگر عملیات بر اساس تعریف معنایی خود از قبل Idempotent (تکرارپذیر) است، شاید نیازی به جدول رسید اضافی نباشد.
این رویکرد با استانداردهای HTTP همسو است؛ جایی که تلاش مجدد برای درخواستهای غیر-Idempotent نیازمند دانشی است که عملیات قبلی هرگز اعمال نشده است. این یک قانون پروتکل است، نه چیزی که بتوان به «اعتمادبهنفس» یا تخمین یک مدل زبانی تکیه کرد.
با استفاده از BEGIN IMMEDIATE در SQLite، سیستم تضمین میکند که تنها یک نویسنده فعال باشد و از Race Condition در هنگام بررسی رسید جلوگیری میکند. برای توسعهدهنده، این یعنی عامل دیگر مجبور نیست «حدس بزند» که آیا یک فراخوانی موفق بوده است یا خیر. منطق از استدلال احتمالی مدل زبانی بزرگ (LLM) — شبیه به حدس زدن محتویات یک بسته بدون باز کردن آن — به محدودیتهای قطعی پایگاهداده منتقل میشود. این کار نیاز به پرسش از مدل درباره اینکه آیا دو فراخوانی یک عملیات بودهاند یا خیر را از بین میبرد؛ وظیفهای که مدلها برای انجام آن ابزار مناسبی ندارند. همچنین باید به یاد داشت که یک نتیجه جستجوی خالی، دلیلی بر شکست نیست، زیرا درخواست اول ممکن است هنوز در جریان باشد یا خواندن دادهها با تأخیر نسبت به نوشتن رخ دهد.
در نهایت، تفاوت بین «من نتیجهای دریافت نکردم» و «عملیات رخ نداد» است. در یک جریان کاری تولیدی، یکسان دانستن این دو، نسخهای برای ناپایداری سیستماتیک است. اگر سرویسی نه قرارداد بازپخش دارد و نه مسیر تطبیق معتبر، عامل باید وضعیت نامعلوم را حفظ کرده و تصمیم را به سطح بالاتر ارجاع دهد، بهجای اینکه بهطور خاموش تکرار یا حذف را انتخاب کند.
گام بعدی شما
- ابتدا APIهای ابزارهای خود را بررسی کنید تا ببینید آیا از کلیدهای Idempotency پشتیبانی میکنند یا خیر.
- در صورت عدم پشتیبانی، یک لایه رسید (Receipt Layer) بین اجراکننده عامل و سرویس هدف پیادهسازی کنید.
- منطق Retry را از لایه پرامپت به لایه کد (Deterministic Logic) منتقل کنید.
اما مدیریت حافظه در این عاملها چالش بزرگتری است — به تحلیل ما دربارهی پروتکل MCP و مدیریت زمینه مراجعه کنید.




گفتگو