Przejdź do treści
Blog

Docstring to interfejs, a nie komentarz: co ADK naprawdę wysyła do modelu

Sprawdziłem w ADK 2.9.0, co trafia do modelu z definicji narzędzia. Cały docstring idzie jako jeden opis, opis z Field znika, a typ zwracany widzi tylko Vertex AI. Pokazuję, jak pisać pod to.

Przemysław Zagórski5 min czytania

W tekście otwierającym serię o ADK napisałem, że docstring narzędzia jest specyfikacją, na podstawie której model decyduje, czy i kiedy wywołać funkcję. To zdanie łatwo przyjąć i trudno zastosować, dopóki nie zobaczy się, co model dokładnie dostaje.

Sprawdziłem to w kodzie. Wziąłem ADK w wersji 2.9.0, zbudowałem kilka wariantów tej samej funkcji i wypisałem deklarację, którą framework przekazuje modelowi. W trzech miejscach wynik jest inny, niż podpowiada intuicja programisty Pythona.

Co model dostaje z definicji narzędzia

Dokumentacja ADK mówi to jednym zdaniem: docstring funkcji służy jako opis narzędzia i jest wysyłany do modelu. Z sygnatury framework buduje schemat parametrów, czyli nazwy, typy, wartości domyślne i listę wymaganych. Parametr jest wymagany, jeśli ma adnotację typu i nie ma wartości domyślnej.

Przykład, od którego zaczyna prawie każdy:

def sprawdz_stan(sku: str, magazyn: Literal["WAW", "KRK"] = "WAW") -> dict:
    """Sprawdza stan magazynowy."""

Z tej funkcji model dostaje taką deklarację (pominąłem dwa pola techniczne ze schematu):

{
  "name": "sprawdz_stan",
  "description": "Sprawdza stan magazynowy.",
  "parameters_json_schema": {
    "properties": {
      "sku": { "title": "Sku", "type": "string" },
      "magazyn": {
        "default": "WAW",
        "enum": ["WAW", "KRK"],
        "title": "Magazyn",
        "type": "string"
      }
    },
    "required": ["sku"]
  }
}

Spójrz na to z perspektywy modelu. Wie, że istnieje funkcja, która „sprawdza stan magazynowy”, przyjmuje napis sku i jeden z dwóch kodów magazynu. Nie wie, czym jest sku w Twojej firmie ani w jakim formacie go podać. Nie wie, co znaczy „stan”: sztuki na półce, sztuki razem z towarem w drodze, a może status produktu w katalogu. Nie wie też, kiedy wybrać tę funkcję zamiast innej i co dostanie z powrotem.

Deklaracja to wszystko, co model wie o Twojej funkcji, a docstring jest jedyną jej częścią, którą piszesz słowami.

Jedna rzecz działa dobrze od razu. Typ Literal zamienia się w listę dozwolonych wartości, a wartość domyślna trafia do schematu. Opłaca się z tego korzystać, zamiast wymieniać dozwolone wartości w opisie.

Trzy rzeczy, które zaskakują

Cały docstring idzie jako jeden tekst. Jeśli piszesz w stylu Google, z sekcjami Args: i Returns:, ADK nie rozbiera ich na opisy poszczególnych parametrów. Wysyła cały docstring, łącznie z nagłówkami sekcji i wcięciami, jako opis funkcji, a schemat parametrów zostaje bez opisów. Model przeczyta sekcję Args:, ale tylko jako fragment tekstu, a nie jako strukturę przypiętą do parametru.

Opis w Field znika. Programiści znający Pydantic odruchowo piszą Annotated[str, Field(description="...")]. W moim teście na wersji 2.9.0 ten opis nie trafił do deklaracji: schemat parametru miał tylko tytuł i typ. Kto opisuje parametry wyłącznie w ten sposób, wysyła modelowi funkcję z nieopisanymi argumentami i nie dostaje żadnego ostrzeżenia.

Typ zwracany widzi tylko Vertex AI. Adnotacja -> dict zamienia się w schemat odpowiedzi wyłącznie wtedy, gdy ADK pracuje z Vertex AI. W kodzie frameworka stoi przy tym komentarz, że schemat odpowiedzi jest dodawany tylko dla tego wariantu. Przy Gemini API model nie dowie się z deklaracji, co funkcja zwraca. Jeśli ma to znaczenie dla jego następnej decyzji, musi to przeczytać w docstringu.

Do tego rzecz oczywista, ale łatwa do przeoczenia w dużym projekcie: funkcja bez docstringa trafia do modelu bez pola opisu. Model zna tylko jej nazwę i parametry. Z dokumentacji warto jeszcze zapamiętać, że wynik, który nie jest słownikiem, ADK opakowuje w słownik z jednym kluczem result, a zalecany format to słownik z kluczem status.

Jak wygląda opis, który coś mówi

Anthropic w dokumentacji narzędzi dla Claude'a pisze, że szczegółowy opis jest zdecydowanie najważniejszym czynnikiem wpływającym na to, jak model korzysta z narzędzi. Zaleca co najmniej trzy, cztery zdania na narzędzie i wymienia, co powinny zawierać: co narzędzie robi, kiedy go używać, a kiedy nie, co znaczy każdy parametr i jakie ma ograniczenia. To dokumentacja innego dostawcy, ale mechanizm jest ten sam, bo opis to jedyny tekst, z którego model wie, do czego służy funkcja.

Ta sama funkcja napisana według tych wskazówek:

