Co function calling naprawdę robi w LLM
Zapytaj model “jaka jest teraz pogoda w Austin” bez żadnego połączenia ze światem zewnętrznym, a masz dwie opcje: powie, że nie wie, albo coś zmyśli. Function calling daje mu trzecią opcję. Model opisuje, czego potrzebuje, przekazuje ten opis twojemu kodowi i czeka na prawdziwą odpowiedź, zanim sam odpowie.
Nazwa jest trochę myląca. Model niczego nie wywołuje. Zwraca blok ustrukturyzowanych danych mówiący, jaki tool chce użyć i z jakimi argumentami, a twoja aplikacja decyduje, czy go uruchomić i co zrobić z wynikiem. “Tool use” to dokładniejszy termin, ten używany w dokumentacji Anthropic; OpenAI i Google zwykle mówią function calling. Mechanizm jest ten sam pod obiema nazwami. Model tworzy tylko żądanie, nigdy sam efekt.
Jak model prosi o tool, ale nigdy go sam nie uruchamia
Wyobraź sobie klienta przy kontuarze, który może wskazać pozycje w menu i powiedzieć, ile ich chce, ale nigdy nie wchodzi za kontuar. Model jest tym klientem: czyta menu, które mu dałeś (definicje tool), wybiera pozycję i mówi, ile sztuk chce (argumenty). Twój kod jest osobą za kontuarem, która realizuje zamówienie i może odmówić tego, które nie ma sensu.
Ten podział to cały model bezpieczeństwa. Wywołanie tool to propozycja, którą twój kod może zwalidować albo odrzucić, zupełnie jak każde inne dane wejściowe z niezaufanego źródła, bo właśnie tym jest. Model, w który wstrzyknięto prompt z wrogiej strony internetowej, wciąż może jedynie poprosić o wywołanie tool. Nie może pominąć twojej walidacji i dotrzeć bezpośrednio do bazy danych.
Jak zdefiniować tool za pomocą JSON Schema
Każdy dostawca chce tego samego dla każdego tool: nazwy, opisu, którym model posługuje się, by zdecydować, kiedy tool ma zastosowanie, oraz JSON Schema dla argumentów. Oto tool pogodowy dla OpenAI Chat Completions API:
| |
Poświęć więcej czasu na opis niż na schemat. Model wybiera tool, porównując zapytanie użytkownika z opisami z listy, którą mu wysłałeś. Niejasny opis (“coś o pogodzie”) albo przegrywa z lepiej opisanym tool, który robi coś innego, albo uruchamia się przy zapytaniach, których nie powinien obsługiwać. Pisz go tak, jakbyś musiał wyjaśnić funkcję koledze, który przeczyta o niej dokładnie jedno zdanie.
Jak działa cykl zapytanie-odpowiedź przy function calling
Wywołanie tool wymaga co najmniej dwóch zapytań. Oto cały przebieg z API OpenAI:
| |
Pierwsza odpowiedź nie odpowiada na pytanie. Przychodzi z finish_reason
ustawionym na tool_calls i bez treści tekstowej, bo model zdecydował, że
potrzebuje get_weather, zanim powie coś użytecznego.
call.function.arguments przychodzi jako string JSON, nie jako już
sparsowany obiekt, więc json.loads nie jest opcjonalne. get_weather
uruchamiasz sam (to zwykła funkcja Python; model nie ma możliwości jej
wykonać), a potem dołączasz wynik jako wiadomość z role: "tool" i
odpowiadającym tool_call_id.
Tylko drugie zapytanie, które niesie już ten wynik, daje ci prawdziwą
odpowiedź w tekście. Jeśli dołączysz wiadomość asystenta z jej
tool_calls, ale pominiesz odpowiadający wynik tool, API odrzuci
następne zapytanie, zamiast zgadywać.
Czym tool use Anthropic różni się od function calling OpenAI
Ten sam mechanizm, inne nazwy pól. Jeśli przełączasz się między dostawcami albo wspierasz obu, właśnie tutaj znajdziesz swoje błędy:
| Koncept | OpenAI | Anthropic |
|---|---|---|
| Pole z listą tool | tools, każdy tool owinięty w {"type": "function", "function": {...}} | tools, każdy tool płasko: {"name", "description", "input_schema"} |
| Klucz schematu argumentów | parameters | input_schema |
| Żądanie modelu | tablica tool_calls w wiadomości, arguments jako string JSON | blok treści tool_use, input już jako sparsowany obiekt |
| Wysyłanie wyniku | nowa wiadomość, role: "tool", tool_call_id | nowa wiadomość użytkownika, blok treści type: "tool_result", tool_use_id |
| Sygnalizowanie nieudanego wywołania | brak dedykowanego pola: błąd wkładasz do content | is_error: true na bloku tool_result |
Pułapka tkwi w formacie argumentów. Anthropic daje ci input już
sparsowane do słownika Pythona albo obiektu JS; OpenAI daje ci string,
który musisz sam przepuścić przez json.loads. Kod napisany dla jednego
dostawcy i skierowany na drugiego importuje się bez problemu, a potem
pada na pierwszym wywołaniu tool. Jeśli wspierasz obu, napisz jedną
funkcję normalizującą, przez którą przechodzą obie ścieżki, zanim twoje
implementacje tool zobaczą argumenty.
Jak działają równoległe wywołania i wieloetapowe pętle
Obie API mogą zwrócić więcej niż jedno wywołanie tool w jednej
odpowiedzi. Poproś o pogodę w trzech miastach i dostaniesz trzy wpisy w
tool_calls, a nie trzy osobne przejścia w obie strony. Wykonaj je
(równolegle, jeśli są niezależne) i dołącz wszystkie trzy wyniki przed
kolejnym zapytaniem. Każde ID wywołania wygenerowane przez model potrzebuje
wyniku; wyślesz jeden z trzech i zapytanie się nie powiedzie.
Niektóre zadania wymagają kilku rund: znajdź użytkownika, potem jego historię zamówień, potem oznacz zaległe zamówienie. Zapisz to jako pętlę, która wciąż wywołuje model i wykonuje to, o co prosi, aż przyjdzie odpowiedź bez wywołań tool. Potem nałóż na nią limit. Nic w API nie powstrzymuje modelu przed proszeniem o ten sam tool dziewięć razy z rzędu, kiedy wyniki nie przybliżają go do odpowiedzi. Sztywny limit, powiedzmy 10 albo 20 iteracji, zmienia to w jasny błąd, a nie w rozpędzony rachunek.
Najczęstsze błędy w function calling
Traktowanie argumentów jako zaufanego wejścia. args["city"] pochodzi
z modelu, co znaczy, że pochodzi z jakiegokolwiek tekstu, który zasilał
rozmowę, w tym ze zescrapowanej strony internetowej albo dokumentu, który
model miał streścić. Zwaliduj go, zanim dotknie zapytania do bazy danych
albo komendy shella. Tool, który buduje SQL przez interpolację argumentów
modelu, ma taką samą powierzchnię ataku na wstrzyknięcie, jak taki, który
buduje SQL z formularza na stronie.
Ufanie schematowi jak gwarancji. Model dobrze trzyma się required i
enum, ale nie jest w tym idealny. Prędzej czy później przekaże "unit": "F" zamiast "fahrenheit" albo pominie wymagane pole. Waliduj i zwróć
jasny komunikat błędu jako wynik tool, zamiast się wywalić. Powiedz
modelowi, co było nie tak, a często w następnej rundzie spróbuje ponownie
z poprawionymi argumentami.
Pozwalanie modelowi samodzielnie wybierać destrukcyjne tool. Jeśli
tool usuwa dane albo wysyła e-mail, zostawienie tool_choice na
automatycznym jest złym ustawieniem domyślnym. Trzymaj destrukcyjne tool
poza listą, dopóki człowiek nie zatwierdzi konkretnej akcji, albo zrób
efekt odwracalnym (szkic zamiast wysłanego e-maila) i pozwól, by osobny
krok go zatwierdzał.
Kiedy nie używać function calling
Każde wywołanie tool to dla modelu dodatkowe przejście w obie strony, plus opóźnienie, jakie wnosi sam tool. Jeśli “tool” i tak zwracałby tylko statyczny fakt, umieść ten fakt w prompcie albo w pobranym kontekście i pomiń to przejście; właśnie do tego służy RAG. A jeśli decyzja to w rzeczywistości klasyfikacja z małym, ustalonym zbiorem wyników (przekieruj ten ticket do jednej z czterech kolejek), typowane, niegeneratywne wywołanie odpowiada szybciej i taniej niż zapytanie ogólnego modelu czatowego o to, który tool wybrać. To właśnie do tego zbudowane są modele system one.
Function calling się opłaca, gdy akcja zależy od danych w czasie działania, których model nie może mieć w prompcie, a zbiór możliwych akcji jest za otwarty, by zakodować go jako maszynę stanów. To też pętla stojąca za każdym agentem CLI do kodowania: czytaj plik, uruchom test, zdecyduj, co dalej, wywołaj kolejny tool.
Zacznij od jednego tool, zakodowanej na stałe funkcji Pythona za nim i pętli, która zatrzymuje się po jednym przejściu w obie strony. Dopracuj walidację argumentów i ścieżkę błędu, zanim dodasz drugi tool. Większość błędów w konfiguracjach z wieloma tool to pierwszy tool, który źle obsługuje argumenty, których się nie spodziewał.