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

الوحدة 7 — الأدوات ونداءات الدوالّ

بلغ مساعد النفقات نقطة عاجزًا فيها عن الحساب: «كم يبقى من سقفي الشهريّ لعشاء العمل؟» ليست مسألة استرجاع نصّ من سياسة، بل حساب على قاعدة بيانات. النموذج اللغويّ لا يحسب موثوقًا؛ يجب أن نُتيح له استدعاء دوالّ حقيقيّة. هذه ميزة تُسمّى «نداء الدوالّ» (function calling) أو، في LangChain، الأدوات.

من دالّة مطبوعة إلى أداة

المُختصر: نُعرِّف دالّة بايثونيّة، نُوثّقها بـtype hints، ونُغلّفها بـ@tool:

from langchain_core.tools import tool

@tool
def convertir_devise(montant: float, source: str, cible: str) -> float:
"""يُحوّل مبلغًا من عملة إلى أخرى بسعر اليوم.

Args:
montant: المبلغ الأصليّ.
source: رمز العملة الأصليّة (مثل EUR).
cible: رمز العملة المستهدفة (مثل USD).
"""
taux = {("USD", "EUR"): 0.92, ("EUR", "USD"): 1.09}
return round(montant * taux[(source, cible)], 2)

يستخرج @tool تلقائيًّا:

  • الاسم من اسم الدالّة.
  • الوصف من docstring (يظهر للنموذج).
  • مخطّط الوسائط من type hints وArgs: في docstring.

هذه الحقول ثلاثتها حاسمة. الاسم يوجّه الاختيار، والوصف يوجّه القرار، ومخطّط الوسائط يمنع النموذج من إرسال قيم من نوع خاطئ.

StructuredTool للحالات الأعقد

حين نحتاج تحكّمًا أدقّ (اسم مختلف عن اسم الدالّة، مخطّط Pydantic صريح، إشارة return_direct)، نستعمل StructuredTool:

from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool

class SchemaPlafond(BaseModel):
categorie: str = Field(description="فئة النفقة، مثل 'عشاء عمل' أو 'نقل'.")
mois: str = Field(description="الشهر بصيغة YYYY-MM.")

def plafond_restant(categorie: str, mois: str) -> float:
... # قراءة من قاعدة البيانات

outil_plafond = StructuredTool.from_function(
func=plafond_restant,
name="plafond_restant",
description="يُرجع السقف المتبقّي لفئة نفقات ولشهر.",
args_schema=SchemaPlafond,
)

استعمال Pydantic صريحًا يمنح رسائل خطأ مقروءة حين يُرسل النموذج وسائط ناقصة أو من النوع الخاطئ.

ربط الأدوات بنموذج

نُخبر النموذج بالأدوات المتاحة عبر bind_tools:

from langchain_openai import ChatOpenAI

modele = ChatOpenAI(model="gpt-4o-mini", temperature=0)
outils = [convertir_devise, outil_plafond]
modele_outille = modele.bind_tools(outils)

reponse = modele_outille.invoke("حوّل 100 دولار إلى يورو.")
print(reponse.tool_calls)

يفحص النموذج السؤال، ويقرّر ما إن يستدعي أداة، وأيّة أداة، وبأيّ وسائط. النتيجة في حقل tool_calls:

[{'name': 'convertir_devise',
'args': {'montant': 100, 'source': 'USD', 'cible': 'EUR'},
'id': 'call_abc123'}]

نقطة أساسيّة: النموذج لا يُنفّذ الأداة؛ يقترحها فقط. المسؤوليّة على شيفرتنا لتنفيذ الاستدعاء وإرجاع النتيجة.

حلقة الاستدعاء اليدويّ

from langchain_core.messages import HumanMessage, ToolMessage

