أهلاً بك في الدرس الأخير من وحدتنا حول المنطق المتقدم ومعالجة الأخطاء.
في الدرس السابق، تعلمنا كيفية تصميم "استراتيجية بديلة" (Fallback Strategy) أو "خطة ب" لكي لا يبقى وكيلنا عالقاً عند فشل أدواته بشكل متكرر. قمنا ببرمجة مسار واضح: حاول استخدام الأداة الأساسية عدة مرات، وإذا فشلت، انتقل إلى الأداة البديلة، وإن فشلت هي الأخرى، أبلغ المستخدم بلطف.
هذا النهج قوي، لكنه لا يزال "مبرمجاً بشكل صارم". بمعنى آخر، نحن كمبرمجين من قررنا مسبقاً تسلسل الخطة البديلة. لكن ماذا لو أردنا أن يكون الوكيل نفسه هو من يقرر ما يجب فعله عند الفشل؟ ماذا لو كان يستطيع تحليل سبب الفشل واختيار الإجراء الأنسب، بدلاً من اتباع مسار واحد فقط؟
هذا هو الهدف من درسنا اليوم: تعديل حلقة المنطق لاتخاذ إجراء مختلف بناءً على نجاح أو فشل الأداة. سنقوم بتحويل حلقة عمل الوكيل من مجرد "منفذ للأوامر" إلى "حلقة تفكير ديناميكية" حيث يتم تقييم نتيجة كل خطوة واستخدامها كمدخل للخطوة التالية. هذا هو ما يربط كل ما تعلمناه في هذه الوحدة معاً ويجعل الوكيل قادراً على التصرف بذكاء حقيقي.
1. من الحلقة الثابتة إلى الحلقة الديناميكية
حتى الآن، كانت حلقة العمل لدينا تشبه إلى حد كبير خط تجميع في مصنع:
- فكر (مرة واحدة): يحدد النموذج اللغوي خطة العمل (مثلاً: "استخدم أداة الطقس").
- نفّذ: يحاول النظام تنفيذ هذه الخطة.
- تعامل مع الخطأ (بشكل ثابت): إذا حدث خطأ، يتبع النظام قواعد مبرمجة مسبقاً (أعد المحاولة 3 مرات، ثم نفّذ الخطة ب).
هذه الحلقة "ثابتة" لأن مسار معالجة الخطأ محدد مسبقاً.
الحلقة "الديناميكية" التي سنبنيها اليوم تشبه طريقة عمل حرفي ماهر:
- فكر: يقرر الحرفي ما هي خطوته الأولى (مثلاً: "سأستخدم المثقاب").
- نفّذ: يقوم بعملية الحفر.
- لاحظ: ينظر إلى نتيجة عمله. هل الثقب نظيف؟ هل انكسر ريشة المثقاب؟ هل الثقب في المكان الخاطئ؟
- فكر مجدداً (بناءً على الملاحظة): بناءً على ما رآه، يقرر خطوته التالية. إذا كان الثقب جيداً، ينتقل للمرحلة التالية. إذا انكسرت الريشة، قد يقرر استبدالها (إعادة المحاولة بـ "أداة" معدلة). إذا كان الثقب في المكان الخاطئ، قد يقرر إصلاح الخطأ أو البدء من جديد.
الفكرة المحورية هنا هي خطوة "الملاحظة" (Observation). لم يعد الفشل مجرد إشارة للانتقال إلى "الخطة ب". بل أصبح الفشل نفسه (مع تفاصيله) معلومة قيمة تُرسل مرة أخرى إلى "عقل" الوكيل (LLM) ليقرر ما يجب فعله.
انظر إلى المخطط التالي. إنه يوضح تدفق عمل وكيل يتلقى رسائل وينفذ مهام.
2. كيف "يفكر" الوكيل في الفشل؟
لكي يتمكن النموذج اللغوي الكبير (LLM) من اتخاذ قرار بناءً على فشل أداة ما، يجب أن نقدم له المعلومات بطريقة يفهمها. بدلاً من إخفاء الخطأ عنه، سنقوم بوصف ما حدث بالضبط.
ستتكون حلقة العمل الجديدة من الخطوات التالية داخل حلقة while:
- التفكير (Think): يُطلب من النموذج اللغوي اقتراح الخطوة التالية بناءً على الهدف وسجل المحادثة.
- التنفيذ (Act): يستدعي النظام الأداة التي اقترحها النموذج.
- الملاحظة (Observe): يسجل النظام نتيجة التنفيذ.
- في حالة النجاح: تكون الملاحظة هي البيانات التي أرجعتها الأداة.
- في حالة الفشل: تكون الملاحظة هي رسالة الخطأ التي حدثت (مثل
ConnectionError).
- التغذية الراجعة (Feedback): يتم تحديث سجل المحادثة بإضافة الخطوة التي تم تنفيذها و"الملاحظة" الناتجة عنها.
- التكرار: تعود الحلقة إلى خطوة "التفكير"، ولكن هذه المرة مع سجل محدّث يتضمن نتيجة الإجراء الأخير، مما يسمح للنموذج باتخاذ قرار مستنير.
بهذه الطريقة، إذا فشلت أداة ما، فإن النموذج في الدورة التالية سيرى شيئاً مثل:"الخطوة السابقة: محاولة استدعاء أداة الطقس. الملاحظة: حدث خطأ اتصال بالخادم."
وعندها، يمكن أن يقرر بذكاء:"حسناً، الأداة الأساسية فشلت بسبب مشكلة في الاتصال. سأجرب الآن الأداة البديلة."
3. التنفيذ العملي: بناء الحلقة الديناميكية
سنقوم الآن بتطبيق هذه الحلقة الديناميكية في بايثون. لبساطة الشرح، سنقوم بمحاكاة سلوك النموذج اللغوي عبر دالة بسيطة mock_llm بدلاً من استدعاء واجهة برمجية حقيقية. هذا سيساعدنا على التركيز على منطق الحلقة نفسها.
سنستخدم نفس الأدوات من الدرس السابق.
import json
# ----- الأدوات (من الدرس السابق) -----
def get_precise_weather(city: str) -> str:
"""الأداة الأساسية: دقيقة ولكنها قد تفشل."""
print(f"--- تنفيذ: محاولة الحصول على طقس دقيق لمدينة {city} ---")
if city == "الرياض":
# محاكاة فشل لهذه المدينة
raise ConnectionError("فشل الاتصال بخادم الطقس الدقيق.")
return json.dumps({"city": city, "temperature": 25, "humidity": 60})
def get_general_weather_forecast(city: str) -> str:
"""الأداة البديلة: عامة وموثوقة."""
print(f"--- تنفيذ: اللجوء إلى توقعات الطقس العامة لمدينة {city} ---")
forecasts = {"الرياض": "مشمس وحار بشكل عام.", "جدة": "رطب مع احتمال أمطار خفيفة."}
return forecasts.get(city, "لا تتوفر توقعات عامة لهذه المدينة.")
# قائمة الأدوات المتاحة للوكيل
available_tools = {
"get_precise_weather": get_precise_weather,
"get_general_weather_forecast": get_general_weather_forecast,
}
# ----- محاكاة النموذج اللغوي (العقل) -----
def mock_llm(history: str) -> str:
"""
يحاكي هذا النموذج اللغوي سلوك التفكير.
يقرأ السجل ويقرر الخطوة التالية.
"""
print("\n🤔 العقل يفكر...")
print(f"السجل الحالي:\n---\n{history}\n---")
# منطق اتخاذ القرار المبسط
if "فشل الاتصال" in history:
# إذا رأى أن المحاولة السابقة فشلت، يقرر استخدام الأداة البديلة
print("💡 القرار: الأداة الأساسية فشلت، سأستخدم الأداة البديلة.")
return '{"action": "get_general_weather_forecast", "args": ["الرياض"]}'
elif "temperature" in history:
# إذا رأى نتيجة ناجحة من أداة الطقس، يقرر الإجابة على المستخدم
print("💡 القرار: حصلت على البيانات، سأجيب على المستخدم.")
return '{"action": "answer_user", "args": ["الطقس الدقيق هو 25 درجة."]}'
elif "مشمس وحار" in history:
# إذا رأى نتيجة من الأداة البديلة، يقرر الإجابة
print("💡 القرار: حصلت على البيانات من الأداة البديلة، سأجيب على المستخدم.")
return '{"action": "answer_user", "args": ["لم أتمكن من الحصول على تفاصيل دقيقة، لكن التوقعات العامة للرياض هي: مشمس وحار بشكل عام."]}'
else:
# في البداية، يقرر استخدام الأداة الأساسية
print("💡 القرار: سأستخدم الأداة الأساسية للحصول على الطقس.")
return '{"action": "get_precise_weather", "args": ["الرياض"]}'
# ----- الحلقة الديناميكية الرئيسية -----
def agent_loop(user_request: str):
history = f"الهدف: {user_request}\n"
max_steps = 5 # لمنع الحلقات اللانهائية
for i in range(max_steps):
print(f"\n🌀----- الخطوة {i + 1} -----🌀")
# 1. التفكير (Think)
llm_response_str = mock_llm(history)
llm_response = json.loads(llm_response_str)
action = llm_response["action"]
args = llm_response["args"]
history += f"\nالخطوة المقترحة: تنفيذ '{action}' بالمدخلات {args}."
# 2. التنفيذ (Act) والملاحظة (Observe)
if action == "answer_user":
# إذا كان الإجراء هو إجابة المستخدم، نطبع الإجابة وننهي الحلقة
print(f"\n✅ الجواب النهائي: {args[0]}")
return
tool_to_call = available_tools.get(action)
observation = ""
try:
# محاولة تنفيذ الأداة
tool_output = tool_to_call(*args)
observation = f"النتيجة: {tool_output}"
except Exception as e:
# في حالة الفشل، تكون الملاحظة هي رسالة الخطأ
print(f"🔴 حدث خطأ أثناء التنفيذ: {e}")
observation = f"خطأ: {e}"
# 3. التغذية الراجعة (Feedback)
print(f"👀 الملاحظة: {observation}")
history += f"\nالملاحظة: {observation}"
# --- لنجرب الحلقة ---
agent_loop("ما هو الطقس في الرياض؟")
شرح ما حدث في الشيفرة:
-
الخطوة 1:
- التفكير: يرى
mock_llmالطلب الأولي فقط، فيقرر استخدام الأداة الأساسيةget_precise_weather. - التنفيذ: يتم استدعاء الأداة، ولكنها تطلق استثناء
ConnectionError. - الملاحظة: يلتقط блока
exceptالخطأ، وتصبح "الملاحظة" هي رسالة الخطأ. - التغذية الراجعة: يتم إضافة نتيجة الفشل هذه إلى السجل
history.
- التفكير: يرى
-
الخطوة 2:
- التفكير: الآن، عندما يتم استدعاء
mock_llmمرة أخرى، يرى السجل الذي يحتوي على "فشل الاتصال". هذا يدفعه إلى اتخاذ قرار مختلف: استخدام الأداة البديلةget_general_weather_forecast. - التنفيذ: يتم استدعاء الأداة البديلة، وهذه المرة تنجح وتعيد "مشمس وحار بشكل عام.".
- الملاحظة: تكون الملاحظة هي النتيجة الناجحة.
- التغذية الراجعة: يتم تحديث السجل بنتيجة النجاح.
- التفكير: الآن، عندما يتم استدعاء
-
الخطوة 3:
- التفكير: يرى
mock_llmأن السجل يحتوي الآن على نتيجة ناجحة من الأداة البديلة. يقرر أن لديه ما يكفي من المعلومات للإجابة على المستخدم. - التنفيذ: الإجراء هو
answer_user، وهو إجراء خاص ينهي الحلقة ويقدم الإجابة النهائية.
- التفكير: يرى
لاحظ كيف أن الوكيل لم يتبع مساراً ثابتاً، بل قام بتحليل نتيجة كل خطوة واستخدمها لتوجيه قراره التالي. هذا هو جوهر الحلقة الديناميكية.
اختبر فهمك!
لنفترض أن النموذج اللغوي اقترح استدعاء أداة غير موجودة، مثلاً get_moon_phase. ماذا سيحدث في الحلقة التي بنيناها؟ وما هي "الملاحظة" التي سيراها النموذج اللغوي في الخطوة التالية؟
Show answer
سيحدث خطأ في هذا السطر: tool_to_call = available_tools.get(action). ستكون قيمة tool_to_call هي None.
عندما يحاول الكود تنفيذ tool_to_call(*args)، سيؤدي ذلك إلى TypeError: 'NoneType' object is not callable.
سيتم التقاط هذا الخطأ بواسطة كتلة except. وبالتالي، فإن "الملاحظة" التي سيراها النموذج في الخطوة التالية ستكون شيئاً مثل: خطأ: 'NoneType' object is not callable.
بناءً على هذه الملاحظة، يمكن للنموذج الذكي أن "يصحح نفسه" في الخطوة التالية ويدرك أن الأداة التي طلبها غير موجودة، ويختار أداة أخرى متاحة.
4. ربط الأفكار معاً
الآن يمكننا أن نرى كيف تتكامل كل مفاهيم هذه الوحدة معاً:
- ReAct (الدرس 10.1): حلقتنا الجديدة هي تطبيق عملي لمبدأ Reason+Act. في كل خطوة، الوكيل "يفكر" (Reason) ثم "ينفذ" (Act).
- التصحيح الذاتي (الدرس 10.2): عندما يرى النموذج اللغوي "ملاحظة" تشير إلى خطأ، وقراره التالي يهدف إلى إصلاح هذا الخطأ (مثل تجربة أداة أخرى)، فهذا شكل من أشكال التصحيح الذاتي.
- استراتيجيات التراجع (الدرس 10.6): بدلاً من أن تكون استراتيجية التراجع مبرمجة بشكل ثابت، يمكن للنموذج اللغوي الآن أن يختار استراتيجية التراجع المناسبة بناءً على نوع الخطأ الذي يراه في "الملاحظة".

