وكيل AI واحد دخل في حلقة tool calls، نده search_database 213 مرة في 12 دقيقة، وحرق 47 دولار من فاتورة Claude قبل ما الـ Kubernetes job يطفي نفسه على الـ timeout. السطور الجاية فيها 4 طبقات دفاع شغّالة على prod، وكود يعيد إنتاج المشكلة في 30 سطر.
المشكلة باختصار
الوكلاء اللي بيستخدموا tool use (Anthropic، OpenAI Assistants، LangChain agents) بيدخلوا في loop داخلي: النموذج بيقرر ينده أداة، ياخد الرد، يقرر ينده أداة تانية، وهكذا لحد ما يجاوب. لو الـ logic بتاع التوقف ضعيف، الوكيل بينده نفس الأداة بنفس المعطيات 50 و100 و200 مرة. مفيش exception، مفيش error — بس الفاتورة بتطلع.
المثال البسيط: الطبّاخ اللي بينده النادل كل دقيقة
تخيّل طبّاخ بيحضّر طلب. كل ما يخلّص خطوة بيقول للنادل "اجبلي ملح"، يجيبه، بعدين "اجبلي ملح" تاني، تالت، رابع. النادل مش هيوقفه — مش شغلته يفهم إذا الطلب اتنفّذ ولا لأ. المطعم هيقفل والطبّاخ لسه بيطلب ملح.
ده بالظبط اللي بيحصل مع وكيل AI. النموذج "بينسى" إنه ندى الأداة دي قبل كده، أو رد الأداة ما ساعدوش يكمّل، فبيكرر النداء بنفس المعطيات.
التعريف العلمي: ReAct Loop وعلاماته
الـ pattern الشائع اسمه ReAct (Reasoning + Acting)، اقترحه Yao et al في ورقة 2022. الفكرة: النموذج بيتبادل بين Thought → Action → Observation لحد ما يوصل لإجابة نهائية. الحلقة بتبقى مرضية (pathological) لما يحصل واحد من 3 أنماط:
- Identical repetition: نفس الأداة بنفس الـ arguments مرتين متتاليتين على الأقل.
- Argument cycling: A→B→A→B→A بدون تقدّم في الـ context.
- No-progress drift: 8+ نداءات بدون تغيّر في الـ
state hashاللي بيمثّل ما تعلّمه الوكيل.
الافتراض هنا: الـ orchestrator بيدير الحلقة (مش النموذج)، وبتقدر تشوف كل tool_use و tool_result قبل ما تبعتهم تاني للنموذج. ده بينطبق على Anthropic Messages API، LangGraph، و CrewAI.
إعادة إنتاج المشكلة في 30 سطر
الكود ده وكيل بسيط بيدوّر على بيانات عميل. الأداة بترجّع not_found فالنموذج بيكرر النداء بنفس الـ ID لحد ما الـ context تتملّى:
import anthropic
client = anthropic.Anthropic()
messages = [{"role": "user", "content": "هات بيانات العميل رقم 9921"}]
tools = [{
"name": "search_database",
"description": "ابحث عن عميل بالـ ID",
"input_schema": {
"type": "object",
"properties": {"customer_id": {"type": "string"}},
"required": ["customer_id"]
}
}]
while True:
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
if resp.stop_reason == "end_turn":
break
tool_call = next(b for b in resp.content if b.type == "tool_use")
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": "not_found"
}]})