اگر برای نوشتن کتابخانههای سطح تولید (Production-grade) به هوش مصنوعی تکیه میکنید، بزرگترین ریسک شما خطای سینتکس نیست، بلکه خطای مرزی است. در ۱۰ ژوئن ۲۰۲۶، یک توسعهدهنده در وبسایت dev.to موردی را منتشر کرد که نشان میدهد مدل Fable5 تنها زمانی توانست شکستهای عملیاتی بحرانی را در PooledMailKit (یک استخر اتصال SMTP در داتنت) شناسایی کند که قراردادهای طراحی صریح برای بررسی در اختیارش قرار گرفت.
بسیاری از برنامهنویسان با هوش مصنوعی مانند یک تولیدکننده کد برخورد میکنند که پیادهسازیهای محتمل را بر اساس منطق محلی میسازد. اما همانطور که نویسنده PooledMailKit دریافت، کدی که در تستهای واحد (Unit Test) موفق میشود، اغلب زیر فشار ترافیک واقعی فرو میپاشد. این کتابخانه که از طریق NuGet (با دستور dotnet add package PooledMailKit) در دسترس است، برای بازاستفاده از اتصالات SMTP طراحی شده تا از فشار ایجاد و حذف مداوم SmtpClient برای هر پیام جلوگیری کند.
در پروتکل SMTP، الگوهای سادهی «اتصال-ارسال-حذف» منجر به طوفان اتصالات TCP، فشار روی پورتهای موقت (Ephemeral Port Pressure) و ایجاد ایمیلهای تکراری خطرناک در هنگام تلاشهای مجدد (Retries) میشود. طبق گزارش نویسنده، هوش مصنوعی بهراحتی کدی میزند که کامپایل شود و ایمیل بفرستد، اما واقعیتهای عملیاتی تحویل SMTP را نادیده میگیرد. این یک تله رایج است: هوش مصنوعی پیادهسازیهای محلیِ پذیرفتنی میسازد، اما محدودیتهای محیط تولید را نمیشناسد مگر اینکه این محدودیتها صریح شوند.
پیچیدگی عملیاتی SMTP
تحویل SMTP یک اقدام اتمیک نیست. این فرآیند از مراحل پروتکل مشخصی عبور میکند: اتصال (Connect)، احراز هویت (Authenticate)، دستور MAIL FROM، دستور RCPT TO، دستور DATA، انتقال بدنه پیام و در نهایت پاسخ سرور. مرحلهای که شکست در آن رخ میدهد، تعیین میکند که آیا تلاش مجدد (Retry) ایمن است یا خیر. اگر اتصال پیش از ارسال بدنه پیام قطع شود، تلاش مجدد عموماً ایمن است. اما اگر شکست پس از شروع دستور DATA رخ دهد، کلاینت ممکن است نداند که آیا سرور پیام را پذیرفته است یا خیر. در این نقطه، یک تلاش مجدد کورکورانه میتواند منجر به ارسال ایمیلهای تکراری شود.
برای داشتن یک سیستم مقاوم، یک فرستنده باید بتواند انواع مختلف شکستها را از هم تفکیک کند:
- شکستهای موقت SMTP (Temporary failures)
- شکستهای دائمی SMTP (Permanent failures)
- شکستهای احراز هویت (Authentication failures)
- رد شدن گیرنده (Recipient rejection)
- شکست در اتصال به میزبان (Host connectivity failure)
- شکستهای مبهم پس از دستور DATA (Ambiguous post-DATA failures)
این تفکیکها برای کتابخانهای که ادعای رفتار ایمن در تلاشهای مجدد را دارد، اختیاری نیستند. برای حل این مشکل، توسعهدهنده رویکرد خود را از درخواست «یک استخر بنویس» به یک گردشکار چندلایه تغییر داد. او بهجای درخواست ساده برای یک استخر SMTP (که احتمالاً منجر به یک Wrapper زیبا دور SmtpClient میشد که دغدغههای عملیاتی را نادیده میگیرد)، کار را به لایههای مختلف تقسیم کرد: تعریف شکستهایی که باید پیشگیری شوند، نوشتن اسناد طراحی برای نشستها (Sessions) و متریکها، درخواست از هوش مصنوعی برای پیادهسازی بر اساس آن اسناد، بررسی نتایج و در نهایت افزودن تستها برای حالتهای شکست.
قراردادهای طراحی
پیش از بررسی، پروژه توسط قراردادهای طراحی صریح هدایت میشد. این اسناد، قضاوتهای ضمنی را به قراردادهای صریح تبدیل کردند و رفتار مورد انتظار را برای چندین حوزه بحرانی تعریف کردند:
- همروندی محدود (Bounded Concurrency): استخر باید
MaxPoolSizeرا اعمال کند. فرآیند دریافت مجوز (Lease acquisition) باید دارای Timeout باشد تا از انتظار نامحدود جلوگیری شود. - طبقهبندی تلاش مجدد: شکستهای SMTP باید دستهبندی شوند. برخی قابل تکرار هستند، برخی نیستند و برخی مبهماند و نباید بهطور خودکار تکرار شوند.
- مرز DATA: شکستها بعد از شروع DATA خطرناکاند مگر اینکه نتیجه پروتکل مشخص باشد. اگر سرور صراحتاً بدنه DATA تکمیلشده را رد کند، پیام پذیرفته نشده است. اما اگر اتصال پس از شروع DATA و پیش از پاسخ نهایی قطع شود، نتیجه مبهم است.
- زمان انتظار بازاتصال (Reconnect Cooldown): بازه Cooldown باید ایجاد اتصال جدید را زمانی که میزبان پایین (Down) به نظر میرسد، متوقف کند. این مکانیسم نباید توسط رد شدن پیام در سطح کاربر (مانند گیرنده اشتباه) فعال شود.
- جایگزینی چند-میزبان (Multi-host Failover): اگر میزبان اصلی SMTP نتواند اتصال برقرار کند، استخر باید در همان جریانِ درخواست (Acquire flow)، میزبان واجد شرایط دیگری را امتحان کند.
- قرارداد متریکها: متریکها باید وضعیت استخر و نتایج ارسال را با استفاده از نامهای پایدار و تگهای کمتعداد (Low-cardinality tags) نمایش دهند.
یافتههای بررسی مبتنی بر قرارداد
تغییر از «بررسی کد» به «بررسی قرارداد» منجر به شناسایی ۶ شکست سیستمی شد که بررسیهای استاندارد هوش مصنوعی احتمالاً آنها را نادیده میگرفتند. Fable5 پیادهسازی را با اسناد طراحی مقایسه کرد و نقاطی را یافت که کد، قرارداد مستند شده را اجرا نمیکرد. این یک بررسی روی استایل، نامگذاری یا چک کردن Null نبود، بلکه بررسی خودِ قرارداد بود.
یافته ۱: ردیابی مرحله SMTP
طراحی، طبقهبندی شکست بر اساس مرحله SMTP را میخواست. پیادهسازی دارای Enumها و طبقهبندیکنندهها بود، اما در مسیر عملیاتی، اطلاعات مرحله هرگز به طبقهبندیکننده متصل نمیشد. این باعث شد مسیرهای مهم طبقهبندی غیرقابل دسترس شوند. شکستهای موقت 4xx که باید در بودجه تلاشهای مجدد تکرار میشدند، هرگز تکرار نشدند و رد شدن دستورات بهطور اشتباه به عنوان شکستهای مبهم پس از DATA گزارش شدند که باعث تورم متریکهای عملیاتی شد.
یافته ۲: رد ambiguous در DATA
هوش مصنوعی در ابتدا پاسخهای MessageNotAccepted را به عنوان موارد مبهم (Ambiguous) در نظر میگرفت. در واقع، این پاسخ سرور به یک بدنه DATA تکمیلشده است و نتیجه مشخص است. رفتار درست این است که پاسخهای 4xx به عنوان شکستهای موقت قابل تکرار و 5xx به عنوان شکستهای دائمی تلقی شوند. در هر دو حالت، تراکنش SMTP بهطور کامل پایان یافته و اتصال همچنان قابل بازاستفاده است. تلقی کردن این موارد به عنوان «مبهم»، اپراتورها را در مورد ریسک ارسال تکراری گمراه میکرد.
یافته ۳: منطق Cooldown میزبان
یک باگ بحرانی باعث میشد شکستهای سطح پیام (مانند گیرنده اشتباه) منجر به فعال شدن Cooldown بازاتصال در سطح کل میزبان شود، هرگاه یک مجوز (Lease) دور انداخته میشد. پیادهسازی، مفاهیم «دور انداختن این اتصال»، «علامتگذاری میزبان به عنوان ناسالم» و «توقف ایجاد اتصال جدید» را در یک اقدام واحد ادغام کرده بود. برای مثال، رد شدن گیرنده با کد 550 یک نتیجه در سطح پیام است، نه شکست در اتصال به میزبان. این موضوع میتوانست استخر مؤثر را کوچک کرده و باعث خطاهای اجتنابپذیر PoolExhausted شود.
یافته ۴: اجرای Failover
با وجود اینکه کتابخانه دارای تنظیمات چند-میزبان بود، پیادهسازی بلافاصله پس از شکست در اتصال، Exception پرتاب میکرد. این امر requirement مستند شده برای رخ دادن Failover در یک جریان واحد acquire را میشکست. جایگزینی چند-میزبان تنها در صورتی کار میکرد که فراخواننده (Caller)، تلاشهای مجدد در سطح ارسال را فعال کرده باشد، که این با هدف طراحی در تضاد بود. داشتن تنظیمات به معنای داشتن Failover عملیاتی نیست.
یافته ۵: وابستگیهای Warm-Pool
شکست در پر کردن مجدد MinPoolSize به فراخواننده گزارش میشد. این بدان معنا بود که یک ارسال موفق ایمیل میتوانست صرفاً به دلیل شکست در نگهداری داخلی استخر پس از ارسال، به عنوان شکست کلی گزارش شود. شکست در نگهداری داخلی استخر نباید به عنوان شکست در تحویل SMTP گزارش شود.
یافته ۶: پاکسازی در برابر تحویل
خطرناکترین یافته این بود که شکست در پاکسازی (Cleanup) پس از پذیرش موفق سرور، به عنوان شکست ارسال گزارش میشد. اگر بازگرداندن مجوز به استخر با شکست مواجه میشد، کل عملیات ارسال به عنوان شکست علامت میخورد. این امر باعث میشد فراخواننده پیامی را که سرور قبلاً پذیرفته است، دوباره ارسال کند و مستقیماً منجر به ایمیلهای تکراری شود. راه حل این بود که تکمیل مجوز در مسیر موفقیت را به صورت «بهترین تلاش» (Best-effort) درآورند.

