cd ../back to blog
$Guide//June 4, 2026//7 min read

حدود المعدّل وإعادات المحاولة والـ backoff: معالجة أخطاء الإنتاج لواجهات الذكاء الاصطناعي

تعامل مع أخطاء 429 و5xx بالطريقة الصحيحة: backoff تصاعدي مع jitter، واحترام Retry-After، وidempotency، ومهل زمنية، وقواطع دارة لنداءات واجهات الذكاء الاصطناعي.

نجح العرض التوضيحي من أول نداء. أما الإنتاج فهو الـ 999,999 نداءً الأخرى — تلك التي تصطدم بحد معدّل أثناء ذروة في حركة المرور، أو تلتقط 503 من المنبع في منتصف عملية نشر، أو تتعلّق على socket لا يجيب أبداً. الفرق بين تكامل لعبة وآخر موثوق يكمن كلياً تقريباً في كيفية تعاملك مع المسار غير السعيد: ماذا تعيد محاولته، وكم تنتظر، ومتى تستسلم. أخطئ في ذلك فتتحوّل عثرة وجيزة لدى المزوّد إلى انقطاع تجلبه على نفسك، إذ يعيد كل عامل من عمّالك المحاولة في تناغم تام ويُكمل المهمة التي بدأها محدّد المعدّل.

هذا شرح عملي بمستوى الإنتاج: صنّف الأخطاء كي تعيد محاولة ما هو قابل لها فقط، وتراجع تصاعدياً مع jitter، واحترم Retry-After، واضبط مهلاً زمنية معقولة، واجعل إعادات المحاولة آمنة عبر idempotency، وتوقّف عن مهاجمة تبعية ميتة عبر circuit breaker. الشيفرة بلغة Python على OpenAI-SDK مقابل https://api.brievio.com/v1، لكن القواعد نفسها على أي عميل HTTP وأي لغة.

أعد محاولة القابل للإعادة — ولا شيء سواه

أكثر الأخطاء شيوعاً في معالجة أخطاء واجهات الذكاء الاصطناعي هو إعادة محاولة أشياء لن تنجح أبداً. خطأ 401 (مفتاح خاطئ)، أو 400 (طلب مشوّه)، أو 422 (prompt الخاص بك طويل جداً) — هذه حتمية. المحاولة الثانية تفشل تماماً كالأولى، إلا أنها الآن خمس محاولات وعدة ثوانٍ لاحقاً. والأسوأ أنك أخفيت خللاً حقيقياً خلف حلقة إعادة محاولة. الـ 4xx الوحيد الجدير بالإعادة هو 429 (تجاوز الحد المعدّل)، لأن ذلك هو عابر فعلاً.

  • أعد المحاولة: 429، و 500 / 502 / 503 / 504 — فهذه حالات عابرة من جانب الخادم أو السعة.
  • أعد المحاولة: أخطاء الاتصال ومهل القراءة (قد لا يكون الطلب قد وصل إلى النموذج أصلاً، أو أجاب النموذج في socket مغلق).
  • لا تعد المحاولة أبداً: أي 4xx آخر — 400، 401، 403، 404، 422. أظهرها، ونبّه عليها، وأصلح المستدعي.

حدس مفيد: إعادة المحاولة رهان على أن الطلب نفسه سيحصل على إجابة مختلفة. وهذا لا يصح إلا حين كان الإخفاق متعلقاً بالتوقيت أو السعة، لا بالطلب نفسه.

backoff تصاعدي مع jitter

حين تعرف أن الإخفاق قابل للإعادة، يبقى السؤال: كم تنتظر؟ إعادة المحاولة فوراً عبثٌ — فالحالة التي سبّبت الـ 429 ما زالت قائمة بعد مللي ثانية. والجواب القياسي هو backoff تصاعدي: انتظر نحو 0.5s، ثم 1s، 2s، 4s، تتضاعف كل محاولة، مع سقف أعلى كي لا يعطّلك تعافٍ طويل إلى الأبد.

لكن للـ backoff التصاعدي الصرف نمط إخفاق شرس عند الحجم الكبير. فإن تجاوز 500 عامل الحد المعدّل في اللحظة نفسها — وهو بالضبط ما يحدث أثناء الذروة — فجميعها يتراجع بالقدر نفسه و يعيد المحاولة في اللحظة نفسها، فيعيد تكوين الذروة على إيقاع ثانيتين. هذا هو القطيع الجامح. والعلاج هو الـ jitter: عشوِ كل تأخير كي تتوزّع الإعادات. الـ jitter الكامل (النوم قدراً عشوائياً بين الصفر وسقف الـ backoff) يفكّ تزامن القطيع أفضل بكثير من إضافة دفعة عشوائية صغيرة إلى تأخير ثابت.

