Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 69

تطبيق آلية إعادة المحاولة (Retry Mechanism)

أهلاً بك مجدداً في رحلتنا لبناء وكلاء ذكاء اصطناعي متقدمين.

في الدرس السابق، قمنا بتزويد وكيلنا بالقدرة الأساسية على تحديد مؤشرات النجاح والفشل في مخرجات أدواته. أصبح الآن بإمكانه التمييز بين استجابة ناجحة وخطأ برمجي صريح أو فشل ضمني. لكن، ماذا يفعل الوكيل بعد أن يكتشف أنه فشل؟

هذا هو موضوع درسنا اليوم. سنتعلم كيف ننتقل من مجرد اكتشاف الفشل إلى التعامل معه بفعالية. هدفنا هو تنفيذ آلية إعادة محاولة بسيطة (Retry Mechanism) عند فشل أداة ما. هذه الآلية هي واحدة من أبسط وأقوى الاستراتيجيات لجعل وكيلنا أكثر صلابة وموثوقية في مواجهة الأخطاء المؤقتة.


1. لماذا نحتاج إلى إعادة المحاولة؟ الأخطاء المؤقتة مقابل الدائمة

ليس كل فشل متساوياً. عندما تفشل أداة ما، يجب أن نفكر في السبب:

  • الأخطاء الدائمة (Permanent Errors): هي أخطاء من غير المرجح أن تُحل بإعادة المحاولة.

    • مثال: استخدام مفتاح API غير صحيح (خطأ 401 Unauthorized). إعادة المحاولة بنفس المفتاح الخاطئ ستؤدي إلى نفس الفشل.
    • مثال: طلب بيانات لمدينة غير موجودة (خطأ 404 Not Found). المدينة لن تظهر فجأة.
  • الأخطاء المؤقتة (Transient Errors): هي أخطاء مؤقتة، وغالباً ما تُحل إذا انتظرنا قليلاً وحاولنا مرة أخرى.

    • مثال: انقطاع مؤقت في شبكة الإنترنت.
    • مثال: الخادم الذي يستضيف الواجهة البرمجية (API) مشغول جدًا في تلك اللحظة (خطأ 503 Service Unavailable).
    • مثال: حدوث خطأ غير متوقع في الخادم (خطأ 500 Internal Server Error) يتم إصلاحه بسرعة.

آلية إعادة المحاولة مصممة خصيصاً للتعامل مع الأخطاء المؤقتة. إنها تمنح الوكيل "فرصة ثانية" (أو ثالثة، أو رابعة) لتجاوز المشاكل العابرة دون أن يتوقف عن العمل تماماً.



2. منطق آلية إعادة المحاولة

قبل كتابة الكود، دعنا نفهم المنطق الأساسي لآلية إعادة المحاولة. المخطط الانسيابي التالي يوضح هذه العملية بشكل مرئي.

مخطط انسيابي لآلية إعادة المحاولة في بايثون باستخدام urllib3
يوضح هذا المخطط الانسيابي الخطوات المنطقية لآلية إعادة المحاولة. تبدأ العملية بمحاولة تنفيذ الطلب. إذا نجح، تنتهي العملية. أما إذا فشل (بسبب خطأ مؤقت)، فإن النظام ينتظر لفترة قصيرة ثم يعيد المحاولة، مع تكرار ذلك حتى يصل إلى الحد الأقصى للمحاولات.

ببساطة، الخطوات هي:

  1. حدد عدد المحاولات الأقصى: كم مرة تريد أن يحاول الوكيل قبل أن يستسلم؟ (مثلاً: 3 مرات).
  2. حدد فترة انتظار: كم من الوقت يجب أن ينتظر بين كل محاولة وأخرى؟ (مثلاً: ثانيتان).
  3. ابدأ حلقة تكرارية:
    • حاول تنفيذ المهمة (مثل استدعاء API).
    • إذا نجحت: اخرج من الحلقة وأرجع النتيجة.
    • إذا فشلت (بسبب خطأ مؤقت):
      • تأكد من أنك لم تصل إلى الحد الأقصى للمحاولات.
      • انتظر للفترة المحددة.
      • عد إلى بداية الحلقة وجرب مرة أخرى.
  4. إذا انتهت كل المحاولات بالفشل: أعلن عن الفشل النهائي وأوقف العملية أو اتخذ إجراءً بديلاً.


3. التنفيذ البسيط باستخدام حلقة for

