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

الوحدة 2 — حلقة الاستدلال والفعل

قلب أيّ وكيل حلقة تحكّم بسيطة: النموذج يفكّر، يقترح فعلًا، نُنفّذ الفعل، نُعيد إليه الملاحظة، ثمّ يقرّر: أَيَنتَهي أم يواصل. هذا ما اقترحته ورقة ReAct (Yao وآخرون، 2022) قبل أن يذوب النمط في نداء الأدوات الأصليّ. في هذه الوحدة نكتب هذه الحلقة يدًا، لأنّ فهمها يحميك من ألف خطأ.

النمط: فكرة، فعل، ملاحظة

يعمل ReAct بتناوب ثلاثيّ:

  • فكرة (thought): النموذج يكتب استدلاله المختصر: «أحتاج أن أعرف تاريخ الإصدار قبل مقارنة النسخ.»
  • فعل (action): يستدعي أداة بوسائط محدّدة: recherche_web("release date Postgres 16").
  • ملاحظة (observation): شيفرتك تنفّذ الأداة وتُعيد النتيجة إليه.

يُكرَّر هذا حتّى يُنتج النموذج جوابًا نهائيًّا بلا استدعاء أداة. الحلقة كلّها ستّون سطرًا أو أقلّ.

التنفيذ الأدنى ببايثون

نبدأ بأربع أدوات لوكيل البحث: recherche_web، lire_page، base_interne، noter_fait. ثمّ حلقة تحكم:

from openai import OpenAI

client = OpenAI()

outils = [
{"type": "function", "function": {
"name": "recherche_web",
"description": "يبحث في الويب ويُعيد قائمة عناوين URL مع مقتطف قصير.",
"parameters": {"type": "object", "properties": {"requete": {"type": "string"}}, "required": ["requete"]},
}},
# ... أدوات lire_page وbase_interne وnoter_fait بنفس الصيغة
]

def executer(nom, args):
if nom == "recherche_web": return recherche_web(args["requete"])
if nom == "lire_page": return lire_page(args["url"])
if nom == "base_interne": return base_interne(args["question"])
if nom == "noter_fait": return noter_fait(args["fait"], args["source"])
return {"erreur": f"أداة غير معروفة: {nom}"}

def boucle_agent(question, max_iterations=8):
messages = [
{"role": "system", "content": "أنت وكيل بحث. استعمل الأدوات المتاحة، لا تخترع مصادر."},
{"role": "user", "content": question},
]
for i in range(max_iterations):
reponse = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=outils)
msg = reponse.choices[0].message
messages.append(msg)
if not msg.tool_calls:
return msg.content
for appel in msg.tool_calls:
resultat = executer(appel.function.name, json.loads(appel.function.arguments))
messages.append({"role": "tool", "tool_call_id": appel.id, "content": json.dumps(resultat)})
return "توقّفت الحلقة عند الحدّ الأقصى للتكرار."

هذه الشيفرة كاملة عمليًّا. لا LangChain ولا LangGraph. الفكرة والفعل يذوبان في tool_calls، والملاحظة تعود عبر رسالة دور tool. حلقة for تُشكّل حدّ التكرار.

شرط التوقّف: الحاجز الأوّل

بلا شرط توقّف واضح، الوكيل يدور بلا نهاية. ثلاث آليّات تعمل معًا:

  • جواب طبيعيّ: النموذج ينتج نصًّا بلا tool_calls؛ نُنهي.
  • حدّ تكرار: عدد أقصى (هنا 8) لأنّ الحلقة قد تُكرِّر خطوات على أمل يتلاشى.
  • حدّ رموز: مجموع رموز المدخل والمخرج على كلّ الخطوات لا يتجاوز سقفًا. سيأتي في الوحدة 7.

القاعدة: كلّ حلقة إنتاجيّة لها الحدّان الأوّلان على الأقلّ. حدّ تكرار مرتفع مثل 40 يُعادل «بلا حدّ» عمليًّا لأنّ الفاتورة تصير مؤلمة قبل أن يصل الوكيل إليه.

الأثر: ما نراه من هذه الحلقة

لنطلب من الوكيل: «هل يدعم Postgres 16 التقسيم المنطقيّ لجدول واحد فقط؟». مسار نموذجيّ:

[الفكرة]  أحتاج البحث عن ميزات Postgres 16.
[الفعل] recherche_web("Postgres 16 logical partitioning single table")
[ملاحظة] ثلاث نتائج، أوّلها ملاحظات إصدار رسميّة.
[الفعل] lire_page("https://postgresql.org/.../release-16.html")
[ملاحظة] فقرة عن «logical replication of partitioned tables».
[الفعل] base_interne("سياسة استعمال Postgres 16 داخل الفريق")
[ملاحظة] «مُعتَمَد منذ مارس 2025، مع قيود على التقسيم.»
[الفعل] noter_fait("Postgres 16 يدعم الاستنساخ المنطقيّ للجداول المُقسَّمة، مع قيد رأس الجدول.", "release-16.html")
[الجواب] نعم، مع تفصيل القيد، مع مصدرَين.

خمس خطوات، وليس نداء واحد. هذا ما يفعله الوكيل: يقرّر كم مرّة يستدعي أدواتك.

الفخّ: الفكرة التي لا تُقرأ

نموذج قد يكتب فكرة، ثمّ يستدعي أداة لا علاقة لها بالفكرة. لا تعوّل على أنّ «الفكرة تُفسّر الفعل». ما يهمّ للتشخيص:

  • سجّل الوسائط الفعليّة المُمرَّرة لكلّ أداة، لا الفكرة قبلها.
  • سجّل ملاحظة الأداة كما رآها النموذج، لا كما ولّدها المصدر.
  • سجّل حالة التوقّف: هل انتهت بحدّ التكرار أم بجواب طبيعيّ؟

هذه العناصر الثلاثة تحلّ 80 ٪ من حالات التصحيح. ستُبنى فوقها لوحة قابلية المراقبة في الوحدة 9.

متى نفعل ذلك بلا إطار

الإطار (LangGraph مثلًا) يقدّم رسمًا صريحًا ونقاط استئناف. لكن حين تكون الحلقة بسيطة (خطوات متتابعة، بلا تفريع)، بايثون خام أقصر وأوضح. كتاب هذه الوحدة يقصد أن يعلّمك ما «يفعله» الإطار في الخفاء. في الوحدة 9، سنقارن هذا التنفيذ بنسخة LangGraph على نفس الوكيل.

اقرأ الأثر قبل تعديل التعليمة

حين يُخطئ الوكيل، أوّل ردّة فعل خاطئة: «سأحسِّن تعليمة النظام». الصواب: اقرأ الأثر كاملًا. في 60 ٪ من الحالات، السبب أداة بلا وصف كاف أو ملاحظة مقتطعة، لا التعليمة.

الخلاصة

  • ReAct تناوب فكرة-فعل-ملاحظة؛ نداء الأدوات الأصليّ في النماذج الحديثة يجعله شبه شفّاف.
  • ستّون سطرًا تكفي لتنفيذ حلقة وكيل عاملة: قائمة أدوات، دالّة توجيه، وحلقة تكرار محدودة.
  • شرط التوقّف ثلاثيّ: جواب طبيعيّ، وحدّ تكرار، وحدّ رموز (لاحقًا)؛ غيابه يُنتج حلقة لانهائيّة.
  • سجلّ الوسائط والملاحظات وحالة التوقّف عناصر لا غنى عنها للتشخيص؛ الفكرة أقلّ أهمّيّة ممّا يظنّ المرء.