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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
weather_tool = {
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current temperature and conditions for a city.",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name, e.g. 'Austin, TX'"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
            },
            "required": ["city"],
        },
    },
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from openai import OpenAI
import json

client = OpenAI()
messages = [{"role": "user", "content": "What's the weather in Austin?"}]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=[weather_tool],
)
message = response.choices[0].message
messages.append(message)

if message.tool_calls:
    for call in message.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(args["city"], args.get("unit", "celsius"))
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

    final = client.chat.completions.create(model="gpt-4o", messages=messages)
    print(final.choices[0].message.content)

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:

KonceptOpenAIAnthropic
Pole z listą tooltools, każdy tool owinięty w {"type": "function", "function": {...}}tools, każdy tool płasko: {"name", "description", "input_schema"}
Klucz schematu argumentówparametersinput_schema
Żądanie modelutablica tool_calls w wiadomości, arguments jako string JSONblok treści tool_use, input już jako sparsowany obiekt
Wysyłanie wynikunowa wiadomość, role: "tool", tool_call_idnowa wiadomość użytkownika, blok treści type: "tool_result", tool_use_id
Sygnalizowanie nieudanego wywołaniabrak dedykowanego pola: błąd wkładasz do contentis_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ł.

Powiązane artykuły