uttapen

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 هر دور را جمع بزن تا هزینهٔ یک مکالمهٔ کامل را بدانی.