تصور کنید تیم onedev.io در تلاش است تا پلتفرم خود را از یک فریمورک قدیمی وب به Next.js منتقل کند و برای این کار از یک عامل کدنویسی (Coding Agent) استفاده میکند. آنها در این مسیر با درس سختی در قالب یک تأخیر ۲۵۰ میلیثانیهای مواجه شدند؛ شکافی که در واقع یک راهکار فنی نبود، بلکه نشانهای از یک شکست سیستماتیک بود.
در عصر کنونی که به آن «کدنویسی بر اساس حس» یا Vibe Coding میگویند، توسعهدهندگان بهطور فزایندهای کل مهاجرتهای معماری را به عاملهای هوش مصنوعی میسپارند. این تغییر، نقش برنامهنویس را از یک کدنویس خطبهخط به یک داور سطحبال برای «حس و حال معماری» تبدیل میکند. وقتی یک عامل کدی تولید میکند که بهظاهر کار میکند اما «حسی» نادرست دارد، تنها چیزی که مانع از تبدیل محیط عملیاتی (Production) به کوهی از وصلههای شکننده میشود، قضاوت توسعهدهنده است. این چالش با پروژه Loupe که بر شناسایی باگهای خاموش در کدهای AI تمرکز داشت همسو است، جایی که کدهای تولید شده ممکن است تستها را پاس کنند اما در لایههای عمیقتر دارای نقص باشند.
تلهی وصلههای پذیرفتنی
تیم در طول این مهاجرت با یک باگ بحرانی روبرو شد: قابلیت «خروج از حساب» (Sign Out) — که یکی از عادیترین عملیاتها در هر وبسایتی است — تنها گاهی اوقات کار میکرد. پس از کلیک بر روی دکنه خروج، کاربر به صفحه اصلی بازگردانده میشد، اما گاهی اوقات حتی پس از انجام یک رفرش سخت (Hard Refresh)، همچنان میتوانست صفحاتی را باز کند که مخصوص کاربران وارد شده (Signed-in) بود.
این رفتار به شکلی متناقض و نامنظم بود:
- گاهی در مرورگر Safari کار میکرد اما در Chrome شکست میخورد.
- پاک کردن تمام کوکیها تنها یک راهکار موقت ارائه میداد.
- باز کردن ابزارهای توسعهدهنده مرورگر (Developer Tools) در واقع احتمال کارکرد صحیح این ویژگی را افزایش میداد.
از آنجایی که عاملهای کدنویسی در یافتن «پذیرفتنیترین اصلاح بعدی» بسیار ماهر هستند، این عامل وارد یک چرخه از وصلهزنیهای محلی شد. اگر کوکی حذف نشده بود، عامل نحوه مدیریت کوکی را تنظیم میکرد. اگر دو مرورگر رفتار متفاوتی داشتند، او رفتار مرورگرها را بررسی میکرد. اگر نتیجه به زمانبندی (Timing) وابسته بود، یک تأخیر اضافه میکرد. هر گام منطقی به نظر میرسید، اما خطر این بود که یک زنجیره طولانی از وصلههای منطقی میتوانست به راهکاری منجر شود که بهظاهر کار کند، بدون اینکه هرگز به مشکل واقعی بپردازد. این وضعیت دقیقاً مشابه «حلقه مرگ» یا Doom Loop است که در چارچوب Agent Rigor برای جلوگیری از توهمات کدنویسی AI مورد بررسی قرار گرفته است.
همانطور که عامل در حال عیبیابی بود، چندین گام را بر اساس علائم ظاهری پیشنهاد داد:
- حذف هر شکل ممکنی از کوکیهای نشست (Session Cookies).
- ایجاد یک نقطه پایانی (Endpoint) اختصاصی برای خروج.
- مستثنی کردن این نقطه پایانی اختصاصی از مدیریت احراز هویت سراسری.
- تغییر نحوه مدیریت کلیک در صورتی که ناوبری (Navigation) خیلی زود شروع میشد.
- منتظر ماندن برای پاسخ سرور پیش از بازگشت به صفحه اصلی.
هیچکدام از این تلاشها احمقانه نبودند، اما همگی بر روی لایه اشتباهی متمرکز بودند. هوش مصنوعی تلاش میکرد یک اقدام شکستخورده را «مستحکمتر» کند، به جای اینکه بپرسد چرا این اقدام در وهله اول شکست میخورد. لیست رو به رشدِ «مدیریتهای خاص» برای یک عملیات روتین، خود مهمترین مشاهده در این مسیر بود.
راهکار «۲۵۰ میلیثانیهای»
سرانجام، عامل راهکاری ارائه داد که بهطور قابل اعتمادی کار میکرد. اپلیکیشن یک صفحه کوتاه با متن «در حال خروج...» نمایش میداد، ۲۵۰ میلیثانیه صبر میکرد و سپس به صفحه اصلی میرفت. پیادهسازی آن به این شکل بود:
Signing out…