دعنا نطبق هذا المنطق في بايثون باستخدام المفاهيم التي تعلمناها سابقاً (الحلقات التكرارية، try/except). سنستخدم مكتبة time لإضافة فترة الانتظار.

import time
import random

# دالة تحاكي أداة قد تفشل مؤقتاً
def tool_that_might_fail():
    """
    هذه الأداة لديها فرصة 50% للنجاح و 50% للفشل
    عبر إطلاق استثناء ConnectionError.
    """
    if random.random() < 0.5:
        # محاكاة فشل مؤقت
        raise ConnectionError("فشل الاتصال بالخادم مؤقتاً")
    else:
        # محاكاة نجاح
        return "تم تنفيذ المهمة بنجاح!"

# آلية إعادة المحاولة باستخدام حلقة for
def run_tool_with_retry(max_retries: int, delay: int):
    """
    تنفذ أداة مع آلية إعادة محاولة بسيطة.
    
    :param max_retries: الحد الأقصى لعدد المحاولات.
    :param delay: فترة الانتظار بالثواني بين المحاولات.
    """
    for attempt in range(max_retries):
        try:
            print(f"المحاولة رقم {attempt + 1}...")
            result = tool_that_might_fail()
            print(result)
            return  # اخرج من الدالة عند النجاح
        except ConnectionError as e:
            print(f"فشلت المحاولة: {e}")
            if attempt < max_retries - 1:
                print(f"الانتظار لمدة {delay} ثانية قبل إعادة المحاولة.")
                time.sleep(delay) # انتظر قبل المحاولة التالية
            else:
                print("تم الوصول إلى الحد الأقصى للمحاولات. فشل نهائي.")

# لنجرب الآن تنفيذها
run_tool_with_retry(max_retries=3, delay=2)

جرب تشغيل هذا الكود عدة مرات. ستلاحظ أنه في بعض الأحيان ينجح من المحاولة الأولى، وفي أحيان أخرى يفشل مرة أو مرتين قبل أن ينجح، وأحياناً يفشل في كل المحاولات. هذا بالضبط ما تفعله آلية إعادة المحاولة!



4. طريقة أكثر أناقة: المُزخرفات (Decorators)

الطريقة السابقة تعمل بشكل جيد، ولكنها تجعلنا نكتب منطق إعادة المحاولة في كل مرة نريد فيها استدعاء أداة. هناك طريقة أفضل في بايثون لتغليف هذه الوظيفة وإعادة استخدامها بسهولة: المُزخرفات (Decorators).

ببساطة، المُزخرف هو دالة "تلتف" حول دالة أخرى لتضيف إليها سلوكًا جديدًا (مثل إعادة المحاولة) دون تعديل كود الدالة الأصلية.

لنشاهد مثالاً عملياً يوضح هذه الفكرة.

Python Retry Mechanism Code Example
هذا الكود يوضح كيفية بناء "مُزخرف" في بايثون اسمه `retry_request`. هذا المُزخرف يضيف وظيفة إعادة المحاولة تلقائيًا لأي دالة يلتف حولها، مما يجعل الكود أكثر نظافة وقابلية لإعادة الاستخدام.

دعنا نحلل هذا الكود خطوة بخطوة (لقد قمت بترجمة التعليقات والمتغيرات للعربية للتوضيح):

import time
import requests # نفترض أننا نستخدم مكتبة requests التي تعلمناها

# 1. الدالة الخارجية التي تنشئ المُزخرف
def retry_request(max_retries=3, delay=2):
    """
    هذا هو مُنشئ المُزخرف. يأخذ إعدادات إعادة المحاولة.
    """
    # 2. المُزخرف الفعلي
    def decorator(func):
        """
        هذا هو المُزخرف. يأخذ الدالة الأصلية (func) كمدخل.
        """
        # 3. الدالة "الملفوفة" التي ستحل محل الدالة الأصلية
        def wrapper(*args, **kwargs):
            """
            هذه الدالة تحتوي على منطق إعادة المحاولة.
            *args و **kwargs تسمح لنا بتمرير أي مدخلات للدالة الأصلية.
            """
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs) # جرب تنفيذ الدالة الأصلية
                except requests.exceptions.RequestException as e:
                    print(f"فشلت المحاولة رقم {attempt + 1}: {e}")
                    if attempt < max_retries - 1:
                        time.sleep(delay)
                    else:
                        print("تم الوصول للحد الأقصى للمحاولات. فشل نهائي.")
                        raise # أعد إطلاق الاستثناء الأخير
        return wrapper
    return decorator

