Projektowanie Narzędzi deweloperskich: efektywne wykorzystanie tokenów w pomocy AI

Data publikacji: 30 stycznia 2026 r.

Podczas tworzenia pomocy AI w zakresie wydajności głównym wyzwaniem inżynieryjnym było sprawne działanie Gemini z zapisami wydajności zarejestrowanymi w Narzędziach deweloperskich.

Duże modele językowe (LLM) działają w ramach „okna kontekstu”, które oznacza ścisłe ograniczenie ilości informacji, jakie mogą przetwarzać jednocześnie. Ta pojemność jest mierzona w tokenach. W przypadku modeli Gemini 1 token to w przybliżeniu grupa 4 znaków.

Ślady wydajności to duże pliki JSON, które często mają kilka megabajtów. Wysłanie surowego śladu natychmiast wyczerpałoby okno kontekstu modelu i nie pozostawiłoby miejsca na Twoje pytania.

Aby umożliwić korzystanie z pomocy AI w przypadku skuteczności, musieliśmy zaprojektować system, który maksymalizuje ilość przydatnych danych dla dużego modelu językowego przy minimalnym wykorzystaniu tokenów. Na tym blogu możesz dowiedzieć się, jakich technik użyliśmy, i wykorzystać je w swoich projektach.

Dostosowywanie kontekstu początkowego

Debugowanie wydajności witryny to złożone zadanie. Deweloper może przejrzeć cały ślad, aby uzyskać kontekst, lub skupić się na podstawowych sygnałach internetowych i powiązanych z nimi przedziałach czasu w śladzie, a nawet przejść do szczegółów i skupić się na poszczególnych zdarzeniach, takich jak kliknięcia lub przewijanie, oraz powiązanych z nimi stosach wywołań.

Aby ułatwić proces debugowania, asystent AI w Narzędziach deweloperskich musi dopasowywać się do ścieżek deweloperów i korzystać tylko z odpowiednich danych, aby udzielać porad dostosowanych do ich potrzeb. Zamiast zawsze wysyłać pełny ślad, stworzyliśmy pomoc opartą na AI, która dzieli dane na podstawie zadania debugowania:

Zadanie debugowania Dane początkowo wysłane do asystenta AI
Rozmowa na czacie na temat śladu wydajności Podsumowanie śledzenia: raport tekstowy zawierający ogólne informacje ze śledzenia i sesji debugowania. Zawiera adres URL strony, warunki ograniczania przepustowości, najważniejsze dane o skuteczności (LCP, INP, CLS), listę dostępnych statystyk oraz, jeśli są dostępne, podsumowanie CrUX.
Rozpoczęcie czatu na temat statystyk dotyczących wydajności podsumowanie śledzenia i nazwę wybranej podpowiedzi dotyczącej wydajności.
Rozmawianie o zadaniu na podstawie śladu Podsumowanie śledzenia i zserializowane drzewo wywołań, w którym znajduje się wybrane zadanie.
Czat dotyczący prośby o dostęp do sieci podsumowanie śledzenia oraz wybrany klucz żądania i sygnatura czasowa.
Generowanie adnotacji śledzenia Zserializowane drzewo wywołań, w którym znajduje się wybrane zadanie. Zserializowane drzewo wskazuje, które zadanie jest wybrane.

Podsumowanie śladu jest prawie zawsze wysyłane, aby zapewnić Gemini, czyli modelowi AI, który jest podstawą pomocy AI, wstępny kontekst. W przypadku adnotacji wygenerowanych przez AI jest on pomijany.

Udostępnianie narzędzi AI

Pomoc AI w Narzędziach deweloperskich działa jak agent. Oznacza to, że może samodzielnie wysyłać zapytania o więcej danych na podstawie początkowego promptu dewelopera i początkowego kontekstu udostępnionego przez niego. Aby wysyłać zapytania o większą ilość danych, udostępniliśmy asystentowi AI zestaw predefiniowanych funkcji, które może wywoływać. Wzorzec ten jest znany jako wywoływanie funkcji lub korzystanie z narzędzi.

Na podstawie opisanych wcześniej ścieżek debugowania zdefiniowaliśmy zestaw szczegółowych funkcji agenta. Te funkcje zagłębiają się w szczegóły, które są uważane za ważne na podstawie początkowego kontekstu, podobnie jak w przypadku debugowania wydajności przez programistę. Zestaw funkcji jest następujący:

Funkcja Opis
getInsightDetails(name) Zwraca szczegółowe informacje o konkretnych statystykach wydajności (np. szczegóły dotyczące tego,dlaczego oznaczono LCP).
getEventByKey(key) Zwraca szczegółowe właściwości pojedynczego, konkretnego zdarzenia.
getMainThreadTrackSummary(start, end) Zwraca podsumowanie aktywności w głównym wątku w podanym zakresie, w tym podsumowania od góry do dołu, od dołu do góry i podsumowania firm zewnętrznych.
getNetworkTrackSummary(start, end) Zwraca podsumowanie aktywności w sieci w podanym zakresie czasu.
getDetailedCallTree(event_key) Zwraca pełne drzewo wywołań dla określonego zdarzenia głównego wątku w śladzie wydajności.
getFunctionCode(url, line, col) Zwraca kod źródłowy funkcji zdefiniowanej w określonym miejscu w zasobie, opatrzony adnotacjami z danymi o wydajności w czasie działania pochodzącymi ze śladu wydajności.
getResourceContent(url) Zwraca zawartość zasobu tekstowego używanego przez stronę (np. HTML lub CSS).

