Da się sprawdzić, czy duży model językowy w ogóle wie, że MiCode istnieje — i czy powie o tym komuś, kto akurat pyta. Zbudowaliśmy do tego coś prostszego, niż to brzmi: skrypt, który raz dziennie zadaje Gemini pytania sformułowane tak, jak zadałby je realny klient, włącza wyszukiwanie z uziemieniem w Google i zapisuje, jakie źródła model przy tym zacytował. Sam pomysł jest banalny. Cała reszta tego artykułu to opis tego, jak upchnęliśmy 54 wywołania modelu w darmowym limicie, który pozwala na 20 dziennie — i dlaczego niemal każdy drobny mechanizm po drodze wziął się z konkretnej awarii, a nie z przezorności na sucho.

Monitor, który pyta AI o nas samych

Coraz więcej pytań, które kiedyś trafiłyby do wyszukiwarki, dziś trafia wprost do ChatGPT, Perplexity albo Gemini — a to, że mi-code.pl dobrze rankuje w Google, nic nie mówi o tym, czy którykolwiek z tych modeli w ogóle wie, że istniejemy. Odpowiedzią jest automatyczny monitor: codziennie, bez udziału człowieka, zadaje Gemini ustaloną listę 27 pytań — o markę, o kategorię usług, o konkretne produkty — z włączonym wyszukiwaniem Google jako uziemieniem, i sprawdza, czy w odpowiedzi pojawia się mi-code.pl jako źródło. Każde pytanie jest zadawane dwukrotnie, żeby jedno przypadkowe trafienie albo jedno przypadkowe pominięcie nie decydowało samo o wyniku — to daje 54 wywołania modelu na pełny przegląd.

To tylko połowa systemu. Gemini ma darmowe API z wyszukiwaniem, więc da się to zautomatyzować; ChatGPT i Perplexity go nie mają, więc ten sam projekt utrzymuje osobny, ręczny przegląd raz w miesiącu — logowanie się do obu, zadawanie tych samych pytań z klawiatury i zapisywanie wyników z ruchu na stronie oraz z ruchu botów AI w Cloudflare. Ten artykuł jest wyłącznie o automatycznej, codziennej części — tej, która musi zmieścić się w limicie darmowego API.

Limit, który nie jest tym limitem z cennika

Pierwsza wersja projektu policzyła sobie budżet z cennika Gemini, gdzie widnieje „do 500 zapytań dziennie” — i się pomyliła. Ta liczba dotyczy narzędzia wyszukiwania, nie samych wywołań modelu. Prawdziwy limit dla generateContent na darmowym tierze jest dziesięciokrotnie niższy i API mówi to wprost, w treści własnego błędu:

Dwadzieścia wywołań modelu na dzień, na projekt, na model. Pomylenie tych dwóch liczb kosztowało nas jeden cały projekt architektury, zanim ktoś przeczytał treść błędu zamiast tabelki cennika.

54 wywołania w limicie na 20

Dwadzieścia dziennie przy 54 potrzebnych do pełnego przeglądu oznacza, że przegląd fizycznie nie mieści się w jednym dniu. Rozwiązaniem nie jest okrojenie listy pytań, tylko rozłożenie przeglądu na kilka dni. Kod trzyma dzienny budżet jako stałą ustawioną na 18, nie na 20 — to celowy margines pod samym limitem, a nie zaokrąglenie w dół. Przy budżecie 18 pełny przegląd 54 wywołań zamyka się dokładnie w trzy dni: 54 podzielone przez 18 to okrągła trójka, bez reszty i bez czwartego, niedopełnionego dnia wiszącego w powietrzu.

Zestawione jako ograniczenie naprzeciw potrzeby, ta niezgodność — i margines, który dzienny budżet zostawia nad samym twardym limitem — wygląda tak:

Ograniczenie darmowego tieruWartość
Wywołań modelu dziennie (na projekt, na model)20
Realnie wykorzystywany dzienny budżet18 (margines pod limitem)
Wywołań na pełny przegląd54 (27 promptów × 2 powtórzenia)
Dni potrzebnych na zamknięcie przeglądu3
Odstęp między wywołaniami (limit na minutę)6,5 sekundy

