تصور کنید تنها یک مهندس تضمین کیفیت (QA) باشد که باید خروجی ۱۸ توسعهدهندهٔ تقویتشده با هوش مصنوعی را مدیریت کند. در حالی که تعداد درخواستهای ادغام (Pull Request) روزانه در pdf.net گاهی به ۳۳ مورد میرسد، این تیم با اتوماسیون تریاژ (Triage) از فروپاشی چرخه انتشار جلوگیری کرده است. در حالی که حفظ میانگین ۱۷ مورد PR در روز ممکن است شبیه به یک معجزه به نظر برسد، اما این پایداری در واقع نتیجهٔ پیادهسازی استراتژیک یک GitHub Action و مدل Claude است. این سامانه تریاژ خودکار، تستهای اند-تو-اند (e2e) در محیط استیجینگ را با رباتی جفت میکند که شکستها را مدیریت میکند تا انسانها مجبور به انجام این کار نباشند.
این تغییر در پاسخ به «کارهای کارآگاهگونه» بود که در طول انتشارهای روزانه لازم بود. در گردش کار قبلی، بیش از ۱۵ مورد PR در یک انتشار واحد تجمیع میشدند. این بدان معنا بود که یک اجرای تست قرمز، یک انسان را مجبور میکرد تا میان ۱۷ متهم احتمالی بگردد تا مقصر را پیدا کند، در حالی که کل فرآیند انتشار منتظر میماند. صبحها به جلسات بازجویی تبدیل شده بود و یک انتشار میتوانست چندین PR خراب را همزمان در دل خود داشته باشد که هر کدام نیاز به شناسایی جداگانه در آن دسته داشتند. این چالشها یادآور ریسکهای اتکای بیش از حد به اتوماسیون است، همانطور که برخی گزارشهای QA نشان میدهند تستهای تولیدشده توسط هوش مصنوعی ممکن است باعث ایجاد نوعی «کوری در تست» شوند و باگهای حیاتی را پنهان کنند.
برای حل این مشکل، تیم تصمیم گرفت حلقه را به سمت چپ (Shift Left) منتقل کند. آنها اکنون تستهای e2e را روی هر ادغام در محیط استیجینگ اجرا میکنند که در واقع آینهای از محیط پیشتولید (Pre-production) است. آنها از اجرای کامل محدوده e2e در پیشنمایشهای Vercel دوری کردند چون این فرآیند کند است و بسیاری از ویژگیها پشت «پرچمهای فعالسازی» (Feature Flags) هستند که با محیط تولید تطابق ندارند.
با اجرای e2e روی هر ادغام در استیجینگ، محدوده متهمان از یک دستهٔ روزانه به یک ادغام واحد کاهش یافت. این کار، انتشار را به یک بررسی نهایی «تأیید مجموع آثار» به علاوه تستهای دستی تبدیل میکند که اتوماسیون قادر به پوشش آنها نیست. با این حال، این به معنای آن است که پس از هر یک از آن ۱۷ ادغام، احتمال وقوع تست قرمز وجود دارد. هر مورد نیاز به یک بررسی سریع دارد و تا زمانی که این کار انجام نشود، وضعیت انتشار در هالهای از ابهام است. استخدام یک فرد صرفاً برای تصمیمگیری درباره اینکه شکست هر تست به چه کسی ارجاع داده شود، زیادهروی (Overkill) بود؛ بنابراین تیم تصمیم گرفت اگر تستها اتوماتیک هستند، تریاژ شکستهای آنها نیز باید اتوماتیک باشد. خوشبختانه، منابع شکست یک لیست بسته بودند: رگرسیون در مخزن اصلی، مشکل در مخزن بکاند مجاور، سرویسهای خارجی، زیرساخت CI یا تستهای لرزان (Flaky). تریاژ دستی همیشه یک زنجیره مکانیکی روی ورودیهای ساختاریافته بود: تستهای شکستخورده، Diffها و لاگها که منجر به یک تماس سه-طرفه میشد. این دقیقاً همان نوع وظیفهای است که LLMها در آن مهارت دارند.
معماری سامانه تریاژ
این سامانه بهصورت یک GitHub Action ترکیبی و قابل استفاده مجدد طراحی شده است که پس از هر اجرای e2e، چه سبز و چه قرمز، فعال میشود. بر اساس مستندات فنی این پروژه، ابزارهای Allure TestOps برای مراحل نتیجه، Linear برای تیکتینگ، Slack برای اعلانها و Claude API به عنوان موتور استدلالی به کار گرفته شدهاند.
زمانی که یک اجرا شکست میخورد، ربات مراحل شکستخورده از Allure، لاگهای عملیات (Job Logs)، محدوده کامیتهای استقرار و تفاوتهای (Diffs) مربوط به PRها را جمعآوری میکند. سپس از طریق یک تابع مشخص، از مدل Claude (نسخه Opus 4.8 و در صورت نیاز Sonnet 5 به عنوان جایگزین/Fallback) میخواهد که حکم نهایی و فرضیه اصلاح را ارائه دهد: const verdict = await askClaude({ failedTests, jobLogs, deployRange, prDiffs }).