Ścisłe ograniczenie pobierania danych do tych wywołań funkcji zapewnia, że do okna kontekstowego trafiają tylko istotne informacje w dobrze zdefiniowanym formacie, co optymalizuje wykorzystanie tokenów.

Przykład działania agenta

Przyjrzyjmy się praktycznemu przykładowi, jak asystent AI wykorzystuje wywoływanie funkcji do uzyskiwania większej ilości informacji. Po początkowym promcie „Dlaczego to żądanie jest powolne?” Asystent AI może wywoływać te funkcje przyrostowo:

  1. getEventByKey: pobiera szczegółowy podział czasu (TTFB, czas pobierania) konkretnego żądania wybranego przez użytkownika.
  2. getMainThreadTrackSummary: sprawdź, czy wątek główny był zajęty (zablokowany), gdy powinno się rozpocząć żądanie.
  3. getNetworkTrackSummary: sprawdź, czy w tym samym czasie inne zasoby nie konkurowały o przepustowość.
  4. getInsightDetails: sprawdź, czy w podsumowaniu śledzenia jest już wzmianka o wskazówce dotyczącej tego żądania jako wąskiego gardła.

Łącząc wyniki tych wywołań, asystent AI może następnie podać diagnozę i zaproponować działania, np. zasugerować ulepszenia kodu za pomocą getFunctionCode lub zoptymalizować ładowanie zasobów na podstawie getResourceContent.

Jednak pobranie odpowiednich danych to tylko połowa sukcesu. Nawet w przypadku funkcji dostarczających szczegółowe dane zwracane przez nie dane mogą mieć duży rozmiar. Na przykład getDetailedCallTree może zwrócić drzewo z setkami węzłów. W standardowym formacie JSON wymagałoby to wielu znaków { i } tylko na potrzeby zagnieżdżania.

Dlatego potrzebny jest format, który jest wystarczająco gęsty, aby był wydajny pod względem tokenów, ale jednocześnie wystarczająco uporządkowany, aby LLM mógł go zrozumieć i się do niego odwoływać.

Serializowanie danych

Przyjrzyjmy się bliżej temu, jak podeszliśmy do tego wyzwania, na przykładzie drzewa wywołań, ponieważ stanowią one większość danych w śladzie wydajności. Poniższy przykład przedstawia pojedyncze zadanie w stosie wywołań w formacie JSON:

{
  "id": 2,
  "name": "animate",
  "selected": true,
  "duration": 150,
  "selfTime": 20,
  "children": [3, 5, 6, 7, 10, 11, 12]
}

Jeden ślad skuteczności może zawierać tysiące takich zdarzeń, jak pokazano na zrzucie ekranu poniżej. Każde małe kolorowe pole jest reprezentowane przez tę strukturę obiektu.

Stos wywołań w zarejestrowanym zrzucie wydajności w Narzędziach deweloperskich

Ten format jest wygodny do programowego używania w Narzędziach deweloperskich, ale jest nieefektywny w przypadku dużych modeli językowych z tych powodów:

  1. Redundant keys: ciągi znaków takie jak "duration", "selfTime""children" są powtarzane w przypadku każdego węzła w drzewie wywołań. Drzewo z 500 węzłami wysłane do modelu zużyje tokeny dla każdego z tych kluczy 500 razy.
  2. Długie listy: wypisywanie każdego identyfikatora podrzędnego osobno za pomocą children zużywa ogromną liczbę tokenów, zwłaszcza w przypadku zadań, które wywołują wiele zdarzeń podrzędnych.

Wdrożenie formatu oszczędzającego tokeny w przypadku wszystkich danych używanych w ramach pomocy AI w zakresie skuteczności było procesem etapowym.

Pierwsza iteracja

Gdy zaczęliśmy pracować nad asystą AI w zakresie wydajności, zoptymalizowaliśmy ją pod kątem szybkości dostawy. Nasze podejście do optymalizacji tokenów było proste. Usunęliśmy z oryginalnego pliku JSON nawiasy klamrowe i przecinki, co dało format podobny do tego:

allUrls = [...]

Node: 1 - update
Selected: false
Duration: 200
Self Time: 50
Children:
   2 - animate

Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children:
   3 - calculatePosition
   5 - applyStyles
   6 - applyStyles
   7 - calculateLayout
   10 - applyStyles
   11 - applyStyles
   12 - applyStyles

Node: 3 - calculatePosition
Selected: false
Duration: 15
Self Time: 2
URL: 0
Children:
   4 - getBoundingClientRect

...