خلاصة الدرس
لقد وصلنا إلى نهاية هذه الوحدة الهامة. في هذا الدرس، قمنا بالخطوة الأخيرة لجعل وكيلنا قادراً على التعامل مع عالم الواقع غير المتوقع.
أهم النقاط التي تعلمناها:
- الحلقة الديناميكية (Think-Act-Observe) تسمح للوكيل باتخاذ قرارات مستنيرة بناءً على نتائج أفعاله السابقة.
- مفهوم "الملاحظة" (Observation) هو المفتاح؛ حيث يتم تحويل كل من النجاح والفشل إلى معلومات يمكن لـ "عقل" الوكيل تحليلها.
- من خلال تزويد النموذج اللغوي بسجل يتضمن "الملاحظات"، فإننا نمكّنه من التفكير بشكل استراتيجي حول الأخطاء واختيار الإجراء الصحيح، بدلاً من اتباع قواعد ثابتة.
- هذه الحلقة توحد مفاهيم مثل ReAct والتصحيح الذاتي واستراتيجيات التراجع في إطار عمل متكامل وذكي.
ماذا بعد؟
بهذا الدرس، نكون قد أكملنا بناء الهيكل المنطقي لوكيل ذكاء اصطناعي قوي وقادر على التكيف. أصبح وكيلنا الآن لا يملك فقط الأدوات والذاكرة والعقل، بل يملك أيضاً الحكمة للتعامل مع الفشل.
في الوحدة القادمة والأخيرة، "الأمان وتحسين الأداء"، سننتقل من بناء الوظائف إلى تحصينها. سنتعلم كيف نحمي وكيلنا من الهجمات، ونضمن خصوصية بيانات المستخدمين، ونراقب أداءه لنجعله أكثر كفاءة وموثوقية. لقد حان الوقت للتأكد من أن الوكيل الذي بنيناه ليس ذكياً فحسب، بل آمن ومسؤول أيضاً.