def sprawdz_stan(sku: str, magazyn: Literal["WAW", "KRK"] = "WAW") -> dict:
    """Zwraca liczbę sztuk produktu dostępnych od ręki w jednym magazynie.

    Używaj, gdy klient pyta, czy produkt jest dostępny albo ile go jest.
    Nie używaj do pytań o termin dostawy: stan nie obejmuje towaru w drodze.

    Args:
        sku: Ośmiocyfrowy kod produktu, np. "40012345". Nie nazwa produktu.
        magazyn: "WAW" (Warszawa) albo "KRK" (Kraków).

    Returns:
        {"status": "ok", "sztuki": liczba} albo {"status": "brak_sku"},
        gdy kod nie istnieje.
    """

Schemat parametrów jest identyczny jak wcześniej. Zmienił się tylko opis, który w całości, razem z sekcjami Args: i Returns:, trafia do pola description. Każde zdanie odpowiada na pytanie, które model musiałby inaczej rozstrzygnąć, zgadując:

Zdanie w opisieNa jakie pytanie odpowiada
„liczbę sztuk dostępnych od ręki w jednym magazynie”Co dokładnie znaczy „stan”
„Używaj, gdy klient pyta…”Kiedy wybrać tę funkcję
„Nie używaj do pytań o termin dostawy”Kiedy wybrać inną
„Ośmiocyfrowy kod… Nie nazwa produktu”Co wpisać w parametr
Returns: z dwoma wariantamiJak odróżnić brak towaru od błędnego kodu

Ostatni wiersz ma praktyczne skutki. Jeśli dla nieistniejącego kodu funkcja zwraca zero sztuk, model powie klientowi, że towaru nie ma. Osobny status w odpowiedzi i jego opis w docstringu pozwalają mu zamiast tego dopytać o poprawny kod.

To nie jest ta sama rada, co przy pliku instrukcji

W tekście o pliku instrukcji dla agenta radziłem pisać krócej i wycinać wszystko, co model wie bez nas. Tu radzę pisać dłużej. Kryterium jest to samo: czy model może to wiedzieć bez Ciebie? O tym, co znaczy sku w Twoim systemie i czym stan różni się od dostępności, nie może. Dlatego opisy narzędzi są w praktyce zwykle za krótkie, a pliki reguł za długie.

Dłuższy opis ma swoją cenę. Deklaracje narzędzi jadą w każdym zapytaniu, razem z instrukcją systemową, więc trzy dodatkowe zdania przy dziesięciu narzędziach to kilkaset tokenów w każdej turze. To dobry wydatek, dopóki każde zdanie odpowiada na prawdziwe pytanie. Stały początek zapytania dobrze się przy tym buforuje, o czym pisałem przy cache'u po stronie dostawcy.

Sprawdź, co naprawdę wysyłasz

Deklarację, którą dostaje model, wypiszesz kilkoma liniami:

import json
from google.adk.tools import FunctionTool

deklaracja = FunctionTool(sprawdz_stan)._get_declaration()
print(json.dumps(deklaracja.model_dump(exclude_none=True, mode="json"),
                 ensure_ascii=False, indent=2))

Metoda z podkreśleniem na początku nazwy jest wewnętrzna i może się zmienić między wersjami. Do sprawdzania wystarcza, do kodu produkcyjnego nie. Wszystko, co opisuję wyżej, sprawdzałem na wersji 2.9.0, więc po aktualizacji frameworka powtórz ten test.

Od czego zacząć

  1. Wypisz deklaracje wszystkich swoich narzędzi fragmentem z ramki wyżej i przeczytaj je tak, jakbyś nie znał kodu.
  2. Znajdź funkcje bez docstringa oraz te, w których parametry opisane są tylko w Field. Przenieś opisy do docstringa.
  3. Dopisz do każdego opisu zdanie „kiedy nie używać”, zwłaszcza tam, gdzie dwie funkcje są do siebie podobne. To tam model najczęściej wybiera źle.
  4. Opisz w docstringu, co funkcja zwraca, szczególnie przypadki błędów, jeśli nie pracujesz na Vertex AI.
  5. Sprawdź efekt na rozmowach testowych, czy model częściej sięga po właściwe narzędzie. Przypadki testowe możesz zapisać tak, jak opisałem przy testowaniu agentów.

Docstring w narzędziu agenta pełni tę samą rolę co dokumentacja publicznego API. Różnica polega na tym, że czyta go wyłącznie model, który nie zadzwoni do autora z pytaniem.

Źródła

  1. [1]Function tools: docstring jako opis narzędzia, parametry wymagane, typ zwracanyGoogle / adk.dev
  2. [2]Budowanie deklaracji funkcji: _automatic_function_calling_util.pygoogle/adk-python (GitHub)
  3. [3]google-adk: historia wydańPyPI
  4. [4]Define tools: best practices for tool definitionsAnthropic
Przemysław Zagórski

Przemysław Zagórski

Programista i architekt, 14+ lat w telco i enterprise IT. Prowadzi warsztaty z inżynierii kontekstu, GitHub Copilota, Google ADK oraz ekosystemu Gemini dla zespołów, które muszą dowozić produkcyjnie.

Jeśli widzisz te problemy u siebie, zwykle da się je rozbroić w kilka dni roboczych. Nie kolejnym narzędziem, tylko zmianą sposobu pracy zespołu. Chętnie omówię Wasz przypadek.

7 min czytania

Plik z instrukcjami dla agenta: 28 procent szybciej, ale nie mądrzej

AGENTS.md ma już pomiar w recenzowanym badaniu: agenci pracują szybciej i taniej. Za to skuteczność zostaje ta sama. Rozbieram, co z tego wynika dla treści takiego pliku i dlaczego popularne szablony szkodzą.

  • Inżynieria kontekstu
  • Agenci
  • Jakość kodu
Czytaj