cd ../back to blog
$Guide//June 4, 2026//7 min read

استخدام الأدوات مع Claude وGemini عبر واجهة API واحدة متوافقة مع OpenAI

عرّف الأدوات، واقرأ tool_calls، وشغّل الحلقة متعددة الأدوار، وعالج النداءات المتوازية — شكل OpenAI نفسه يعمل عبر Claude وGemini خلف base_url واحد.

استخدام الأدوات — أو ما يُعرف باستدعاء الدوال (function calling) — هو ما يحوّل نموذج المحادثة إلى شيء قادر على أن يفعل أشياء: يجلب سجلاً، أو يطرق واجهة API، أو يجري عملية حسابية، أو يستعلم قاعدة بياناتك. النموذج لا يشغّل الكود؛ بل يخبرك بأي دالة يجب أن تستدعي وبأي وسائط، فتشغّلها أنت، ثم تعيد له النتيجة ليكمل الإجابة. والخبر السار: شكل tools من OpenAI هو المعيار الفعلي السائد، ومن خلال Brievio يقود الكود نفسه تماماً كلاً من Claude وGemini خلف base_url واحد. غيّر سلسلة model، واترك كل شيء آخر كما هو.

هذه المقالة هي النسخة العملية: عرّف أداة، واقرأ tool_calls، وشغّل الحلقة متعددة الأدوار من بدايتها إلى نهايتها، وعالج النداءات المتوازية. كل مقطع قابل للتشغيل ضد https://api.brievio.com/v1 باستخدام OpenAI Python SDK. وسأشير إلى المواضع القليلة التي يختلف فيها السلوك فعلاً بين عائلات النماذج كي لا تُفاجأ في بيئة الإنتاج.

النموذج الذهني: حلقة، لا نداء سحري

استدعاء الدوال محادثة، لا نداء واحد. وهو يتبع دائماً الإيقاعات الأربعة نفسها:

  • أنت ترسل رسالة المستخدم إضافة إلى قائمة الأدوات المسموح للنموذج باستخدامها.
  • النموذج يقرّر. إما أن يجيب بنص، أو يعيد واحداً أو أكثر من tool_calls — اسم دالة ونص JSON من الوسائط — ثم يتوقف.
  • أنت تشغّل الدالة داخل كودك الخاص وتعيد إضافة النتيجة إلى قائمة الرسائل بوصفها رسالة tool.
  • أنت تستدعي النموذج مجدداً بالسجل الأطول. فيقرأ النتيجة ثم إما أن يجيب أو يطلب أداة أخرى. كرّر إلى أن لا تبقى أي نداءات أدوات.

النموذج لا يمسّ أنظمتك أبداً. هو يقترح فقط؛ وكودك هو من ينفّذ. هذا الحد الفاصل هو كامل قصة الأمان في استخدام الأدوات — تعامل مع كل وسيط يرسله النموذج بوصفه مدخلاً غير موثوق، وتحقّق منه كما تتحقق من حقل في نموذج.

الخطوة 1 — عرّف أداة واقرأ النداء

الأداة عبارة عن JSON Schema ملفوف داخل {"type": "function", ...}. وحقول description ليست زينة — فهي الشيء الوحيد الذي يقرأه النموذج ليقرّر متى يستدعي وكيف. اكتبها كأنك تكتب docstring لمهندس مبتدئ:

define_tool.py
# عرّف أداة بمخطط "function" المعياري من OpenAI، ثم اقرأ
# الـ tool_calls الراجعة من النموذج. الشكل نفسه تماماً لـ Claude وGemini.
from openai import OpenAI
import json

client = OpenAI(
    api_key="sk-brievio-...",
    base_url="https://api.brievio.com/v1",
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "e.g. 'Tokyo'"},
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit.",
                    },
                },
                "required": ["city"],
            },
        },
    }
]

messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]

resp = client.chat.completions.create(
    model="claude-sonnet-4-6",   # بدّلها إلى "gemini-2.5-pro" — الكود نفسه أدناه
    messages=messages,
    tools=tools,
    tool_choice="auto",          # دع النموذج يقرّر هل يستدعي أم لا
)

msg = resp.choices[0].message

# النموذج لم يجب بنص — بل طلب منك تشغيل دالة.
if msg.tool_calls:
    for call in msg.tool_calls:
        print(call.function.name)             # "get_weather"
        print(call.function.arguments)         # '{"city": "Tokyo", "unit": "celsius"}'
        args = json.loads(call.function.arguments)  # دائماً نص JSON — حلّله
else:
    print(msg.content)           # إجابة عادية، لا حاجة لأداة

