Function calling

Rozmowa z modelem to fajne i przydatne narzędzie. W kilku pierwszych wpisach zwracałem uwagę na to, że przy produkcyjnych aplikacjach wystawienie pola dla użytkownika może być ryzykowne, ponieważ nie wiemy jak model zareaguje i trudno jest skutecznie bronić się przed np przejęciem modelu.

Function calling nie sprawia, że problem znika. Natomiast może niezwykle ułatwić łączenie modelu z różnymi zewnętrznymi usługami, czy po prostu w uzbrojeniu asystenta w narzędzia, które przewidzieliśmy lub których po prostu potrzebujemy.

Jak to działa? Model dostaje zapytanie od użytkownika, w formie prośby, czy polecenia, oraz listę dostępnych funkcji wraz z opisem parametrów wejściowych. W takim przypadku zamiast przygotowywać odpowiedź dla użytkownika, model zwraca informację jaką metodę należy wywołać i z jakimi parametrami. Pozostaje wywołać funkcję, a do użytkownika zwrócić wiadomość o wykonaniu zadania. Spróbujmy wykorzystac ten mechanizm robiąc coś praktycznego. Niedawno do slacka podłączyłem wirtualnego aystenta Iris. Rozwiązanie było bardzo prymitywne i podstawowe, bo jedyne co robiło to wysyłało wiadomość z minimalnym opisem roli asystenta wprost do GPT-4, a odpowiedź była wysyłana jako wiadomość na slacku. Model nie otrzymywał nawet historii wiadomości, aby zachować charakter rozmowy. Function calling nie rozwiąże tego problemu, zapewne zajmę się nim w przyszłości. Dajmy jednak Iris jakieś inne przydatne możliwości / narzędzia.

Krok 0. Założenia

Powiedzmy, że chciałbym, aby mój asystent potrafił mi podać aktualną pogodę w dowolnym mieście. Z oczywistych względów obecnie GPT nie jest w stanie odpowiedzieć na to pytanie. Ale czy na pewno? Sprawdźmy jak poradzi sobie chatGPT:

Otrzymałem odpowiedź. Trwało to bardzo długo, bo najpierw wywoływany był Bing, a dopiero potem serwis pogodowy. Jest to nowość w chatGPT, który bez konieczności używania pluginów potrafi sięgać do internetu. No to sprawdźmy, czy możemy zrobić to samodzielnie i uzbroić Iris w podobną funkcję.

Krok 1. Rozpoznanie akcji – function calling

Tym razem od razu zaczynamy od głównego tematu. Dotychczas, gdy Iris otrzymywała wiadomość od razu wywoływała gpt-4 i zwracała odpowiedź. Tym razem będzie inaczej. Pierwsze zapytanie pójdzie do gpt-3.5-turbo, w celu rozpoznania akcji do wykonania.

$response = $this->AIChat->chat(
    [
        'model' => 'gpt-3.5-turbo-1106',
        'messages' => [
            [
                'role' => 'system',
                'content' => 'Determine function to run based on question. Don\'t answer the question.'
            ],
            [
                'role' => 'user',
                'content' => $question
            ],
        ],
        'tools' => $this->chatTools->getFunctionsDescriptions()
    ]
);

return $response->choices[0]->message->toolCalls;

Zapytanie do modelu wygląda dosyć standardowo. Największą różnicą jest węzeł tools, którego nie było wcześniej. Zawiera on następujący opis dostępnych funkcji:

public function getFunctionsDescriptions(): array
{
    return [
        [
            'type' => 'function',
            'function' => [
                'name' => 'getCurrentWeather',
                'description' => 'Get current weather based on city name',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'city' => [
                            'type' => 'string',
                            'description' => 'City name eg. Warsaw, Paris, Opole'
                        ]
                    ],
                    'required' => [
                        'city'
                    ]
                ]
            ]
        ],
        [
            'type' => 'function',
            'function' => [
                'name' => 'answerFromGeneralKnowledge',
                'description' => 'Answer from general knowledge',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'question' => [
                            'type' => 'string',
                            'description' => 'question'
                        ]
                    ],
                    'required' => [
                        'question'
                    ]
                ]
            ]
        ],
    ];
}

Znajduje się opis dwóch funkcji dostępnych dla modelu. Jego zadaniem jest zwrócić informację o tym jakie funkcje może wywołać i jakie parametry te funkcje przyjmują. W odpowiedzi modelu znajdziemy specjalny węzeł toolCalls, który będzie zawierał te informacje.
Następnie wystarczy wywołać tę metodę w swoim kodzie. Oprócz generalnej funkcji do znalezienia odpowiedzi dodałem metodę do pobierania pogody.

Krok 2. Pobranie pogody

W celu pobierania pogody wykorzystam WeatherAPI. Przygotowuję prostą metodę:

$cityName = json_decode($weatherJsonCity)->city;
$url = 'https://api.weatherapi.com/v1/current.json?key=' . $this->weatherApiKey . '&q=' . $cityName . '&aqi=no';

$client = HttpClient::create();
$response = $client->request('GET', $url);
$weatherJson = $response->getContent();
$weather = json_decode($weatherJson);

$dataForModel = [
    'last_updated' => $weather->current->last_updated,
    'temp_c' => $weather->current->temp_c,
    'condition_text' => $weather->current->condition->text,
    'pressure_mb' => $weather->current->pressure_mb,
    'humidity' => $weather->current->humidity,
];

$response = $this->AIChat->chat(
    [
        'model' => 'gpt-3.5-turbo-1106',
        'messages' => [
            ['role' => 'system', 'content' => 'Prepare short information about current weather based on provided data. Message will be directly send to slack. ### Weather Data ###' . json_encode($dataForModel)],
            ['role' => 'user', 'content' => $originalQuestion],
        ],
    ]
);

return $response->choices[0]->message->content ?? "Sorry, I was unable to get the weather for $cityName.";

Metoda od razu po pobraniu danych wysyła je do modelu GPT w celu przygotowania na jej podstawie odpowiedzi dla użytkownika.

Krok 3. Testy

Zapytajmy więc Iris o pogodę.

Zadałem jej również ogólne pytanie. Jak widać model prawidłowo rozpoznaje funkcję do wykonania i reaguje na input. A co jeśli spróbujemy połączyć i jednocześnie zapytać o pogodę i wiedzę ogólną?

Co prawda model nie do końca zrozumiał moje intencje, ale jakoś z tego wybrnął (myślę, że można mu wybaczyć, bo to tylko gpt-3.5) i połączył informacje o pogodzie z wiedzą ogólną. Ale jak to się w ogóle stało? Okazuje się, że model potrafi łączyć narzędzia, które posiada i wykorzystywać je równolegle, w celu spójnego przygotowania odpowiedzi:

"toolCalls":[
    {
      "id":"call_uX1gBpVKU0bmnhP7t5ecRYkb",
      "type":"function",
      "function":{
        "name":"getCurrentWeather",
        "arguments":"{\"city\": \"Opole\"}"
      }
    },
    {
      "id":"call_01dbaIxn4EVCIVR833zFpAwC",
      "type":"function",
      "function":{
        "name":"answerFromGeneralKnowledge",
        "arguments":"{\"question\": \"What is a famous AC/DC song?\"}"
      }
    }
  ]

Gdy poznał aktualną pogodę i sławną piosenke AC/DC był już w stanie połączyć te dwie informacje i wygenerować spójną odpowiedź.