# --- كيفية الاستخدام ---

# نضع المُزخرف فوق الدالة التي نريد إضافة آلية إعادة المحاولة إليها
@retry_request(max_retries=4, delay=1)
def get_some_data_from_internet(url):
    """
    هذه الدالة تحاول جلب بيانات من الإنترنت وقد تفشل.
    """
    print(f"محاولة جلب البيانات من {url}...")
    response = requests.get(url)
    response.raise_for_status() # تطلق استثناءً لأكواد الخطأ 4xx أو 5xx
    return response.json()

# الآن، عند استدعاء الدالة، سيتم تفعيل آلية إعادة المحاولة تلقائيًا
try:
    # سنستخدم رابطًا غير موجود لمحاكاة الفشل
    get_some_data_from_internet("https://api.example.com/non_existent_endpoint")
except requests.exceptions.RequestException:
    print("لم نتمكن من جلب البيانات بعد عدة محاولات.")

لماذا هذه الطريقة أفضل؟

  • إعادة الاستخدام: يمكنك الآن إضافة @retry_request() فوق أي دالة تتعامل مع الشبكة، وستحصل على آلية إعادة المحاولة فوراً.
  • نظافة الكود: تبقى دالة get_some_data_from_internet بسيطة ومركزة على مهمتها الأساسية (جلب البيانات)، بينما يتم التعامل مع منطق إعادة المحاولة بشكل منفصل.
اختبر فهمك!

في الدرس السابق، تحدثنا عن أنواع الفشل الصريحة والضمنية. في الكود أعلاه، المُزخرف @retry_request يتعامل مع requests.exceptions.RequestException، وهو نوع من الفشل الصريح (خطأ برمجي).

هل سيكون من المنطقي تعديل هذا المُزخرف ليعيد المحاولة أيضاً إذا كانت الاستجابة ناجحة (كود 200) ولكنها تحتوي على قائمة نتائج فارغة (فشل ضمني)؟ لماذا؟

Show answer

لا، ليس من المنطقي إعادة المحاولة في هذه الحالة. الحصول على قائمة فارغة ليس خطأً مؤقتًا. إنه يعني أن الخادم قام بمعالجة الطلب بنجاح ولم يجد أي نتائج تطابق البحث. إعادة المحاولة لن تغير هذه الحقيقة ومن المحتمل أن تعيد نفس النتيجة الفارغة، مما يهدر الوقت والموارد. آلية إعادة المحاولة تكون فعالة فقط مع الأخطاء التي يُحتمل أن تزول مع مرور الوقت (Transient Errors).



خلاصة الدرس

لقد أضفنا اليوم قدرة مهمة إلى مجموعة مهارات وكيلنا: الصلابة (Resilience). بدلاً من الاستسلام عند أول عقبة، أصبح الآن يعرف كيف يحاول مرة أخرى عند مواجهة أخطاء مؤقتة.

أهم النقاط التي تعلمناها:

  • أهمية التمييز بين الأخطاء المؤقتة (التي تستفيد من إعادة المحاولة) والأخطاء الدائمة (التي لا تستفيد).
  • المنطق الأساسي لآلية إعادة المحاولة: حلقة تكرارية، محاولة، انتظار، ثم إعادة المحاولة حتى الوصول إلى حد أقصى.
  • طريقتان للتنفيذ في بايثون:
    1. حلقة for بسيطة مع try/except: سهلة الفهم ومباشرة.
    2. المُزخرفات (Decorators): طريقة أكثر أناقة وقابلية لإعادة الاستخدام لفصل منطق إعادة المحاولة عن منطق العمل الأساسي.

ماذا بعد؟

الآن وكيلنا يستطيع اكتشاف الفشل، ويعرف كيف يعيد المحاولة عند حدوث أخطاء مؤقتة. ولكن ماذا يحدث عندما تنجح الأداة في النهاية (ربما بعد عدة محاولات) وتعيد بيانات؟ هل يمكننا الوثوق بهذه البيانات مباشرة؟

في الدرس القادم، سنتناول هذه النقطة بالضبط. سنتعلم كيفية "بناء نظام للتحقق من صحة مخرجات الأدوات قبل استخدامها". هذا يضمن أن البيانات التي يتلقاها "عقل" الوكيل (LLM) ليست صحيحة فحسب، بل هي بالشكل والتنسيق الذي يتوقعه، مما يمنع حدوث أخطاء في مراحل لاحقة من عملية التفكير.