مثال على استدعاء الأدوات في واجهة برمجة Ollama باستخدام SDK الخاص بـ OpenAI
يسمح استدعاء الأدوات للنموذج بطلب تشغيل دالة في الكود الخاص بك ثم استخدام النتيجة في إجابته. فيما يلي مثال كامل باستخدام SDK الخاص بـ OpenAI مع نقطة نهاية مستضافة، بالإضافة إلى البث المتدفق والأخطاء التي تسبب معظم الفشل.
محدّث

نقاط رئيسية
- أرسل دالتك كـ JSON schema في الحقل tools؛ سيعيد النموذج tool_calls بدلاً من النص.
- تشغّل الدالة، تضيف النتيجة كرسالة بدور tool، وتستدعي واجهة برمجة التطبيقات مرة أخرى.
- قم دائماً بتحليل الحجج بشكل دفاعي، لأن النموذج يمكن أن يعيد JSON غير صالح أو غير متوقع.
- تصل استدعاءات الدوال المتدفقة على شكل أجزاء يجب عليك تجميعها حسب الفهرس.
كيف تبدو الحلقة
إذا كنت تبحث عن مثال لاستدعاء الدوال في واجهة برمجة التطبيقات الخاصة بـ ollama يعمل مع نقطة نهاية مستضافة، فإن الشكل هو الشكل القياسي الخاص بـ OpenAI. تصف دوالك في مصفوفة tools. إما أن يجيب النموذج بالنص أو يعيد قائمة tool_calls تسمى الدالة وحججها. لا يشغل النموذج أي شيء. الكود الخاص بك يشغل الدالة، ويرسل المخرجات مرة أخرى، ويكتب النموذج الرد النهائي.
هذا يجعل دورة واحدة لاستخدام الأدوات تتألف من طلبين API:
- أرسل رسالة المستخدم وتعريفات الأداة.
- استلم رسالة مساعد تحتوي على
tool_calls. نفذ كل استدعاء محلياً. - أرسل الرسائل الأصلية، ورسالة المساعد، ورسالة
role: "tool"واحدة لكل استدعاء مع ناتجها. - استلم النص النهائي للإجابة.
تستخدم الأمثلة Base URL https://api.ollamaapi.com/v1 ومعرف النموذج uncensored. نقطة النهاية هي خدمة مستضافة، لذا يأتي المفتاح من صفحة مفتاح API. يعمل نفس الكود أيضًا مع خادم Ollama المحلي إذا قمت بتغيير Base URL والمفتاح واسم النموذج، طالما أن النموذج المحلي يدعم الأدوات.
مثال Python كامل
يعطي المثال النموذج دالة واحدة، get_order_status، مدعومة بقاموس صغير. في التطبيق الفعلي، سيتم استعلام قاعدة بيانات أو خدمة داخلية في هذه المرحلة.
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.ollamaapi.com/v1", api_key="YOUR_KEY")
ORDERS = {"A1001": "shipped", "A1002": "processing", "A1003": "delivered"}
def get_order_status(order_id: str) -> str:
return ORDERS.get(order_id, "not found")
tools = [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Look up the status of an order by its ID.",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "Order ID such as A1001"}
},
"required": ["order_id"],
},
},
}]
messages = [{"role": "user", "content": "Where is order A1002?"}]
first = client.chat.completions.create(
model="uncensored",
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = first.choices[0].message
if msg.tool_calls:
messages.append(msg) # keep the assistant turn with its tool_calls
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = get_order_status(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
final = client.chat.completions.create(
model="uncensored",
messages=messages,
tools=tools,
)
print(final.choices[0].message.content)
else:
print(msg.content)ثلاث تفاصيل مهمة هنا. يجب أن تعود رسالة المساعد التي تحتوي على tool_calls إلى السجل قبل نتائج الأدوات، وإلا سيتم رفض الطلب التالي أو إرباك النموذج. يجب أن تحمل كل رسالة أداة معرف استدعاء الأداة tool_call_id الخاص بالاستجابة لها. ويجب أن يتضمن الطلب الثاني أيضًا tools، لأن النموذج قد يرغب في استدعاء دالة أخرى بعد رؤية النتيجة الأولى.
نفس الطلب باستخدام curl
يؤدي الاختبار باستخدام curl إلى إزالة حزمة SDK من الصورة، وهو ما يساعد عندما تكون غير متأكد مما إذا كانت المشكلة في الكود الخاص بك أم في الطلب.
curl https://api.ollamaapi.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "uncensored",
"messages": [{"role": "user", "content": "Where is order A1002?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Look up the status of an order by its ID.",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"]
}
}
}],
"tool_choice": "auto"
}'الرد الأول الناجح يحتوي على finish_reason مضبوطاً على tool_calls، وcontent فارغ أو null، ومصفوفة tool_calls. حقل arguments هو سلسلة تحتوي على JSON، وليس كائنًا متداخلاً، وهذا هو السبب في أن مثال Python يستدعي json.loads.
يحدد الحقل tool_choice مدى إلحاح الطلب. يسمح auto للنموذج باتخاذ القرار. تمرير كائن يحدد اسم دالة يطلب تلك الدالة المحددة. استخدم auto ما لم يكن لديك سببٌ لذلك، لأن فرض استدعاء على رسالة لا تحتاج إلى ذلك ينتج حججًا مختلقة.
نفس الحلقة في Node
تستخدم النسخة Node حزمة openai الرسمية وتتبع نفس الخطوات. تتعامل هذه النسخة مع أكثر من استدعاء واحد لكل دور باستخدام Promise.all.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.ollamaapi.com/v1",
apiKey: process.env.API_KEY,
});
const handlers = {
get_order_status: async ({ order_id }) =>
({ A1001: "shipped", A1002: "processing" })[order_id] ?? "not found",
};
const tools = [{
type: "function",
function: {
name: "get_order_status",
description: "Look up the status of an order by its ID.",
parameters: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
},
},
}];
const messages = [{ role: "user", content: "Where is order A1001?" }];
const first = await client.chat.completions.create({
model: "uncensored", messages, tools,
});
const msg = first.choices[0].message;
if (msg.tool_calls?.length) {
messages.push(msg);
const results = await Promise.all(msg.tool_calls.map(async (call) => {
const args = JSON.parse(call.function.arguments);
const out = await handlers[call.function.name](args);
return { role: "tool", tool_call_id: call.id, content: String(out) };
}));
messages.push(...results);
const final = await client.chat.completions.create({
model: "uncensored", messages, tools,
});
console.log(final.choices[0].message.content);
} else {
console.log(msg.content);
}بث استدعاءات الدوال
مع stream: true، تصل الإجابة عبر SSE، وتصل استدعاءات الأدوات على شكل أجزاء. يحمل كل جزء إدخال delta.tool_calls مع index. يحتوي الجزء الأول الخاص بمؤشر على معرف الاستدعاء واسم الدالة. تضيف الأجزاء اللاحقة شرائح من سلسلة arguments. قم بدمج الشرائح حسب المؤشر، ولا تحلل JSON إلا بعد انتهاء البث.
stream = client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": "Where is order A1003?"}],
tools=tools,
stream=True,
)
calls = {}
for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta
for part in (delta.tool_calls or []):
slot = calls.setdefault(part.index, {"id": "", "name": "", "args": ""})
if part.id:
slot["id"] = part.id
if part.function and part.function.name:
slot["name"] = part.function.name
if part.function and part.function.arguments:
slot["args"] += part.function.arguments
for slot in calls.values():
print(slot["name"], json.loads(slot["args"]))تذكر أن الطلب المتدفق يحتفظ بمقعد التزامن الخاص به حتى انتهاء البث. يسمح المفتاح الكامل بـ 8 طلبات في وقت واحد و300 في الدقيقة، بينما يسمح المفتاح التجريبي بـ 2 و60. إذا كنت تشغل حلقات الأدوات للمستخدمين المتعددين بالتوازي، فاحسب كل بث مفتوح ضد الرقم الأول.
المزالق وكيفية إصلاحها
حجج غير صالحة أو مفقودة
النموذج هو نموذج مفتوح الأوزان، والنماذج المفتوحة أحيانًا تُرجع حججًا ليست JSON صالحًا، أو تترك حقلًا مطلوبًا فارغًا، أو تخترع حقلًا إضافيًا. لف json.loads داخل كتلة try، وقم بالتحقق من صحة البيانات مقابل مخططك، وعند الفشل، أرسل الخطأ مرة أخرى كرسالة أداة. عادةً ما يصحح النموذج نفسه في الدور التالي. حدد الحد الأقصى للحلقة بعدد قليل من التكرارات حتى لا يستمر الفشل العنيد إلى الأبد.
الوصف الغامض جدًا
يختار النموذج دالةً بقراءة اسمها ووصفها. وصفٌ مثل "جلب البيانات" لا يمنحه أي مادة للعمل عليها. اشرح ما تفعله الدالة، وما تعنيه مدخلاتها، وضمّن قيمة مثال في وصف المعامل.
أدوات كثيرة في آنٍ واحد
يعتبر كل تعريف أداة رمزًا (token) إدخالًا في كل طلب. بسعر $0.15 لكل مليون رمز إدخال، تضيف عشرة أدوات بحجم 150 رمزًا (token) لكل منها 1,500 رمز (token)، وهو ما يعادل $0.000225 لكل طلب، مبلغ صغير لكنه ليس صفرًا عند الحجم الكبير. والأهم من ذلك، أن القائمة الطويلة تجعل الاختيار أقل موثوقية. قدم فقط الأدوات ذات الصلة بالخطوة الحالية.
نتائج الأدوات كبيرة جدًا
أي شيء تُرجعه يتم تغذيته مرة أخرى كإدخال. قم بتقليل صفوف قاعدة البيانات الكبيرة أو صفحات الويب إلى الحقول التي يحتاجها النموذج. نافذة السياق هي 100,000 رمز (token) في المجموع، لذا فإن نتيجة كبيرة جدًا يمكن أن تطغى على المحادثة.
خلط الأدوات مع وضع JSON
وضع JSON (response_format مع json_object) متاح، لكن استخدمه للإجابة النهائية، وليس للدورات التي تريد فيها من النموذج استدعاء دالة. اختبر التركيب على موجّهاتك الخاصة قبل الاعتماد عليه.
الانتقال من Ollama المحلي وما يجب التحقق منه
إذا كنت قد بنيت حلقة الأدوات الأولى الخاصة بك على خادم Ollama المحلي، فلن يتغير تقريبًا أي شيء. استبدل Base URL بـ https://api.ollamaapi.com/v1، واستخدم مفتاحًا حقيقيًا، واضبط النموذج على uncensored. مسارات Ollama الأصلية مثل /api/chat غير موجودة هنا. فقط POST /v1/chat/completions و GET /v1/models متاحان، لذا يحتاج الكود المكتوب لـ API الأصلي إلى استدعاء بأسلوب OpenAI بدلاً من ذلك.
قبل النشر، شغّل قائمة التحقق القصيرة هذه:
- أكد أن المفتاح يعمل مع
GET /v1/models. - سجّل رأس
X-Request-Idلكل استدعاء، حتى يمكن تتبع التنفيذ السيئ. - راقب الرأس
X-Balance-USDفي الاستجابة عند تشغيل حلقات استدعاء الدوال دون إشراف. - عالج الخطأ HTTP 429: انتظر عند
rate_limitedأوconcurrency، واحترمRetry-After: 5عند الحمل الزائد. - حدد حدًا أقصى صارمًا لتكرارات الحلقة.
تتطلب حلقة الأدوات طلبين على الأقل لكل سؤال من المستخدم، لذا تتناسب التكاليف مع ذلك. على سبيل المثال، 2,000 حلقة مع 1,200 رمز (token) إدخال و150 رمز (token) مخرجات لكل طلب، وطلبين لكل منهما، تصل إلى 4,000 طلب، و4.8M رمز (token) إدخال و0.6M رمز (token) مخرجات. هذا يعادل $0.72 بالإضافة إلى $0.30، أو $1.02 في المجموع. يحتوي صفحة التسعير على خيارات شحن الرصيد، و الوثائق تسرد حقول الطلب المدعومة. يمكن للمستخدمين الجدد تجربة كل هذا مع رصيد تجريبي بقيمة $0.50، والذي لا يحتاج إلى بطاقة.
المزيد من الأدلة
أسئلة وأجوبة
هل تدعم نقطة النهاية استدعاء الدوال؟
نعم. تقبل الأدوات وtool_choice، وتعيد tool_calls، وتبثها كقيم متفرقة، وتأخذ النتائج مرة أخرى كرسائل مع role: tool.
لماذا يفشل الطلب الثاني بعد استدعاء الدالة؟
السبب المعتاد هو رسالة مساعد مفقودة أو رسالة أداة بدون tool_call_id المطابق. أضف أولاً نوبة المساعد التي تحتوي على tool_calls، ثم رسالة أداة واحدة لكل استدعاء.
هل يمكن للنموذج استدعاء عدة أدوات في دور واحد؟
يمكنه إرجاع قائمة تحتوي على أكثر من عنصر، لذا قم بالتكرار على جميعها وأرسل نتيجة واحدة لكل منها. لا تفترض أن القائمة تحتوي على عنصر واحد فقط.
هل أحتاج إلى مكتبة Ollama لهذا؟
لا. حزم SDK الرسمية من OpenAI كافية. وجّه عنوان URL الأساسي إلى نقطة النهاية واستخدم النموذج uncensored.
مفتاحك على بُعد استمارة واحدة
أنشئ حساباً، وانسخ المفتاح، ثم غيّر عنوان URL الأساسي. هذه هي الإعدادات الكاملة.