messages = [HumanMessage("حوّل 100 دولار إلى يورو ثم اطرح 20 يورو.")]
while True:
ai = modele_outille.invoke(messages)
messages.append(ai)
if not ai.tool_calls:
break
for appel in ai.tool_calls:
outil = {"convertir_devise": convertir_devise, "plafond_restant": outil_plafond}[appel["name"]]
resultat = outil.invoke(appel["args"])
messages.append(ToolMessage(content=str(resultat), tool_call_id=appel["id"]))

هذه الحلقة تفسِّر «الوكيل» في نسخته الابتدائيّة. الوحدة 8 ستُغلّفها بأمان الحدود.

التحقّق من الوسائط: الخطأ الأشيع

يُحدث النموذج بانتظام أخطاء دقيقة: تاريخ بصيغة 3 مارس بدل 2026-03-03، وعملة يورو بدل EUR، ورقم داخل سلسلة. Pydantic يرفعها فورًا:

from pydantic import ValidationError
try:
outil_plafond.invoke({"categorie": "عشاء عمل", "mois": "مارس 2026"})
except ValidationError as e:
print(e) # mois doesn't match YYYY-MM

استراتيجيّة معياريّة: نلتقط الخطأ ونُعيده إلى النموذج كـToolMessage. يقرأ النموذج رسالة الخطأ ويُعيد الاستدعاء بصيغة صحيحة. لا تفشل الأداة صامتة، ولا تُنفَّذ بمُدخل خاطئ.

أدوات الخيط الأحمر

مساعد النفقات يمتلك الآن ثلاث أدوات:

  • convertir_devise(montant, source, cible) — لنفقات بالعملات الأجنبيّة.
  • plafond_restant(categorie, mois) — لقراءة قاعدة بيانات النفقات.
  • ajouter_ligne_tableur(date, montant, fournisseur, categorie) — لإدراج طلب استرداد.

الأداة الثالثة تُغيّر حالة (كتابة في قاعدة). هنا يظهر خطر جديد: يستدعيها النموذج بلا تأكيد. حلّان: تأكيد بشريّ صريح قبل التنفيذ (الإنسان في الحلقة، الوحدة 8)، أو إذن مسبق يشترط تعبيرًا واضحًا مثل «أضِف».

الفخّ: الأداة المكلفة

نُعرّف أداة analyser_pdf_complet(chemin) التي تُشغّل OCR وتحليل نموذج على 40 صفحة. يستدعيها النموذج «للتأكّد» عند كلّ سؤال. الفاتورة تنفجر.

علاج:

  • الوصف صريح: «هذه أداة مكلفة. استعملها مرّة واحدة في بداية المحادثة».
  • نتائج مؤقّتة في الذاكرة تُعاد بدل استدعائها مجدّدًا.
  • سقف على عدد الاستدعاءات لكلّ أداة في تشغيل واحد (يُفرَض عند مستوى الوكيل، الوحدة 8).
اقرأ Docstring كتعليمة للنموذج

النموذج يستعمل docstring حرفيًّا لاختيار الأداة. فقرة غامضة تُنتج قرارات سيّئة. اعتَبِر كلّ docstring تعليمة موجَّهة للنموذج، لا وثيقة موجَّهة للمطوّر.

الخلاصة

  • @tool يُغلّف دالّة بايثونيّة بمخطّط تلقائيّ من type hints وdocstring؛ وStructuredTool للحالات الأعقد.
  • bind_tools يُشير للنموذج بالأدوات؛ الاستدعاء يبقى مسؤوليّة شيفرتنا.
  • Pydantic يمنع الوسائط الخاطئة قبل التنفيذ؛ خطأ التحقّق يُعاد للنموذج كرسالة أداة ليُصحّح.
  • وصف الأداة كتعليمة موجَّهة للنموذج؛ صياغته السيّئة تنتج قرارات سيّئة وفواتير كبيرة.

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

الوحدة التالية تُدمج كلّ هذا في حلقة وكيل حقيقيّة مع LangGraph، حيث تُقرَّر الأدوات وتُنفَّذ وتُراجَع نتائجها ضمن دورة راسون-عمل.