در حالی که این راهکار از نظر فنی موفق بود، اما از نظر منطقی مضحک بود. یک خروج روتین هرگز نباید به یک وقفه تصادفی وابسته باشد. توسعهدهنده با سؤالاتی تکاندهنده باقی ماند: چرا ۲۵۰ میلیثانیه؟ آیا ۱۰۰ میلیثانیه کافی بود؟ آیا در یک اتصال کندتر باز هم کار میکرد؟ چرا باز کردن ابزارهای توسعهدهنده باید چیزی را تغییر دهد؟
این تأخیر یک پاسخ رضایتبخش نبود، بلکه گواه این بود که دو اتفاق در ترتیب اشتباه رخ میدهند و یک «شرایط رقابتی» (Race Condition) ایجاد شده است که هوش مصنوعی صرفاً با یک تایمر روی آن سرپوش گذاشته بود. مرورگر جایی بود که مشکل در آن «مرئی» میشد، نه جایی که مشکل در آن «ساکن» بود.
پرامپت «گام به عقب»
در این داستان، نقش توسعهدهنده منحصربهفرد بود: او کاملاً با React و Next.js غریبه بود. او برای مهاجرت وبسایت و بررسی مشکل، بهطور کامل به عامل کدنویسی متکی بود. او میتوانست آنچه را میدید توصیف کند و تغییرات را آزمایش کند، اما نمیتوانست به عامل بگوید از کدام ویژگی فریمورک استفاده کند یا کدام بخش از معماری را بازرسی نماید.
به همین دلیل، او نمیتوانست یک تشخیص فنی ارائه دهد. در عوض، از یک پرامپت مستقل از تکنولوژی برای به چالش کشیدن نتیجه استفاده کرد:
«این یک گردشکار بسیار رایج است. نباید به یک ترفند زمانی نیاز داشته باشد. لطفاً کد مربوطه را بازبینی کن، بررسی کن که آیا پیادهسازی از قراردادهای رایج و بهترین روشها (Best Practices) پیروی میکند یا خیر، ریشه مشکل را پیدا کن و این راهکار موقت را حذف کن.»
این پرامپت جهت investigation (بررسی) را تغییر داد. این دستور، دیدگاه عامل را از «وصله زدن روی علامت» به «بازبینی سیستم» تغییر داد. به جای تمرکز بر مستحکم کردن عملیات خروج، عامل یک گام به عقب رفت تا کل گردشکار احراز هویت را بازبینی کند: کجا نشست ایجاد میشد، کجا بررسی میشد، کجا رفرش میشد و کجا حذف میشد.
ریشه مشکل: مسئولیت تکراری
بررسیها فاش کرد که در طول مهاجرت، یک رفتار قدیمی در سطح درخواست (Request-wide) منتقل شده بود. دستورالعمل اصلی در طول مهاجرت این بود که «رفتارهای موجود حفظ شوند» در حالی که فریمورک تغییر میکند. این امر باعث شده بود پیادهسازی جدید، یک پوشش (Wrapper) احراز هویت سراسری را حفظ کند.
در فرم ساده شده، کد به این شکل بود:
export default auth((request) => { // آمادهسازی اطلاعات مورد نیاز برای وبسایت مهاجرت شده. // احراز هویت تقریباً برای هر درخواست اجرا میشود. return NextResponse.next(); });
این پوشش راحت بود زیرا اطلاعات احراز هویت را در همه جا در دسترس قرار میداد. با این حال، اپلیکیشن دوباره دسترسی را در اطراف صفحاتی که واقعاً به کاربر وارد شده نیاز داشتند، بررسی میکرد. به زبان ساده، دو بخش مجزا از اپلیکیشن مسئولیت یک چیز واحد را بر عهده داشتند.
این تکرار باعث ایجاد یک شرایط رقابتی شد. درخواست خروج به مرورگر میگفت که نشست را حذف کند، اما یک درخواست صفحه در نزدیکی آن که از پوشش سراسری عبور میکرد، همان نشست را رفرش میکرد. هر پاسخی که مرورگر در نهایت پردازش میکرد، تعیین میکرد که کاربر «وارد شده» به نظر برسد یا «خارج شده». این موضوع تمام رفتارهای عجیب را توضیح میداد: مرورگرهای مختلف، ابزارهای توسعه و تأخیر ۲۵۰ میلیثانیهای صرفاً زمانبندیِ اینکه چه کسی در این رقابت پیروز میشود را تغییر میدادند.
درمان ساختاری
راهکار این بود که به هر بخش از اپلیکیشن، یک وظیفه شفاف داده شود. هندلر درخواست سراسری به یک پروکسی ساده تبدیل شد که تنها اطلاعات لازم برای صفحه مهاجرت شده را از طریق هدرها (headers) فراهم میکرد:
export default function proxy(request: NextRequest) { const headers = new Headers(request.headers); headers.set("x-pathname", request.nextUrl.pathname); return NextResponse.next({ request: { headers }, }); }
صفحاتی که واقعاً به کاربر وارد شده نیاز داشتند، همچنان خودشان را محافظت میکردند. کتابخانه احراز هویت نیز به مدیریت خروج از طریق تابع داخلی استاندارد خود بازگشت:
export async function logout() { await signOut({ redirectTo: "/" }); }
با این تغییر ساختاری، تیم توانست نقطه پایانی سفارشی، حذفهای دستی کوکی، مدیریت خاص کلیکها و تأخیر ۲۵۰ میلیثانیهای را حذف کند. خروج در هر دو مرورگر Chrome و Safari بهطور قابل اعتمادی کار میکرد. کد نهایی نه تنها درستتر بود، بلکه بسیار سادهتر از نسخه وصلهزده شده بود؛ نشانه قدرتمندی که نشان میداد ریشه واقعی مشکل پیدا شده است.
تحلیل: مهارتهای جدید توسعهدهنده
این مورد یک روند خطرناک در توسعه با کمک هوش مصنوعی را نشان میدهد: انباشت «بدهی فنی هوش مصنوعی» (AI Technical Debt). از آنجایی که عاملها میتوانند کدهای کاراکرد را سریع تولید کنند، هزینه یک راهکار «زشت اما کارآمد» در کوتاهمدت صفر است، اما هزینه نگهداری بلندمدت آن بسیار عظیم است. Vibe Coding به معنای پذیرفتن هر کدی که اتفاقاً کار میکند نیست.
برای توسعهدهنده مدرن، ارزشمندترین مشارکت دیگر نحو (Syntax) فنی نیست، بلکه توانایی تشخیص این است که چه زمانی «شکل» یک راهکار نادرست است. شما ممکن است ندانید کوکیهای نشست دقیقاً چگونه کار میکنند، اما میدانید که خروج از حساب یک عملیات روتین است. ممکن است سیستم روتینگ فریمورک را نشناسید، اما میدانید که باز کردن ابزارهای توسعهدهنده نباید رفتار محیط عملیاتی را اصلاح کند.
اگر یک ویژگی ساده، مجموعهای از تأخیرها، تکرارهای مجدد، منطقهای تکراری و شاخههای مخصوص به مرورگر را جمعآوری کرده است، این سیگنالی است برای توقف درخواستِ «اصلاحات محلی». یک پرامپت مفید و مستقل از تکنولوژی برای استفاده در این لحظات عبارت است از:
«این یک گردشکار رایج است، اما راهکار شبیه به یک ترفند (Hack) است. لطفاً یک گام به عقب بردار و کل گردشکار را بازبینی کن. کد مربوطه را با قراردادهای رایج و بهترین روشهای فریمورک تطبیق بده، ریشه مشکل را پیدا کن، طراحی را ساده کن و راهکار موقت را حذف کن.»
استفاده از وصلهها به عنوان شواهد، نه معماری
تأخیر ۲۵۰ میلیثانیهای کار بیهودهای نبود؛ این تأخیر ثابت کرد که زمانبندی اهمیت دارد و به محدود کردن دامنه بررسی کمک کرد. وصلههای موقت آزمایشهای مفیدی هستند، اما اشتباه این است که آن آزمایش را در جای خود باقی بگذاریم و تسک را «تمام شده» اعلام کنیم. این رویکردی است که در روشهای مهندسی آشوب Strands Evals برای شناسایی نقاط شکست نیز دیده میشود، جایی که از رفتارهای غیرمنتظره برای سختتر کردن سیستم استفاده میشود.
وقتی یک راهکار موقت باعث ناپدید شدن باگ میشود، بپرسید که آن راهکار چه چیزی را تغییر داد:
- یک تأخیر، ترتیب اجرای عملیات را تغییر میدهد.
- یک تکرار مجدد (Retry) ممکن است وضعیت (State) غیرقابل اعتماد را پنهان کند.
- پاکسازی دستی ممکن است جبرانی برای این باشد که دو کامپوننت مسئولیت یک چیز واحد را بر عهده دارند.
- یک شاخه مخصوص مرورگر ممکن است مشکلی را پنهان کند که متعلق به سمت سرور است.
راهکار موقت حاوی سرنخی درباره ریشه مشکل است. با آن به عنوان یک «شاهد» برخورد کنید و سپس پس از اصلاح طراحی، آن را حذف کنید.
ساده کردن دوبارهی مسیرهای ساده
ارزشمندترین مشارکتی که در این جلسه صورت گرفت، فنی نبود. توسعهدهنده متوجه نشد که پوشش احراز هویت سراسری مشکل اصلی است و معماری نهایی را پیشنهاد نداد. او صرفاً باور نکرد که یک گردشکار روتین باید به یک تأخیر تصادفی نیاز داشته باشد.
این درس از onedev.io آموخته شد، جایی که عاملهای کدنویسی اکنون در جریانهای کاری Issue، Pull Request، Workspace و CI/CD ادغام شدهاند. این اصل بسیار فراتر از احراز هویت کاربرد دارد: کدِ کارکرد، یک نقطه عطف مهم است، اما وقتی یک ویژگی عادی به یک راهکار خارقالعاده (و عجیب) نیاز دارد، نباید آن را خط پایان بدانیم. توسعهدهندگان باید PRهای اخیر تولید شده توسط هوش مصنوعی را برای یافتن «اعداد جادویی» یا تأخیرهای تصادفی بازرسی کنند؛ اینها اغلب نشانگرهایی برای شرایط رقابتی عمیقتر در معماری هستند.




گفتگو