مدلهای زبانی بزرگ فینفسه کاری فراتر از تولید متن انجام نمیدهند. آنها نه به ساعت دسترسی دارند، نه آبوهوای لحظهای را میدانند و نه میتوانند مستقیماً به پایگاه داده وصل شوند. اگر بخواهید چتباتی برای کارهای واقعی مانند جستوجوی اینترنت، رزرو بلیت یا اجرای کد بسازید، صرفاً ارسال پرامپت کافی نیست. اینجاست که مفهوم «فراخوانی تابع» (Function Calling) یا استفاده از ابزار مطرح میشود؛ سازوکاری که به مدل اجازه میدهد قصد خود برای انجام یک عملیات را در قالب یک ساختار مشخص اعلام کند و اجرای آن را به کد برنامه بسپارد.
در توسعهی ایجنتها برای کاربران ایرانی، اتصال مستقیم به API سرویسهایی مثل OpenAI یا Claude ممکن است نیازمند حساب ارزی یا عبور از محدودیت دسترسی به سرویسها با تغییر IP باشد. از طرف دیگر، میتوانید از گیتویهای ارائهدهندهی مدل یا مدلهای بازوزن محلی (مثل Llama یا Qwen) استفاده کنید؛ در هر صورت ساختار هستهی کدها و حلقهی اجرای ابزارها مبتنی بر وبسرویس و JSON Schema یکسان باقی میماند.
در این درس گامبهگام پیادهسازی حلقهی تعامل با ابزارها (Agent Loop)، طراحی دقیق ساختار ابزار، اعتبارسنجی ورودیهای تولیدشده توسط هوش مصنوعی، فراخوانی موازی و استاندارد باز MCP را یاد میگیرید تا بتوانید رباتهای خود را از دانشنامههای متنی صرف، به مجریانی کاربردی در پروژههای واقعی تبدیل کنید.
مدلهای زبانی بزرگ هیچ کاری نمیتوانند انجام دهند. آنها فقط متن تولید میکنند؛ کل توانمندیشان همین است. نمیتوانند آبوهوا را بررسی کنند، از پایگاه داده کوئری بگیرند، ایمیل بفرستند، کدی اجرا کنند یا فایلی بخوانند. هر «ایجنت هوش مصنوعی» که تا به حال دیدهاید، در واقع یک مدل زبانی است که خروجی JSON تولید میکند تا بگوید کدام تابع باید فراخوانی شود؛ سپس کد شماست که واقعاً آن را اجرا میکند. مدل، مغز است؛ ابزارها، دستها هستند؛ و فراخوانی تابع (Function calling)، سیستم عصبی رابط میان آنهاست.
مرتبط: فاز 11 · 14 (پروتکل بستر مدل یا MCP) — وقتی ابزاری بین چند میزبان به اشتراک گذاشته میشود، باید از فراخوانی تابع درونبرنامهای فراتر رفته و سراغ سرور MCP بروید. این درس حالت درونبرنامهای را پوشش میدهد و MCP به حالت مبتنی بر پروتکل میپردازد.
اهداف یادگیری
- پیادهسازی حلقهی فراخوانی تابع: تعریف اسکیمای ابزارها، اعتبارسنجی و خواندن خروجی JSON مدل برای فراخوانی ابزار، اجرای توابع و بازگرداندن نتایج
- طراحی اسکیمای ابزار با توضیحات شفاف و پارامترهای تیپبندیشده بهگونهای که مدل بتواند با اطمینان آنها را فراخوانی کند
- ساخت حلقهی ایجنت چندنوبتی که چند فراخوانی تابع را پشت سر هم زنجیره میکند تا به پرسشهای پیچیده پاسخ دهد
- مدیریت حالتهای خاص فراخوانی ابزار: فراخوانیهای موازی، انتشار خطا و جلوگیری از افتادن در حلقههای بیپایان ابزار
صورت مسئله
ربات چتی میسازید. کاربری میپرسد: «الان هوای توکیو چطوره؟»
مدل پاسخ میدهد: «من به دادههای آبوهوای لحظهای دسترسی ندارم، اما با توجه به فصل، دمای توکیو احتمالاً حدود 15 درجه سانتیگراد است...»
این همان توهم (Hallucination) است که در پوشش رفع مسئولیت ارائه میشود. مدل از وضعیت آبوهوا بیخبر است و هرگز هم باخبر نخواهد شد؛ چون هوا هر ساعت تغییر میکند، در حالی که دادههای آموزشی مدل متعلق به ماهها قبل است.
پاسخ درست نیازمند فراخوانی API سرویس OpenWeatherMap، دریافت دمای لحظهای و بازگرداندن عدد واقعی است. مدل قادر به فراخوانی API نیست، اما کد شما میتواند این کار را بکند. حلقهی مفقوده اینجاست: پروتکلی ساختاریافته که به مدل اجازه دهد بگوید «میخواهم این API آبوهوا را با این آرگومانها فراخوانی کنم» و به کد شما امکان دهد آن را اجرا کند و نتیجه را به مدل پس بفرستد.
این فرایند همان فراخوانی تابع است. مدل یک خروجی JSON ساختاریافته تولید میکند که نشان میدهد چه تابعی باید با چه آرگومانهایی فراخوانی شود. برنامهی شما تابع را اجرا میکند، نتیجه به جریان گفتوگو برمیگردد و در نهایت مدل از این نتیجه برای تولید پاسخ نهایی بهره میبرد.
بدون فراخوانی تابع، مدلهای زبانی شبیه دانشنامهاند؛ با آن، به ایجنت تبدیل میشوند.
مفاهیم پایه
حلقهی فراخوانی تابع
هر تعامل مبتنی بر ابزار از یک حلقهی 5 مرحلهای ثابت پیروی میکند.
مرحلهی 1: کاربر پیامی میفرستد. مرحلهی 2: مدل پیام را همراه با تعاریف ابزارها (یک JSON Schema که توابع موجود را توصیف میکند) دریافت میکند. مرحلهی 3: مدل به جای پاسخ متنی، یک فراخوانی ابزار (Tool call) صادر میکند؛ یعنی یک آبجکت JSON ساختاریافته شامل نام تابع و آرگومانهای آن. مرحلهی 4: کد شما تابع را اجرا و خروجی را دریافت میکند. مرحلهی 5: نتیجه به مدل برمیگردد و اکنون مدل با تکیه بر دادههای واقعی، پاسخ نهایی را تولید میکند.
مدل هرگز خودش چیزی را اجرا نمیکند؛ فقط تصمیم میگیرد چه چیزی با چه آرگومانهایی فراخوانی شود. مجری واقعی کد شماست.
تعاریف ابزار: قرارداد JSON Schema
هر ابزار با یک JSON Schema تعریف میشود که به مدل میگوید این تابع چه کاری انجام میدهد، چه آرگومانهایی میگیرد و نوع این آرگومانها چیست.
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city. Returns temperature in Celsius and conditions.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Tokyo' or 'San Francisco'"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature units"
}
},
"required": ["city"]
}
}
}
فیلدهای description نقشی حیاتی دارند. مدل با خواندن آنها تصمیم میگیرد چه زمانی و چگونه از ابزار استفاده کند. توضیح مبهمی مثل «gets weather» در مقایسه با «Get current weather for a city. Returns temperature in Celsius and conditions»، به انتخاب ضعیفتر ابزار توسط مدل منجر میشود. در واقع، این توضیحات برای انتخاب ابزار حکم پرامپت را دارند.
مقایسهی ارائهدهندگان
همهی ارائهدهندگان بزرگ از فراخوانی تابع پشتیبانی میکنند، اما ساختار رابط برنامهنویسی (API Surface) آنها متفاوت است.
| ارائهدهنده | پارامتر API | فرمت فراخوانی ابزار | فراخوانیهای موازی | فراخوانی اجباری |
|---|---|---|---|---|
| OpenAI (GPT-5, o4) | tools |
tool_calls[].function |
بله (چند مورد در هر نوبت گفتوگو) | tool_choice="required" |
| Anthropic (Claude 4.6/4.7) | tools |
content[].type="tool_use" |
بله (چندین بلوک) | tool_choice={"type":"any"} |
| Google (Gemini 3) | function_declarations |
functionCall |
بله | function_calling_config |
| بازوزن (Llama 4, Qwen3, DeepSeek-V3) | tools بومی در Llama 4؛ فرمت Hermes یا ChatML در بقیه |
متغیر | وابسته به مدل | مبتنی بر پرامپت یا tool_choice در صورت پشتیبانی |
تا سال 2026، هر سه ارائهدهندهی سرویسهای اختصاصی روی فرمتهای تقریباً یکسان مبتنی بر JSON Schema به توافق رسیدهاند. Llama 4 با فیلد بومی tools عرضه میشود که دقیقاً با ساختار OpenAI همخوانی دارد. مدلهای فاینتیونشدهی بازوزن هنوز تفاوتهایی دارند؛ هرچند فرمت Hermes (کاری از NousResearch) رایجترین ساختار میان مدلهای فاینتیونشدهی شخص ثالث است. اگر قرار است ابزارها میان چند میزبان به اشتراک گذاشته شوند، استفاده از MCP (فاز 11 · 14) را به فراخوانی تابع درونبرنامهای ترجیح دهید؛ سرور برای همهی آنها یکسان خواهد بود.
گزینهی انتخاب ابزار: خودکار، اجباری، مشخص
کنترل این موضوع که مدل چه زمانی از ابزارها استفاده کند دست شماست.
خودکار یا Auto (حالت پیشفرض): مدل تصمیم میگیرد که ابزاری را فراخوانی کند یا مستقیماً پاسخ دهد. اگر بپرسید «2+2 چند میشه؟»، مستقیم پاسخ میدهد؛ اما برای «هوا چطوره؟»، ابزار را فراخوانی میکند.
اجباری یا Required: مدل باید حداقل یک ابزار را فراخوانی کند. این گزینه زمانی کاربرد دارد که میدانید نیت کاربر نیازمند دسترسی به ابزار است. با این کار جلوی حدس زدن مدل به جای بررسی دادههای واقعی گرفته میشود.
تابع مشخص (Specific function): مدل را مجبور میکند تابع خاصی را فراخوانی کند. تنظیم tool_choice={"type":"function", "function": {"name": "get_weather"}} تضمین میکند که صرفنظر از متن پرسش، حتماً ابزار آبوهوا فراخوانی شود. این حالت برای مسیریابی (Routing) مناسب است؛ زمانی که منطق بالادستی پیشتر مشخص کرده به چه ابزاری نیاز است.
فراخوانی موازی توابع
مدلهای GPT-4o و Claude میتوانند در یک نوبت گفتوگو چند تابع را همزمان فراخوانی کنند. برای نمونه، کاربر میپرسد: «هوای توکیو و نیویورک چطوره؟» مدل همزمان دو فراخوانی ابزار صادر میکند:
[
{"name": "get_weather", "arguments": {"city": "Tokyo"}},
{"name": "get_weather", "arguments": {"city": "New York"}}
]
کد شما هر دو تابع را (ترجیحاً بهشکل همروند) اجرا میکند، هر دو نتیجه را برمیگرداند و مدل پاسخی جامع از ترکیب آنها میسازد. با این روش، رفتوبرگشتها از 2 به 1 کاهش مییابد. برای ایجنتهایی که در هر پرسش 5 تا 10 فراخوانی ابزار دارند، اجرای موازی تاخیر را بین 60 تا 80 درصد کم میکند.
خروجیهای ساختاریافته در برابر فراخوانی توابع
در درس 03 به خروجیهای ساختاریافته پرداختیم. فراخوانی تابع از همان سازوکار JSON Schema استفاده میکند، اما هدفی کاملاً متفاوت دارد.
خروجیهای ساختاریافته: مدل را وادار میکنند خروجی را با ساختاری معین تولید کند. این خروجی، محصول نهایی کار است؛ مثل استخراج اطلاعات یک محصول از دل متن به شکل {name, price, in_stock}.
فراخوانی توابع: مدل نیت خود را برای اجرای یک عملیات اعلام میکند. این خروجی صرفاً یک گام میانی است؛ مثل get_weather(city="Tokyo") که در آن مدل درخواست انجام یک کار را دارد، نه ارائهی پاسخ نهایی.
وقتی هدفتان استخراج داده است از خروجیهای ساختاریافته استفاده کنید، و وقتی میخواهید مدل با سیستمهای خارجی تعامل داشته باشد، سراغ فراخوانی تابع بروید.
امنیت: قواعد غیرقابلمذاکره
فراخوانی تابع خطرناکترین قابلیتی است که میتوانید به یک مدل زبانی بدهید؛ زیرا مدل انتخاب میکند چه کدی اجرا شود. اگر مجموعهی ابزارهای شما شامل کوئریهای پایگاه داده باشد، مدل متن کوئری را مینویسد؛ و اگر شامل دستورات شل باشد، مدل آنها را مینویسد.
قاعدهی 1: هرگز SQL تولیدشده توسط مدل را مستقیماً روی پایگاه داده اجرا نکنید. مدل میتواند دستوراتی مثل DROP TABLE یا تزریق UNION بنویسد یا کوئریهایی بسازد که همهی ردیفها را برگردانند. همیشه کوئریها را پارامتری کنید، ورودیها را بسنجید و عملیات مجاز را به یک لیست سفید محدود نگه دارید.
قاعدهی 2: توابع را در لیست سفید قرار دهید. مدل تنها باید توابعی را صدا بزند که صراحتاً تعریف کردهاید. هرگز ابزاری عمومی با عنوان «اجرای هر تابعی بر اساس نام» نسازید. اگر برنامهتان 50 تابع داخلی دارد، فقط همان 5 موردی را که کاربر نیاز دارد در دسترس مدل قرار دهید.
قاعدهی 3: آرگومانها را اعتبارسنجی کنید. ممکن است مدل نام شهری را به صورت "; DROP TABLE users; --" ارسال کند. پیش از اجرا، تمام آرگومانها را از نظر نوع، محدوده و فرمت مورد انتظار بررسی کنید.
قاعدهی 4: نتایج ابزارها را پاکسازی کنید. اگر ابزاری دادههای حساس بازمیگرداند (کلیدهای API، دادههای شناسایی شخصی یا خطاهای داخلی)، پیش از ارسال به مدل آنها را فیلتر کنید؛ چون مدل عین خروجی ابزار را در پاسخ خود میگنجاند.
قاعدهی 5: محدودیت نرخ درخواست برای ابزارها اعمال کنید. مدلی که در یک حلقه گیر بیفتد ممکن است صدها بار ابزارها را فراخوانی کند. حداکثری تعیین کنید (10 تا 20 فراخوانی در هر مکالمه معقول است) تا حلقههای بیپایان متوقف شوند.
مدیریت خطا
ابزارها با خطا مواجه میشوند، APIها تایماوت میدهند، پایگاههای داده از دسترس خارج میشوند و فایلها ممکن است پیدا نشوند. مدل باید بداند ابزار چه زمانی و به چه دلیلی شکست خورده است.
خطاها را به شکل نتایج ساختاریافتهی ابزار برگردانید، نه استثناهای برنامهنویسی (Exception):
{
"error": true,
"message": "City 'Toky' not found. Did you mean 'Tokyo'?",
"code": "CITY_NOT_FOUND"
}
مدل این متن را میخواند، آرگومانهایش را اصلاح میکند و دوباره تلاش میکند. مدلها در اصلاح خود بر اساس پیامهای خطای ساختاریافته عالی عمل میکنند؛ اما اگر با پاسخی خالی یا یک خطای کلی مثل «مشکلی پیش آمد» مواجه شوند، نمیتوانند کار را پیش ببرند.
MCP: پروتکل بستر مدل
پروتکل MCP استاندارد باز شرکت Anthropic برای تعاملپذیری ابزارهاست. به جای اینکه هر برنامه ابزارهای اختصاصی خودش را تعریف کند، MCP پروتکلی جامع ارائه میدهد: سرورهای MCP ابزارها را ارائه میکنند و کلاینتهای MCP (مانند Claude Code، Cursor یا برنامهی شما) آنها را مصرف میکنند.
یک سرور MCP میتواند ابزارهایش را در اختیار هر کلاینت سازگاری بگذارد. یک سرور MCP مخصوص Postgres دسترسی به پایگاه داده را برای هر ایجنت سازگار فراهم میکند. یک سرور MCP برای GitHub امکان تعامل با مخزن را مهیا میسازد. ابزارها یکبار تعریف میشوند و همهجا به کار میروند.
نسبت MCP به فراخوانی تابع، مثل نسبت HTTP به شبکهسازی است؛ لایهی انتقال را استاندارد میکند تا ابزارها قابلحمل شوند.
پیادهسازی گامبهگام
گام 1: تعریف رجیستری ابزارها
سیستم ثبتی بسازید که تعاریف ابزارها و پیادهسازی آنها را نگهداری کند. هر ابزار یک تعریف JSON Schema (چیزی که مدل میبیند) و یک تابع پایتون (کدی که سیستم شما اجرا میکند) دارد.
import ast
import json
import math
import time
import hashlib
TOOL_REGISTRY = {}
def register_tool(name, description, parameters, function):
TOOL_REGISTRY[name] = {
"definition": {
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": parameters,
},
},
"function": function,
}
گام 2: پیادهسازی 5 ابزار
یک ماشینحساب، ابزار بررسی آبوهوا، شبیهساز جستوجوی وب، فایلخوان و اجراکنندهی کد بسازید.
def calculator(expression, precision=2):
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return {"error": True, "message": f"Invalid characters in expression: {expression}"}
try:
result = eval(expression, {"__builtins__": {}}, {"math": math})
return {"result": round(float(result), precision), "expression": expression}
except Exception as e:
return {"error": True, "message": str(e)}
WEATHER_DB = {
"tokyo": {"temp_c": 18, "condition": "cloudy", "humidity": 72, "wind_kph": 14},
"new york": {"temp_c": 22, "condition": "sunny", "humidity": 45, "wind_kph": 8},
"london": {"temp_c": 12, "condition": "rainy", "humidity": 88, "wind_kph": 22},
"san francisco": {"temp_c": 16, "condition": "foggy", "humidity": 80, "wind_kph": 18},
"sydney": {"temp_c": 25, "condition": "sunny", "humidity": 55, "wind_kph": 10},
}
def get_weather(city, units="celsius"):
key = city.lower().strip()
if key not in WEATHER_DB:
suggestions = [c for c in WEATHER_DB if c.startswith(key[:3])]
return {
"error": True,
"message": f"City '{city}' not found.",
"suggestions": suggestions,
"code": "CITY_NOT_FOUND",
}
data = WEATHER_DB[key].copy()
if units == "fahrenheit":
data["temp_f"] = round(data["temp_c"] * 9 / 5 + 32, 1)
del data["temp_c"]
data["city"] = city
return data
SEARCH_DB = {
"python function calling": [
{"title": "OpenAI Function Calling Guide", "url": "https://platform.openai.com/docs/guides/function-calling", "snippet": "Learn how to connect LLMs to external tools."},
{"title": "Anthropic Tool Use", "url": "https://docs.anthropic.com/en/docs/tool-use", "snippet": "Claude can interact with external tools and APIs."},
],
"MCP protocol": [
{"title": "Model Context Protocol", "url": "https://modelcontextprotocol.io", "snippet": "An open standard for connecting AI models to data sources."},
],
"weather API": [
{"title": "OpenWeatherMap API", "url": "https://openweathermap.org/api", "snippet": "Free weather API with current, forecast, and historical data."},
],
}
def web_search(query, max_results=3):
key = query.lower().strip()
for db_key, results in SEARCH_DB.items():
if db_key in key or key in db_key:
return {"query": query, "results": results[:max_results], "total": len(results)}
return {"query": query, "results": [], "total": 0}
FILE_SYSTEM = {
"data/config.json": '{"model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096}',
"data/users.csv": "name,email,role\nAlice,alice@example.com,admin\nBob,bob@example.com,user",
"README.md": "# My Project\nA tool-use agent built from scratch.",
}
def read_file(path):
if ".." in path or path.startswith("/"):
return {"error": True, "message": "Path traversal not allowed.", "code": "FORBIDDEN"}
if path not in FILE_SYSTEM:
available = list(FILE_SYSTEM.keys())
return {"error": True, "message": f"File '{path}' not found.", "available_files": available, "code": "NOT_FOUND"}
content = FILE_SYSTEM[path]
return {"path": path, "content": content, "size_bytes": len(content), "lines": content.count("\n") + 1}
def run_code(code, language="python"):
if language != "python":
return {"error": True, "message": f"Language '{language}' not supported. Only 'python' is available."}
try:
tree = ast.parse(code)
except SyntaxError as e:
return {"error": True, "message": f"SyntaxError: {e}", "code": "SYNTAX_ERROR"}
unsafe_names = {"exec", "eval", "compile", "__import__", "open", "globals", "locals", "vars", "getattr", "setattr", "delattr"}
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
return {"error": True, "message": "Forbidden operation: import is not allowed", "code": "SECURITY_VIOLATION"}
if isinstance(node, ast.Attribute) and node.attr.startswith("__") and node.attr.endswith("__"):
return {"error": True, "message": "Forbidden operation: dunder attribute access is not allowed", "code": "SECURITY_VIOLATION"}
if isinstance(node, ast.Name) and node.id in unsafe_names:
return {"error": True, "message": f"Forbidden operation: {node.id} is not allowed", "code": "SECURITY_VIOLATION"}
try:
local_vars = {}
exec(code, {"__builtins__": {"print": print, "range": range, "len": len, "str": str, "int": int, "float": float, "list": list, "dict": dict, "sum": sum, "min": min, "max": max, "abs": abs, "round": round, "sorted": sorted, "enumerate": enumerate, "zip": zip, "map": map, "filter": filter, "math": math}}, local_vars)
result = local_vars.get("result", None)
return {"success": True, "result": result, "variables": {k: str(v) for k, v in local_vars.items() if not k.startswith("_")}}
except Exception as e:
return {"error": True, "message": f"{type(e).__name__}: {e}"}
یک لیست سیاه مبتنی بر زیررشته، کد را صرفاً به عنوان متن میخواند؛ بنابراین هر چیزی را که با تطبیق رشتهای پیدا نشود نادیده میگیرد. تجزیهی کد به درخت نحو (Syntax tree) و پیمایش آن به گارد امنیتی اجازه میدهد مواردی مثل دستورات import، دسترسی به اتریبیوتهای داندر (زنجیرههای __class__ و __globals__ که به مفسر واقعی متصل میشوند) و نامهای درونی ناامن را بر اساس ساختارشان مسدود کند، نه املای آنها. با این حال، به این ساختار به چشم فیلتری آموزشی نگاه کنید، نه یک مرز امنیتی واقعی. هر گارد درونبرنامهای مفسر را با کدی که اجرا میکند به اشتراک میگذارد و یک فراخواننده باانگیزه همواره میتواند اشیای در دسترس را پیدا کند. سیستمهای محیط تولید، کدهای نامطمئن را در پروسهها یا کانتینرهای مجزا اجرا میکنند (مانند یک پروسهی فرعی با سلب اختیارات، gVisor، Firecracker یا محیطهای میزبانی اجرای کد)؛ جایی که در صورت فرار از محدوده، مهاجم وارد محیطی دورریختنی میشود نه سرویس اصلی شما.
گام 3: ثبت تمامی ابزارها
def register_all_tools():
register_tool(
"calculator", "Evaluate a mathematical expression. Supports +, -, *, /, parentheses, and decimals. Returns the numeric result.",
{"type": "object", "properties": {"expression": {"type": "string", "description": "Math expression, e.g. '(10 + 5) * 3'"}, "precision": {"type": "integer", "description": "Decimal places in result", "default": 2}}, "required": ["expression"]},
calculator,
)
register_tool(
"get_weather", "Get current weather for a city. Returns temperature, condition, humidity, and wind speed.",
{"type": "object", "properties": {"city": {"type": "string", "description": "City name, e.g. 'Tokyo' or 'San Francisco'"}, "units": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units, defaults to celsius"}}, "required": ["city"]},
get_weather,
)
register_tool(
"web_search", "Search the web for information. Returns a list of results with title, URL, and snippet.",
{"type": "object", "properties": {"query": {"type": "string", "description": "Search query"}, "max_results": {"type": "integer", "description": "Maximum results to return", "default": 3}}, "required": ["query"]},
web_search,
)
register_tool(
"read_file", "Read the contents of a file. Returns the file content, size, and line count.",
{"type": "object", "properties": {"path": {"type": "string", "description": "Relative file path, e.g. 'data/config.json'"}}, "required": ["path"]},
read_file,
)
register_tool(
"run_code", "Run a small Python snippet behind a static-analysis guard and a restricted interpreter. This is a teaching filter, not real isolation. Set a 'result' variable to return output.",
{"type": "object", "properties": {"code": {"type": "string", "description": "Python code to execute"}, "language": {"type": "string", "enum": ["python"], "description": "Programming language"}}, "required": ["code"]},
run_code,
)
گام 4: ساخت حلقهی فراخوانی تابع
این بخش، موتور اصلی است. تصمیمگیری مدل برای انتخاب ابزار را شبیهسازی میکند، ابزار را اجرا میکند و نتایج را دوباره به مدل بازمیگرداند.
def simulate_model_decision(user_message, tools, conversation_history):
msg = user_message.lower()
if any(word in msg for word in ["weather", "temperature", "forecast"]):
cities = []
for city in WEATHER_DB:
if city in msg:
cities.append(city)
if not cities:
for word in msg.split():
if word.capitalize() in [c.title() for c in WEATHER_DB]:
cities.append(word)
if not cities:
cities = ["tokyo"]
calls = []
for city in cities:
calls.append({"name": "get_weather", "arguments": {"city": city.title()}})
return calls
if any(word in msg for word in ["calculate", "compute", "math", "what is", "how much"]):
for token in msg.split():
if any(c in token for c in "+-*/"):
return [{"name": "calculator", "arguments": {"expression": token}}]
if "+" in msg or "-" in msg or "*" in msg or "/" in msg:
expr = "".join(c for c in msg if c in "0123456789+-*/.() ")
if expr.strip():
return [{"name": "calculator", "arguments": {"expression": expr.strip()}}]
return [{"name": "calculator", "arguments": {"expression": "0"}}]
if any(word in msg for word in ["search", "find", "look up", "google"]):
query = msg.replace("search for", "").replace("look up", "").replace("find", "").strip()
return [{"name": "web_search", "arguments": {"query": query}}]
if any(word in msg for word in ["read", "file", "open", "cat", "show"]):
for path in FILE_SYSTEM:
if path.split("/")[-1].split(".")[0] in msg:
return [{"name": "read_file", "arguments": {"path": path}}]
return [{"name": "read_file", "arguments": {"path": "README.md"}}]
if any(word in msg for word in ["run", "execute", "code", "python"]):
return [{"name": "run_code", "arguments": {"code": "result = 'Hello from the sandbox!'", "language": "python"}}]
return []
def execute_tool_call(tool_call):
name = tool_call["name"]
args = tool_call["arguments"]
if name not in TOOL_REGISTRY:
return {"error": True, "message": f"Unknown tool: {name}", "code": "UNKNOWN_TOOL"}
tool = TOOL_REGISTRY[name]
func = tool["function"]
start = time.time()
try:
result = func(**args)
except TypeError as e:
result = {"error": True, "message": f"Invalid arguments: {e}"}
elapsed_ms = round((time.time() - start) * 1000, 2)
return {"tool": name, "result": result, "execution_time_ms": elapsed_ms}
def run_function_calling_loop(user_message, max_iterations=5):
conversation = [{"role": "user", "content": user_message}]
tool_definitions = [t["definition"] for t in TOOL_REGISTRY.values()]
all_tool_results = []
for iteration in range(max_iterations):
tool_calls = simulate_model_decision(user_message, tool_definitions, conversation)
if not tool_calls:
break
results = []
for call in tool_calls:
result = execute_tool_call(call)
results.append(result)
conversation.append({"role": "assistant", "content": None, "tool_calls": tool_calls})
for result in results:
conversation.append({"role": "tool", "content": json.dumps(result["result"]), "tool_name": result["tool"]})
all_tool_results.extend(results)
break
return {"conversation": conversation, "tool_results": all_tool_results, "iterations": iteration + 1 if tool_calls else 0}
گام 5: اعتبارسنجی آرگومانها
اعتبارسنجی بسازید که قبل از اجرا، آرگومانهای فراخوانی ابزار را با JSON Schema تطبیق دهد.
def validate_tool_arguments(tool_name, arguments):
if tool_name not in TOOL_REGISTRY:
return [f"Unknown tool: {tool_name}"]
schema = TOOL_REGISTRY[tool_name]["definition"]["function"]["parameters"]
errors = []
if not isinstance(arguments, dict):
return [f"Arguments must be an object, got {type(arguments).__name__}"]
for required_field in schema.get("required", []):
if required_field not in arguments:
errors.append(f"Missing required argument: {required_field}")
properties = schema.get("properties", {})
for arg_name, arg_value in arguments.items():
if arg_name not in properties:
errors.append(f"Unknown argument: {arg_name}")
continue
prop_schema = properties[arg_name]
expected_type = prop_schema.get("type")
type_checks = {"string": str, "integer": int, "number": (int, float), "boolean": bool, "array": list, "object": dict}
if expected_type in type_checks:
if not isinstance(arg_value, type_checks[expected_type]):
errors.append(f"Argument '{arg_name}': expected {expected_type}, got {type(arg_value).__name__}")
if "enum" in prop_schema and arg_value not in prop_schema["enum"]:
errors.append(f"Argument '{arg_name}': '{arg_value}' not in {prop_schema['enum']}")
return errors
گام 6: اجرای دمو
def run_demo():
register_all_tools()
print("=" * 60)
print(" Function Calling & Tool Use Demo")
print("=" * 60)
print("\n--- Registered Tools ---")
for name, tool in TOOL_REGISTRY.items():
desc = tool["definition"]["function"]["description"][:60]
params = list(tool["definition"]["function"]["parameters"].get("properties", {}).keys())
print(f" {name}: {desc}...")
print(f" params: {params}")
print(f"\n--- Argument Validation ---")
validation_tests = [
("get_weather", {"city": "Tokyo"}, "Valid call"),
("get_weather", {}, "Missing required arg"),
("get_weather", {"city": "Tokyo", "units": "kelvin"}, "Invalid enum value"),
("calculator", {"expression": 123}, "Wrong type (int for string)"),
("unknown_tool", {"x": 1}, "Unknown tool"),
]
for tool_name, args, label in validation_tests:
errors = validate_tool_arguments(tool_name, args)
status = "VALID" if not errors else f"ERRORS: {errors}"
print(f" {label}: {status}")
print(f"\n--- Tool Execution ---")
direct_tests = [
{"name": "calculator", "arguments": {"expression": "(10 + 5) * 3 / 2"}},
{"name": "get_weather", "arguments": {"city": "Tokyo"}},
{"name": "get_weather", "arguments": {"city": "Mars"}},
{"name": "web_search", "arguments": {"query": "python function calling"}},
{"name": "read_file", "arguments": {"path": "data/config.json"}},
{"name": "read_file", "arguments": {"path": "../etc/passwd"}},
{"name": "run_code", "arguments": {"code": "result = sum(range(1, 101))"}},
{"name": "run_code", "arguments": {"code": "import os; os.system('rm -rf /')"}},
]
for call in direct_tests:
result = execute_tool_call(call)
print(f"\n {call['name']}({json.dumps(call['arguments'])})")
print(f" -> {json.dumps(result['result'], indent=None)[:100]}")
print(f" time: {result['execution_time_ms']}ms")
print(f"\n--- Full Function Calling Loop ---")
test_queries = [
"What's the weather in Tokyo?",
"Calculate (100 + 250) * 0.15",
"Search for MCP protocol",
"Read the config file",
"Run some Python code",
"Tell me a joke",
]
for query in test_queries:
print(f"\n User: {query}")
result = run_function_calling_loop(query)
if result["tool_results"]:
for tr in result["tool_results"]:
print(f" Tool: {tr['tool']} ({tr['execution_time_ms']}ms)")
print(f" Result: {json.dumps(tr['result'], indent=None)[:90]}")
else:
print(f" [No tool called -- direct response]")
print(f" Iterations: {result['iterations']}")
print(f"\n--- Parallel Tool Calls ---")
multi_city_query = "What's the weather in tokyo and london?"
print(f" User: {multi_city_query}")
result = run_function_calling_loop(multi_city_query)
print(f" Tool calls made: {len(result['tool_results'])}")
for tr in result["tool_results"]:
city = tr["result"].get("city", "unknown")
temp = tr["result"].get("temp_c", "N/A")
print(f" {city}: {temp}C, {tr['result'].get('condition', 'N/A')}")
print(f"\n--- Security Checks ---")
security_tests = [
("read_file", {"path": "../../etc/passwd"}),
("run_code", {"code": "import subprocess; subprocess.run(['ls'])"}),
("calculator", {"expression": "__import__('os').system('ls')"}),
]
for tool_name, args in security_tests:
result = execute_tool_call({"name": tool_name, "arguments": args})
blocked = result["result"].get("error", False)
print(f" {tool_name}({list(args.values())[0][:40]}): {'BLOCKED' if blocked else 'ALLOWED'}")
نحوهی استفاده
فراخوانی تابع در OpenAI
# from openai import OpenAI
#
# client = OpenAI()
#
# tools = [{
# "type": "function",
# "function": {
# "name": "get_weather",
# "description": "Get current weather for a city",
# "parameters": {
# "type": "object",
# "properties": {
# "city": {"type": "string"},
# "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
# },
# "required": ["city"]
# }
# }
# }]
#
# response = client.chat.completions.create(
# model="gpt-4o",
# messages=[{"role": "user", "content": "Weather in Tokyo?"}],
# tools=tools,
# tool_choice="auto",
# )
#
# tool_call = response.choices[0].message.tool_calls[0]
# args = json.loads(tool_call.function.arguments)
# result = get_weather(**args)
#
# final = client.chat.completions.create(
# model="gpt-4o",
# messages=[
# {"role": "user", "content": "Weather in Tokyo?"},
# response.choices[0].message,
# {"role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result)},
# ],
# )
# print(final.choices[0].message.content)
در پلتفرم OpenAI، فراخوانیهای ابزار با فرمت response.choices[0].message.tool_calls برگردانده میشوند. هر فراخوانی یک id دارد که هنگام ارسال نتیجه حتماً باید آن را بگنجانید؛ چون مدل از این شناسه برای تطبیق نتایج با فراخوانیها استفاده میکند. GPT-4o میتواند در یک پاسخ چندین فراخوانی ابزار را برگرداند — روی همهی آنها پیمایش و تکتکشان را اجرا کنید.
استفاده از ابزار در Anthropic
# import anthropic
#
# client = anthropic.Anthropic()
#
# response = client.messages.create(
# model="claude-sonnet-5",
# max_tokens=1024,
# tools=[{
# "name": "get_weather",
# "description": "Get current weather for a city",
# "input_schema": {
# "type": "object",
# "properties": {
# "city": {"type": "string"},
# "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
# },
# "required": ["city"]
# }
# }],
# messages=[{"role": "user", "content": "Weather in Tokyo?"}],
# )
#
# tool_block = next(b for b in response.content if b.type == "tool_use")
# result = get_weather(**tool_block.input)
#
# final = client.messages.create(
# model="claude-sonnet-5",
# max_tokens=1024,
# tools=[...],
# messages=[
# {"role": "user", "content": "Weather in Tokyo?"},
# {"role": "assistant", "content": response.content},
# {"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_block.id, "content": json.dumps(result)}]},
# ],
# )
مدلهای Anthropic فراخوانیهای ابزار را در قالب بلوکهای محتوایی با مشخصهی type: "tool_use" برمیگردانند. نتیجهی ابزار نیز در یک پیام کاربر با type: "tool_result" قرار میگیرد. تفاوت کلیدی را در نظر داشته باشید: Anthropic از کلید input_schema برای تعریف پارامترهای ابزار استفاده میکند، در حالی که OpenAI کلید parameters را به کار میبرد.
اتصال به MCP
# سرورهای MCP ابزارها را روی پروتکلی استاندارد ارائه میدهند.
# هر کلاینت سازگار با MCP میتواند این ابزارها را شناسایی و فراخوانی کند.
#
# مثال: اتصال به سرور MCP پُستگرس
#
# from mcp import ClientSession, StdioServerParameters
# from mcp.client.stdio import stdio_client
#
# server_params = StdioServerParameters(
# command="npx",
# args=["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
# )
#
# async with stdio_client(server_params) as (read, write):
# async with ClientSession(read, write) as session:
# await session.initialize()
# tools = await session.list_tools()
# result = await session.call_tool("query", {"sql": "SELECT count(*) FROM users"})
معماری MCP پیادهسازی ابزار را از مصرف آن تفکیک میکند. سرور Postgres با ساختار SQL آشناست و سرور GitHub از API سر درمیآورد. ایجنت شما صرفاً این ابزارها را کشف و فراخوانی میکند؛ بدون اینکه برای هر ادغام نیازی به نوشتن کدهای اختصاصی مربوط به هر ارائهدهنده داشته باشد.
تحویل نهایی
این درس دو خروجی تولید میکند:
یک قالب پرامپت قابل استفادهی مجدد با نام outputs/prompt-tool-designer.md برای طراحی تعاریف ابزارها. شرح کاری را که از ابزار میخواهید به آن بدهید تا تعریف کامل JSON Schema را همراه با توضیحات، انواع داده و محدودیتها تحویلتان دهد.
همچنین فایل outputs/skill-function-calling-patterns.md که چارچوبی تصمیمگیری برای پیادهسازی فراخوانی تابع در محیط تولید است و مباحثی چون طراحی ابزار، مدیریت خطا، امنیت و الگوهای اختصاصی هر ارائهدهنده را پوشش میدهد.
تمرینها
-
اضافه کردن ابزار ششم: کوئری پایگاه داده. یک ابزار SQL شبیهسازیشده همراه با جدولی درون حافظه (In-memory) پیادهسازی کنید. این ابزار به جای SQL خام، نام جدول و شرایط فیلتر را دریافت میکند. بررسی کنید که نام جدول حتماً در لیست سفید باشد و عملگرهای فیلتر فقط به
=,>,<,>=و<=محدود شوند. ردیفهای منطبق را در قالب JSON برگردانید. -
پیادهسازی تلاش مجدد با بازخورد خطا. وقتی فراخوانی ابزاری با شکست مواجه شد (مثلاً پیدا نشدن نام شهر)، پیام خطا را به تابع تصمیمگیری مدل بازگردانید تا آرگومانهای خود را اصلاح کند. تعداد دفعات تلاش مجدد را برای هر فراخوانی ثبت کنید. سقف 3 بار تلاش مجدد برای هر فراخوانی ابزار در نظر بگیرید.
-
ساخت ایجنت چندمرحلهای. برخی درخواستها نیازمند زنجیرهای از فراخوانی ابزارها هستند: «فایل کانفیگ رو بخون، ببین چه مدلی تنظیم شده، بعد قیمت اون مدل رو توی وب سرچ کن.» حلقهای پیاده کنید که تا زمان تشخیص مدل مبنی بر عدم نیاز به ابزارهای بیشتر ادامه یابد، و نتایج قبلی را به هر مرحلهی تصمیمگیری بفرستد. برای جلوگیری از حلقههای بیپایان، سقف 10 تکرار را اعمال کنید.
-
سنجش دقت در انتخاب ابزار. 30 پرسش آزمایشی همراه با نام ابزار مورد انتظار برای هرکدام بسازید. تابع تصمیمگیریتان را روی هر 30 پرسش اجرا کنید و بسنجید چند درصد موارد ابزار درست انتخاب شده است. مشخص کنید کدام پرسشها بیشترین سردرگمی را میان ابزارها به وجود میآورند.
-
پیادهسازی کش برای فراخوانی ابزارها. اگر ابزاری با آرگومانهای کاملاً یکسان در فاصلهی زمانی کمتر از 60 ثانیه فراخوانی شد، به جای اجرای دوباره، نتیجهی کششده را برگردانید. از دیکشنری با کلید
(tool_name, frozenset(args.items()))استفاده کنید. نرخ برخورد موفق با کش (Hit rate) را در گفتوگویی با 20 پرسش اندازه بگیرید.
اصطلاحات کلیدی
| اصطلاح | در گفتار عمومی | معنای فنی و دقیق |
|---|---|---|
| فراخوانی تابع (Function calling) | «استفاده از ابزار» | مدل یک JSON ساختاریافته تولید میکند که تابع مورد نظر و آرگومانهایش را مشخص میسازد — کد شما آن را اجرا میکند، نه خود مدل. |
| تعریف ابزار (Tool definition) | «اسکیمای تابع» | آبجکتی بر اساس JSON Schema که نام، هدف، پارامترها و انواع دادههای ابزار را مشخص میکند — مدل با خواندن آن تصمیم میگیرد چگونه و چه وقت ابزار را به کار ببرد. |
| انتخاب ابزار (Tool choice) | «حالت فراخوانی» | مشخص میکند که آیا مدل باید ابزاری را فراخوانی کند (required)، مجاز به انتخاب است (auto)، یا باید تابع خاصی را صدا بزند (named). |
| فراخوانی موازی (Parallel calling) | «چند ابزاری» | مدل در یک نوبت گفتوگو چند ابزار را فراخوانی میکند و رفتوبرگشتها را کاهش میدهد — GPT-4o و Claude هر دو از آن پشتیبانی میکنند. |
| نتیجهی ابزار (Tool result) | «خروجی تابع» | مقدار بازگشتی حاصل از اجرای ابزار که در قالب پیام به مدل فرستاده میشود تا بتواند با تکیه بر اطلاعات واقعی پاسخ دهد. |
| اعتبارسنجی آرگومان (Argument validation) | «بررسی ورودی» | اطمینان از همخوانی آرگومانهای تولیدشده توسط مدل با نوع، محدوده و محدودیتهای مورد انتظار، پیش از اجرای ابزار. |
| پروتکل MCP | «پروتکل ابزار» | پروتکل بستر مدل (Model Context Protocol) — استاندارد باز Anthropic برای ارائهی ابزارها از طریق سرورها به نحوی که هر کلاینت سازگاری بتواند آنها را بیابد و فراخوانی کند. |
| حلقهی ایجنت (Agent loop) | «حلقهی ReAct» | چرخهی تکرارشوندهی تصمیمگیری مدل، اجرای ابزار توسط کد، و بازگشت نتیجه به مدل، تا زمانی که اطلاعات کافی برای پاسخ حاصل شود. |
| آلودهسازی ابزار (Tool poisoning) | «تزریق پرامپت از طریق ابزار» | حملهای که در آن نتایج ابزار حاوی دستوراتی برای دستکاری رفتار مدل است — تمام خروجیهای ابزارها را پاکسازی کنید. |
| محدودیت نرخ درخواست (Rate limiting) | «بودجهی فراخوانی» | تعیین سقف برای تعداد فراخوانیهای ابزار در یک گفتوگو برای جلوگیری از بروز حلقههای بیپایان و هزینههای سرسامآور API. |
برای مطالعهی بیشتر
- راهنمای فراخوانی تابع در OpenAI — مرجع اصلی استفاده از ابزارها با GPT-4o شامل فراخوانی موازی، فراخوانی اجباری و آرگومانهای ساختاریافته
- راهنمای استفاده از ابزار در Anthropic — نحوهی پیادهسازی ابزارها در Claude با استفاده از input_schema، پاسخهای چندابزاری و پیکربندی tool_choice
- مشخصات فنی پروتکل بستر مدل (MCP) — استاندارد باز تعاملپذیری ابزارها میان برنامههای مختلف هوش مصنوعی با معماری سرور/کلاینت
- مقالهی شیک و همکاران، 2023: "Toolformer: Language Models Can Teach Themselves to Use Tools" — مقالهی بنیادین دربارهی آموزش مدلهای زبانی برای تصمیمگیری در خصوص زمان و نحوهی فراخوانی ابزارهای خارجی
- مقالهی پاتیل و همکاران، 2023: "Gorilla: Large Language Model Connected with Massive APIs" — فاینتیونینگ مدلهای زبانی برای فراخوانی دقیق API میان 1645 رابط برنامهنویسی همراه با کاهش توهم
- جدول ردهبندی فراخوانی تابع برکلی (BFCL) — بنچمارک لحظهای مقایسهی دقت فراخوانی تابع در مدلهای GPT-4o، Claude، Gemini و مدلهای متنباز
- یائو و همکاران: "ReAct: Synergizing Reasoning and Acting in Language Models" (ICLR 2023) — چرخهی فکر-عمل-مشاهده (Thought-Action-Observation) که حلقهی بیرونی ایجنت را پیرامون هر فراخوانی ابزار شکل میدهد؛ جایی که این درس پایان مییابد و فاز 14 آغاز میشود.
- Anthropic — ساخت ایجنتهای کارآمد (دسامبر 2024) — پنج الگوی ساختاری ترکیبی (زنجیرهسازی پرامپت، مسیریابی، موازیسازی، ارکستریتور-کارگر، ارزیاب-بهینهساز) که از همان مولفهی پایهای استفاده از ابزار ساخته شدهاند.