Kursor, który pamięta, gdzie przegląd stanął

Każdego dnia zadanie mierzy kolejny kawałek stałej listy zadań i zapisuje swoje miejsce — pozycję kursora i dotychczas zebrane próby — w jednym pliku, partial.json. Dopiero uruchomienie, które dochodzi do końca tej listy, składa wszystko w jeden komplet: zapisuje pełny wynik przeglądu do pliku z datą, przebudowuje raport i porównuje go z poprzednim przeglądem. Alert na Telegramie leci wyłącznie z tego zamykającego uruchomienia — i tylko wtedy, gdy z porównania wynika, że zbiór promptów, które faktycznie nas cytują, się zmienił. Dzień pośredni, który tylko przesuwa kursor, nie generuje żadnego powiadomienia, bo nie ma jeszcze o czym mówić.

Plik z wynikiem zamkniętego przeglądu trafia do katalogu, w którym trzymany jest jeden plik na każdy zakończony przegląd, więc historia tego, co model cytował miesiąc czy dwa temu, zostaje — a nie tylko to, co pokazuje najświeższy raport. Rytm trzech dni, i miejsce, w którym ponowna próba w środku przeglądu wraca do tej samej porcji, a nie skacze dalej, wygląda tak:

Przegląd rozłożony na trzy dni: kursor nad listą 54 zadań, zamykany trzeciego dnia

Sama lista zadań to nic więcej niż 27 promptów powtórzonych dwukrotnie, ułożonych w ustalonym porządku — każde powtórzenie danego promptu stoi bezpośrednio po jego pierwszym wystąpieniu, a nie zbite na koniec listy. Właśnie ten porządek sprawia, że krojenie na porcje jest przewidywalne: funkcja pomocnicza bierze kursor i budżet, a zwraca kolejny kawałek listy plus nowy kursor, ograniczony długością całej listy, oraz informację, czy ten nowy kursor dotarł do końca. Policzone na papierze, trzy dni wychodzą dokładnie tak, jak obiecuje stała. Dzień pierwszy zaczyna się od kursora 0, bierze pozycje od 0 do 17 i zostawia nowy kursor na 18 — jeszcze niedomknięte, bo 18 jest mniejsze niż 54. Dzień drugi zaczyna się tam, gdzie pierwszy się zatrzymał, bierze pozycje od 18 do 35 i zostawia kursor na 36 — wciąż niedomknięte. Dzień trzeci bierze ostatnią porcję, pozycje od 36 do 53, a nowy kursor trafia na 54, co nie jest mniejsze od długości listy równej 54 — więc to właśnie to uruchomienie zamyka przegląd. Nic z tego nie jest teoretyczne: plik partial.json, który ten projekt trzyma w swoim własnym repozytorium, pokazuje w tej chwili dokładnie cursor: 18, calls: 18 po jednym prawdziwym dniu pracy — czyli wynik dnia pierwszego opisany wyżej, zarejestrowany na żywo, a nie wyliczony po fakcie.

Przegląd może się też rozciągnąć na coś więcej niż te same trzy prompty trafiające dwa razy w ten sam dzień. Ponieważ powtórzenia jednego promptu nie stoją zaraz przy powtórzeniach innego promptu w sposób respektujący granice dni — lista jest ułożona według promptów, nie według dni — dwie próby tego samego promptu mogą trafić na całkowicie różne dzienne porcje, jedna w partii poniedziałkowej, druga w środowej. Etap składania, który zamyka przegląd, grupuje każdą próbę według identyfikatora promptu niezależnie od tego, którego dnia była mierzona, i zachowuje najlepszy status widziany wśród wszystkich prób, jakie ten prompt zebrał — więc cytowanie znalezione choć raz, nawet innego dnia niż jego bratnia próba, wciąż się liczy.

Weryfikacja cytowania, a nie tylko jego liczenie