أمران يعثر فيهما الناس هنا. أولاً، function.arguments هو نص JSON، لا قاموس — فعليك دائماً تحليله بـ json.loads. ثانياً، قد يختار النموذج ألا يستدعي أداة، وعندها يكون tool_calls فارغاً ويحمل content إجابة عادية. تفرّع على كلتا الحالتين. وهذا مطابق سواء ضبطت model على claude-sonnet-4-6 أو gemini-2.5-pro؛ فـ Brievio يمرّر الطلب إلى النموذج الأصلي من المصدر الأول ويعيد نداءات أدوات أصيلة — لا يعيد تشكيلها ولا يزيّفها.

الخطوة 2 — الحلقة متعددة الأدوار

الآن وصّل رحلة الذهاب والإياب. الشكل المهم هنا: أضف رسالة المساعد كما رجعت بالضبط (فهي تحمل معرّفات النداء)، ثم أضف رسالة tool واحدة لكل نداء، كل منها يعيد معرّفها tool_call_id. اجعل معرّفاً غير مطابق وسيرد الطلب التالي بخطأ 400. وإليك الحلقة كاملة، تعمل مع المزوّدَين من دالة واحدة:

tool_loop.py
# الحلقة متعددة الأدوار: النموذج يطلب -> تشغّل أنت الدالة ->
# تعيد له النتيجة -> يكتب النموذج الإجابة النهائية.
def run_get_weather(city: str, unit: str = "celsius") -> dict:
    # تنفيذك الحقيقي: نداء HTTP، استعلام قاعدة بيانات، أي شيء.
    return {"city": city, "temp": 18, "unit": unit, "sky": "clear"}

TOOL_IMPLS = {"get_weather": run_get_weather}

def answer(question: str, model: str) -> str:
    messages = [{"role": "user", "content": question}]

    while True:
        resp = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )
        msg = resp.choices[0].message

        # لم يُطلب أي أداة -> هذه هي الإجابة النهائية. انتهينا.
        if not msg.tool_calls:
            return msg.content

        # 1. أضف رسالة المساعد كما رجعت بالضبط (فهي تحمل
        #    معرّفات tool_call التي يجب أن تشير إليها الرسائل التالية).
        messages.append(msg)

        # 2. شغّل كل دالة مطلوبة وأضف رسالة tool واحدة لكل نداء،
        #    معيداً معرّف tool_call_id المطابق.
        for call in msg.tool_calls:
            fn = TOOL_IMPLS[call.function.name]
            args = json.loads(call.function.arguments)
            result = fn(**args)
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,          # يجب أن يطابق معرّف النداء
                "content": json.dumps(result),    # حوّل النتيجة إلى نص
            })
        # 3. كرّر: يرى النموذج الآن مخرجات الأداة ويواصل.

# الدالة نفسها تعمل مع المزوّدَين خلف base_url الواحد:
print(answer("What's the weather in Tokyo?", "claude-sonnet-4-6"))
print(answer("What's the weather in Tokyo?", "gemini-2.5-pro"))

تلك while True هي محرّك كل وكيل (agent) استخدمته يوماً. يستطيع النموذج أن يسلسل الأدوات — يستدعي search، ويقرأ النتيجة، ثم يستدعي get_details على أعلى نتيجة، ثم يجيب — والحلقة تعالج أي عمق دون معالجة خاصة. أضف عدّاد جولات بوصفه حاجز أمان كي لا يدور نموذج مرتبك إلى ما لا نهاية؛ و8–10 جولات سقف معقول لمعظم التطبيقات.

تحفّظ صادق واحد بشأن قابلية النقل: البروتوكول مطابق بين Claude وGemini، لكن السلوك ليس نسخة طبق الأصل. فعائلات النماذج المختلفة تختار أدوات مختلفة، وتصوغ الوسائط بشكل مختلف، وتتفاوت في مدى ميلها إلى الاستدعاء مقابل الإجابة من معرفتها السابقة. الكود لا يتغيّر؛ بل يتغيّر الحُكم. اختبر مطالباتك (prompts) على كل نموذج تنوي إطلاقه عليه بدل افتراض أن أحدها ينتقل بشكل مثالي إلى الآخر.

الخطوة 3 — نداءات الأدوات المتوازية

حين يحتاج سؤال إلى عدة عمليات بحث مستقلة — طقس ثلاث مدن، أو مخزون خمسة SKUs — يستطيع نموذج قدير أن يعيد كل النداءات في رسالة مساعد واحدة. تشغّلها (بالتوازي إن كان العمل مقيّداً بالإدخال/الإخراج) وتعيد رسالة tool واحدة لكل معرّف قبل أن تسأل مجدداً:

parallel_calls.py
# نداءات أدوات متوازية: يمكن لرسالة مساعد واحدة أن تطلب عدة دوال
# دفعة واحدة. تشغّلها (بالتوازي إن شئت) وتعيد رسالة tool واحدة
# لكل معرّف نداء. تباطن النماذج للنداءات يختلف — لذا اعمل دائماً
# على المرور عبر القائمة بدل افتراض نداء واحد فقط.
messages = [{"role": "user",
             "content": "Compare the weather in Tokyo, Paris and Cairo."}]

