Tool calling (فراخوانی تابع)
تعریف ابزار با اسکیمای OpenAI، حلقهٔ کامل اجرای tool call در Python، کنترل با tool_choice، استریم tool_calls و انتخاب مدل با نشان tools.
بهروزرسانی: ۱۶ شهریور ۱۴۰۵
Tool calling یعنی به مدل بگویی چه توابعی در اختیار دارد، مدل تصمیم بگیرد کدام را با چه آرگومانهایی صدا بزند، تو اجرایش کنی و نتیجه را برگردانی تا پاسخ نهایی ساخته شود. یوتاپن همان اسکیمای OpenAI را عبور میدهد؛ چیزی که با gpt-5 یاد گرفتهای، با anthropic/claude-sonnet-4.5 یا google/gemini-2.5-flash هم همان است.
کدام مدلها
در صفحهٔ مدلها نشان tools را ببین. در GET /v1/models معادلش وجود "tools" در آرایهٔ supported_parameters است. بیشتر مدلهای اصلی پشتیبانی میکنند؛ اگر مدلی نمیکند، درخواست با 400 از سمت provider برمیگردد یا مدل ابزار را نادیده میگیرد. برای کار تولیدی مدلی انتخاب کن که parallel_tool_calls هم دارد.
تعریف ابزار
tools = [
{
"type": "function",
"function": {
"name": "get_order_status",
"description": "وضعیت سفارش را از دیتابیس فروشگاه میخواند.",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "شمارهٔ سفارش، مثل ORD-1042"},
},
"required": ["order_id"],
"additionalProperties": False,
},
},
}
]
description را جدی بگیر؛ مدل از روی همین تصمیم میگیرد کی ابزار را صدا بزند. توضیح فارسی مشکلی ندارد، ولی نام تابع و نام فیلدها را لاتین و snake_case نگه دار.
حلقهٔ کامل در Python
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.uttapen.ir/v1", api_key="sk-up-...")
MODEL = "openai/gpt-5-mini"
def get_order_status(order_id: str) -> dict:
# اینجا دیتابیس خودت را بخوان
return {"order_id": order_id, "status": "shipped", "eta_days": 2}
available = {"get_order_status": get_order_status}
messages = [{"role": "user", "content": "سفارش ORD-1042 من کجاست؟"}]
while True:
resp = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
msg = resp.choices[0].message
messages.append(msg) # پیام assistant با tool_calls باید در تاریخچه بماند
if not msg.tool_calls:
print(msg.content)
break
for call in msg.tool_calls:
fn = available[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, ensure_ascii=False),
})
نکتههای حلقه:
- پیام assistant که
tool_callsدارد باید عیناً به تاریخچه اضافه شود، وگرنه provider پیامtoolبعدی را رد میکند. tool_call_idهر پاسخ باید باidهمان call یکی باشد. با چند call موازی، ترتیب مهم نیست ولی شناسهها مهماند.argumentsرشتهٔ JSON است؛ همیشهjson.loadsکن و روی خطای parse آماده باش. مدلها گاهی JSON ناقص میدهند؛ در آن حالت پیامtoolبا متن خطا برگردان تا مدل دوباره تلاش کند.- یک سقف تکرار (مثلاً ۸ دور) بگذار تا مدلی که در حلقه گیر میکند کیف پولت را خالی نکند. هر دور یک درخواست کامل با تمام تاریخچه است و هزینه دارد.
tool_choice
| مقدار | رفتار |
|---|---|
"auto" (پیشفرض وقتی tools هست) | مدل خودش تصمیم میگیرد |
"none" | فقط متن؛ ابزارها را نادیده میگیرد |
"required" | حتماً حداقل یک ابزار صدا زده شود |
{"type": "function", "function": {"name": "get_order_status"}} | این ابزار مشخص را صدا بزن |
"required" و اجبار یک تابع برای مواردی خوب است که خروجی ساختیافته میخواهی و به response_format مدل اعتماد نداری؛ آرگومانهای تابع در عمل یک JSON معتبر با اسکیمای تو هستند. برای این کار خروجی ساختیافته را هم ببین.
استریم با tool_calls
در استریم، delta.tool_calls تکهتکه میرسد: اول id و name، بعد arguments بهصورت رشتههای ناقص که باید به هم بچسبانی. finish_reason در چانک آخر tool_calls است.
stream = client.chat.completions.create(model=MODEL, messages=messages, tools=tools, stream=True)
calls = {}
for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta
for tc in delta.tool_calls or []:
slot = calls.setdefault(tc.index, {"id": None, "name": "", "arguments": ""})
if tc.id:
slot["id"] = tc.id
if tc.function and tc.function.name:
slot["name"] = tc.function.name
if tc.function and tc.function.arguments:
slot["arguments"] += tc.function.arguments
if delta.content:
print(delta.content, end="", flush=True)
بعد از پایان استریم، calls را مثل حالت غیراستریم اجرا کن و پیامهای tool را برگردان. اگر این جمعکردن دستی را نمیخواهی، در Node و Go accumulator داخلی SDK همین کار را میکند (SDK Go).
هزینه
هر دور حلقه یک درخواست است و تعریف ابزارها هر بار جزو prompt شمرده میشود. با ۱۰ ابزار پرتوضیح، هر دور چند صد توکن ورودی اضافه داری. راههای کمکردن هزینه: ابزارها را بر اساس زمینهٔ گفتگو فیلتر کن، توضیحها را کوتاه و دقیق بنویس، و از مدلهای سبک برای دورهای تصمیمگیری و مدل قوی فقط برای پاسخ نهایی استفاده کن. هدر X-Uttapen-Cost-Toman هر دور را جمع بزن تا هزینهٔ یک مکالمهٔ کامل را بدانی.