سقف هفتگی سهمیه هوش مصنوعی و فرق «تمام شد» با «خراب شد»
تولید تصویر جلد مقالهها بیدلیل شکست میخورد. علت سقف هفتگی سهمیه بود که شبیه خرابی ظاهر میشد؛ طبقهبندی خطا با 429 و 5xx راهش را باز کرد.
تولید تصویر جلد مقالهها در چرخه خودکار ما گاهی شکست میخورد و این شکستها الگوی روشنی نداشت. بعضی روزها همهچیز درست پیش میرفت و بعضی روزها بیدلیل ظاهری خطا میگرفتیم. بعد از ردگیری معلوم شد سرویس تولید تصویر فقط محدودیت چندساعته نداشت و یک سقف هفتگی هم داشت. پیغام خطای این سقف شبیه خطای عادی بود و سیستم ما هم هر خطا را «خرابی» میدانست. این یادداشت درباره همین تفاوت است: چیزی که تمام شده با چیزی که خراب شده فرق دارد، و کد وضعیت HTTP باید این فرق را به سیستم و به انسان بگوید.
ماجرا و علامتها
در خط تولید محتوای ما، یک مرحله تصویر جلد مقاله را از یک سرویس هوش مصنوعی میگیرد. این مرحله روزهایی شکست میخورد که بهنظر ما دلیلی نداشت. نه تغییری در کد داده بودیم، نه زیرساخت مشکلی نشان میداد، و فردا همان کار بدون دخالت ما دوباره جواب میداد.
علامت دوم هشدارها بود. هر شکست تولید تصویر یک هشدار «سرویس خراب است» میساخت. نتیجه این بود که هشدار میآمد، ما سراغ بررسی میرفتیم و چیزی برای تعمیر پیدا نمیکردیم. هشداری که بارها بیمورد بیاید کمکم نادیده گرفته میشود، و این برای یک سیستم پایش از خود خطا بدتر است.
از بیرون دو حالت کاملاً متفاوت در یک ظرف ریخته شده بود: «سرویس نمیتواند کار کند» و «سرویس فعلاً اجازه نمیدهد کار کند». برای اولی باید کسی بجنبد. برای دومی باید صبر کرد یا مسیر دیگری رفت.
ردگیری علت
طبیعیترین فرض این بود که مشکل از سمت خودمان یا از ناپایداری سرویس است، چون همه خطاها را همینطور دستهبندی کرده بودیم. با این فرض دنبال چیزی میگشتیم که «خراب شده»، و هر بار که چیزی پیدا نمیشد، شکست را اتفاقی حساب میکردیم.
نقطه چرخش وقتی بود که بهجای نگاه کردن به تعداد شکستها، به زمانبندیشان توجه کردیم. شکستها پراکنده نبودند؛ بعد از مدتی کار عادی، یکباره پشت سر هم میآمدند و بعد دوباره همهچیز برمیگشت. این شکل بیشتر شبیه یک سقف مصرف بود تا خرابی. وقتی مستندات و رفتار سرویس را دقیقتر خواندیم، دیدیم علاوه بر محدودیت چندساعته، یک سقف هفتگی هم وجود دارد. وقتی این سقف پر میشد، درخواستها رد میشدند، اما پیغام رد شدن آنقدر عمومی بود که از یک خطای ساده قابل تشخیص نبود.
درس این مرحله برای ما این بود که وقتی خطاها «بیدلیل» به نظر میرسند، احتمالاً دلیلش در یک لایهای است که ما اصلاً برایش مدل نساختهایم.
علت اصلی: سهمیه و خرابی دو چیز متفاوتاند
علت اصلی دو لایه داشت. لایه اول: سرویس چند پنجره محدودیت همزمان دارد؛ یکی کوتاهمدت که چند ساعت یکبار آزاد میشود و یکی هفتگی که خیلی دیرتر. ما فقط ذهنیت محدودیت کوتاهمدت را داشتیم. لایه دوم، و مهمتر: کد ما بین «سهمیه تمام شد» و «سرویس خراب شد» فرقی نمیگذاشت.
در خود HTTP برای این تفاوت جا هست. کد 429 در RFC 6585 تعریف شده و یعنی کاربر در بازه زمانی مشخصی درخواست زیادی فرستاده (rate limiting). طبق همان RFC، پاسخ میتواند هدر Retry-After داشته باشد تا بگوید چه مدت باید صبر کرد، و پاسخ 429 نباید در کش ذخیره شود. کدهای 5xx خانواده دیگریاند: یعنی سرور نتوانسته درخواستی را که ظاهراً معتبر بود انجام دهد. حتی هدر Retry-After طبق مستندات MDN در پاسخ 503 هم کاربرد دارد، ولی معنایش «سرویس این مدت در دسترس نیست» است و نه «سهمیه تو پر شده».
پس تصمیمگیری درست برای کلاینت این است:
با 429: خطا از سمت ما نیست و سرویس هم خراب نیست. نباید هشدار خرابی داد، نباید فوراً و پشت سر هم تلاش مجدد کرد، و بهتر است تا زمان مشخصشده صبر کنیم یا به ارائهدهنده بعدی برویم.
با 5xx: احتمالاً مشکل واقعی است. تلاش مجدد با فاصله منطقی، شمارش در پایش و در صورت تکرار، هشدار.
این دقیقاً همان چیزی بود که در سیستم ما یکدست شده بود. نکته تلخ این است که اگر سرویس مقصد خودش پیغام و کد دقیقی برنگرداند، ما نمیتوانیم از بیرون حدس بزنیم. پس بخشی از کار این بود که در لایه خودمان، پاسخ ارائهدهنده را به کد درست ترجمه کنیم.
نکات کلیدی
- 429 یعنی محدودیت مصرف؛ خطا از جنس «بعداً دوباره» است، نه «خراب شد».
- 5xx یعنی شکست واقعی در سمت سرور و مستحق تلاش مجدد و هشدار.
- Retry-After اگر موجود باشد، بهترین راهنمای زمان صبر است.
راهحل: طبقهبندی خطا، سوییچ خودکار و وضعیت در پنل
راهحل ما سه بخش داشت. اول، طبقهبندی خطا در لایهای که با ارائهدهندههای تصویر صحبت میکند: تمام شدن سهمیه را به خطای 429 با پیام «بعداً دوباره» تبدیل میکنیم و خرابی واقعی را به 5xx. دوم، وقتی ارائهدهندهای سهمیهاش تمام شد، سیستم خودکار به ارائهدهنده بعدی میرود تا تولید تصویر متوقف نشود. سوم، وضعیت هر ارائهدهنده در پنل نمایش داده میشود تا یک نگاه کافی باشد: «در دسترس»، «سهمیه تمام شده» یا «خراب».
نمونه عمومی از منطق طبقهبندی (مقادیر و نامها ساختگی است):
// provider-error.ts — نمونه عمومی
type ProviderState = 'ok' | 'quota_exhausted' | 'down';
function classify(status: number, body: string): ProviderState {
// پیامهای تمامشدن سهمیه ممکن است با کد غیرمنتظره برگردند؛
// الگوها را از پاسخهای واقعی هر ارائهدهنده جمع کنید
const quotaHint = /quota|limit reached|try again later/i.test(body);
if (status === 429 || quotaHint) return 'quota_exhausted';
if (status >= 500) return 'down';
return 'ok';
}
async function generateCover(prompt: string) {
for (const p of providers) {
if (p.blockedUntil && p.blockedUntil > Date.now()) continue;
const res = await p.generate(prompt);
if (res.ok) return res.image;
const state = classify(res.status, res.body);
p.state = state;
if (state === 'quota_exhausted') {
const wait = Number(res.headers['retry-after']) || 6 * 3600;
p.blockedUntil = Date.now() + wait * 1000; // هشدار نمیدهیم
continue; // ارائهدهنده بعدی
}
if (state === 'down') {
alertOps(`provider ${p.name} failing`); // فقط خرابی واقعی هشدار میشود
continue;
}
}
throw new Error('all providers unavailable');
}نکات کلیدی
- هشدار فقط برای خرابی واقعی ارسال شود، نه برای تمام شدن سهمیه.
- اگر Retry-After نبود، یک زمان صبر پیشفرض و محافظهکارانه داشته باشید.
- وضعیت هر ارائهدهنده باید در پنل دیده شود تا تشخیص سریع باشد.
درسها و چکلیست
مشکل اصلی ما خود سقف هفتگی نبود؛ هر سرویس سهمیهای دارد. مشکل این بود که سیستم ما هیچ زبانی برای گفتن «این خطا از جنس خرابی نیست» نداشت. وقتی همهچیز «خراب» حساب شود، هم ماشین تصمیم بدی میگیرد (تلاش مجدد بیفایده) و هم انسان (نادیده گرفتن هشدارها).
این مسئله با ماجرای دیگری که قبلاً نوشتهایم همخانواده است: در صفی که هر روز بیصدا یک تلاش را میسوزاند هم شکستها بهجای دیده شدن، در صف ناپدید میشدند. اگر در حال ساخت اتوماسیون مبتنی بر هوش مصنوعی هستید، طراحی مسیر خطا همانقدر مهم است که مسیر موفقیت. و همانطور که در تست سلامتی که از خود سرویس خرابتر بود دیدیم، پایشی که معنای خطا را اشتباه بخواند، بیشتر گمراه میکند تا کمک.
چکلیست ما برای جلوگیری از تکرار:
۱. محدودیتهای هر سرویس بیرونی را فهرست کنید: پنجره کوتاه، پنجره بلندتر مثل هفتگی، و محدودیت همزمانی.
۲. پاسخ هر ارائهدهنده را به یک مدل داخلی ترجمه کنید: ok، quota_exhausted، down.
۳. برای سهمیه از 429 (و در صورت امکان Retry-After) استفاده کنید، برای خرابی واقعی از 5xx.
۴. هشدار را فقط به خرابی واقعی وصل کنید؛ برای سهمیه، نمایش وضعیت در پنل کافی است.
۵. حداقل یک ارائهدهنده جایگزین با سوییچ خودکار داشته باشید.
۶. زمان انسداد موقت هر ارائهدهنده را نگه دارید تا درخواستهای بیفایده فرستاده نشود.
اگر این بحث با زیرساختهای خودتان هم ارتباط دارد، نگاهی به خدمات سرور و زیرساخت هم بیندازید؛ بسیاری از همین اصول درباره پایش و تفکیک انواع خطا در آنجا هم صدق میکند.
جمعبندی
«تمام شد» و «خراب شد» دو وضعیت متفاوتاند و واکنش درست به هر کدام فرق دارد. با طبقهبندی صریح خطا (429 برای سهمیه، 5xx برای خرابی)، سوییچ خودکار به ارائهدهنده بعدی و نمایش وضعیت هر ارائهدهنده در پنل، هم هشدارهای بیمورد حذف شدند و هم تولید تصویر دیگر به سقف یک سرویس گره نخورد.
منابع
- RFC 6585: Additional HTTP Status Codes (بخش 429 Too Many Requests)datatracker.ietf.org
- MDN: 429 Too Many Requestsdeveloper.mozilla.org
- MDN: Retry-After headerdeveloper.mozilla.org
- RFC 9110: HTTP Semanticsrfc-editor.org