resp = client.chat.completions.create(
    model="claude-sonnet-4-6",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)
msg = resp.choices[0].message
messages.append(msg)

# قد يحمل msg.tool_calls الآن ثلاثة نداءات get_weather بمعرّفات مميزة.
from concurrent.futures import ThreadPoolExecutor

def handle(call):
    args = json.loads(call.function.arguments)
    result = TOOL_IMPLS[call.function.name](**args)
    return {
        "role": "tool",
        "tool_call_id": call.id,
        "content": json.dumps(result),
    }

with ThreadPoolExecutor() as pool:
    tool_msgs = list(pool.map(handle, msg.tool_calls or []))

messages.extend(tool_msgs)   # أضف كل النتائج قبل الطلب التالي

final = client.chat.completions.create(
    model="claude-sonnet-4-6", messages=messages, tools=tools,
)
print(final.choices[0].message.content)

# ملاحظة: إذا أعاد النموذج النداءات واحداً تلو الآخر بدل دفعة واحدة، فإن الحلقة
# من المقطع السابق تعالج ذلك مجاناً — إذ تشغّل ببساطة جولات أكثر.

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

لماذا base_url واحد هو المكسب الحقيقي

بدون gateway، فإن دعم Claude وGemini يعني SDK اثنين، ونظامَي مصادقة، وشكلَي حمولة، ومجموعتين من سباكة نتائج الأدوات — كتل محتوى tool_use/tool_result الخاصة بـ Anthropic من جهة، وأجزاء استدعاء الدوال الخاصة بـ Google من جهة أخرى. أما خلف نقطة نهاية Brievio المتوافقة مع OpenAI، فيتحدث كلاهما لهجة tools الخاصة بـ Chat Completions التي رأيتها أعلاه، فيصبح اختبار A/B بين النماذج فرقاً من سطر واحد، وتُكتب طبقة أدواتك مرة واحدة. وعقد الطلب/الاستجابة الكامل — بما في ذلك حقول الأدوات — موجود في وثائق Chat Completions، وقائمة النماذج الحيّة بمعرّفاتها الدقيقة موجودة في صفحة النماذج.

ويجدر قول ذلك بوضوح: القيمة لا تثبت إلا إذا كان النموذج في الطرف الآخر هو النموذج الحقيقي. واستدعاء الأدوات في الواقع إشارة أصالة مفيدة — فالنموذج الرائد الأصيل يُنتج بشكل موثوق tool_calls سليمة الصياغة بوسائط معقولة على مخططات غير تافهة، بينما يميل البديل الأرخص إلى التخبّط في الـ JSON أو تجاهل الأداة. يقدّم Brievio النماذج الأصلية من المصدر الأول (Claude Sonnet 4.6، وOpus 4.7، وGemini 2.5 Pro/Flash وغيرها)، ويحترم استدعاء الأدوات الأصلي، ويبلّغ عن أعداد توكنات صادقة؛ وإن أردت أن تتأكد من ذلك بنفسك، فانظر كيف تتحقق أن Claude لديك هو Claude حقاً.

قائمة تحقّق ميدانية قصيرة

  • حلّل الوسائط، دائماً. function.arguments نص؛ حلّله بـ json.loads وتحقّق منه قبل الاستخدام.
  • أعِد المعرّفات. أضف رسالة المساعد حرفياً، ثم رسالة tool واحدة لكل نداء بمعرّف tool_call_id المطابق. كلها قبل الطلب التالي.
  • مُرَّ عبر القائمة. لا تفترض أبداً نداءً واحداً لكل جولة — عالج الصفر والواحد والكثير. تلك العادة الواحدة تجعل النماذج المتوازية والمتتابعة تعمل جميعها بلا عناء.
  • حُدّ الجولات. عدّاد الجولات يمنع دوامة استدعاء أدوات لا نهائية ويضبط تكلفتك.
  • لا تثق بشيء. الوسائط مخرجات نموذج. تحقّق من الأنواع والنطاقات والصلاحيات تماماً كما تفعل مع مدخلات المستخدم.

أتقن هذه الخمسة ويكون لديك وكيل يستخدم الأدوات ويعمل دون تغيير عبر Claude وGemini، مع خيار التوجيه بحسب التكلفة أو القدرة لكل طلب. ولاحظ أن النداءات الفاشلة 4xx/5xx على Brievio لا تُحتسب، فجولات ضبط المخطط الحتمية ريثما تضبط تعريفات الأدوات بشكل صحيح تكون مجانية. وحين تكون جاهزاً لاختيار النماذج التي تضعها خلف أدواتك، فإن دليل اختيار الـ gateway يستعرض المفاضلات التي تهمّ فعلاً في بيئة الإنتاج.