retry.py
# غلاف لإعادة المحاولة يمكنك إطلاقه فعلاً. قاعدتان تقومان بمعظم العمل:
#   1. أعد المحاولة فقط لما هو قابل لها (429 + 5xx + أخطاء الاتصال). لا تكرر أبداً 400/401/422.
#   2. تراجع تصاعدياً (backoff) وأضف jitter، وإلا أعاد كل عميل المحاولة في توقيت واحد متزامن.
import random
import time

from openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutError

client = OpenAI(
    api_key="sk-brievio-...",
    base_url="https://api.brievio.com/v1",
    timeout=30,          # سقف صارم لكل طلب — انظر «المهل الزمنية» أدناه
)

RETRYABLE_STATUS = {429, 500, 502, 503, 504}
MAX_ATTEMPTS = 5
BASE_DELAY = 0.5         # بالثواني
MAX_DELAY = 20.0         # ضع سقفاً للـ backoff كي لا يتعطّل التعافي البطيء إلى ما لا نهاية

def chat_with_retry(**kwargs):
    for attempt in range(MAX_ATTEMPTS):
        try:
            return client.chat.completions.create(**kwargs)
        except APIStatusError as e:
            # خطأ 4xx غير الـ 429 هو خللك أنت (وسائط خاطئة، مفتاح خاطئ، طول زائد).
            # إعادة محاولته تحرق زمن الاستجابة فقط — افشل بسرعة.
            if e.status_code not in RETRYABLE_STATUS:
                raise
            last_error = e
        except (APIConnectionError, APITimeoutError) as e:
            # عطل شبكي عابر أو انطلقت مهلتنا الزمنية. آمن أن تعيد محاولة قراءة.
            last_error = e

        if attempt == MAX_ATTEMPTS - 1:
            break

        # backoff تصاعدي مع jitter كامل: sleep ∈ [0, base * 2**attempt].
        # الـ jitter الكامل (لا «backoff + قدر عشوائي صغير») هو ما يفكّ فعلاً
        # تزامن القطيع الجامح. انظر مدوّنة AWS Architecture حول backoff مع jitter.
        ceiling = min(MAX_DELAY, BASE_DELAY * (2 ** attempt))
        time.sleep(random.uniform(0, ceiling))

    raise last_error

لاحظ السقف على المحاولات (MAX_ATTEMPTS = 5) والسقف على التأخير (MAX_DELAY). الإعادات غير المحدودة هي ما يحوّل عثرة عابرة إلى تراكم لا ينضب أبداً: تتكدّس الطلبات أسرع مما تُصرّف، ويتسلّق زمن الاستجابة، فيستنفد المستدعون من المنبع مهلهم ويعيدون محاولة طلباتهم هم أيضاً. قيّد كليهما، ودع الإخفاق يكون مرئياً لا مكبوتاً.

احترم Retry-After — لا تخمّن

منحنى الـ backoff لديك تخمين لموعد تحرّر السعة. وحين يخبرك الخادم بالجواب مباشرة، فاستخدمه. غالباً ما يحمل خطأ 429 ترويسة Retry-After (فرق ثوانٍ أو تاريخ HTTP) تعكس نافذة إعادة الضبط الحقيقية. والنوم بتلك القيمة أفضل قطعاً من أي صيغة، لأنها حقيقة ثابتة لا اجتهاد تقريبي.

retry_after.py
# حين يخبرك الخادم بمدة الانتظار، أصغِ إليه. يحمل خطأ 429 (وأحياناً 503)
# ترويسة Retry-After. احترامها أفضل من أي منحنى backoff تخترعه أنت، لأنها
# تعكس نافذة إعادة الضبط الحقيقية — لا تخميناً.
import email.utils as eut
import time

