Function Calling

Function Calling: Czym jest i jak z niego korzystać?

Function Calling ("wywoływanie funkcji") to technologia umożliwiająca inteligentnym modelom językowym wykonywanie funkcji aplikacji/systemu w oparciu o analizę wprowadzonego kontekstu i danych. Pozwala to na budowanie bardziej interaktywnych, dynamicznych i praktycznych aplikacji. Dzięki integracji z funkcjami utworzonymi przez programistę, modele mogą rozwiązywać konkretne problemy, zamiast jedynie generować tekstowe odpowiedzi. Przykładowym modelem obsługującym technologię Function Calling jest Llama-3.3-70B-Instruct, który jest dostępny w usłudze Sherlock CloudFerro.

Czy wszystkie modele językowe pozwalają korzystać z Function Callingu?

Function Calling aktualnie staje się standardem w nowych modelach językowych. Trzeba jednak wziąć pod uwagę fakt, że wciąż nie wszystkie modele wspierają tą technologię.

Przykład zastosowania

Użytkownik czatu na stronie internetowej pizzerii, chce złożyć zamówienie na pizzę Margherittę. Wyraża swoją chęć na czacie, podając swoje imię, nazwisko oraz nazwę pizzy. Tradycyjny system czatowy jest w stanie odpowiedzieć użytkownikowi jedynie w formie tekstowej ("Niestety nie mam możliwości złożyć dla Ciebie tego zamówienia, jestem tu tylko po to, aby pomóc w wyborze pizzy."). Dzięki zastosowaniu Function Callingu, system czatowy przed udzieleniem odpowiedzi na czacie, złoży zamówienie dla użytkownika (zakładając, że funkcjonalność do składania zamówień będzie dostępna dla systemu czatowego, oraz będzie posiadał wszystkie wymagane dane). Dopiero w kolejnym kroku odpowie w formie tekstowej - "Twoje zamówienie zostało złożone".

Dlaczego warto używać Function Callingu?

W tradycyjnym podejściu modele językowe ograniczają się do odpowiadania tekstem. Jednak wiele aplikacji wymaga bardziej złożonych działań, takich jak:

  • Wydobywanie kluczowych informacji z bazy danych oraz ich przetwarzanie (np. wyświetlenie listy zamówień, które zostały złożone przez użytkownika w sklepie internetowym);
  • Reagowanie na potrzeby użytkownika przez wywoływanie odpowiednich funkcji aplikacji (np. złożenie zamówienia w sklepie internetowym, gdy użytkownik wyrazi taką chęć na czacie);
  • Komunikacja z zewnętrznymi API w momencie, gdy wystąpi taka potrzeba (np. pobranie danych pogodowych, w odpowiedzi na pytanie użytkownika "Jaka będzie jutro pogoda w Krakowie?").

Jak korzystać z Function Callingu we własnych aplikacjach?

Integracja Function Callingu z istniejącymi aplikacjami jest wyjątkowo prosta. Funkcjonalności, które programista chce udostępnić do ewentualnego wykorzystania przez model językowy obsługujący na przykład czat, muszą posiadać opis w odpowiedniej strukturze JSON'owej, a sam proces komunikacji z modelem językowym należy uzupełnić o obsługę wywołań funkcji aplikacji. W celu lepszego zrozumienia integracji Function Callingu z istniejącym systemem posłużymy się przykładem internetowego systemu składania zamówień na pizzę.

Wyobraźmy sobie stronę internetową pizzerii, która pozwala użytkownikom zamawiać pizzę za pomocą czatu obsługiwanego przez model językowy. Dzięki wywołaniom funkcji (Function Calling), model językowy może:

  1. Zrozumieć prośby użytkownika, takie jak "Chciałbym zamówić dwie pizze Pepperoni i jedną Margheritę".
  2. Wykonać funkcje odpowiedzialne za złożenie zamówienia i zapisać to zamówienie w systemie.
  3. Wyświetlić użytkownikowi listę jego aktualnych zamówień na żądanie, pobierając ją uprzednio z bazy danych.

Definicje funkcji

Aplikacja do składania zamówień zbudowana jest w oparciu o dwie funkcje. Jedna z nich pozwala na złożenie zamówienia (place_order), a druga na uzyskanie listy zamówień, złożonych przez danego użytkownika (get_orders).