مسیر اصلاح: نسخه 0.1.1.1
این یافتهها منجر به انتشار نسخه 0.1.1.1 شد. این بهروزرسانی بهجای تغییر در API، بر اصلاح رفتار تمرکز داشت تا کد را با قرارداد طراحی همسو کند.
اصلاحات فنی کلیدی:
- استنتاج مرحله SMTP: فرستنده اکنون اطلاعات مرحله را از
SmtpCommandException.ErrorCodeاستخراج میکند. کدهایSenderNotAcceptedوRecipientNotAcceptedبه مرحلهEnvelopeStartedو کدMessageNotAcceptedبه مرحلهDataCompletedمتصل میشوند. شکستهای ناشناخته در دستورات همچنان محافظهکارانه مدیریت میشوند. - منطق DATA: پاسخ
MessageNotAcceptedدیگر مبهم نیست. کدهای 4xx قابل تکرار و 5xx دائمی هستند و اتصال همچنان قابل بازاستفاده میماند زیرا تراکنش بهطور کامل انجام شده است. - بهبود Cooldown: بازه بازاتصال اکنون فقط برای شکستهای ایجاد اتصال اعمال میشود، نه شکستهای سطح پیام یا لغو توسط فراخواننده. این کار باعث حفظ سرکوب طوفانهای بازاتصال برای شکستهای واقعی و جلوگیری از سرکوب نادرست میزبان میشود.
- حلقه Acquire: جایگزینی میزبان (Failover) اکنون داخل حلقه
acquireاتفاق میافتد. اگر ایجاد اتصال برای یک میزبان شکست بخورد، حلقه با میزبان واجد شرایط بعدی ادامه مییابد. - پاکسازی Best-Effort: پر کردن مجدد Warm-pool و پاکسازی مجوز پس از ارسال موفق، اکنون به صورت «بهترین تلاش» است. شکست در پاکسازی دیگر باعث نمیشود یک پیام پذیرفته شده در SMTP به عنوان شکست ارسال گزارش شود.
اعتبارسنجی این نسخه شامل یک مجموعه سختگیرانه از تستها در چارچوبهای هدف .NET بود. نتایج نهایی شامل موارد زیر بود:
- ۶۰ تست واحد (Unit Test) پاس شده
- ۱۰ تست کامپوننت پاس شده
- ۸ تست یکپارچهسازی مبتنی بر Docker با smtp4dev پاس شده
- ۹ تست استرس دستی پاس شده
شایان ذکر است که برخی تستها باید بهروزرسانی میشدند زیرا رفتار کد اکنون با قرارداد مطابقت داشت؛ مثلاً Failover چند-میزبان اکنون در یک تلاش ارسال موفق میشد، به این معنی که SmtpSendResult.Attempts میتواند ۱ باقی بماند، زیرا تلاشهای انتخاب میزبان در داخل acquire به عنوان تلاشهای ارسال مجزا شمارش نمیشوند.
تحلیل: گذار به مهندسی مبتنی بر قصد (Intent-Based Engineering)
این تجربه، فرض بنیادی درباره بررسی کدهای تولید شده توسط AI را تغییر میدهد. بررسیهای استاندارد بر استایل، چک کردن Null و نامگذاری تمرکز دارند. بررسیهای مبتنی بر قرارداد بر «قصد» (Intent) تمرکز میکنند. وقتی به AI گفته شود: «تو این مرز را مستند کردی، اما پیادهسازی آن را اعمال نمیکند»، AI از یک Linter ساده به یک معمار سیستم تبدیل میشود.
برای توسعهدهندگان، این بدان معناست که باارزشترین فعالیت دیگر درخواست کد نیست، بلکه بیرونی کردن قضاوتهای ضمنی در قالب قراردادهای صریح است. AI میتواند بهطور بهینه بررسی کند که آیا کد از یک قانون پیروی میکند یا خیر، اما هنوز نمیتواند تصمیم بگیرد کدام مرزهای عملیاتی (مانند تفاوت بین شکست میزبان و شکست پیام) برای یک مورد تجاری خاص اهمیت دارند. تصمیماتی در مورد اینکه ریسک ارسال تکراری از کجا شروع میشود، یا اینکه آیا Greylisting باید در کتابخانه باشد یا خارج از آن، مرزهای محصولی و عملیاتی هستند که نیاز به قضاوت انسانی دارند.
اگر شما فقط کد ارائه دهید، AI شکافها را با پیشفرضهای «محتمل» پر میکند. در محیط تولید، «محتمل» اغلب پیشدرآمدی برای یک قطعی فاجعهبار است. کیفیت در عصر AI مستلزم دانستن دقیق مواردی است که باید حفظ شوند و مستند کردن آنها به عنوان یک قرارداد قابل بررسی. مناطق خطرناک بهندرت خطاهای سینتکسی هستند؛ آنها خطاهای مرزیاند: مانند تفاوت نتیجه تحویل در برابر نتیجه پاکسازی، یا رد DATA شناختهشده در برابر شکست مبهم DATA.
برای بهبود گردشکار کمکگرفته از AI، بهجای ارائه صرفاً کد منبع، این موارد را اضافه کنید:
- اسناد طراحی و نمودارهای توالی (Sequence Diagrams)
- قوانین طبقهبندی خطاها
- قراردادهای ناوردا (Invariant contracts) و قراردادهای متریک
- محدودیتهای شناخته شده و مرزهای شکست سیاست Retry
- انتظارات مربوط به سازگاری (Compatibility)
سپس بهطور مشخص از AI بپرسید: «آیا این پیادهسازی واقعاً این قراردادها را برآورده میکند؟» این کار AI را از یک بررسیکننده استایل به یک بررسیکننده قرارداد طراحی تبدیل میکند و تضمین میکند که پیادهسازی از قصد اولیه فاصله نگیرد.
جمعبندی
نسخه 0.1.1.1 از PooledMailKit انتشار کوچکی است، اما فرآیند پشت آن اهمیت داشت. یک پیادهسازی با کمک AI یک کتابخانه مفید ساخت و یک بررسی مجزا توسط AI نقاطی را یافت که آن پیادهسازی در رعایت رفتار مستند شده شکست خورده بود. سپس این اصلاحات به تستهای رگرسیون، یادداشتهای انتشار و یادداشتهای سازگاری تبدیل شدند. بررسی AI زمانی ارزشمند نیست که کامنتها را برای بازنویسی پیدا کند؛ بلکه زمانی ارزشمند است که کمک کند تشخیص دهیم کجا پیادهسازی از «قصد» فاصله گرفته است. اما برای اینکه این اتفاق بیفتد، قصد باید نوشته شود. کد به تنهایی کافی نیست. قراردادها هستند که بررسی را ممکن میسازند.




گفتگو