Zmieszczenie 54 wywołań w limicie na 20 ma sens tylko wtedy, gdy to, co przychodzi z każdego wywołania, jest odczytywane poprawnie, a odpowiedzi z uziemieniem nie oddają czystego adresu źródła. Metadane uziemienia Google zwracają cytowania jako linki przekierowujące przez vertexaisearch.cloud.google.com — porównanie tego adresu z mi-code.pl nigdy niczego by nie znalazło, bez względu na to, ile razy naprawdę zostaliśmy zacytowani.

Rozwikłanie tego przekierowania to własny, mały łańcuch kroków, ułożony od najtańszego do najkosztowniejszego. Jeśli fragment metadanych uziemienia już niesie pole z domeną, jest ono zaufane bezpośrednio — bez dodatkowej pracy. Jeśli go nie ma, pole tytułu tego fragmentu zwykle okazuje się gołą domeną wydawcy, a nie tytułem strony — potwierdzone na prawdziwej odpowiedzi z żywego wywołania, zapisanej we własnych fixture'ach tego projektu — więc wyrażenie regularne sprawdzające „czy to wygląda jak domena” — litery, cyfry i myślniki, co najmniej jedna kropka — wystarcza, by je zaufać. Tylko gdy nie ma ani pola z domeną, ani tytułu wyglądającego jak domena, kod sięga po żądanie HEAD wysłane na sam adres cytowania i śledzi przekierowanie, by zobaczyć, gdzie ono trafia. Ten ostatni krok jest celowo ostatnim wyjściem, a nie domyślnym zachowaniem: pełny przegląd może zobaczyć kilkaset cytowań w swoich 54 wywołaniach, a zamienienie każdego z nich w osobne żądanie HTTP zwielokrotniłoby liczbę wywołań wobec limitu, który sam jest całym ograniczeniem, wokół którego ten projekt w ogóle istnieje.

Kiedy prawdziwy adres cytowania jest już znany, klasyfikacja to proste rankingowanie, a nie jedno pytanie tak-nie. Marka może pojawić się w treści odpowiedzi, nigdy nie będąc podlinkowana jako źródło — kod śledzi to osobno jako „wspomniano”, i to nigdy nie może liczyć się jako cytowanie, choćby na pierwszy rzut oka wyglądało podobnie. Tylko adres, który rozwiązuje się do własnej domeny, zdobywa status „zacytowano”; wszystko inne, co przynajmniej wymienia markę w treści odpowiedzi, to „wspomniano”; cała reszta to „brak”. Sam dopasowywanie własnych domen ma jedno celowe zabezpieczenie: kandydujący adres liczy się jako nasz tylko wtedy, gdy jest identyczny z jedną z własnych domen albo kończy się tą domeną poprzedzoną kropką — więc podobna z wyglądu rejestracja, jak noteksiegowyai.pl, nigdy nie zostanie pomylona z prawdziwą eksiegowyai.pl tylko dlatego, że ten ciąg znaków w niej się znajduje. A skoro każdy prompt jest zadawany dwukrotnie, obie próby nie są ze sobą uśredniane — wygrywa lepsza z dwóch, „zacytowano” bije „wspomniano”, „wspomniano” bije „brak” — więc jedno szczęśliwe trafienie z dwóch wciąż liczy się jako prawdziwe cytowanie, zamiast rozmyć się przez pominięcie.

Trzy rodzaje błędów, trzy różne reakcje

Nie każdy błąd zasługuje na tę samą reakcję, więc kod ich nie traktuje tak samo.

Zerwane połączenie — fetch, który rzuca wyjątkiem zamiast cokolwiek odpowiedzieć — jest ponawiane raz, po pięciu sekundach, tak jak błąd serwera. Ten konkretny przypadek istnieje, bo kiedyś się zdarzył naprawdę: jedno zerwane połączenie, które nie było wtedy ponawiane, przerwało cały przegląd w połowie i zabrało ze sobą cały dzienny limit — kolejne wywołania tego dnia po prostu nie miały już z czego korzystać.

Limit szybkości, kod 429, dostaje inne traktowanie: jedną ponowną próbę, ale dopiero po trzydziestu sekundach, nie pięciu. Limit szybkości mija z zegarem — po określonym czasie po prostu przestaje obowiązywać — podczas gdy błąd serwera mija, kiedy mu się podoba. Trzydzieści sekund to czas dobrany tak, by faktycznie wyjść poza okno limitu na minutę, zamiast oddać jedyną ponowną próbę o kilka sekund za wcześnie.

