الدرس 4 من 10

الـ Agent Loop خطوة خطوة

قلب أي agent: ٢٠ سطر لو فهمتهم، فهمت كل الـ frameworks.

البروتوكول

  • بتبعت للموديل: الرسايل + قايمة الـ tools.
  • لو الموديل عايز يستخدم أداة: الرد بيبقى stop_reason = "tool_use"، وفي الـ content بلوك نوعه tool_use فيه name و input و id فريد.
  • انت بتنفذ الأداة، وترجع النتيجة في رسالة user فيها بلوك tool_result بنفس الـ id بالظبط (في حقل tool_use_id).
  • تبعت تاني. لو stop_reason بقى حاجة غير tool_use (زي end_turn)، يبقى الموديل خلص وده الرد النهائي.

والموديل ممكن يطلب كذا أداة في رد واحد لو مش معتمدين على بعض (مثلاً يسأل عن أوردرين مع بعض). لازم تنفذهم كلهم وترجع كل النتايج في رسالة واحدة.

الـ loop الحقيقي مع Claude

agent.py
import anthropic
from dotenv import load_dotenv
from tools_registry import TOOLS
from dispatcher import run_tool

load_dotenv()
client = anthropic.Anthropic()
SYSTEM = ("You are a customer service agent for an Egyptian online store. "
          "Use tools to check facts; never guess order details. Reply in Egyptian Arabic.")

def run_agent(user_message: str, max_steps: int = 10) -> str:
    messages = [{"role": "user", "content": user_message}]
    tools = [t["schema"] for t in TOOLS.values()]

    for step in range(max_steps):                    # hard limit: never loop forever
        response = client.messages.create(
            model="claude-sonnet-5", max_tokens=1024,
            system=SYSTEM, tools=tools, messages=messages,
        )
        if response.stop_reason != "tool_use":       # done -> return final text
            return "".join(b.text for b in response.content if b.type == "text")

        messages.append({"role": "assistant", "content": response.content})
        results = []
        for block in response.content:               # may contain SEVERAL tool calls
            if block.type == "tool_use":
                content, is_error = run_tool(block.name, block.input)
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": content, "is_error": is_error})
        messages.append({"role": "user", "content": results})   # all results together

    return "Sorry, I couldn't finish this request. A human agent will follow up."

print(run_agent("الأوردر 1024 فين؟ وكمان عايز أعرف الشحن لأسوان بكام لو طلبت بـ 640 جنيه"))

شوف الـ loop بعينك من غير API key

عشان نشوف كل خطوة بالتفصيل (ونختبر الـ loop من غير ما ندفع)، هنبدل الـ client بـ FakeClient: موديل وهمي «مكتوب له السيناريو» بيرجع ردود بنفس شكل ردود الـ API. الـ loop نفسه هو هو من غير أي تغيير. ده أسلوب حقيقي بيستخدم في اختبار الـ agents:

trace_loop.py
from types import SimpleNamespace as NS
from dispatcher import run_tool

# FakeClient: a scripted stand-in with the same response shape as the real API.
# It lets us test the loop without an API key. NOT a real model.
class FakeClient:
    def __init__(self, script):
        self.script, self.calls = script, 0
        self.messages = NS(create=self._create)
    def _create(self, **kwargs):
        reply = self.script[self.calls]; self.calls += 1
        return reply

def tool_use(id, name, input): return NS(type="tool_use", id=id, name=name, input=input)
def text(t): return NS(type="text", text=t)

client = FakeClient([
    # step 1: the "model" asks for TWO tools in parallel
    NS(stop_reason="tool_use", content=[
        text("Let me check both."),
        tool_use("tu_1", "get_order_status", {"order_id": "1024"}),
        tool_use("tu_2", "shipping_cost", {"governorate": "Aswan", "order_total": 640}),
    ]),
    # step 2: final answer
    NS(stop_reason="end_turn", content=[text("أوردرك خارج للتوصيل وهيوصل النهارده قبل 9، والشحن لأسوان 75 جنيه.")]),
])

messages = [{"role": "user", "content": "الأوردر 1024 فين؟ والشحن لأسوان بكام؟"}]
for step in range(10):
    response = client.messages.create(messages=messages)
    print(f"--- step {step + 1}: stop_reason = {response.stop_reason}")
    if response.stop_reason != "tool_use":
        print("FINAL:", response.content[0].text)
        break
    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type == "tool_use":
            content, is_error = run_tool(block.name, block.input)
            print(f"  tool {block.name}({block.input}) -> {content}")
            results.append({"type": "tool_result", "tool_use_id": block.id,
                            "content": content, "is_error": is_error})
    messages.append({"role": "user", "content": results})

print("messages in history:", len(messages))

الـ history بقى فيه ٣ رسايل: سؤال العميل، وطلب الموديل للأداتين، ونتايجهم. ولو فيه خطوة تالتة، الموديل هيشوف الـ ٣ دول كلهم. الموديل مالوش ذاكرة غير الـ messages اللي انت بتبعتها.

⚠ ٣ غلطات بتكسر الـ loop(١) ترجع tool_result بـ tool_use_id مش مطابق، أو تنسى نتيجة أداة من الأدوات اللي اتطلبت. (٢) تحط الـ tool_result في رسالة assistant بدل user. (٣) مفيش max_steps، فلو الموديل فضل يطلب أدوات، الـ loop مبيخلصش والفاتورة بتكبر.
الدرس اللي فات