أهلاً بك مجدداً في رحلتنا لبناء وكلاء الذكاء الاصطناعي!
في الدرس السابق، قمنا ببناء دفاعات مهمة لوكيلنا. لقد تعلمنا "مبدأ أقل الامتيازات" وطبقنا استراتيجيات عملية مثل تقييد نطاق الأدوات وطلب الموافقة البشرية لجعل وكيلنا أكثر أماناً. لقد وضعنا الأقفال القوية على الأبواب. ولكن، كيف نعرف ما إذا كان شخص ما يحاول العبث بهذه الأقفال؟ أو ما إذا كان الوكيل نفسه يرتكب أخطاء خلف الأبواب المغلقة؟
هنا يأتي دور درسنا اليوم. هدفنا هو تطبيق مبادئ أساسية لمراقبة أداء الوكيل (مثل تسجيل الأخطاء والقرارات). سنقوم بتركيب "كاميرات مراقبة" ونظام تسجيل لنتمكن من رؤية وفهم كل ما يفعله وكيلنا، سواء كان صواباً أم خطأ. هذا لا يساعدنا فقط في اكتشاف المشاكل، بل هو الخطوة الأولى لتحسين أداء الوكيل وجعله أكثر ذكاءً وموثوقية مع مرور الوقت.
1. ما هي مراقبة الوكيل (Agent Monitoring) ولماذا هي ضرورية؟
ببساطة، مراقبة الوكيل تعني تتبع سلوكه وقراراته ومخرجاته لفهم أدائه. بما أن وكلاء الذكاء الاصطناعي، خاصة تلك التي تعتمد على نماذج اللغة الكبيرة، ليست دائماً متوقعة في تصرفاتها، فإن المراقبة ليست ترفاً، بل هي ضرورة.
تخيل أن وكيلك هو موظف جديد. لن تتركه يعمل في الظلام. سترغب في معرفة:
- ما هي المهام التي يستلمها؟
- كيف يفكر في حلها؟
- ما هي الإجراءات التي يتخذها؟
- هل ينجح في مهامه أم يواجه صعوبات؟
هذا بالضبط ما تفعله المراقبة. في عالم الذكاء الاصطناعي، يُستخدم أحياناً مصطلح أكثر تقدماً وهو "قابلية الملاحظة" (Observability)، والذي يعني القدرة على فهم الحالة الداخلية للنظام من خلال مخرجاته الخارجية (مثل السجلات والبيانات).
هناك العديد من الجوانب التي يمكننا مراقبتها في وكيل الذكاء الاصطناعي.