Błąd 4xx inny niż 429 nie jest ponawiany wcale. To zepsute zapytanie, które za drugim razem zawiedzie dokładnie tak samo — ponowienie nie naprawia niczego, tylko płaci za to drugim wywołaniem z tego samego dziennego budżetu.

Odstęp 6,5 sekundy, którego na początku nie było

Sam limit dzienny to nie jedyny limit, na jaki trafia seria kilkudziesięciu wywołań wystrzelonych jedno po drugim — jest jeszcze limit na minutę. Pierwsze prawdziwe uruchomienie strzeliło wywołania bez żadnej przerwy między nimi i w ciągu kilku sekund dostało 429, mimo że pojedyncze wywołanie w spoczynku przechodziło bez problemu. Odpowiedzią jest pauza 6,5 sekundy między kolejnymi wywołaniami w obrębie jednej dziennej porcji — nie przed pierwszym wywołaniem dnia, bo czekanie, zanim cokolwiek się zaczęło, tylko marnuje czas, tylko między kolejnymi.

Odcisk palca, który nie pozwala porównać połówki z całością

Lista pytań w prompts.json nie jest zamrożona — zmienia się, kiedy dochodzi nowy produkt albo artykuł, i to normalne, bo pojedynczy przegląd rozciąga się na kilka dni. Problem w tym, że przegląd zmierzony na dwóch różnych listach pytań nie znaczy nic: część pytań zostałaby policzona podwójnie, a nowe nigdy by nie zostały zadane w tym przeglądzie. Dlatego partial.json trzyma odcisk palca — skrót sha1 policzony z identyfikatorów promptów i liczby powtórzeń. Jeśli lista promptów zmieni się w trakcie przeglądu, odcisk przy kolejnym uruchomieniu się nie zgadza, niedokończony przegląd jest porzucany, a nowy zaczyna się od zera, z pustym kursorem. To jedyny sposób, żeby zmiana pytań w trakcie tygodnia nigdy nie zepsuła danych — kosztem odrzucenia częściowo zrobionej pracy, kiedy do tego dojdzie.

Lista ma też stały rdzeń: prompty o samej marce zostają w niej zawsze, właśnie dlatego, że są kanarkiem. Jeśli nawet pytanie tak proste jak „co robi MiCode” przestanie nas znajdować, oznacza to, że popsuło się indeksowanie, a nie że zabrakło treści — i żaden nowy artykuł tego nie naprawi.

Co domyka przegląd, a co go tylko chroni

Codzienne zadanie, które po cichu nic nie robi i wciąż wychodzi z zieloną fajką, jest gorsze niż takie, które hałaśliwie zawodzi — bo nikt nie patrzy na zielone. To ryzyko jest tu konkretne, nie teoretyczne: dzienny budżet jest odczytywany ze zmiennej środowiskowej, a literówka, która zamieni AI_VIS_DAILY_BUDGET w wartość nieliczbową, w momencie parsowania zamieni się w NaN. Budżet równy NaN nie zmierzyłby niczego, nie przesunąłby niczego, a skrypt i tak wyszedłby z kodem 0 — zadanie byłoby zielone każdego dnia, podczas gdy REPORT.md po cichu przestałby się aktualizować, aż w końcu ktoś zauważyłby, że daty przestały się zmieniać. Kod broni się przed tym dokładnie taką funkcją pomocniczą, która wymaga, żeby skonfigurowana wartość była liczbą całkowitą dodatnią, a jeśli nie jest, rzuca wyjątkiem — zamieniając cichy, bezwysiłkowy sukces w głośną, czerwoną awarię CI tego samego dnia, w którym się zdarzy. Nieustawiona zmienna wraca do wartości domyślnej, a nie uruchamia tego zabezpieczenia, bo Actions renderuje nieustawioną zmienną jako pusty ciąg znaków, a nie jako coś naprawdę niezdefiniowanego — a traktowanie tego jako błędnej wartości sprawiłoby, że zabezpieczenie zawodziłoby na typowym przypadku, a nie na tym zepsutym.