def place_order(user, products):
    order = {
        "user": user,
        "products": products,
        "status": "accepted"
    }
    database["orders"].append(order)
    return {
        "message": f"Zamówienie zostało złożone: {products}",
        "order_id": len(database["orders"]) - 1
    }

def get_orders(user):
    user_orders = [o for o in database["orders"] if o["user"] == user]
    if not user_orders:
        return {"message": "Nie masz żadnych zamówień."}
    return {
        "orders": [
            {
                "order_id": idx,
                "products": o["products"],
                "status": o["status"]
            }
            for idx, o in enumerate(user_orders)
        ]
    }

Baza danych

Aplikacja korzysta z bazy danych, którą w tym przypadku imituje następujący obiekt:

database = {
    "orders": [
        {"user": "john_doe", "products": ["Margherita"], "status": "accepted"},
        {"user": "jane_smith", "products": ["Pepperoni", "Napoletana"], "status": "accepted"}
    ]
}

Definicje funkcji dla modelu

Istotnym krokiem jak wspomniano wcześniej, jest utworzenie opisu funkcji udostępnianych modelowi, przez stworzenie odpowiedniej struktury JSON'owej. Dzięki temu model językowy będzie rozumiał jakie funkcje są dostępne w naszym systemie, jakich wymagają parametrów i będzie mógł podjąć decyzję o ich wykorzystaniu.