def retry_delay(resp_headers, attempt, base=0.5, cap=20.0):
    # 1. فضّل تعليمات الخادم.
    ra = resp_headers.get("retry-after")
    if ra is not None:
        try:
            return float(ra)                       # صيغة فرق الثواني: "2"
        except ValueError:
            when = eut.parsedate_to_datetime(ra)   # صيغة تاريخ HTTP
            return max(0.0, when.timestamp() - time.time())

    # 2. لا ترويسة؟ ارجع إلى backoff تصاعدي مع jitter كامل.
    import random
    return random.uniform(0, min(cap, base * (2 ** attempt)))

# ملاحظات:
#   - بعض المزوّدين يرسلون أيضاً X-RateLimit-Reset؛ عامله بالطريقة نفسها.
#   - أضف حداً أدنى ضئيلاً (مثلاً 50ms) كي لا تسبّب "Retry-After: 0" حلقة محمومة.
#   - يُظهر Brievio ترويسة Retry-After على 429 بدلاً من تعطيل الـ socket صامتاً،
#     فهذا المسار قابل للوصول — يمكنك فعلاً التراجع بناءً على إشارة.

لا ينجح هذا إلا إذا أعاد الـ gateway الترويسة فعلاً بدلاً من امتصاص الحد وتعطيل الـ socket لديك تسعين ثانية. يفشل Brievio بسرعة وبوضوح — يعود تجاوز الحد المعدّل بصيغة 429 نظيف مع Retry-After، لا اتصالاً معلّقاً — فيكون مسار «أصغِ إلى الخادم» قابلاً للوصول. تصنيف الأخطاء الكامل، مع بيان أي الرموز يحمل أي الترويسات، موجود في مرجع الأخطاء.

المهل الزمنية: نمط الإخفاق الذي لا يختبره أحد

سياسة إعادة المحاولة بلا فائدة إذا لم يعد الطلب أصلاً كي تعاد محاولته. نداء ذكاء اصطناعي بلا مهلة سيتعلّق في النهاية حتماً — اتصال نصف مفتوح، أو منبع عالق، أو موازِن حمل أسقط التدفق. وبلا أجل محدد، يحجز ذلك الطلب الواحد عاملاً واتصالاً ومقعداً في كل طابور خلفه إلى أن يستسلم نظام التشغيل بعد دقائق. اضبط مهلة صريحة لكل طلب (الـ timeout=30 أعلاه) كي يتحوّل النداء العالق إلى APITimeoutError يمكنك إعادة محاولته، لا إلى عبء ميت.

  • مهلة لكل طلب تحدّ محاولة واحدة. في حالة البث، الميزانية ذات المعنى هي الزمن حتى أول توكن إضافة إلى مهلة خمول بين الأجزاء، لا إجمالي زمن على الساعة الجدارية.
  • أجل إجمالي يحدّ تتابع إعادة المحاولة بأكمله. تتبّع ميزانية (مثلاً 60s من الطرف إلى الطرف) وتوقّف عن الإعادة حالما تُستنفد — فالمستدعي الذي ينتظرك له أجله الخاص.
  • المهلة > p99 المتوقع، لا p50. اضبطها فوق زمن استجابة الذيل الحقيقي بقليل. التضييق المفرط يلغي طلبات جيدة كانت على وشك النجاح، فتصنع حملاً من نفاد الصبر.

الـ Idempotency: جعل إعادات المحاولة آمنة

تُدخِل إعادات المحاولة خطراً دقيقاً. حين ينفد وقت طلب، فأنت لا تعرف ما إذا كان الخادم قد عالجه — فشلت قراءتك للاستجابة، لكن العمل ربما اكتمل. أعد المحاولة عمياء فقد تحاسب عميلاً مرتين، أو ترسل إشعاراً مكرراً، أو تكتب صفاً مزدوجاً. القراءات آمنة بطبيعتها للإعادة. أما الآثار الجانبية فلا.

الدفاع هو مفتاح idempotency: معرّف فريد تربطه بعملية منطقية كي يطوي الخادم (أو معالجك الخاص) التكرارات. لنداءات الاستدلال الصرفة لا يوجد عادة أثر جانبي خارجي يُقلق منه — لكن في اللحظة التي يُطلق فيها إكمالٌ كتابةً في قاعدة بيانات، أو دفعة، أو رسالة صادرة، ولّد مفتاحاً ثابتاً لكل وحدة عمل منطقية وأزِل التكرار اعتماداً عليه. القاعدة العملية: إن أمكن لإعادة محاولة أن تحدث مرتين، فصمّم كأنها ستحدث.

الـ Circuit breakers: كفّ عن ركل تبعية ميتة