Warto nazwać jeszcze drugi przypadek szczególny, bo z zewnątrz wygląda identycznie jak pierwszy, a trzeba go obsłużyć odwrotnie: porcja, która wraca pusta, bo kursor już dawno minął koniec listy zadań. To nie jest błąd konfiguracji — to po prostu przegląd, który się skończył — i wciąż musi się prawidłowo zamknąć, zapisując przegląd z datą i resetując kursor, bo inaczej zadanie wyglądałoby dokładnie jak ten cichy no-op opisany wyżej, każdego dnia od tej pory. Tylko porcja, która jest pusta i jednocześnie jeszcze niedomknięta, jest traktowana jako prawdziwy przypadek „nic do zrobienia”, logowany i kończący się wcześnie, bez dotykania żadnych plików.

Zamknięcie przeglądu jest jedyną ścieżką, która wykonuje tę kosztowną, widoczną pracę: plik przeglądu z datą trafia do katalogu, który gromadzi jeden plik na każdy zakończony przegląd, REPORT.md jest przebudowywany od zera na podstawie tego nowego przeglądu, nowy przegląd jest porównywany z tym poprzednim, jaki jest zapisany, i wysyłane jest podsumowanie na Telegramie — ale zawsze niosące porównanie, nigdy w dniu, który tylko przesunął kursor. Ostatni krok resetuje partial.json do świeżego kursora na zerze na potrzeby następnego przeglądu, ale celowo przenosi ten sam odcisk palca dalej, a nie generuje nowego — bez tego kolejny dzień odczytałby częściowy wynik, który niczemu, czego oczekuje, nie odpowiada, i zaczynałby przegląd od zera każdego dnia, z kursorem na zawsze zablokowanym w pobliżu zera.

Dlaczego to wciąż Gemini 2.5 Flash

Wyszukiwanie z uziemieniem w Google jest darmowe wyłącznie na Gemini 2.5 Flash i Flash-Lite. Model z serii 3.x zacząłby naliczać opłaty za samo narzędzie wyszukiwania po pięciu tysiącach wywołań miesięcznie — a przy monitorze, który i tak żyje na granicy darmowego limitu, jeden przypadkowy update modelu zamieniłby projekt bez kosztów w projekt z rachunkiem. Dlatego domyślny model jest zaszyty w kodzie na sztywno jako gemini-2.5-flash, zamiast dryfować w stronę czegokolwiek nowszego przy pierwszej okazji.

Czego nie widzi automatyczna połowa

Gemini to nie ChatGPT, a codzienny skrypt, który rozmawia tylko z jednym modelem, nie zastąpi dwóch pozostałych. Ponieważ ani ChatGPT, ani Perplexity nie oferują porównywalnego, darmowego i automatyzowalnego API z uziemieniem, projekt trzyma w zamian drugi, ręczny przegląd raz w miesiącu — te same pytania, wpisywane ręcznie w oba interfejsy, z wynikami zapisywanymi razem z ruchem na stronie i ruchem botów AI. Ostatni zarejestrowany przegląd, przeprowadzony 12.08.2026, wykazał, że ChatGPT zacytował mi-code.pl w 0 z 6 wypróbowanych promptów, a Perplexity — w 1 z 1.

To zero mówi więcej, niż się wydaje. Dla jednego polskiego pytania o to, czy istnieje asystent AI, który łączy się z wFirma i odpowiada na pytania o VAT i PIT, ChatGPT zacytował dokładnie jedno źródło — samo wfirma.pl — i, nie znajdując tam nic o takim asystencie, oświadczył prosto z mostu, że taki produkt nie istnieje. To nie jest przegrana w rankingu i nie jest problemem jakości niczego, co MiCode opublikowało: dla tego pytania ChatGPT idzie do jednej konkretnej strony i czyta tylko to, co jest na niej. Wpis na blogu, choćby najlepiej napisany, i tak nigdy nie miał szansy zostać przeczytany dla tego pytania. Utrata cytowania, w przypadkach takich jak ten, okazuje się mniej sprawą pisania więcej, a bardziej sprawą nieobecności w tym jednym źródle, które silnik już uznał za autorytet dla właśnie tego pytania — co jest innym problemem niż ten, który ma wychwytywać automatyczny przegląd Gemini, i właśnie dlatego ręczny przegląd zostaje w projekcie, choć nie da się go zautomatyzować.

