Function Calling چیست؟ آموزش ابزاردهی به مدل با نمونه کد
ربات پشتیبانیتان را راه انداختهاید و کاربر میپرسد «سفارش NTZ-1042 من کجاست؟». مدل با اعتمادبهنفس کامل یک تاریخ تحویل میسازد که هیچجا وجود ندارد. مشکل از مدل نیست: مدل به پایگاه دادهٔ شما دسترسی ندارد و هیچ راهی هم برای گفتن «نمیدانم، باید نگاه کنم» به آن ندادهاید. Function Calling دقیقاً همین راه را باز میکند. در این مقاله حلقهٔ کامل را قدمبهقدم میسازیم؛ همان حلقهای که در بیشتر منابع فارسی یا اصلاً توضیح داده نشده یا فقط نیمهٔ اولش نشان داده شده است.
یک تصور غلط رایج را همین اول کنار بگذاریم: مدل تابع شما را اجرا نمیکند. مدل هیچ دسترسیای به سرور، پایگاه داده یا شبکهٔ شما ندارد. کاری که میکند این است که نگاه میکند به فهرست ابزارهایی که معرفی کردهاید و میگوید: «برای جوابدادن به این پرسش، تابع get_order_status را با آرگومان {"order_id": "NTZ-1042"} صدا بزن». اجرای واقعی کاملاً بر عهدهٔ کد شماست و همین نکته است که Function Calling را هم امن میکند و هم قابل کنترل.
پس Function Calling در واقع یک قرارداد است: شما توضیح میدهید چه توابعی دارید و هرکدام چه ورودی میخواهند، مدل تصمیم میگیرد کدامیک لازم است، و نتیجه را دوباره به مدل برمیگردانید تا جواب انسانی بسازد.
حلقهٔ دو رفتوبرگشتی؛ قلب ماجرا
بیشتر آموزشها فقط تا مرحلهٔ دوم پیش میروند و همانجا رها میکنند، در حالی که کاربر شما هنوز هیچ جوابی ندیده است. جدول زیر کل چرخه را نشان میدهد:
| مرحله | چه کسی حرف میزند | محتوا | نقش در آرایهٔ messages |
|---|---|---|---|
| ۱. درخواست اول | برنامهٔ شما به مدل | پرسش کاربر بهعلاوهٔ فهرست ابزارها | system و user |
| ۲. تصمیم مدل | مدل به برنامه | پیامی بدون متن، با آرایهٔ tool_calls شامل نام تابع و آرگومانهای JSON | assistant |
| ۳. اجرا | فقط برنامهٔ شما | تابع را صدا میزنید و خروجی را به رشتهٔ JSON تبدیل میکنید | tool (بههمراه tool_call_id) |
| ۴. درخواست دوم | برنامهٔ شما به مدل | کل تاریخچه، این بار با نتیجهٔ ابزار | همهٔ پیامهای قبلی |
| ۵. پاسخ نهایی | مدل به برنامه | متن فارسی قابل نمایش به کاربر | assistant |
سه اشتباه که تقریباً همه در قدم سوم مرتکب میشوند: پیام assistant حاوی tool_calls را به تاریخچه اضافه نمیکنند، tool_call_id را برنمیگردانند، یا خروجی تابع را بهجای رشته بهصورت شیء میفرستند. هر سه به خطای ۴۰۰ ختم میشود.
تعریف ابزار: توصیف مهمتر از اسکیماست
هر ابزار سه بخش دارد: نام، توصیف و اسکیمای پارامترها. توصیف را جدی بگیرید؛ مدل فقط از روی همین جمله تصمیم میگیرد که کی از ابزار استفاده کند. «وضعیت سفارش را برمیگرداند» خیلی بهتر از «تابع سفارش» عمل میکند.
TOOLS = [
{
"type": "function",
"function": {
"name": "get_order_status",
"description": "وضعیت، شرکت پستی و زمان تحویل تخمینی یک سفارش را با شمارهٔ سفارش برمیگرداند.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "شمارهٔ سفارش، مثل NTZ-1042",
}
},
"required": ["order_id"],
"additionalProperties": False,
},
},
}
]
سه توصیهٔ عملی دربارهٔ اسکیما: نام پارامترها را انگلیسی و خوانا بگذارید چون مدل از روی همانها هم معنا برداشت میکند؛ هر جا مقدارها محدودند از enum استفاده کنید تا مدل ورودی مندرآوردی نسازد (مثلاً وضعیت سفارش فقط یکی از چند مقدار مشخص باشد)؛ و پارامترهای اختیاری را بهجای حذف از required، با نوع قابل تهی تعریف کنید تا رفتار مدل قابل پیشبینی بماند. اگر پارامتری واقعاً اجباری نیست، در توصیفش بنویسید که در چه شرایطی باید پر شود.
گذاشتن additionalProperties برابر false و پرکردن required باعث میشود مدل آرگومان اضافی از خودش نسازد. اگر با نوشتن JSON Schema راحت نیستید یا میخواهید خروجی نهایی مدل هم ساختار مشخصی داشته باشد، راهنمای خروجی JSON قابل اعتماد همین موضوع را از زاویهٔ دیگری باز کرده است.
حلقهٔ کامل در پایتون
کد زیر یک نمونهٔ کامل و اجراشدنی است. بهجای پایگاه داده از یک دیکشنری استفاده کردهایم تا بتوانید همین حالا اجرایش کنید:
import json
from openai import OpenAI
client = OpenAI(base_url="https://netarz.ir/api/ai/v1", api_key="sk-ntz-v1-...")
ORDERS = {
"NTZ-1042": {"status": "ارسال شده", "carrier": "پست پیشتاز", "eta": "۱۴۰۵/۰۶/۱۲"},
"NTZ-1043": {"status": "در حال آمادهسازی", "carrier": None, "eta": "۱۴۰۵/۰۶/۱۴"},
}
def get_order_status(order_id: str) -> dict:
order = ORDERS.get(order_id.strip().upper())
if order is None:
return {"found": False, "message": "سفارشی با این شماره پیدا نشد."}
return {"found": True, **order}
AVAILABLE = {"get_order_status": get_order_status}
def run(question: str, max_rounds: int = 4) -> str:
messages = [
{
"role": "system",
"content": "تو پشتیبان فروشگاه هستی. برای وضعیت سفارش حتماً از ابزار استفاده کن و از خودت تاریخ نساز.",
},
{"role": "user", "content": question},
]
for _ in range(max_rounds):
response = client.chat.completions.create(
model="gpt-5-mini",
messages=messages,
tools=TOOLS,
)
message = response.choices[0].message
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
return message.content
for call in message.tool_calls:
function = AVAILABLE.get(call.function.name)
try:
arguments = json.loads(call.function.arguments or "{}")
result = function(**arguments) if function else {"error": "ابزار ناشناخته است"}
except Exception as exc:
result = {"error": str(exc)}
messages.append(
{
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
}
)
return "پاسخ نهایی در تعداد دورهای مجاز به دست نیامد."
print(run("سفارش NTZ-1042 من کجاست؟"))
دو جزئیات ریز اما حیاتی: اول اینکه خطای اجرای تابع را با try میگیریم و بهعنوان نتیجهٔ ابزار به مدل برمیگردانیم نه اینکه برنامه را بترکانیم؛ مدل بعد از دیدن خطا معمولاً یا دوباره با ورودی درست تلاش میکند یا مؤدبانه به کاربر میگوید مشکلی پیش آمده. دوم اینکه ensure_ascii=False باعث میشود متن فارسی بهشکل خوانا برای مدل ارسال شود.
همان حلقه در JavaScript
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://netarz.ir/api/ai/v1",
apiKey: process.env.NETARZ_AI_KEY,
});
const ORDERS = {
"NTZ-1042": { status: "ارسال شده", carrier: "پست پیشتاز", eta: "۱۴۰۵/۰۶/۱۲" },
};
const AVAILABLE = {
get_order_status: ({ order_id }) =>
ORDERS[order_id.trim().toUpperCase()] ?? { found: false, message: "سفارش پیدا نشد." },
};
export async function run(question, maxRounds = 4) {
const messages = [
{ role: "system", content: "تو پشتیبان فروشگاه هستی و برای وضعیت سفارش از ابزار استفاده میکنی." },
{ role: "user", content: question },
];
for (let round = 0; round < maxRounds; round++) {
const completion = await client.chat.completions.create({
model: "gpt-5-mini",
messages,
tools: TOOLS,
});
const message = completion.choices[0].message;
messages.push(message);
if (!message.tool_calls?.length) return message.content;
for (const call of message.tool_calls) {
let result;
try {
const args = JSON.parse(call.function.arguments || "{}");
result = AVAILABLE[call.function.name]?.(args) ?? { error: "ابزار ناشناخته است" };
} catch (error) {
result = { error: String(error) };
}
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(result),
});
}
}
return "پاسخ نهایی به دست نیامد.";
}
ساختار کد در دو زبان یکسان است و فقط دو تفاوت ریز دارد: در پایتون باید پیام مدل را با model_dump به دیکشنری تبدیل کنید تا دوباره قابل ارسال باشد، ولی در جاوااسکریپت شیء پاسخ خودش قابل استفاده است. همچنین در جاوااسکریپت حتماً وجود آرایهٔ tool_calls را با اپراتور اختیاری بررسی کنید، چون در پاسخهای متنی این فیلد اصلاً وجود ندارد و دسترسی مستقیم به طولش خطا میدهد.
چند ابزار، فراخوانی موازی و کنترل رفتار مدل
وقتی چند ابزار معرفی میکنید، مدل ممکن است در یک پاسخ چند فراخوانی همزمان برگرداند؛ به همین دلیل در کد بالا روی tool_calls حلقه زدهایم و نه فقط عضو اول را برداشتهایم. برای هر فراخوانی باید دقیقاً یک پیام tool با شناسهٔ خودش برگردد، وگرنه درخواست بعدی رد میشود.
حالت دیگری هم هست که تازهکارها را غافلگیر میکند: گاهی مدل بهجای فراخوانی ابزار، یک سؤال روشنکننده میپرسد. مثلاً کاربر نوشته «سفارشم کجاست؟» بدون شمارهٔ سفارش، و مدل بهدرستی میفهمد که آرگومان لازم را ندارد. این رفتار مطلوب است و نباید با کد آن را دور بزنید؛ فقط کافی است در رابط کاربری اجازهٔ پاسخ دادن به کاربر را بدهید و پیام بعدی او را به همان تاریخچه اضافه کنید.
پارامتر tool_choice هم به شما اجازه میدهد رفتار مدل را مهار کنید:
| مقدار tool_choice | رفتار مدل | کجا به کار میآید |
|---|---|---|
| auto (پیشفرض) | خودش تصمیم میگیرد ابزار لازم است یا نه | دستیار عمومی و چتبات پشتیبانی |
| none | هیچ ابزاری صدا نمیزند و فقط متن میدهد | وقتی میخواهید مطمئن شوید فقط خلاصهسازی انجام میشود |
| required | حتماً یکی از ابزارها را صدا میزند | گردش کاری که خروجی متنی آزاد در آن بیمعناست |
| ابزار مشخص | همان تابع نامبردهشده را صدا میزند | استخراج داده با یک تابع ثابت |
یک نکتهٔ هزینهای هم هست: در هر دور، کل تاریخچه دوباره فرستاده میشود، پس یک پرسش با یک ابزار معمولاً دو برابر یک پرسش ساده هزینه دارد. اگر میخواهید بدانید این عدد در عمل چقدر میشود، راهنمای محاسبهٔ توکن و هزینه روش حسابکردنش را نشان میدهد.
نکتههای تولیدی که نادیده گرفته میشوند
- ورودی مدل را اعتبارسنجی کنید. آرگومانهایی که مدل میسازد ورودی کاربر محسوب میشوند. هیچوقت آنها را مستقیم داخل کوئری SQL یا فرمان سیستمی نگذارید.
- سقف حلقه بگذارید. اگر مدل بین دو ابزار گیر کند، بدون سقف دورها هزینه بینهایت بالا میرود. عدد سه تا پنج در عمل کافی است.
- ابزارهای پرخطر را تأییدمحور کنید. خواندن وضعیت سفارش بیخطر است، اما «لغو سفارش» یا «ارسال ایمیل» باید تأیید انسانی بگیرد.
- تعداد ابزارها را کم نگه دارید. با بیش از ده تا پانزده ابزار، دقت انتخاب مدل بهشکل محسوسی افت میکند. اگر ابزار زیاد دارید، آنها را دستهبندی کنید.
- خطاها را درست تفسیر کنید. اگر پاسخها با کد ۴۰۰ برمیگردند، معمولاً ساختار پیامهاست نه منطق شما؛ فهرست علتها در راهنمای خطاهای رایج API آمده است.
از Function Calling تا «ایجنت»؛ تفاوت واقعی چیست؟
حالا که حلقه را ساختهاید، یک راز کوچک را بدانید: چیزی که این روزها به آن «عامل هوش مصنوعی» یا AI Agent میگویند، در هستهٔ خودش همین حلقه است. فرق ایجنت با نمونهٔ بالا در سه چیز است و هیچکدام مفهوم جدیدی نیستند: تعداد دورها بیشتر است (مدل میتواند نتیجهٔ یک ابزار را ببیند و بر اساسش ابزار بعدی را انتخاب کند)، ابزارها قدرتمندترند (خواندن فایل، اجرای کد، جستجوی وب) و معمولاً یک لایهٔ حافظه هم اضافه میشود تا نتایج قبلی گم نشوند.
به بیان دیگر، اگر همین کد را با یک حلقهٔ دهدوری و پنج ابزار اجرا کنید، یک ایجنت ساده ساختهاید. این را میگوییم چون فهمیدن آن از خرید هر چارچوب آمادهٔ ایجنت مفیدتر است: وقتی حلقه را بلدید، میدانید کجا خراب شده و چطور هزینهاش را کنترل کنید. اگر میخواهید ابزارهایتان روی اسناد داخلی شرکت کار کنند، ترکیب طبیعی همین حلقه با بازیابی است که در راهنمای ساخت چتبات RAG روی اسناد فارسی ساختهایم.
قدم بعدی
اگر میخواهید همین حلقه را داخل یک ربات پیامرسان بالا بیاورید، ساختار آماده و نکات استریم را در راهنمای ساخت ربات تلگرام فارسی با پایتون نوشتهایم.
پیشنهاد ما این است که با یک ابزار واقعی از سیستم خودتان شروع کنید، نه با نمونهٔ آبوهوا. یک تابع خواندنی و بیخطر انتخاب کنید، همین حلقه را دورش بپیچید و ده پرسش واقعی کاربران را رویش تست کنید. کیفیت توصیف ابزار را هم دستکم نگیرید؛ اصول نوشتن دستور روشن برای مدل را در راهنمای پرامپتنویسی جمع کردهایم.
برای اجرای کد بالا فقط به یک کلید و یک آدرس پایه نیاز دارید. جزئیات فنی و نمونهٔ درخواستها در مستندات API نِت اَرز آمده و مدلهای فعال در صفحهٔ مدلها فهرست شدهاند؛ هماکنون مدلهای OpenAI فعال است و بقیهٔ ارائهدهندگان در حال اتصالاند. اعتبار حساب را هم میتوانید از صفحهٔ افزایش اعتبار وبسرویس ChatGPT با قیمت لحظهای همان صفحه شارژ کنید.
پرسشهای پرتکرار
آیا مدل خودش تابع من را اجرا میکند؟
خیر، و این مهمترین نکتهٔ Function Calling است. مدل فقط نام تابع و آرگومانهای پیشنهادی را بهصورت JSON برمیگرداند و اجرای واقعی کاملاً بر عهدهٔ کد شماست. به همین دلیل باید آرگومانها را مثل هر ورودی کاربر دیگری اعتبارسنجی کنید.
چرا بعد از فراخوانی ابزار خطای 400 میگیرم؟
تقریباً همیشه یکی از سه دلیل است: پیام assistant که حاوی tool_calls بوده به تاریخچه اضافه نشده، مقدار tool_call_id در پیام tool برنگشته، یا خروجی تابع بهجای رشته بهصورت شیء فرستاده شده است. خروجی را با json.dumps یا JSON.stringify به رشته تبدیل کنید.
استفاده از ابزار چقدر هزینه را بالا میبرد؟
هر دور از حلقه یک درخواست کامل است و کل تاریخچه دوباره ارسال میشود، بنابراین یک پرسش با یک ابزار معمولاً حدود دو برابر یک پرسش ساده هزینه دارد. برای همین توصیه میشود سقف دورها را محدود و توصیف ابزارها را کوتاه و دقیق نگه دارید.
تفاوت Function Calling با عامل هوش مصنوعی چیست؟
تفاوت ماهوی نیست، تفاوت مقیاس است. عامل هوش مصنوعی همین حلقه است با دورهای بیشتر، ابزارهای قدرتمندتر و معمولاً یک لایهٔ حافظه. اگر حلقهٔ دو رفتوبرگشتی را درست پیاده کنید، ساختن یک عامل ساده فقط افزودن چند ابزار و بالا بردن سقف دورهاست.
نظر خوانندگان
هنوز نظری ثبت نشده. اگر این مقاله پرسشتان را جواب داد یا جای چیزی در آن خالی ماند، همینجا بنویسید.