انتقل إلى المحتوى الرئيسي

الوحدة 5 — الأدوات المشتركة بين الوكلاء

الوكيل بلا أدوات مساعد نصّيّ ذكيّ لكن معزول: يعرف ما تعلّمه النموذج فقط، ولا يستطيع قراءة ملفّ، ولا الاتّصال بواجهة برمجيّة، ولا حفظ نتيجة. الأدوات هي ما يجعل الوكيل قادرًا على العمل الحقيقيّ. CrewAI يعرض ثلاث طبقات للأدوات، ولكلّ منها استعمال مبرَّر.

الطبقة الأولى: الأدوات المدمجة

حزمة crewai_tools تحوي عشرات الأدوات الجاهزة: قراءة ملفّات، بحث في PDF، استفسار موقع ويب، تنفيذ استعلام SQL، وغيرها. الاستيراد مباشر:

from crewai_tools import FileReadTool, PDFSearchTool, SerperDevTool

lecture_specs = FileReadTool(file_path="./specs/produit.md")
recherche_pdf = PDFSearchTool(pdf="./specs/ancien-produit.pdf")
recherche_web = SerperDevTool() # يتطلّب SERPER_API_KEY

قاعدة: قبل كتابة أداة مخصّصة، ابحث في crewai_tools عن مكافئ. تُنقذك ساعات، وتُعطيك إدارة أخطاء ناضجة تعامل عليها مطوّرو المكتبة.

الطبقة الثانية: أدوات مخصّصة بـ@tool

للأدوات البسيطة، @tool من crewai_tools أو langchain_core.tools تُنشئ أداة من دالّة بايثونيّة. docstring تُصبح وصف الأداة للنموذج، وتلميحات الأنواع (type hints) تُصبح مخطّط الوسائط:

from crewai.tools import tool

@tool("chercher_terme")
def chercher_terme(terme: str, dossier: str = "./docs") -> str:
"""يبحث عن مصطلح في كلّ الملفّات النصّيّة داخل مجلّد،
ويُرجع أوّل خمس نتائج مع اسم الملفّ ورقم السطر."""
import pathlib, re
resultats = []
for f in pathlib.Path(dossier).rglob("*.md"):
for i, ligne in enumerate(f.read_text(encoding="utf-8").splitlines(), 1):
if re.search(terme, ligne, re.IGNORECASE):
resultats.append(f"{f.name}:{i}{ligne.strip()[:80]}")
if len(resultats) >= 5:
return "\n".join(resultats)
return "\n".join(resultats) or "لا نتائج"

النموذج يقرأ docstring كتعليمة استعمال، ويرى وسيطًا terme من نوع str ووسيطًا اختياريًّا dossier. الوسائط الخاطئة تُرفَض قبل التنفيذ.

الطبقة الثالثة: أدوات عبر BaseTool

للأدوات الأعقد (حالة داخليّة، إعدادات، تصنيف صريح)، نرث BaseTool:

from crewai.tools import BaseTool
from pydantic import BaseModel, Field

class LireFichierArgs(BaseModel):
chemin: str = Field(description="مسار الملفّ المطلق أو النسبيّ")
max_octets: int = Field(default=20000, description="حدّ أعلى للقراءة")

class LireFichierAvance(BaseTool):
name: str = "lire_fichier_avance"
description: str = "قراءة ملفّ نصّيّ مع حدّ حجم قابل للتعديل."
args_schema: type[BaseModel] = LireFichierArgs

def _run(self, chemin: str, max_octets: int = 20000) -> str:
with open(chemin, "r", encoding="utf-8") as f:
return f.read(max_octets)

هذا الأسلوب أوضح للتوثيق التقنيّ ولإعادة استعمال الأداة في مشاريع متعدّدة، لكنّه أطول بقليل من @tool.

توزيع الأدوات على الوكلاء

قاعدة: كلّ وكيل لا يرى إلاّ الأدوات التي يحتاجها. المحلِّل يقرأ الملفّات ويبحث؛ لا يحتاج كتابة تسويقيّة. المحرِّر يكتب؛ لا يحتاج بحثًا في السجلّات. المراجِع يقرأ ويقارن؛ لا يحتاج إرسال بريد.