tools = [
    {
        "type": "function",
        "function": {
            "name": "place_order",
            "description": "Składa zamówienie na pizzę.",
            "parameters": {
                "type": "object",
                "properties": {
                    "user": {
                        "type": "string",
                        "description": "Imię i nazwisko użytkownika składającego zamówienie.",
                    },
                    "products": {
                        "type": "array",
                        "description": "Nazwy zamówionych pizz.",
                        "items": {"type": "string"},
                    },
                },
                "required": ["user", "products"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_orders",
            "description": "Zwraca listę złożonych zamówień.",
            "parameters": {
                "type": "object",
                "properties": {
                    "user": {
                        "type": "string",
                        "description": "Imię i nazwisko użytkownika.",
                    }
                },
                "required": ["user"],
            },
        },
    },
]

Komunikacja z modelem

Kolejnym etapem jest obsługa komunikacji z modelem językowym. Na zapytanie użytkownika model ten może odpowiedzieć standardowo w postaci tekstowej, ale może także zamiast tego zlecić wykonanie jednej lub kilku funkcji. W tym drugim przypadku zamiast od razu zwracać odpowiedź do użytkownika, należy najpierw wykonać funkcje wybrane przez model, po czym dołączyć do historii czatu wartości zwracane z tych funkcji. Na sam koniec pozostaje wygenerować odpowiedź modelem językowym na czacie, do którego historii dołączono odpowiedzi z wykonanych funkcji. Cały ten proces rozłożony został poniżej na bardziej szczegółowe fragmenty.

Function Calling - Function calling
Komunikacja z modelem

Zacznijmy od utworzenia prostego skryptu łączącego się z API modelu, przy użyciu biblioteki openai. Pozwala ona nie tylko na komunikację z API udostępnianym przez OpenAI, lecz także z API udostępnianym przez innych dostawców, w tym CloudFerro w usłudze Sherlock.

import openai

client = openai.OpenAI(
    base_url="https://api-sherlock.cloudferro.com/openai/v1",
    api_key="XUznNjfktdvQpebkfnzLmvdaEpBQJ",
)

Następnie utwórzmy na potrzeby przykładu listę wiadomości w historii czatu, która będzie zawierała tylko jedną wiadomość, w której użytkownik wyraża chęć złożenia zamówienia na pizzę.

chat_history = [
    {"role": "user", "content": "Chciałbym złożyć zamówienie na pizzę Margherittę na nazwisko Jan Nowak."}
]

Wygenerujmy teraz odpowiedź na tą historię czatu. Ze względu na to, że użytkownik wprost prosi o złożenie zamówienia w systemie, odpowiedzią nie będzie tekst, a właśnie zlecenie wywołania funkcji.

completion = client.chat.completions.create(
    model="Llama-3.3-70B-Instruct",
    messages=chat_history,
    tools=tools,
)

W odpowiedzi otrzymujemy obiekt zawierający następujące, szczególnie istotne dane:

  • completion.choices[0].message.content - tekst odpowiedzi modelu (przyjmuje wartość pustego stringa, w przypadku gdy model zleca Function Calling);
  • completion.choices[0].message.tool_calls - lista funkcji do wykonania (jest pusta w przypadku standardowej odpowiedzi).

Odpowiedź (completion) zawiera także wiele informacji, które wykraczają poza tematykę tego artykułu. Pełny opis obiektu zwracanego przez client.chat.completions.create znajduje się w dokumentacji biblioteki openai.

Standardowo odpowiedź modelu powinno się umieścić w historii czatu.

chat_history.append(completion.choices[0].message)

W naszym przypadku, gdy użytkownik prosi o złożenie zamówienia na pizzę, model oczywiście powinien skorzystać z funkcji place_order. Żeby zweryfikować, czy rzeczywiście tak się dzieje, powinno się sprawdzić czy completion zawiera listę completion.choices[0].message.tool_calls. Jeśli tool_calls przyjmuje wartość None oznacza to, że model nie zlecił wykonania żadnej z funkcji. W przeciwnym wypadku uzyskujemy listę funkcji do wywołania, która w naszym przykładzie wygląda następująco:

[
    ChatCompletionMessageToolCall(
        id="RpElTEjLK",
        function=Function(
            arguments='{"products": ["Margheritta"], "user": "Jan Nowak"}',
            name="place_order",
        ),
        type=None,
        index=0,
    )
]

Kolejnym krokiem jest wywołanie funkcji z powyższej listy. Domyślnie - powinno to się odbyć automatycznie, za co odpowiedzialny jest programista systemu, w którym osadzamy funkcjonalność Function Callingu. Jednak na potrzeby przykładu pominiemy ten etap, ponieważ nie jest on związany bezpośrednio z Function Callingiem, a z logiką działania systemu. Przechodzimy zatem bezpośrednio do wywołania funkcji place_order, która jako jedyna znalazłą się na liście w odpowiedzi modelu.

import json

tool_call = completion.choices[0].message.tool_calls[0]
arguments = json.loads(tool_call.function.arguments)
function_call_result = str(
    place_order(user=arguments["user"], products=arguments["products"]),
)

Należy pamiętać o tym, żeby wartość zwracana z funkcji była przekonwertowana do typu string, ponieważ wartość ta zostanie wprowadzona ponownie do historii czatu, która jest sekwencją danych tekstowych.

Wartość zwrócona z funkcji place_order: {'message': "Zamówienie zostało złożone: ['Margheritta']", 'order_id': 2}

Wynik działania funkcji zostaje umieszczony w histori czatu. Należy zwrócić uwagę, że w polu role powinno znaleźć się słowo tool, a nie assistant, które stosuje się tylko do tekstowej odpowiedzi z modelu. Ponadto należy dołączyć parametr tool_call_id, który stanowi referencję do odpowiedniego zlecenia wywołania funkcji z completion.choices[0].message.tool_calls.

chat_history.append(
    {
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": function_call_result,
    }
)

Finalnie, po wzbogaceniu historii chatu o zlecenie wywołania funkcji (Function Call) oraz wartości zwrócone przez funkcje, generujemy kolejną odpowiedź modelu.

completion = client.chat.completions.create(
    model="Llama-3.3-70B-Instruct",
    messages=chat_history,
    tools=tools,
)

Zwrócona odpowiedź:

Zamówienie pizzy Margheritta zostało złożone na nazwisko Jan Nowak. Identyfikator zamówienia to 2.

W tym prostym przykładzie model zlecił wywołanie tylko jednej funkcji. Może zdarzyć się jednak sytuacja, gdy lista funkcji do wykonania będzie dłuższa. Wtedy koniecznym jest wywołanie każdej z funkcji i umieszczenie w historii czatu tyle elementów, ile funkcji należało wykonać.

Przykład:

tool_calls = completion.choices[0].message.tool_calls
...
# tutaj wywołanie każdej z funkcji
...
function_call_results = [...] # lista wartości zwróconych przez każdą funkcję
for value, tool_call in zip(function_call_results, tool_calls):
    chat_history.append(
        {
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": str(value),
        }
    )

Możliwy jest przypadek, w którym parametrem jednej z funkcji, będzie wartość zwracana przez inną funkcję. Załóżmy, że istnieje funkcja pozwalająca zamienić ID użytkownika na jego imię i nazwisko.

id_to_name = {
    "0": "Jan Nowak"
}
def get_name_from_id(user_id):
    return id_to_name[user_id]

W przykładzie dotyczącym pizzerii, gdyby użytkownik nie podał imienia i nazwiska, a zamiast tego - swój identyfikator, model powinien pierwsze zlecić wykonanie jednej funkcji - get_name_from_id. A dopiero w kolejnym kroku poprosić system o wykonanie funkcji place_order. Czy tak się faktycznie stanie, zależy od jakości wykorzystanego modelu i jego umiejętności wnioskowania, dlatego istotnym jest zabezpieczyć się na wypadek niepoprawnych parametrów podanych do wywołania funkcji. W przypadku błędu podczas wykonania funkcji, dobrą praktyką jest zwracanie do modelu przejrzystego opisu, który określa dlaczego dana funkcja nie została wykonana poprawnie.

Streaming

Biblioteka openai pozwala na komunikację z API modelu językowego w dwóch trybach:

  • tryb standardowy (stream = False) - odpowiedź z API zawiera cały wygenerowany tekst;
  • tryb streamingu (stream = True) - odpowiedź z API jest streamowana w segmentach (jednym z zastosowań tego trybu jest dynamiczne wyświetlanie odpowiedzi modelu na czacie, na bieżąco token po tokenie, tak szybko jak tylko dany token zostanie wygenerowany).

Gdy odpowiedź jest streamowana w segmentach, zlecenie Function Callingu także jest dynamicznie rozdzielane na segmenty, na bieżąco podczas generowania odpowiedzi.

Przykład: API Sherlocka

chat_history = [
    {
        "role": "user",
        "content": "Chciałbym złożyć zamówienie na pizzę Margherittę na nazwisko Jan Nowak.",
    }
]
stream = client.chat.completions.create(
    model="Llama-3.3-70B-Instruct", messages=chat_history, tools=tools, stream=True
)

for chunk in stream:
    delta = chunk.choices[0].delta
    print(delta, end="\n\n")

Wynik działania programu:

ChoiceDelta(content='', function_call=None, refusal=None, role='assistant', tool_calls=None)

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id='chatcmpl-tool-2e7340ad8926437199ebcfd64bcd8090', function=ChoiceDeltaToolCallFunction(arguments=None, name='place_order'), type='function')])

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='{"user": "', name=None), type=None)])

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='Jan', name=None), type=None)])

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments=' Now', name=None), type=None)])

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='ak"', name=None), type=None)])

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments=', "products": ["', name=None), type=None)])

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='Marg', name=None), type=None)])

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='her', name=None), type=None)])

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='itta"]}', name=None), type=None)])

ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='', name=None), type=None)])