الـ backoff يعالج طلباً واحداً متعثّراً. ولا يفعل شيئاً لانقطاع مستمر — فإن سقط منبع سقوطاً تاماً لدقيقتين، خاض كل طلب سلّم إعاداته كاملاً، وانتظر الحد الأقصى، ثم أخفق على أي حال، بينما ينفجر زمن استجابتك وعمق طابورك. الـ circuit breaker يقطع هذا الدارة قصيرة: بعد N من الإخفاقات المتتالية «يفتح» ويُفشل النداءات الجديدة فوراً (أو يوجّهها إلى مسار بديل) لنافذة تهدئة، ثم يسمح بمسبار واحد بالمرور لاختبار التعافي قبل أن يُغلق من جديد.

  • مغلق: تشغيل طبيعي، تتدفق الطلبات، وتُعدّ الإخفاقات.
  • مفتوح: انطلقت العتبة — ارفض بسرعة لفترة تهدئة بدلاً من تكديس إعادات محاولة محكوم عليها بالفشل.
  • نصف مفتوح: بعد التهدئة، اسمح بطلب تجريبي واحد؛ النجاح يغلق القاطع، والإخفاق يعيد فتحه.

اقرن القاطع بمسار بديل فيتحوّل الانقطاع إلى تدهور بدلاً من إخفاق تام. وهنا أيضاً يثبت الـ gateway جدارته: يقوم Brievio بـ تجاوز فشل عبر المزوّدين، فبإمكان مزوّد واحد يخمد أن يُوجَّه إلى مزوّد سليم قبل أن يحتاج قاطعك إلى الفتح أصلاً. ولفهم كيف يلائم ذلك ميزانية موثوقية حقيقية، انظر كيف نهندس اتفاقية مستوى خدمة 99.95%.

ملاحظة حول ما تكلّفه إعادات المحاولة

يجعل الضبط العدواني لإعادات المحاولة الناس قلقين بشأن الفاتورة — فكل إعادة نداء آخر قابل للمحاسبة، أليس كذلك؟ ليس على gateway يحاسب بصدق. لا يفرض Brievio أي رسوم على نداءات 4xx/5xx الفاشلة، فالـ 429 الذي أطلق backoff لديك والـ 503 الذي تجاوزته بالإعادة كلاهما مجاني. أنت تدفع مقابل المحاولة التي تعيد إكمالاً فعلاً، لا مقابل تلك التي رفضها النظام. ذلك يعني أنه بإمكانك ضبط عدد المحاولات والمهل الزمنية من أجل الموثوقية دون عقوبة على العدّاد لفعلك ذلك — وإن بقي جموح الإعادات مصدر قلق على الميزانية، فحُدّ الإنفاق مباشرة كما هو موضّح في كيف تضع سقفاً لإنفاق واجهة الذكاء الاصطناعي لديك.

الخلاصة

معالجة أخطاء الإنتاج لواجهات الذكاء الاصطناعي ست قواعد، ويمكنك إطلاقها جميعاً في فترة بعد ظهر:

  • أعد محاولة 429 و5xx فقط. لا تعد محاولة أي 4xx آخر — فهو خللك أنت، قد ظهر.
  • تراجع تصاعدياً مع jitter كامل، وضع سقفاً للمحاولات والتأخير معاً.
  • احترم Retry-After حين يرسله الخادم — فهو يتفوّق على أي صيغة.
  • اضبط مهلاً صريحة لكل طلب وأجلاً إجمالياً؛ ولا تدع نداءً يتعلّق أبداً.
  • اجعل إعادات المحاولة ذات الأثر الجانبي آمنة عبر مفتاح idempotency.
  • أضف circuit breaker كي يتدهور انقطاع مستمر بدلاً من أن ينهار.

لا شيء من هذا غريب — إنها البنية التحتية المملّة التي تحفظ الوعد الممل بالبقاء قيد التشغيل. وهي تعمل على أفضل وجه فوق طبقة أساس تفشل بسرعة، وتُظهر إشارات حقيقية، ولا تحاسبك على الإخفاقات، كي يكون ضبطك صادقاً ومجانياً. وإن كنت لا تزال تختار وجهة حركة مرورك، فسلوك الموثوقية تحت الإخفاق من أوّل ما يستحق الاختبار — وهو مشروح في كيف تختار gateway لواجهة الذكاء الاصطناعي.