تصور کنید یک عامل هوش مصنوعی را برای بهروزرسانی هزاران خط کد استخدام کردهاید، اما درست در لحظهی حساس، سیستم بهدلیل محدودیت زمانی خاموش شود و تمام هزینهها و زمان شما هدر رود. این کابوسِ رایج در پروژههای مقیاسبزرگ، حالا با یک معماری جدید به نام RepoModernizer به پایان رسیده است. در واقع، با وجود اینکه یک عامل AI میتواند مهاجرت کدهای یک پروژه را خودکار کند، اما این فرآیند همچنان شبیه به یک قمار پرهزینه است. این ریسکهای مالی در پروژههای عاملمحور پیشتر در تحلیلهایی دربارهی حلقههای تکرار مدلهای زبانی بررسی شده است که نشان میدهد چگونه یک اشتباه در مدیریت چرخه، میتواند هزینههای سرسامآوری ایجاد کند. برای بازنویسی فایلها و اجرای مجموعهی تستهای مقصد پس از هر تغییر، عامل باید ابتدا مخزن (Repo) را کلون کند، وابستگیها را نصب نماید و ترتیب فایلها را برنامهریزی کند. این مراحل در مخازن با اندازه متوسط، زمان واقعی زیادی میبرد و حجم بالای فراخوانیهای Bedrock را میطلبد. اگر یک فرآیند در فایل هفتم از دوازده فایل متوقف شود، شروع مجدد از ابتدا باعث هدررفت پول و زمانی میشود که برای شش فایل موفق اول صرف شده بود. در ۳۱ ژوئیه ۲۰۲۶، یک بررسی عمیق فنی روی RepoModernizer فاش کرد که چگونه میتوان با پیادهسازی پایداری واقعی از طریق معماری «محاسبات تفکیکشده» (Split-Compute)، از عاملهای ساده و «اسباببازی» فراتر رفت.
بسیاری از عامل (Agent) — همان دستیاران هوشمندی که میتوانند بهطور مستقل ابزارها را اجرا کنند — با سقف ۱۵ دقیقهای اجرای AWS Lambda دستوپنجه نرم میکنند. برای یک مخزن کد (Repository) متوسط، فرآیند مهاجرت بهطور معمول بسیار بیشتر از این زمان طول میکشد و این محدودیت را میشکند. این چالش مدیریت تکالیف طولانیمدت، موضوعی است که انویدیا نیز با معماریهای جدید خود برای جلوگیری از شکست عاملها به آن پرداخته است. توسعهدهندگان این پروژه برای حل این مشکل، مدیریت درخواستها (API) را از عملیات اجرایی (Worker) کاملاً جدا کردند.
تفکیک زیرساختی
در این معماری، RepoModernizer از یک اپلیکیشن FastAPI روی Lambda برای مدیریت درخواستهای اولیه استفاده میکند. این API از طریق Mangum و در پشت API Gateway اجرا میشود. وظیفهی این بخش بسیار ساده است: درخواست را میگیرد، هیچ دادهی مهمی را ذخیره نمیکند و صرفاً یک پیام را در صف SQS میاندازد. این API هرگز خودش عملیات مهاجرت را اجرا نمیکند.
سپس یک Lambda مصرفکننده (Consumer) پیام را از SQS میخواند و با فراخوانی ecs.run_task یک تسک تکبار (One-shot) در AWS Fargate را اجرا میکند. این تسک Fargate در واقع همان Worker یا کارگر است. این worker خطلوله LangGraph را تا پایان (یا تا زمانی که کراش کند) اجرا کرده و سپس خارج میشود.
از آنجایی که Fargate سقف ۱۵ دقیقهای ندارد، میتواند تسکهای طولانیمدت را برای نیم ساعت یا بیشتر اجرا کند. این امر تضمین میکند که عامل بتواند کار را بدون اینکه توسط زیرساخت کشته شود، به پایان برساند. همچنین چون هیچ بخشی بین دو اجرای مختلف «گرم» (Warm) نمیماند، هزینه حالت بیکاری (Idle cost) صفر است.