Pierwsza wersja była jednak tylko nieznacznie lepsza od zwykłego formatu JSON. W dalszym ciągu zawierała ona listę elementów podrzędnych węzła z identyfikatorami i nazwami, a przed każdym wierszem dodawała powtarzające się klucze opisowe (Node:, Selected:, Duration:, …).

Optymalizacja list węzłów podrzędnych

Aby jeszcze bardziej zoptymalizować działanie, usunęliśmy nazwy elementów podrzędnych węzła (calculatePosition, applyStyles, … w poprzednim przykładzie). Ponieważ asystent AI ma dostęp do wszystkich węzłów za pomocą wywoływania funkcji, a te informacje są już w nagłówku węzła (Node: 3 - calculatePosition), nie trzeba ich powtarzać. Dzięki temu mogliśmy zredukować Children do prostej listy liczb całkowitych:

Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children: 3, 5, 6, 7, 10, 11, 12

..

Chociaż była to znaczna poprawa w porównaniu z wcześniejszymi wynikami, nadal było miejsce na dalszą optymalizację. W poprzednim przykładzie możesz zauważyć, że Children jest prawie ciągły, brakuje tylko 4, 89.

Dzieje się tak, ponieważ w pierwszej próbie użyliśmy algorytmu przeszukiwania w głąb (DFS) do serializacji danych drzewa ze śladu wydajności. W rezultacie węzły równorzędne miały niekolejne identyfikatory, co wymagało od nas indywidualnego wymieniania każdego identyfikatora.

Zauważyliśmy, że jeśli ponownie zaindeksujemy drzewo za pomocą algorytmu przeszukiwania wszerz (BFS), uzyskamy kolejne identyfikatory, co umożliwi kolejną optymalizację. Zamiast wymieniać poszczególne identyfikatory, możemy teraz reprezentować nawet setki dzieci za pomocą jednego zwartego zakresu, np. 3-9 w pierwotnym przykładzie.

Ostateczna notacja węzła ze zoptymalizowaną listą Children wygląda tak:

allUrls = [...]

Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children: 3-9

Zmniejsz liczbę kluczy

Po zoptymalizowaniu list węzłów przeszliśmy do zbędnych kluczy. Zaczęliśmy od usunięcia wszystkich kluczy z poprzedniego formatu, co dało nam ten wynik:

allUrls = [...]

2;animate;150;20;0;3-10

Chociaż jest to wydajne pod względem tokenów, musieliśmy przekazać Gemini instrukcje, jak interpretować te dane. Dlatego, gdy po raz pierwszy wysłaliśmy do Gemini drzewo połączeń, dołączyliśmy ten prompt:

...
Each call frame is presented in the following format:

'id;name;duration;selfTime;urlIndex;childRange;[S]'

Key definitions:

*   id: A unique numerical identifier for the call frame.
*   name: A concise string describing the call frame (e.g., 'Evaluate Script', 'render', 'fetchData').
*   duration: The total execution time of the call frame, including its children.
*   selfTime: The time spent directly within the call frame, excluding its children's execution.
*   urlIndex: Index referencing the "All URLs" list. Empty if no specific script URL is associated.
*   childRange: Specifies the direct children of this node using their IDs. If empty ('' or 'S' at the end), the node has no children. If a single number (e.g., '4'), the node has one child with that ID. If in the format 'firstId-lastId' (e.g., '4-5'), it indicates a consecutive range of child IDs from 'firstId' to 'lastId', inclusive.
*   S: **Optional marker.** The letter 'S' appears at the end of the line **only** for the single call frame selected by the user.

....

Opis formatu generuje koszt tokenów, ale jest to koszt statyczny, który jest ponoszony tylko raz w przypadku całej rozmowy. Koszt jest mniejszy niż oszczędności uzyskane dzięki poprzednim optymalizacjom.

Podsumowanie

Optymalizacja wykorzystania tokenów jest kluczową kwestią podczas tworzenia aplikacji z wykorzystaniem AI. Dzięki przejściu z surowego formatu JSON na specjalny format niestandardowy, ponownemu indeksowaniu drzew za pomocą przeszukiwania wszerz i używaniu wywołań narzędzi do pobierania danych na żądanie znacznie zmniejszyliśmy liczbę tokenów zużywanych przez pomoc AI w Narzędziach deweloperskich w Chrome.

Te optymalizacje były warunkiem wstępnym włączenia pomocy AI w przypadku śladów wydajności. W innym przypadku nie byłby w stanie przetworzyć tak dużej ilości danych ze względu na ograniczone okno kontekstu. Zoptymalizowany format umożliwia jednak agentowi wydajności utrzymywanie dłuższej historii rozmowy i udzielanie dokładniejszych odpowiedzi uwzględniających kontekst bez przytłaczania go przez szum.

Mamy nadzieję, że te techniki zainspirują Cię do ponownego przyjrzenia się własnym strukturom danych podczas projektowania pod kątem AI. Aby rozpocząć korzystanie z AI w aplikacjach internetowych, zapoznaj się z materiałami edukacyjnymi o AI w witrynie web.dev.