بالنسبة لنا كمطورين، فإن أبسط وأقوى شكل من أشكال المراقبة هو التسجيل (Logging).
2. التسجيل (Logging): الصندوق الأسود لوكيلك
التسجيل هو عملية كتابة رسائل حول الأحداث المهمة التي تقع أثناء تشغيل البرنامج إلى وجهة معينة، مثل الشاشة أو ملف نصي. هذا الملف يعمل بمثابة "الصندوق الأسود" للطائرة؛ ففي حال وقوع "حادث" (خطأ برمجي)، يمكننا الرجوع إلى السجلات لمعرفة ما حدث بالضبط خطوة بخطوة.
ما هي الأحداث الرئيسية التي يجب أن نسجلها في دورة حياة الوكيل؟
- استلام المهمة: ما هو الطلب الأولي من المستخدم؟
- التفكير والتخطيط: ما هي "سلسلة الأفكار" (Chain-of-Thought) للوكيل؟ كيف حلل الطلب وقرر ما سيفعله؟
- قرار استدعاء الأداة: أي أداة قرر استخدامها؟ وما هي المُدخلات (arguments) التي مررها لها؟
- تنفيذ الأداة: هل نجحت الأداة؟ ما هي النتيجة التي أعادتها؟ (هنا يمكننا تسجيل نتيجة الموافقة البشرية من الدرس الماضي!)
- الأخطاء والاستثناءات: أي خطأ يحدث في أي خطوة يجب تسجيله فوراً مع أكبر قدر ممكن من التفاصيل.
- الاستجابة النهائية: ما هي الإجابة النهائية التي قدمها الوكيل للمستخدم؟
من خلال تسجيل هذه النقاط الست، نحصل على قصة كاملة لكل مهمة ينفذها الوكيل، مما يجعل تصحيح الأخطاء (debugging) وتحليل الأداء أسهل بكثير.
3. التطبيق العملي: استخدام مكتبة logging في بايثون
صحيح أن استخدام print() يمكن أن يؤدي الغرض مبدئياً، لكنه غير عملي للتطبيقات الحقيقية. توفر بايثون مكتبة مدمجة اسمها logging وهي الأداة الاحترافية لهذه المهمة.
لماذا logging أفضل من print()؟
- مستويات الخطورة: تتيح لك تصنيف رسائلك (DEBUG, INFO, WARNING, ERROR, CRITICAL). يمكنك إظهار رسائل الأخطاء فقط في بيئة الإنتاج، بينما تظهر كل التفاصيل في بيئة التطوير.
- المرونة في الإخراج: يمكنك بسهولة توجيه السجلات إلى الشاشة، أو إلى ملف، أو كليهما.
- التنسيق الآلي: تضيف تلقائياً معلومات مفيدة مثل التاريخ والوقت ومستوى الخطورة لكل رسالة.
دعنا نرى كيف يمكننا دمج التسجيل في حلقة عمل وكيلنا.
الخطوة 1: إعداد المسجِّل (Logger)
أولاً، نقوم بإعداد المسجِّل في بداية برنامجنا ليقوم بالكتابة في ملف اسمه agent.log.
import logging
import time # سنستخدمها لمحاكاة التأخير
# --- إعداد المسجِّل (Logger) ---
# سيقوم هذا الإعداد بتسجيل الرسائل في ملف وتنسيقها مع الطابع الزمني ومستوى الخطورة
logging.basicConfig(
level=logging.INFO, # سجل الرسائل من مستوى INFO وما فوق
format='%(asctime)s - %(levelname)s - %(message)s',
filename='agent.log', # اسم ملف السجل
filemode='a' # 'a' للإضافة إلى الملف في كل مرة يتم تشغيل البرنامج
)
# إضافة معالج (handler) لإظهار السجلات على الشاشة أيضاً
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logging.getLogger('').addHandler(console_handler)
print("Logger is set up. Check agent.log for detailed output.")
الخطوة 2: دمج التسجيل في منطق الوكيل
الآن، سنأخذ نسخة مبسطة من حلقة عمل الوكيل ونضيف استدعاءات logging في كل مرحلة مهمة.
# --- دوال الوكيل الوهمية ---
def think_and_plan(prompt: str) -> str:
"""يحاكي تفكير النموذج اللغوي وتخطيطه."""
logging.info(f"بدأ الوكيل في التفكير بشأن: '{prompt}'")
time.sleep(1) # محاكاة تأخير استدعاء الـ API
# منطق وهمي: إذا كان الطلب يحتوي على كلمة "تقرير"
if "تقرير" in prompt:
thought = "المستخدم يريد تقريراً. سأستخدم أداة `write_file`."
action = "call_tool:write_file:report.txt:هذا هو التقرير."
logging.info(f"خطة الوكيل: {thought}")
return action
# منطق وهمي للتحذير من الدرس السابق
elif "حذف" in prompt:
thought = "المستخدم يطلب إجراءً خطيراً (حذف). سأستخدم الأداة مع طلب الموافقة."
action = "call_tool:delete_file_with_confirmation:important_data.csv"
logging.warning(f"الوكيل يخطط لإجراء خطير: {thought}")
return action
else:
thought = "لم أفهم الطلب جيداً. سأبلغ عن خطأ."
logging.error(f"فشل الوكيل في فهم الطلب: '{prompt}'")
raise ValueError("Cannot determine action for the given prompt.")
def execute_tool(action: str) -> str:
"""يحاكي تنفيذ الأداة المختارة."""
logging.info(f"الوكيل ينفذ الإجراء: {action}")
parts = action.split(':')
tool_name = parts[1]
if tool_name == "write_file":
# محاكاة كتابة ملف
time.sleep(0.5)
logging.info(f"تم استدعاء أداة 'write_file' بنجاح.")
return "تمت كتابة الملف بنجاح."
elif tool_name == "delete_file_with_confirmation":
# محاكاة طلب الموافقة البشرية من الدرس الماضي
logging.info("الأداة تتطلب موافقة بشرية.")
# في تطبيق حقيقي، ستتوقف هنا وتنتظر إدخال المستخدم
# user_approval = input("Approve? y/n")
# if user_approval == 'y': ...
result = "رفض المستخدم الإجراء."
logging.info(f"نتيجة الموافقة البشرية: {result}")
return result
else:
logging.error(f"محاولة استدعاء أداة غير معروفة: '{tool_name}'")
return f"خطأ: الأداة '{tool_name}' غير موجودة."
# --- حلقة العمل الرئيسية للوكيل ---
user_prompt = "أريد حذف بعض الملفات"
logging.info(f"--- جلسة جديدة: استلام طلب من المستخدم ---")
logging.info(f"طلب المستخدم: '{user_prompt}'")
try:
# 1. التفكير والتخطيط
agent_action = think_and_plan(user_prompt)
logging.info(f"القرار النهائي للوكيل: {agent_action}")
# 2. تنفيذ الأداة
tool_result = execute_tool(agent_action)
logging.info(f"نتيجة تنفيذ الأداة: '{tool_result}'")
# 3. الاستجابة النهائية
final_response = f"لقد أكملت المهمة. النتيجة: {tool_result}"
logging.info(f"الاستجابة النهائية للمستخدم: '{final_response}'")
print(f"\nالرد على المستخدم: {final_response}")
except Exception as e:
logging.error(f"حدث خطأ فادح في حلقة عمل الوكيل: {e}", exc_info=True)
print("\nالرد على المستخدم: عذراً، حدث خطأ ما ولم أتمكن من إكمال طلبك.")
بعد تشغيل هذا الكود، افتح ملف agent.log. ستجد شيئاً كهذا (مع اختلاف التواريخ):
2023-10-27 10:30:00,123 - INFO - --- جلسة جديدة: استلام طلب من المستخدم ---
2023-10-27 10:30:00,124 - INFO - طلب المستخدم: 'أريد حذف بعض الملفات'
2023-10-27 10:30:00,125 - INFO - بدأ الوكيل في التفكير بشأن: 'أريد حذف بعض الملفات'
2023-10-27 10:30:01,128 - WARNING - الوكيل يخطط لإجراء خطير: المستخدم يطلب إجراءً خطيراً (حذف). سأستخدم الأداة مع طلب الموافقة.
2023-10-27 10:30:01,129 - INFO - القرار النهائي للوكيل: call_tool:delete_file_with_confirmation:important_data.csv
2023-10-27 10:30:01,130 - INFO - الوكيل ينفذ الإجراء: call_tool:delete_file_with_confirmation:important_data.csv
2023-10-27 10:30:01,131 - INFO - الأداة تتطلب موافقة بشرية.
2023-10-27 10:30:01,132 - INFO - نتيجة الموافقة البشرية: رفض المستخدم الإجراء.
2023-10-27 10:30:01,133 - INFO - نتيجة تنفيذ الأداة: 'رفض المستخدم الإجراء.'
2023-10-27 10:30:01,134 - INFO - الاستجابة النهائية للمستخدم: 'لقد أكملت المهمة. النتيجة: رفض المستخدم الإجراء.'
هذا السجل يروي قصة كاملة وواضحة لما حدث. لاحظ كيف سجلنا رسالة WARNING عندما اكتشف الوكيل طلباً خطيراً. هذا النوع من التسجيل لا يقدر بثمن!
اختبر فهمك!
لنفترض أن دالة execute_tool يمكن أن تفشل إذا تم تمرير اسم أداة غير صالح لها. في الكود أعلاه، قمنا بالفعل بتسجيل هذا كـ ERROR.
الآن، تخيل أن أداة write_file قد تفشل بسبب عدم وجود مساحة كافية على القرص. أين وفي أي دالة (think_and_plan أم execute_tool) ستضيف كتلة try/except ورسالة logging.error للتعامل مع هذا الفشل وتسجيله؟
Show answer
المكان الصحيح هو داخل دالة execute_tool، وتحديداً حول الجزء الذي يحاكي تنفيذ الأداة. هذا لأن فشل الكتابة على القرص هو خطأ يحدث أثناء تنفيذ الأداة، وليس أثناء التخطيط.
سيبدو الكود المعدل هكذا داخل دالة execute_tool:
if tool_name == "write_file":
try:
# محاكاة كتابة ملف
time.sleep(0.5)
# لنفترض أن هذه الدالة قد تثير خطأ
# write_to_disk(filename, content)
logging.info(f"تم استدعاء أداة 'write_file' بنجاح.")
return "تمت كتابة الملف بنجاح."
except Exception as e: # بشكل مثالي سيكون Exception أكثر تحديداً مثل InsufficientStorageError
logging.error(f"فشلت أداة 'write_file' أثناء التنفيذ: {e}")
return f"خطأ: فشلت كتابة الملف."
خلاصة الدرس
لقد قمنا اليوم بتركيب نظام عصبي لوكيلنا، مما يسمح لنا بالشعور بكل ما يحدث داخله. المراقبة ليست مجرد أداة لتصحيح الأخطاء، بل هي أساس بناء الثقة في الأنظمة المعقدة والمستقلة.
أهم النقاط التي تعلمناها:
- المراقبة ضرورية: لفهم سلوك الوكلاء غير المتوقع، وتشخيص الأخطاء، وتحسين الأداء.
- التسجيل (Logging) هو حجر الأساس: وهو الطريقة الأبسط والأكثر فعالية للمراقبة على مستوى المبتدئين.
- سجل القصة الكاملة: من استلام الطلب، مروراً بالتفكير والقرار والتنفيذ، وانتهاءً بالاستجابة النهائية والأخطاء.
- استخدم مكتبة
logging: فهي توفر مستويات خطورة وتنسيقاً ومرونة تفوق بكثير استخدامprint.
ماذا بعد؟
الآن بعد أن أصبح لدينا ملف agent.log غني بالمعلومات، ماذا نفعل به؟ مجرد وجود البيانات لا يكفي.
في الدرس القادم، سنتعلم كيف ننتقل من مجرد جمع البيانات إلى استخدامها. سنتناول تحليل نقاط الضعف في أداء الوكيل وتحديد مجالات التحسين. سنتعلم كيف نقرأ سجلاتنا بعين ناقدة، ونبحث عن الأنماط، ونطرح الأسئلة الصحيحة: "لماذا يختار الوكيل هذه الأداة دائماً بشكل خاطئ؟" أو "ما هو نوع الأسئلة الذي يسبب له الارتباك؟". وبهذا، نبدأ دورة التحسين المستمر.