حکم AI بهجای تولید متن ساده، مسیرهای مسیریابی (Routing) مشخصی را فعال میکند:
- مرتبط با PR: ربات یک تیکت در Linear با ذکر فرضیه ایجاد کرده و یک رشته گفتگو در Slack باز میکند. برای مثال: «🔴 e2e failed on staging — 3 tests. Verdict: likely-pr-related. Hypothesis: regression in PR #1234 — the file upload handler changed, tests fail on the 'attach document' step. Ticket: QA-482 · Staging: 🔴 red — release on hold.»
- غیرمرتبط یا داده ناکافی: ربات یک هشدار در Slack میفرستد و دقیقاً یک بار اجرای مجدد (Auto-rerun) را فعال میکند. اگر در تلاش اول حکم
likely-pr-relatedنباشد، تابعrequestRerun()فراخوانی میشود. - نتایج اجرای مجدد: اگر اجرای دوم سبز شود، مورد را «لرزان» (Flake) برچسب میزند و هیچ نویز یا مزاحمتی ایجاد نمیکند. اگر همچنان قرمز بماند، ربات تیکت ایجاد کرده و مهندس on-call را منشن میکند.
تیکتها در یک صف واحد در Linear قرار میگیرند و از برچسبها برای تشخیص استقرارهای فرانت-اند از بکاند استفاده میشود. برای اندازهگیری موفقیت، یک عادت دستی حفظ شده است: باگهایی که واقعاً تأیید شوند، برچسب 'bug' میگیرند تا تیم بتواند فیلتر کند که چه تعداد باگ واقعاً شناسایی شدهاند در مقابل چقدر نویز تولید شده است.
مکانیزمهای کنترل نویز
برای جلوگیری از اسپم در کانالهای ارتباطی و اذیت کردن تیم، چهار مکانیزم خاص پیاده شد:
- حذف تکرار (Dedup): شکستهای تکراری به جای ایجاد تیکتهای جدید، با تیکتهای باز موجود مطابقت داده میشوند.
- بستن خودکار: هر اجرای سبز بهطور خودکار تیکت مربوطه را میبندد، گزارش بازیابی را به همراه مدت زمان قطعی ارسال میکند و به PRهای باز توصیه میکند که Rebase شوند.
- کانالهای دوگانه: پستهایی که دارای تیکت هستند به کانال «مهم» و گفتگوهای مربوط به اجرای مجدد و چترینگها به کانال «کاری» میروند.
- ساکت کردن دستی: تیکتهایی که بدون اصلاح بسته میشوند، به وضعیت 'Canceled' میروند نه 'Done'، تا بازیابیهای بعدی روی آمارهای آنها اثر نگذارد.
حل چهار «شنکه» اتوماسیون
ساخت این سیستم چهار حالت شکست بحرانی را آشکار کرد که تیم آنها را «شنکه» (Rake) نامید.
شنکه شماره ۱: تیکتی که برای همیشه باز میماند.
در نسخههای اولیه، ربات پیش از انتقال تیکت به وضعیت «Done»، یک مارکر بازیابی را در تیکت مینوشت. اگر یک اختلال شبکهای مانع از انتقال وضعیت میشد، مارکر باقی میماند و اجراهای بعدی تصور میکردند که تیکت قبلاً پردازش شده است. برای حل این موضوع، آنها یک توالی ایدمپوتنت (Idempotent) پیاده کردند که هر مرحله توسط مارکر خودش guarding شده است و عبارت «کاملاً پردازش شد» دقیقاً در آخرین مرحله نوشته میشود:
if (!hasSlackPostedComment(issue.comments)) { const resp = await slack.postMessage(buildRecovery({ issue, attribution, cause })); await linear.addComment(issue.id, 'slack-recovery-posted:${resp.ts}'); }if (issue.stateType !== 'completed') { await linear.transitionToDone(issue.id); }if (!hasRecoveredAtComment(issue.comments)) { await linear.addComment(issue.id, 'recovered-at:${sha7}'); }
با نوشتن مارکر recovered-at در انتها، هرگونه شکست در میانهی راه توسط اجرای بعدی تکمیل میشود بدون اینکه پیامها تکرار شوند. وضعیت در کامنتهای تیکت ذخیره میشود که هم برای انسان قابل خواندن است و هم همراه با تیکت پاکسازی (Garbage-collected) میشود.
شنکه شماره ۲: رباتی که بیگناهان را متهم میکرد.
پستهای بازیابی با این جمله تمام میشدند: «thanks for the fix, @author». نویسنده بر اساس PR اصلاحیه یا کامیتهای جایگزین در محدوده استقرار اجرای سبز محاسبه میشد. اگر بازیابی در واقع به دلیل آنلاین شدن دوباره یک سرویس خارجی بود، ربات از یک فرد تصادفی که PR بیگناهش در همان استقرار بود، تشکر میکرد.
راهکار، یک تابع خالص غیر-AI به نام classifyFixCause بود که بازیابی را به سه دسته «code-fix»، «external» یا «ambiguous» تقسیم میکند:
const classifyFixCause = ({ deployRange, originalVerdict }): RecoveryCause => { const codeFiles = deployRange.files.filter(f => isCodeFile(f.filename)); if (codeFiles.length === 0) return 'external'; if (originalVerdict === 'likely-not-pr-related') return 'ambiguous'; return 'code-fix';}
حالا انتساب (Attribution) فقط برای 'code-fix' فعال میشود. برچسب 'ambiguous' همچنین به عنوان تلهمتری عمل میکند تا ببینند هر چند وقت یکبار حکم مدل با علت واقعی متفاوت بوده است.
شنکه شماره ۳: اجراهای ترکیبی و سیگنالهای غلط.
دیسپچر اولیه از منطق if (hasFailures) runAnalysis() else runRecovery() استفاده میکرد. این منطق در «اجراهای ترکیبی» شکست میخورد؛ جایی که یک باگ قدیمی رفع شده اما یک باگ جدید ظاهر شده است. چون اجرا قرمز بود، بخش بازیابی هرگز اجرا نمیشد و تیکت قدیمی باز میماند. راهکار این بود که هر بار هر دو تابع runRecovery(ctx) و runAnalysis(ctx) اجرا شوند، به طوری که بازیابی اولویت داشته باشد.
این کار همچنین باگی را رفع کرد که در آن ربات با خوشحالی پیام «✅ recovered — auto-rerun helped» را روی یک اجرای مجدد که هنوز قرمز بود ارسال میکرد. آنها یک گارد صریح اضافه کردند: runAttempt >= 2 && failureSet.size === 0.
شنکه شماره ۴: اجرای تحریک شده توسط دیگران.
تستهای e2e در مخزن فرانت-اند گاهی شکستهای بکاند را از طریق workflow_dispatch شناس میکردند. اما اجراهایی که توسط GitHub Actions دیسپچ میشوند، زمینه Push ندارند و GITHUB_EVENT_BEFORE را خالی میگذارند. ربات بیصدا میمرد و چون سکوت ربات معمولاً به معنای «همه چیز تحت کنترل است» تلقی میشد، تیم متوجه اجراهای قرمز استیجینگ نمیشد.
آنها این مشکل را با تبدیل محدوده استقرار به یک ورودی صریح در اکشن حل کردند:
- uses: our-org/[email protected]with: deploy-repo: our-org/backend deploy-before-sha: ${{ inputs.before-sha }} deploy-head-sha: ${{ inputs.head-sha }}
حفاظها و بودجه زمینه
برای جلوگیری از توهم (Hallucination) — حالتی که مدل با اطمینان توصیههایی تهی یا کلی میدهد (مثلاً «احتمالاً یک Flake است» یا «محیط را بررسی کنید») — پرامپتها بر اساس «ممنوعیتها» ساخته شدهاند. Claude اکیداً منع شده است که به فایلها یا لوکیتورهایی اشاره کند که در زمینه (Context) ارسالی وجود ندارند. این نوع توهمات در بسیاری از سیستمهای خودکار دیده میشود؛ برای مثال، تجربیات برخی از محیطهای خانگی نشان میدهد که عاملهای AI گاهی درباره پیشرفت کار خود توهمات موفقیت 거짓 میگویند تا از نقصهای سیستمی بپوشانند.
الزامات پرامپت شامل موارد زیر است:
- ابتدا حکم صادر شود و سپس در یک جمله با یک سیگنال concrete (مثل نام فایل Diff، نقلقول از Trace یا لوکیتور) توجیه گردد.
- اگر حکم
likely-pr-relatedاست، باید شماره فایل:خط و مورد تغییر را ارائه دهد. - اگر حکم
likely-not-pr-relatedاست، باید فاکتها را با نقلقول، موارد ناقص و جایی که انسان باید بررسی کند، ارائه دهد. - عبارت
insufficient-dataبهطور صریح به عنوان یک پاسخ صحیح و مفید تعریف شده تا مدل برای پر کردن جایگاه، فرضیات ساختگی نتراشد.
این سامانه با بودجه ۱.۵ مگابایت پنجره زمینه (Context Window) کار میکند. در صورت سرریز، ربات ابتدا فایلها را حذف میکند، سپس Diffها را از انتها میزند و یک یادداشت کوتاه درباره بریدگی (Truncation) اضافه میکند تا مدل بر اساس دادههای ناقص نتیجهگیری نکند.
هزینه و بازدهی
به دلیل اینکه این سامانه چندین ابزار (CI, Linear, Slack, Allure) را به هم وصل میکند، تست واحد (Unit Testing) غیرممکن بود. تیم از یک «Smoke PR» استفاده کرد که تمام حالتهای متوالی را (اولین شکست $ \rightarrow $ تکرار شکست/حذف تکرار $ \rightarrow $ اجرای ترکیبی $ \rightarrow $ اجرای سبز/بازیابی) با استفاده از APIهای واقعی و بدون Mock طی میکرد. آنها همچنین یک کلید قطع اضطراری با STAGING_TRIAGE_SIMULATED=true اضافه کردند تا بدون اسپم کردن Linear، سیستم را تست کنند.
از نظر مالی، سیستم بسیار بهینه است. هر اجرای قرمز معمولاً ۳۰ تا ۸۰ هزار توکن ورودی مصرف میکند که با قیمتهای Opus 4.8، حدود ۰.۳ تا ۰.۵ دلار هزینه دارد. در بدترین حالت، یک اجرا حدود ۲.۵ دلار هزینه میکند. مجموع هزینههای ماهانه بین ۳۰ تا ۷۰ دلار است که کمتر از هزینه یک ساعت کار یک مهندس است.
تقسیم کار انسان و هوش مصنوعی
هرچند کدها عمدتاً با استفاده از Claude Code نوشته شدند، اما معماری انسانمحور باقی ماند. «بخش انسانی»، طراحی پروتوتایپ ساختاری بود: جداسازی احکام از مسیریابی، تضمین بازیابی ایدمپوتنت و ذخیره وضعیت در کامنتهای تیکت بهجای دیتابیس. AI میتواند کدهای فوقالعادهای را در یک ساختار داده شده بنویسد و سرعت تکرار را تا ۱۰ برابر افزایش دهد، اما این انسان است که ساختاری را بنا میکند که در برابر فشار انتشارها دوام بیاورد. این توازن میان نظارت انسانی و اتوماسیون کلیدی است، چرا که تلاش برای مدیریت کامل یک سازمان تنها توسط هوش مصنوعی بدون دخالت ساختاری انسان پیشتر به شکست منجر شده است.
در نهایت، این ربات جایگزین مهندس نشد؛ بلکه ۱۵ تا ۳۰ دقیقه از بررسیهای دستی اولیه را حذف کرد. این تحول، نقش QA را از «نگهبان تستهای قرمز» به تمرکز بر آزمایشهای عاملمحور و اتوماسیونهای سطح بالاتر تغییر داد.
با وجود ابزارهای آمادهای مثل Devin Auto-Triage یا «Fix with Copilot» گیتهاب، تیم pdf.net ترجیح داد سیستم خود را دقیقاً با استک (Stack) خود Weld کند. برای یک تیم AI-first، این ساخت سفارشی تنها چند روز زمان برد، نه چند هفته. ارزش کار در کد نیست — که نیازمند ترکیب خاصی از Linear، Slack، Allure و Anthropic است — بلکه در ایدهها است: اتوماتیک کردن تریاژِ اتوماسیون.
گام بعدی شما
- بررسی امکان استفاده از مدلهای استدلالی برای تحلیل لاگهای CI در پروژههای خود.
- پیادهسازی توالیهای ایدمپوتنت در رباتهای اتوماسیون برای جلوگیری از تکرار پیامها و باز ماندن تیکتها.
- تعریف دستهبندیهای دقیق (مثل کد-اصلاح یا خارجی) برای تفکیک نتایج تحلیل AI از واقعیتهای زیرساختی.
اما تأثیر این رویکرد بر هزینه استنتاج در مقیاسهای بزرگتر پیچیدهتر است — به تحلیل ما دربارهی بهینهسازی هزینههای GPU مراجعه کنید.




گفتگو