استدعاء أداة في Ollama على سيرفرك الخاص
كيف تجعل الموديل الذي يعمل على VPS يستدعي دوالك أنت: شكل مصفوفة tools في نقطة /api/chat، وسبب تجاهل بعض الموديلات لها، والحلقة التي يملكها كودك وليس الموديل.
ما معنى استدعاء أداة في Ollama
استدعاء أداة في Ollama يعني أنك ترسل مع سؤالك قائمة بدوالك الخاصة في مصفوفة اسمها tools، فيقرر الموديل أي دالة يحتاج وبأي وسائط، ويرد باقتراح استدعاء بدل نص عادي. الموديل لا ينفّذ شيئًا أبدًا. كودك هو الذي ينفّذ الدالة، ثم يعيد النتيجة إلى الموديل في رسالة جديدة. هذه هي الفكرة كاملة، وكل ما بعدها تفاصيل تنفيذ.
هذا الدليل يكمل تشغيل Ollama على VPS واستضافة موديل محلي. نفترض أن لديك سيرفرًا يعمل، وموديلًا صغيرًا مسحوبًا عليه، ونقطة /api/chat تردّ عليك بشكل سليم.
قاعدة واحدة قبل كل شيء: الأوامر هنا تشغّلها أنت على سيرفرك وبالموديل الذي اخترته أنت. شكل الرد يختلف بين إصدار وإصدار من Ollama، وبين وسم موديل ووسم آخر. لا تنسخ أسماء حقول الرد من أي مقال، ولا من هذا المقال. شغّل الطلب، واقرأ ردك أنت، واكتب كودك على ما رأيته.
ثبّت إصدار Ollama ووسم الموديل قبل أي اختبار
أول خطوة ليست كتابة كود. أول خطوة هي تسجيل رقمين ستحتاجهما في كل مرة تسأل فيها عن مشكلة، أو تعود فيها إلى كودك بعد شهرين.
ollama --version
ollama pull qwen3:8b
ollama show qwen3:8bاكتب في ملاحظاتك إصدار Ollama ووسم الموديل بالكامل، مثل qwen3:8b لا qwen3 وحده. الوسم بلا حجم يتغير معناه عندما يُحدّث المستودع، فتجد سلوك الأدوات تغيّر عندك بلا سبب ظاهر.
ollama show يطبع قسمًا باسم Capabilities. إذا وجدت فيه كلمة tools فقالب الموديل يعرف الأدوات. وإذا وجدت completion وحدها فهو لا يعرفها، وستضيع ساعتين في تصحيح كود سليم. النقطة نفسها متاحة عبر واجهة HTTP إن كنت تعمل من جهاز آخر:
curl -s http://127.0.0.1:11434/api/show -d '{"model":"qwen3:8b"}' | jq .capabilitiesلماذا يتجاهل الموديل الأدوات ويجيب بنص عادي؟
لأن دعم الأدوات صفة في قالب الموديل، لا في Ollama نفسه. يمرر Ollama تعريفاتك في tools إلى قالب الموديل (template)، والقالب هو الذي يحوّلها إلى نص يراه الموديل، ويحدد كيف يعلن الموديل عن استدعاء. الموديل الذي لا يحتوي قالبه على موضع للأدوات يرى سؤالك وحده، كأنك لم ترسل tools من الأصل. فيرد بنص مهذب من نوع «سأستخدم دالة get_order_status للبحث عن طلبك»، بلا أي استدعاء حقيقي. هذا ليس خطأ في كودك، وإعادة صياغة السؤال لن تصلحه.
اقرأ القالب بنفسك:
ollama show --template qwen3:8bابحث في المخرج عن أي موضع يذكر الأدوات. إن لم تجد للأدوات ذكرًا في القالب فالموديل لن يستدعي شيئًا، والحل موديل آخر، لا prompt أذكى. جرّب موديلًا يعلن tools في Capabilities، وأعد نفس الطلب قبل أن تلمس سطرًا واحدًا من كودك.
شكل الطلب: مصفوفة tools على نقطة /api/chat
الأدوات تعيش على /api/chat وحدها. احفظ الطلب في ملف بدل لصقه في الطرفية، لأنك ستعدّله كثيرًا، ولأن النص العربي داخل سلسلة صدفة طويلة يصعب قراءته. هذا محتوى request.json:
{
"model": "qwen3:8b",
"stream": false,
"messages": [
{"role": "user", "content": "ما حالة الطلب رقم 10427؟"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_order_status",
"description": "يعيد حالة الطلب وتاريخ الشحن من قاعدة بيانات المتجر",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "رقم الطلب كما يظهر للعميل في رسالة التأكيد"
}
},
"required": ["order_id"]
}
}
}
]
}كل عنصر في tools كائن فيه "type": "function" وكائن function يحمل name و description و parameters. و parameters مكتوب بصيغة JSON Schema: نوع الكائن، ثم خصائصه، ثم ما هو مطلوب منها في required.
الحقل الذي يقرر جودة النتيجة هو description. الموديل لا يرى كودك ولا اسم جدولك، بل يرى هذه الجملة وحدها ويقارنها بكلام المستخدم. وصف فضفاض مثل «يجيب عن أسئلة الطلبات» يدفع الموديل إلى استدعاء الأداة في كل رسالة، حتى في التحية. ووصف يذكر ما تعيده الدالة ومتى تُستخدم يقلّل ذلك كثيرًا. واجعل الأسماء متباعدة أيضًا: وجود get_order_status و get_order_details معًا في نفس الطلب يعني أن الموديل سيخلط بينهما، وأنت من صنع الالتباس.
و "stream": false هنا متعمّد. تعيد لك النقطة كائن JSON واحدًا تقرأه بعينك، وهذا ما تحتاجه في مرحلة الاستكشاف.
اقرأ الرد بنفسك قبل أن تكتب سطر تحليل واحدًا
curl -s http://127.0.0.1:11434/api/chat -d @request.json | jq .لا تقفز إلى الكود. اقرأ هذا الرد كاملًا، وأجب عن أربعة أسئلة من مخرجاتك أنت:
- أين يقع اقتراح الاستدعاء في الرد، وما اسم المفتاح الذي يحمل اسم الدالة ووسائطها؟
- هل الوسائط كائن JSON جاهز، أم نص تحتاج تمريره على
json.loadsقبل استخدامه؟ - ماذا يحمل الحقل النصي عندما يوجد استدعاء؟ هل هو فارغ، أم يحمل شرحًا معه؟
- هل اقترح الموديل استدعاء واحدًا أم عدة استدعاءات في رد واحد؟ كودك يجب أن يتحمل أكثر من واحد.
سجّل الأجوبة مع إصدار Ollama ووسم الموديل في تعليق أعلى ملفك. هذه الثلاثة معًا هي عقدك مع الكود. وعندما تحدّث Ollama لاحقًا ويتوقف التحليل عن العمل، ستعرف في دقيقة أي نصف تغيّر.
الحلقة التي يملكها كودك، لا الموديل
الموديل يقترح وأنت تنفّذ. هذه الحلقة كلها داخل تطبيقك، ولا يوجد في Ollama ما يديرها عنك:
- ترسل رسائل المستخدم مع مصفوفة
toolsفي كل طلب، لا في الطلب الأول وحده. - تقرأ الرد. إن لم يكن فيه اقتراح استدعاء فقد انتهت المهمة، فاعرض النص للمستخدم.
- تضيف رسالة الموديل كما جاءت إلى مصفوفة
messagesبلا تعديل. - تتحقق من الوسائط، ثم تنفّذ الدالة في كودك.
- تضيف رسالة نتيجة بالدور
tool، وفيهاtool_nameباسم الدالة وcontentنصًا. - ترسل الطلب من جديد بنفس
toolsوبالمصفوفة الأطول، وتعود إلى الخطوة الثانية.
وهذا هيكل الحلقة بلغة Python وبمكتبة requests وحدها، بلا أي اعتماد إضافي:
import json, requests
URL = "http://127.0.0.1:11434/api/chat"
MODEL = "qwen3:8b" # ثبّت الوسم، وسجّل إصدار Ollama في ملاحظاتك
def get_order_status(order_id):
# هنا تتصل بقاعدة بياناتك الحقيقية
return {"order_id": order_id, "status": "shipped"}
TOOLS = json.load(open("tools.json")) # نفس مصفوفة tools أعلاه
messages = [{"role": "user", "content": "ما حالة الطلب رقم 10427؟"}]
for _ in range(5): # سقف للحلقة، مقصود
r = requests.post(URL, json={"model": MODEL, "messages": messages,
"tools": TOOLS, "stream": False}, timeout=180)
r.raise_for_status()
reply = r.json()
print(json.dumps(reply, ensure_ascii=False, indent=2)) # اقرأ هذا أولًا
# الأسطر التالية مبنية على أسماء رأيتها في مخرجاتك. صحّحها إن اختلفت:
message = reply["message"]
calls = message.get("tool_calls") or []
if not calls:
print(message.get("content", ""))
break
messages.append(message)
for call in calls:
fn = call["function"]
args = fn["arguments"]
if isinstance(args, str):
args = json.loads(args)
result = get_order_status(args["order_id"])
messages.append({"role": "tool", "tool_name": fn["name"],
"content": json.dumps(result, ensure_ascii=False)})لاحظ ثلاثة أشياء في هذا الهيكل. الأول أن سطر print موضوع هناك عن قصد: أسماء المفاتيح في الأسطر التي تليه تصحّحها أنت من مخرجاتك، لأنها الشيء الوحيد الذي لا يستطيع أي دليل أن يضمنه لك. الثاني أن range(5) سقف وليس زينة، فالموديل الصغير قد يكرر نفس الاستدعاء بنفس الوسائط بلا توقف، وبلا سقف تكتشف ذلك من حمل المعالج ومن سجل لا ينتهي. الثالث أن content في رسالة النتيجة نص، ولذلك نمرر القاموس على json.dumps مع ensure_ascii=False كي تبقى العربية عربية في سجلاتك.
وتحقق من الوسائط قبل التنفيذ دائمًا. الوسائط تأتي من موديل لغوي، أي من مصدر غير موثوق. قيمة order_id قادمة من الموديل لا تُدخل في استعلام SQL مبني بلصق السلاسل، ولا تُمرر إلى subprocess ولا إلى صدفة. عاملها كأنها حقل في نموذج HTML مفتوح على الإنترنت، لأنها كذلك فعلًا.
هل تعمل الأدوات مع البثّ streaming؟
مع "stream": true تعيد لك النقطة سلسلة من كائنات JSON، كائنًا في كل سطر، لا كائنًا واحدًا. هذا يكسر الكود السابق: تمرير السلسلة كاملة إلى json.loads في Python يرفع json.decoder.JSONDecodeError: Extra data. ووثائق Ollama توصي بتجميع كل القطع أولًا، المحتوى والتفكير واستدعاءات الأدوات، ثم إرسالها مجتمعة مع نتائج الأدوات في الطلب التالي.
والسؤال الذي عليك أن تجيبه من مخرجاتك أنت: هل يصل اقتراح الاستدعاء كاملًا في كائن واحد، أم مقسّمًا على عدة كائنات تحتاج تجميعها قبل أن تنفّذ أي شيء؟ تنفيذ دالة على نصف استدعاء يكتب في قاعدة بياناتك وسيطًا ناقصًا.
النصيحة العملية: ابنِ الحلقة كلها على "stream": false حتى تعمل من أولها إلى آخرها، ثم أضف البثّ في الواجهة وحدها إن أردت. البثّ يحسّن إحساس المستخدم بالسرعة، وهو لا يقصّر زمن الاستدعاء ولا يجعل اختيار الأداة أدق.
أخطاء أول يوم، وسببها
أرسلت tools ولم يحدث شيء. تأكد أنك على /api/chat لا على /api/generate. الثانية لا تعرف الرسائل ولا الأدوات، فهي تتجاهل الحقل بصمت.
الموديل ينسى أول الحوار بعد ثلاث أو أربع خطوات. تعريفات الأدوات ونتائجها كلها نص يُحسب من نافذة السياق، والمصفوفة تطول مع كل دورة. مع نافذة ضيقة يُقتطع أول الحوار، فتظن أن الموديل ضعيف. وسّع النافذة بوعي عبر ضبط طول السياق num_ctx في Ollama، واقطع نتائج الأدوات الطويلة قبل إضافتها.
أول استدعاء بعد فترة سكون بطيء جدًا. الموديل أُفرغ من الذاكرة وأُعيد تحميله من القرص. هذا سلوك keep_alive الافتراضي، وتضبطه كما في إبقاء الموديل محمّلًا في الذاكرة.
نقطة Ollama مفتوحة على الإنترنت. الأدوات تغيّر حجم هذا الخطر. من يصل إلى النقطة يصل إلى موديل يقود حلقة تنفّذ دوالك أنت، فتصير النتيجة تنفيذ كود على سيرفرك بطلب من غريب. اربطها على 127.0.0.1 أو على شبكة خاصة، وضع تطبيقك وحده أمامها، كما في تأمين نقطة Ollama على الإنترنت.
متى تبقي الموديل المحلي، ومتى تتركه
الموديل المحلي مع الأدوات رخيص وخاص. لا فاتورة على كل استدعاء، ولا تخرج أرقام طلبات عملائك ولا أسماؤهم من سيرفرك. هذه ميزة حقيقية، ولها ثمن يجب أن تعرفه قبل أن تبني عليها.
أصعب ما يفعله الموديل هنا ليس كتابة JSON صحيح، بل اختيار الأداة المناسبة وتعبئة وسائطها من كلام المستخدم. والموديلات الصغيرة التي تعمل بذاكرة VPS متوسطة أضعف في هذا الاختيار من الموديلات المستضافة الكبيرة. هذا فرق تراه بعينك كلما زاد عدد أدواتك أو تقارب معناها.
أبقِ الموديل المحلي إذا كانت أدواتك قليلة وواضحة، ثلاثًا أو أربعًا بأسماء ومعانٍ متباعدة، والمهمة متكررة وشكلها ثابت، والبيانات حساسة، وكل تنفيذ يمر على تحقق تكتبه أنت. وانتقل إلى موديل مستضاف إذا صار عدد الأدوات كبيرًا ومعانيها متقاربة، أو إذا كان كل استدعاء يكتب في نظام حقيقي لا يحتمل الخطأ مثل فاتورة أو شحنة أو حذف، أو إذا احتجت سلسلة من عدة خطوات تمر بلا إشراف بشري.
ولا تحسم هذا بالتخمين. اكتب عشرين طلبًا حقيقيًا بالعربية كما يكتبها عملاؤك، بما فيها الطلبات الناقصة مثل «وين طلبي؟» بلا رقم، وشغّلها على موديلك، وأحصِ كم مرة اختار الأداة الصحيحة وكم مرة استخرج الرقم سليمًا. النسبة التي تخرج من سيرفرك أصدق من أي مقارنة منشورة. وإذا أردت الخطوة التالية بعد أن تعمل الحلقة، فالطريق المعتاد هو بناء تطبيق حولها كما في بناء وكيل AI على سيرفرك الخاص، أو توصيل نفس الموديل بأدوات جاهزة كما في ربط Ollama بوكيل البرمجة الذي تستخدمه.
FAQ
لماذا يشرح الموديل الأداة بدل أن يستدعيها؟
لأن دعم الأدوات صفة في قالب الموديل لا في Ollama. القالب الذي لا يحتوي موضعًا للأدوات يجعل الموديل يرى سؤالك وحده، فيرد بنص يشبه النية مثل «سأستخدم الدالة الفلانية» بلا استدعاء. شغّل ollama show qwen3:8b وابحث عن tools في قسم Capabilities، ثم ollama show --template qwen3:8b لترى القالب. إن لم تكن الأدوات مذكورة فيه، غيّر الموديل. لا يوجد prompt يصلح هذا.
كيف أعرف أن موديلي يدعم استدعاء الأدوات؟
من قسم Capabilities في مخرج ollama show، أو من طلب /api/show ثم jq .capabilities على الرد. ثم أكّد الأمر عمليًا: أرسل طلبًا واحدًا فيه أداة واحدة وسؤال يحتاجها بوضوح، واقرأ الرد بـ jq. الاختبار على إصدارك ووسم موديلك هو الدليل الوحيد الذي يُعتمد عليه، لأن السلوك يتغير بين الإصدارات.
من ينفّذ الدالة، Ollama أم كودي؟
كودك. يمرر Ollama تعريفات tools إلى الموديل ويعيد لك اقتراح الاستدعاء، ثم يتوقف عند هذا الحد. أنت من يتحقق من الوسائط، وينفّذ الدالة، ويضيف النتيجة في رسالة بالدور tool مع tool_name و content، ثم ينادي من جديد. الحلقة ملك المُنادي دائمًا، وهذا في مصلحتك: لا شيء يُنفّذ على سيرفرك إلا ما سمحت به أنت سطرًا بسطر.
هل أستطيع استخدام الأدوات مع البثّ streaming؟
نعم، لكن ابنِ الحلقة أولًا على "stream": false. مع البثّ يعود الرد كسلسلة كائنات JSON لا كائنًا واحدًا، ولذلك يرفع json.loads خطأ Extra data إن مررت له السلسلة كلها. جمّع القطع قبل أن تنفّذ أي دالة، وتأكد من مخرجاتك أن الاستدعاء وصل كاملًا. تنفيذ دالة على وسيط ناقص أسوأ من تأخر الرد بثانيتين.