Co z tego wychodzi

Nic w tym monitorze nie jest efektowne — to codzienne zadanie, które w większości dni po prostu przesuwa kursor o kawałek i nic nikomu nie mówi. Cała robota poszła w rzeczy, które nigdy nie trafią na zrzut ekranu: retry, który wie, kiedy się poddać zamiast próbować bez sensu; pauza, która zapobiega problemowi, zanim ten w ogóle wystąpi; odcisk palca, który pilnuje, żeby przegląd rozciągnięty na kilka dni mierzył cały czas to samo; zabezpieczenie, które zamienia literówkę w głośną awarię, a nie w cichy, wieczny no-op. Właśnie to — a nie sam fakt, że coś pyta model AI raz dziennie — jest różnicą między zadaniem, które działa bez nadzoru miesiącami, a takim, które trzeba restartować ręcznie za każdym razem, gdy coś pójdzie nie tak.

Budujemy podobne rzeczy — automatyzacje LLMOps, które muszą przeżyć w realnych limitach API, a nie tylko w demie — również dla klientów. Jeśli mierzysz się z podobnym ograniczeniem, napisz do nas na development@mi-code.pl.

Najczęstsze pytania

Czym jest monitor cytowań AI w MiCode?
To codzienne, automatyczne zadanie, które zadaje Gemini (gemini-2.5-flash) 27 pytań z włączonym wyszukiwaniem Google jako uziemieniem i sprawdza, czy w odpowiedziach model cytuje mi-code.pl. Każde pytanie jest zadawane dwukrotnie, co daje 54 wywołania modelu na pełny przegląd.
Dlaczego 54 wywołania modelu nie mieszczą się w jednym dniu?
Darmowy tier Gemini pozwala na 20 wywołań generateContent dziennie na projekt i na model — API mówi to wprost w treści własnego błędu (id = GenerateRequestsPerDayPerProjectPerModel-FreeTier, value = 20). To osobny limit od „do 500 zapytań dziennie” z cennika, który dotyczy narzędzia wyszukiwania, a nie samych wywołań modelu. Kod trzyma dzienny budżet na 18, więc pełny przegląd 54 wywołań rozkłada się dokładnie na trzy dni.
Co się dzieje, gdy wywołanie do Gemini się nie powiedzie?
Zależy od typu błędu. Zerwane połączenie i błąd serwera 5xx są ponawiane raz, po pięciu sekundach. Limit szybkości, kod 429, też jest ponawiany raz, ale po trzydziestu sekundach, bo limit szybkości mija z zegarem, a błąd serwera — kiedy mu się podoba. Błąd 4xx inny niż 429 nie jest ponawiany wcale, bo to zepsute zapytanie, które zawiedzie tak samo za drugim razem.
Dlaczego edycja listy promptów w trakcie przeglądu zeruje postęp?
Bo przegląd zmierzony na dwóch różnych listach pytań nic nie znaczy — część promptów policzyłaby się podwójnie, a nowe nigdy nie zostałyby zadane w tym przeglądzie. Plik partial.json trzyma odcisk palca (sha1 z identyfikatorów promptów i liczby powtórzeń); kiedy prompts.json się zmienia, odcisk przy kolejnym uruchomieniu się nie zgadza, niedokończony przegląd jest porzucany, a nowy zaczyna się od zera.
Dlaczego monitor wciąż używa Gemini 2.5 Flash, a nie nowszego modelu?
Bo wyszukiwanie z uziemieniem w Google jest darmowe wyłącznie na Gemini 2.5 Flash i Flash-Lite. Model z serii 3.x zacząłby naliczać opłaty za samo narzędzie wyszukiwania po pięciu tysiącach wywołań miesięcznie, więc domyślny model jest zaszyty w kodzie na sztywno jako gemini-2.5-flash, zamiast dryfować w stronę czegokolwiek nowszego.