Podsumowanie

Function Calling staje się standardem we współczesnych dużych modelach językowych (LLM). Jak pokazują powyższe przykłady, pozwala na uzyskanie efektu, który wcześniej był dużo bardziej skomplikowany do osiągnięcia i wiązał się z większym progiem błędu. Nowoczesne modele LLM cechują się wysoką skutecznością w zadaniu Function Callingu ze względu na fakt, że zostały w tym celu specjalnie wytrenowane, na podstawie odpowiednich zbiorów danych.

Wywoływanie funkcji zwiększa możliwości integracji modeli językowych z istniejącymi systemami, oraz pozwala na korzystanie z tych systemów w najbardziej naturalny sposób - przez użycie języka, którym człowiek się porozumiewa. Dzięki tym narzędziom aplikacje staną się jeszcze prostsze w użyciu. Modele językowe staną się inteligentnymi pośrednikami między użytkownikiem i systemem. Mechanizm ten znacznie uprości wykonywanie czynności w różnego rodzaju aplikacjach - od systemu zamówień pizzy, aż po załatwianie spraw urzędowych online.

Poznaj AI Sherlock

Odkryj w pełni zarządzaną platformę generatywnej sztucznej inteligencji z punktami dostępu zgodnymi z OpenAI, umożliwiającą płynną integrację zaawansowanych możliwości sztucznej inteligencji z aplikacjami. Uzyskaj dostęp do wysokowydajnych modeli językowych za pośrednictwem ujednoliconego interfejsu API, eliminując złożoność zarządzania infrastrukturą i operacji na modelach.