analyste = Agent(
role="محلّل مواصفات",
goal="...",
backstory="...",
tools=[lecture_specs, recherche_web],
)

redacteur = Agent(
role="محرّر تقنيّ",
goal="...",
backstory="...",
tools=[chercher_terme], # ليطابق أسلوب توثيق سابق
)

relecteur = Agent(
role="مراجِع تقنيّ",
goal="...",
backstory="...",
tools=[FileReadTool(file_path="./guide-style.md"), chercher_terme],
)

لماذا لا نُعطي الجميع كلّ الأدوات؟ لثلاثة أسباب: (1) كلّ أداة إضافيّة تُطيل تعليمة النظام وتزيد فرصة الخطأ في الاختيار، (2) الوكيل قد يستدعي أداة لا يحتاجها فيُضيّع نداءً كاملًا، (3) الأدوات الحسّاسة (إرسال بريد، كتابة قاعدة بيانات) يجب أن تكون محصورة في وكيل واحد يُراقَب.

أدوات على مستوى المهمّة

يمكن أيضًا تمرير tools=[...] إلى Task نفسها. هذا مفيد حين تحتاج مهمّة محدّدة أداة إضافيّة لا تنتمي إلى وكيلها بشكل دائم:

tache_verification = Task(
description="...",
expected_output="...",
agent=relecteur,
tools=[verificateur_orthographe], # فقط لهذه المهمّة
)

الوكيل يرى في هذه اللحظة أدواته الخاصّة بالإضافة إلى أدوات المهمّة، ثمّ يفقدها بعد انتهائها.

أخطاء الأدوات: تحويلها إلى رسائل مقروءة

كما رأينا في دورة LangChain، الأداة يجب ألاّ ترفع استثناءً غير معالَج إلى الوكيل. try/except تُحوّل الخطأ إلى نصّ يقرأه النموذج ويقرّر:

@tool("appeler_api_produit")
def appeler_api_produit(id_produit: str) -> str:
"""يستعلم واجهة كتالوج المنتجات..."""
try:
r = httpx.get(f"https://api.exemple.com/produits/{id_produit}", timeout=5)
r.raise_for_status()
return r.text
except httpx.HTTPStatusError as e:
return f"فشل نداء الواجهة (رمز {e.response.status_code}). قد يكون المنتج غير موجود."
except httpx.TimeoutException:
return "انتهت مهلة الواجهة. أعِد المحاولة أو تجاوز هذه الخطوة."

النموذج يقرأ الرسالة ويقرّر: يحاول مرّة أخرى، يستدعي أداة أخرى، أو يذكر المشكلة في المخرَج النهائيّ.

الفخّ: أداة تُعيد آلاف الأسطر

أداة تقرأ ملفّ سجلّ بحجم 200 كيلو تُعيد 200 كيلو من النصّ للنموذج. النموذج يبتلع كلّ ذلك في نافذته، تنتفخ الفاتورة، وأحيانًا يُقطَع السياق. قاعدة: كلّ أداة تُعيد بيانات كبيرة يجب أن تحوي حدًّا صريحًا (max_octets, limit, nb_lignes) قابلًا للتحكّم. القيمة الافتراضيّة معقولة، والنموذج يعرف أنّه يمكن رفعها إن احتاج.

صلاحيّات

لا تعطِ الوكيل أداة تكتب في قاعدة بيانات إنتاج، أو ترسل بريدًا حقيقيًّا، أو تحوّل نقودًا، بدون طبقة موافقة بشريّة صريحة. المعمار الآمن: أداة تُنتج «طلبًا» في قائمة، وسكربت مراقَب يُنفّذه بعد موافقة.

الخلاصة

  • ثلاث طبقات للأدوات: مدمجة (crewai_tools)، مخصّصة بسيطة (@tool)، متقدّمة (BaseTool).
  • كلّ وكيل يحصل على أدواته الضروريّة فقط؛ الأدوات الحسّاسة تُحصَر في وكيل واحد مُراقَب.
  • الأدوات تُعيد رسائل مقروءة عند الفشل، لا استثناءات؛ النموذج يقرأها ويقرّر.
  • كلّ أداة تُعيد بيانات كبيرة تحتاج حدًّا صريحًا لمنع انتفاخ السياق والفاتورة.