دو لایهی پایداری
برای اینکه عامل بتواند پس از یک کراش دوباره از همان نقطه شروع کند، سیستم کار فیزیکی را از پیشرفت منطقی جدا میکند. این کار مستلزم استفاده از دو ذخیرهساز مختلف است، زیرا هیچکدام به تنهایی برای بازگشت به وضعیت قبلی کافی نیستند:
- AWS EFS (Elastic File System): این لایه بایتهای واقعی کد را نگه میدارد. تسک Fargate سیستم EFS را در مسیر
/mnt/workspaceمتصل (Mount) میکند. تسک در این مسیر مخزن مقصد را کلون میکند، یک شاخه (Branch) میسازد و هر تغییر (Diff) که اعمال میکند، مستقیماً روی این سیستم فایل قرار میگیرد. وقتی یک تسک جدید همان شغل را بر عهده میگیرد، فایلها دقیقاً در همان حالتی که آخرین اجرا رها کرده بود، آنجا حضور دارند. - Amazon DynamoDB: این پایگاهداده، نقشه راه، نشانگر (Cursor) و وضعیت هر فایل را ذخیره میکند. هر گره در خطلوله LangGraph که با موفقیت به پایان برسد، به عنوان یک نقطه بازرسی (Checkpoint) ثبت میشود. این همان چیزی است که به یک پردازش جدید اجازه میدهد بفهمد که مراحل دریافت کد و نصب وابستگیها انجام شده و فایلهای ۰ تا ۶ مهاجرت شدهاند، بنابراین میتواند از فایل ۷ شروع کند.
ساخت یک Checkpointer سفارشی
در حالی که LangGraph ذخیرهسازهای آمادهای مانند savers در-حافظه ارائه میدهد (که برای انتقال بین پردازشهای مختلف کاملاً بیفایده هستند)، تیم یک BaseCheckpointSaver اختصاصی ساخت که مستقیماً توسط DynamoDB پشتیبانی میشد. این انتخاب به این دلیل بود که DynamoDB پیش از این در استک تکنولوژی آنها بود و تیم میخواست الگوهای دسترسی تک-جدولی (Single-table access patterns) را تحت کنترل داشته باشد.
جزئیات پیادهسازی
- مکانیسم Put: هستهی این سیستم متد
putاست. این متد نقطه بازرسی را با استفاده ازJsonPlusSerializerمربوط به LangGraph سریالسازی کرده و آن را تحت یک کلید تفکیک مخصوص هر تسک (TASK#{thread_id}) و یک کلید مرتبسازی برای نقطه بازرسی (CKPT#{checkpoint_id}) مینویسد. - مکانیسم Get: متد
get_tupleاین فرآیند را معکوس میکند. با داشتن یک Task ID و بدون نیاز به یک checkpoint خاص، این متد با استفاده ازScanIndexForward=FalseوLimit=1آخرین وضعیت ثبت شده را برای بازیابی میخواند. - مدیریت چرخه حیات: هر نقطه بازرسی دارای یک TTL (Time to Live) ۱۴ روزه است. این تضمین میکند که وضعیتهای قدیمی تسکها بهطور خودکار از جدول حذف شوند و فضای ذخیرهسازی اشغال نشود.
- اتصالات: در شروع سرد (Cold start)، Worker گراف را با این checkpointer متصل میکند:
graph = build_graph(deps, checkpointer=checkpointer). در اینجاthread_id(که همان Task ID است) باعث میشود پردازش در حال بازگشت، به تمام چیزهایی که پردازش قبلی نوشته بود، متصل شود.
معنای واقعی «ادامهکردن» (Resume)
باید در مورد معنای «ادامهکردن» در اینجا دقیق بود. Resume در سطح گره (Node) عمل میکند، نه در سطح بایتهای داخل یک فایل. اگر Worker هنگام مهاجرت فایل شماره ۷ کراش کند، از وسط آن فایل ادامه نمیدهد.
در اجرای بعدی، حلقه دوباره گره migrate_file را برای نشانگر فعلی از ابتدای گره اجرا میکند. فایل را دوباره از EFS میخواند، محتوای جدید را از مدل درخواست میکند، Diff را اعمال کرده و تستها را مجدداً اجرا میکند. فایلهای ۰ تا ۶ در وضعیت Checkpoint شده به عنوان «اتمام یافته» علامتگذاری شدهاند و دیگر لمس نمیشوند. بنابراین، عبارت «ادامهکردن در میانهی مهاجرت» در رابطه با پیشرفت کلی پروژه درست است، اما در رابطه با یک فایل واحد کهe interrupted شده، غلط است.
باگ «دوبار-اجرا» (Double-Resume)
پایداری باعث ایجاد یک نقص بحرانی در همزمانی شد. خطلوله شامل یک دروازهی «انسان-در-حلقه» (human-in-the-loop) است. وقتی برنامهریز ریسک یک فایل را بالاتر از حد معینی تخمین میزند، گره migrate_file متد interrupt() لنگگراف را با Diff مربوطه فراخوانی میکند و اجرا متوقف میشود. سپس یک انسان تغییر را تایید یا رد میکند. تایید به صورت یک اقدام مجزا ارسال میشود که گراف را از طریق Command(resume=...) از سر میگیرد.
از آنجایی که گره migrate_file در هر بار Resume از ابتدای خود اجرا میشود، زمانی که یک interrupt معلق وجود دارد، مشکلی نیست. اما مشکل زمانی پیش میآید که اقدام Resume دو بار اجرا شود — اتفاقی که بیشتر از حد انتظار رخ میدهد. این اتفاق به دلیل اینکه SQS ممکن است یک پیام را بیش از یک بار تحویل دهد، یا کاربر در حالی که یک Fargate در حال استارت-آپ سرد است و هنوز checkpoint را آپدیت نکرده، دوباره روی «تایید» کلیک کند، رخ میدهد.
در یک مورد واقعی، یک کلیک روی دکمه تایید، منجر به ایجاد ۶ تا ۹ کامیت یکسان در یک Pull Request واحد شد. تیم ابتدا سعی کرد با بررسی وضعیت با استفاده از graph.get_state(config) و چک کردن has_pending_interrupt این مشکل را حل کند، اما این الگوی «اول چک کن بعد عمل کن» (check-then-act) شکست خورد؛ زیرا دو فراخوان موازی میتوانستند هر دو وضعیت را True بخوانند و سپس هر دو عملیات Resume را اجرا کنند.
دستیابی به اتمیک بودن (Atomicity)
راه حل نهایی نیازمند یک «نوشت مشروط» (Conditional Write) در DynamoDB بود تا اتمیک بودن تضمین شود. تیم متد try_claim را پیاده کرد که یک put مشروط انجام میدهد: نوشتن تنها زمانی موفق میشود که آیتم (آن شناسه نقطه بازرسی خاص که قرار است Resume شود) هنوز وجود نداشته باشد.
# Example of the atomic claim
claimed = checkpointer.try_claim(task_id, f"resume:{checkpoint_id}")
if has_pending_interrupt and claimed:
result = graph.invoke(Command(resume={...}), config=config)
else:
result = snapshot.values
تنها اولین فراخواننده که موفق به claim کردن آن checkpoint خاص شود، اجازه invokes کردن گراف را دارد؛ فراخوانندههای بعدی مقدار False دریافت کرده و وضعیت موجود را بدون اجرای مجدد گره برمیگردانند. به عنوان یک اقدام احتیاطی اضافی (Belt and Suspenders)، گره finalize اکنون بررسی میکند که آیا تسک قبلاً یک URL برای PR ثبت کرده است یا خیر، و در صورت مثبت بودن، عملیات را متوقف میکند تا از ایجاد PRهای تکراری عمومی جلوگیری شود.
تلههای Git و سیستم فایل
فراتر از منطق وضعیت، تیم با دو باگ محیطی مربوط به متصل کردن EFS به کانتینری که Git را اجرا میکند مواجه شد:
- مالکیت مشکوک (Dubious Ownership): نقطه دسترسی EFS تمام فایلها را به UID 1000 میپیند. چون کانتینر با دسترسی root اجرا میشود، Git نسخه 2.35.2 و بالاتر به دلیل بررسی امنیتی CVE-2022-24765 مربوط به مالکیت مشکوک، از عملیات روی مخزن خودداری میکند. این مشکل با اجرای دستور
subprocess.run(["git", "config", "--global", "--add", "safe.directory", "*"], check=True)در هنگام استارتآپ حل شد. - خطوط جدید انتهایی (Trailing Newlines): یک مشکل مربوط به خط جدید بهطور خاص در مخازن JS و JSON ظاهر شد. مرحله Diff، فایلهای مقصد را نرمالسازی میکند تا در انتها یک خط جدید داشته باشند. با این حال،
difflibهرگز نشانگر «No newline at end of file» را ارسال نمیکند وgit applyمحتوا را با بایتهای واقعی تطبیق میدهد. در نتیجه، فایلهای بدون خط جدید انتهایی بهطور تمیز اعمال نمیشدند. این مورد در تستها نادیده گرفته شده بود چون کدهای منبع پایتون تقریباً همیشه با خط جدید به پایان میرسند.
درسهای آموخته شده
باگ کامیتهای تکراری یک درس معماری کلیدی را برجسته کرد: در ابتدا تلاش شد ایدمپوتنس (Idempotency) در لایه اشتباهی پیاده شود. یک بررسی وضعیت (State Check) نمیتواند همزمانی را حل کند؛ تنها یک «نوشت مشروط» (Conditional Write) قادر به این کار است. اگر این سیستم دوباره ساخته شود، claim اتمیک باید از همان ابتدا در هر مرز Resume پیادهسازی شود.
علاوه بر این، طراحی فعلی migrate_file که در هر بار Resume از ابتدا اجرا میشود، هزینهبر است؛ زیرا برای محتوایی که احتمالاً قبلاً تولید شده، دوباره هزینه فراخوانی Bedrock را میپردازد. یک طراحی بهینهتر، ذخیره (Cache) محتوای تولید شده در مقابل نقطه بازرسی بود. در واقع، شناسایی اینگونه ناکارآمدیها و بدهیهای فنی در کدهای تولیدشده توسط AI، نیازمند رویکردهایی مانند Vibe Coding است تا نقاط شکست مدلها دقیقتر شناسایی شوند. در نهایت، پایداری یک تثلیث است: وضعیت پایدار، ذخیرهساز پایدار و منطق Resume ایدمپوتنت.
گام بعدی شما
- اگر از عاملهای Lambda-based استفاده میکنید، معماری Split-Compute (جداسازی API از Worker) را برای تسکهای طولانی پیاده کنید.
- برای مدیریت وضعیت عاملها، به جای ذخیرهسازهای در-حافظه (In-memory)، از DynamoDB با قابلیت Conditional Writes استفاده کنید تا از اجرای تکراری گرهها جلوگیری شود.
- در محیطهای کانتینری که از EFS استفاده میکنند، تنظیمات
safe.directoryگیت را در اسکریپت استارتآپ قرار دهید.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو