Tool calling w OpenAI API w Pythonie: funkcje i schematy Pydantic

Piotr Dul

27-10-2026

(aktualizacja: 27-10-2026)

Jak sprawić, by model OpenAI wywołał Twoją funkcję w Pythonie: opis narzędzia schematem z Pydantic, odczyt tool_calls, odesłanie wyniku i walidacja argumentów.

Tool calling w OpenAI API w Pythonie: funkcje i schematy Pydantic
Tool calling w OpenAI API polega na tym, że model nie wykonuje Twojej funkcji, tylko zwraca jej nazwę i argumenty w formacie JSON, a Ty wykonujesz ją w Pythonie. Opisujesz narzędzie schematem (najwygodniej wygenerowanym z modelu Pydantic), odczytujesz tool_calls z odpowiedzi i odsyłasz wynik wiadomością z rolą tool i właściwym tool_call_id. Zanim użyjesz argumentów, zwaliduj je w Pydantic, bo model potrafi podać pole o złej nazwie lub typie.

Model językowy sam z siebie wie tylko to, czego nauczył się podczas treningu. Nie sprawdzi dzisiejszej ceny w Twoim sklepie, nie zajrzy do bazy ani nie wyśle maila. Żeby to zmienić, dajesz mu narzędzia: funkcje z Twojego kodu, które model może „poprosić” o wywołanie. Ten mechanizm nazywa się tool calling (w starszej nomenklaturze OpenAI: function calling) i jest podstawą każdego agenta.

To pierwsza część cyklu o agentach w Pythonie. Zakładam, że umiesz już wywołać API OpenAI i policzyć tokeny, a w kolejnych częściach zbudujemy na tym pętlę agenta, a potem serwer i klienta MCP. Do opisu narzędzi użyjemy Pydantica, więc jeśli go jeszcze nie znasz, zajrzyj do tego artykułu przed lekturą.

Czym jest tool calling i kto naprawdę wykonuje funkcję?

Najważniejsza rzecz do zapamiętania: model niczego nie uruchamia. Gdy przekażesz mu opis narzędzi, może zamiast tekstu odpowiedzieć strukturą: „chcę wywołać funkcję get_price z argumentem product="klawiatura"”. Funkcję wykonujesz Ty, po swojej stronie, a wynik odsyłasz modelowi, który dopiero wtedy formułuje odpowiedź dla użytkownika.

Cały obieg wygląda tak:

KrokKto działaCo się dzieje
1Twój kodWysyłasz pytanie użytkownika wraz z opisem dostępnych narzędzi.
2ModelZwraca tool_calls: nazwę funkcji i argumenty jako tekst JSON.
3Twój kodParsujesz i sprawdzasz argumenty, a potem wykonujesz funkcję.
4Twój kodOdsyłasz wynik wiadomością z rolą tool.
5ModelNa podstawie wyniku pisze końcową odpowiedź.

Dzięki temu to Ty decydujesz, co wolno wykonać. Model może tylko o coś poprosić. Do tej zasady wrócimy w części o uprawnieniach i zatwierdzaniu akcji.

Jak opisać narzędzie schematem z Pydantic?

Model wybiera narzędzie na podstawie jego nazwy, opisu i schematu parametrów. Schemat to JSON Schema, a Pydantic potrafi wygenerować go z klasy, więc nie musisz pisać go ręcznie. Zdefiniujmy dwa proste narzędzia dla sklepu internetowego:

from pydantic import BaseModel, Field


class GetPrice(BaseModel):
    """Zwraca cenę produktu w złotych."""

    product: str = Field(description="Nazwa produktu, np. 'klawiatura'")


class CheckStock(BaseModel):
    """Sprawdza, ile sztuk produktu jest w magazynie."""

    product: str = Field(description="Nazwa produktu, np. 'klawiatura'")


PRICES = {"klawiatura": 249.0, "mysz": 129.0}
STOCK = {"klawiatura": 12, "mysz": 0}


def get_price(args: GetPrice) -> str:
    price = PRICES.get(args.product)
    return f"{price} zł" if price is not None else "Nie znaleziono produktu."


def check_stock(args: CheckStock) -> str:
    return f"{STOCK.get(args.product, 0)} szt."

Teraz zamieniamy klasy na definicje w formacie, którego oczekuje API. Rejestr TOOLS_MODELS łączy nazwę narzędzia z jego modelem i funkcją, dzięki czemu później nie potrzebujesz długiego if:

TOOLS_MODELS = {
    "get_price": (GetPrice, get_price),
    "check_stock": (CheckStock, check_stock),
}


def to_tool_definition(name: str, model: type[BaseModel]) -> dict:
    return {
        "type": "function",
        "function": {
            "name": name,
            "description": model.__doc__,
            "parameters": model.model_json_schema(),
        },
    }


TOOLS = [to_tool_definition(name, model) for name, (model, _) in TOOLS_MODELS.items()]

Opis (description) to nie ozdoba. To jedyna wskazówka, po której model rozpozna, kiedy dane narzędzie jest właściwe, więc pisz go tak jak krótką instrukcję dla nowego współpracownika. To samo dotyczy opisów pól w Field(description=...).

Dlaczego opis narzędzia traktować jak prompt?

Traktuj description jak instrukcję, a nie komentarz do kodu. Dopisz, kiedy narzędzia nie używać (np. „nie używaj do pytań o dostawę”) i co zwraca w przypadku braku wyniku. Dwa narzędzia o podobnych opisach to najczęstsza przyczyna wyboru złego z nich.

Jak wywołać model z narzędziami i odczytać tool_calls?

Narzędzia przekazujesz w parametrze tools. Jeśli model uzna, że potrzebuje któregoś z nich, w message.tool_calls dostaniesz listę wywołań, a message.content będzie puste:

from openai import OpenAI

client = OpenAI()

messages = [{"role": "user", "content": "Ile kosztuje klawiatura i czy jest na stanie?"}]

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=TOOLS,
)

message = response.choices[0].message

for call in message.tool_calls or []:
    print(call.id, call.function.name, call.function.arguments)

Każde wywołanie ma trzy ważne pola:

PoleCo zawiera
call.idIdentyfikator wywołania, którego użyjesz przy odsyłaniu wyniku.
call.function.nameNazwa narzędzia, np. get_price.
call.function.argumentsArgumenty jako tekst JSON, a nie słownik. Trzeba go sparsować.

Jeśli model odpowiedział bez narzędzi, tool_calls ma wartość None. Dlatego w pętli stoi or [], a w prawdziwym kodzie sprawdzasz tę sytuację osobno i zwracasz message.content.

Jak zwalidować argumenty od modelu?

Argumenty przychodzą jako tekst wygenerowany przez model. Zwykle są poprawne, ale nie ma gwarancji: pole może mieć inną nazwę, zły typ albo go brakować. Zamiast ślepo robić json.loads i przekazywać wynik do funkcji, zwaliduj go tym samym modelem Pydantic, z którego powstał schemat:

from pydantic import ValidationError


def run_tool(name: str, raw_arguments: str) -> str:
    if name not in TOOLS_MODELS:
        return f"Błąd: nie ma narzędzia {name}."

    model, function = TOOLS_MODELS[name]
    try:
        args = model.model_validate_json(raw_arguments)
    except ValidationError as error:
        return f"Błąd argumentów: {error}"

    return function(args)

Zwróć uwagę, że przy błędzie nie rzucamy wyjątku, tylko zwracamy tekst z opisem problemu. Ten tekst trafi do modelu jako wynik narzędzia, więc model zobaczy, co poszło nie tak, i może spróbować ponownie z poprawnymi argumentami. Wyjątek zatrzymałby cały program, a zwrócony błąd pozwala modelowi się poprawić. W pętli agenta wykorzystamy to w następnej części cyklu.

Czy błąd narzędzia zwracać jako wynik, czy rzucać wyjątek?

Błąd walidacji lub brak danych zwracaj modelowi jako tekst, zamiast przerywać program. Model zwykle potrafi go zinterpretować i poprawić zapytanie. Wyjątek zostaw na naprawdę niespodziewane awarie (np. brak dostępu do bazy), których model naprawić nie może.

Jak odesłać wynik narzędzia do modelu?

To miejsce, w którym najczęściej pojawia się błąd 400. Reguły są dwie i obie muszą być spełnione:

  1. W liście messages musi znaleźć się wiadomość assistant zawierająca tool_calls (czyli odpowiedź modelu, którą właśnie dostałeś).
  2. Zaraz po niej musi pojawić się osobna wiadomość z rolą tool dla każdego tool_call_id, z wynikiem w polu content jako tekstem.
messages.append(message)  # odpowiedź modelu z tool_calls

for call in message.tool_calls:
    result = run_tool(call.function.name, call.function.arguments)
    messages.append(
        {
            "role": "tool",
            "tool_call_id": call.id,
            "content": result,
        }
    )

final = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=TOOLS,
)

print(final.choices[0].message.content)

Jeśli którąś z tych zasad złamiesz, API odrzuci zapytanie komunikatem, że po wiadomości assistant z tool_calls muszą następować wiadomości tool odpowiadające na każdy tool_call_id. Najczęstsze przyczyny to:

ObjawPrzyczynaCo zrobić
Błąd 400 o brakującej odpowiedzi na tool_call_idOdesłałeś wynik tylko dla części wywołań albo pominąłeś wiadomość assistant z tool_calls.Dodaj message do messages i odpowiedz na każde wywołanie.
Błąd o niepasującym tool_call_idWpisałeś id ręcznie albo użyłeś id z innego zapytania.Zawsze bierz call.id z bieżącej odpowiedzi.
Błąd typu contentPrzekazałeś słownik, liczbę lub None.Zamień wynik na tekst, np. json.dumps(wynik, ensure_ascii=False).
Model powtarza to samo wywołanieW historii brakuje wyniku poprzedniego wywołania.Sprawdź, czy wiadomość tool trafiła do messages przed kolejnym zapytaniem.

Pamiętaj też, że API nie ma pamięci. Wiadomości assistant i tool dopisujesz do tej samej listy, którą wysyłasz za każdym razem, więc historia rośnie i zjada okno kontekstu, o czym była mowa w artykule o tokenach i kosztach.

Co się dzieje, gdy model wywoła kilka narzędzi naraz?

W przykładzie z pytaniem „Ile kosztuje klawiatura i czy jest na stanie?” model może poprosić o get_price i check_stock w jednej odpowiedzi. Domyślnie API na to pozwala, więc tool_calls zawiera wtedy dwa elementy i musisz odpowiedzieć na oba, każdy własnym tool_call_id. Pętla for z poprzedniej sekcji już to robi.

Jeśli Twoje narzędzia nie mogą działać równolegle, na przykład zapisują do tego samego pliku, wyłącz to parametrem parallel_tool_calls=False. Wtedy model zgłosi najwyżej jedno wywołanie na odpowiedź.

Jak wymusić albo zablokować użycie narzędzia?

Domyślnie model sam decyduje, czy narzędzie jest potrzebne. Parametr tool_choice pozwala to zmienić:

Wartość tool_choiceEfekt
"auto" (domyślnie)Model sam wybiera: odpowiada tekstem albo wywołuje narzędzie.
"none"Model nie wywoła żadnego narzędzia, nawet jeśli je przekazałeś.
"required"Model musi wywołać co najmniej jedno narzędzie.
{"type": "function", "function": {"name": "get_price"}}Model musi wywołać dokładnie to narzędzie.

"none" przydaje się na przykład w ostatnim kroku rozmowy, gdy chcesz już tylko tekstowej odpowiedzi. Wymuszanie konkretnego narzędzia sprawdza się, gdy używasz tool callingu do wyciągania danych o znanej strukturze, a nie do prowadzenia rozmowy.

Dlaczego model czasem nie wywołuje mojego narzędzia?

Najczęściej winny jest opis, a nie kod. Sprawdź kolejno:

  • Czy opis mówi, do czego służy narzędzie? „Pobiera dane” nic nie znaczy. „Zwraca cenę produktu w złotych na podstawie jego nazwy” już tak.
  • Czy opisy pól są konkretne? Przykład wartości (np. 'klawiatura') pomaga bardziej niż samo string.
  • Czy narzędzi nie jest za dużo? Im dłuższa lista, tym łatwiej o pomyłkę. Przekazuj tylko te, które mają sens w danym kroku.
  • Czy pytanie rzeczywiście wymaga narzędzia? Jeśli model zna odpowiedź z treningu, może jej udzielić bez wywołania. Wtedy pomaga tool_choice="required" albo wskazówka w wiadomości system.

Warto też wiedzieć, że OpenAI oferuje nowsze Responses API, które ma analogiczny mechanizm z innymi nazwami pól. W tym cyklu trzymamy się chat.completions, spójnie z poprzednimi artykułami, bo zasady działania są te same.

Co dalej?

Masz już jeden obieg: pytanie, wywołanie narzędzia, wynik, odpowiedź. Agent to ten sam mechanizm powtarzany w pętli, aż model uzna, że ma wystarczająco dużo informacji. W kolejnej części napiszemy pętlę agenta w Pythonie od zera, razem z limitem kroków i kosztów, który chroni przed nieskończonym wywoływaniem narzędzi. Jeśli chcesz mieć kod, który łatwo przetestować bez prawdziwego API, zobacz też testowanie kodu z API LLM, a o walidacji danych w aplikacjach webowych przeczytasz w artykule o Pydantic w FastAPI.

Konsultacja i warsztaty devmentor.pl

Masz pytania?

Partnerzy

Firmy, z którymi współpracujemy

To partnerzy odpowiedzialni za staże po naszych kursach. Organizują je dla najlepszych absolwentów.

Zobacz wszystkich partnerów →
  • VR Medical
  • Sophona
  • Robot Force