W artykule o inference padło zdanie, że każdy kolejny token wymaga osobnego przejścia przez cały model. Brzmi to jak katastrofa obliczeniowa — i byłoby nią, gdyby przy każdym kroku model liczył wszystko od nowa.
Nie liczy. To jest miejsce na sprostowanie uproszczenia, które ciągnęło się przez całą serię, i przy okazji na wyjaśnienie, skąd biorą się limity długości kontekstu.
Sprostowanie do artykułu pierwszego
W pierwszym artykule napisaliśmy, że po doklejeniu tokenu cały proces zaczyna się od nowa. Na tamtym poziomie to prawda wystarczająca — pętla faktycznie się zamyka i całość wraca na wejście.
Dokładniej jest tak: wraca ciąg, ale nie wraca praca. Model nie przelicza od zera reprezentacji tokenów, które już przetworzył. Zachowuje je i przy kolejnym kroku dolicza wyłącznie to, co nowe.
Gdyby robił inaczej, wygenerowanie tysiąca tokenów oznaczałoby tysiąc pełnych przebiegów po coraz dłuższej sekwencji. Nikt by na to nie czekał.
Co dokładnie się zapamiętuje
Przypomnijmy mechanizm z artykułu o attention: każdy token pyta wszystkie pozostałe, na ile są dla niego istotne. Żeby na takie pytanie odpowiedzieć, każdy token musi mieć przygotowane dwie rzeczy — coś w rodzaju wizytówki, po której inni go rozpoznają, i treści, którą przekazuje dalej.
Te dwie rzeczy są dla danego tokenu zawsze takie same. Nie zmieniają się, gdy dopisujesz kolejne słowa na końcu, bo zależą wyłącznie od tego, co było wcześniej.
Skoro tak, nie ma powodu liczyć ich dwa razy. KV cache to po prostu ich przechowanie: raz wyliczone, leżą w pamięci i czekają, aż kolejny token o nie zapyta.
Nowy token dokłada do tej pamięci własną parę i odpytuje wszystkie poprzednie. Tyle. Zamiast kwadratu pracy przy każdym kroku — jeden przebieg dla jednego tokenu.
To pamięć podręczna zjada kartę, nie model
Tu jest fakt, który zaskakuje przy pierwszym wdrożeniu lokalnym.
Wagi modelu zajmują tyle, ile zajmują — wartość stała, znana z góry. KV cache rośnie: liniowo z długością sekwencji i liniowo z liczbą równocześnie obsługiwanych sesji.
Przy krótkich rozmowach to margines. Przy długich kontekstach i wielu użytkownikach naraz to właśnie pamięć podręczna, a nie wagi, wyczerpuje kartę. Model, który spokojnie mieści się w pamięci, przestaje się mieścić, gdy trzeba obsłużyć dwadzieścia równoległych sesji z długim kontekstem każda.
Stąd biorą się rzeczy, które z zewnątrz wyglądają na arbitralne: limity długości kontekstu, limity przepustowości, wyższe stawki za dłuższe okna, kolejkowanie przy większym obciążeniu. To nie polityka cenowa, tylko odbicie tego, ile pamięci trzeba utrzymać na jedną aktywną rozmowę.
Dwa różne koszty, których nie warto mylić
W tej serii pojawiły się dwa wzrosty i łatwo je pomieszać, więc rozdzielmy je wprost.
Koszt kwadratowy dotyczy przetwarzania promptu. Skoro każda pozycja jest porównywana z każdą, podwojenie długości wsadu oznacza czterokrotnie więcej pracy w warstwie uwagi. To jednorazowy koszt na starcie przebiegu.
Wzrost liniowy dotyczy pamięci podręcznej. Podwojenie długości sekwencji to dwa razy więcej pamięci do utrzymania przez cały czas trwania rozmowy.
Pierwszy odczujesz jako czas do pierwszego tokenu. Drugi jako limit tego, ile rozmów da się prowadzić naraz. To dlatego długi prompt bywa jednocześnie „szybki w liczeniu” i „drogi w utrzymaniu”.
Cache promptu, czyli gdzie realnie oszczędzasz
Skoro wizytówki tokenów zależą wyłącznie od tego, co stoi przed nimi, to dla stałego początku promptu są zawsze identyczne — przy każdym wywołaniu, w każdej rozmowie.
Na tym opiera się cache promptu, udostępniany dziś przez wszystkich większych dostawców. Powtarzalny prefiks — prompt systemowy, definicje narzędzi, instrukcje formatu, stała baza wiedzy — jest liczony raz i przy kolejnych wywołaniach rozliczany taniej.
Warunek jest jeden i bezwzględny: prefiks musi być bajt w bajt taki sam. Cache działa od początku sekwencji do pierwszej różnicy. Cokolwiek zmienisz na pozycji dziesiątej, unieważnia wszystko od dziesiątej dalej.
I tu jest najczęstszy błąd, jaki widuje się w kodzie agentów: znacznik czasu albo identyfikator sesji wstawiony na początku promptu systemowego. Wygląda niewinnie, bywa dodany dla logów, i skutecznie unieważnia cache przy każdym pojedynczym wywołaniu. Rachunek rośnie po cichu, bo nic się nie psuje — po prostu nigdy nie trafiasz w pamięć podręczną.
Reguła jest prosta: stałe na początku, zmienne na końcu.
Dlaczego to szczególnie dotyczy agentów
Agent jest wzorcowym przypadkiem dla tego mechanizmu, i to z dwóch powodów naraz.
Powtarza ten sam prefiks przy każdym kroku pętli. Prompt systemowy i definicje narzędzi idą do modelu tyle razy, ile obrotów wykona agent. Bez cache płacisz za nie za każdym razem od nowa.
Jego kontekst rośnie w trakcie pracy. Każdy wynik narzędzia dokłada się do historii, więc dziesiąty krok operuje na znacznie dłuższej sekwencji niż pierwszy. Koszt jednego zadania rośnie szybciej niż liczba kroków.
Z tego wynika też, dlaczego kolejność budowania kontekstu przestaje być obojętna. Historia kroków powinna narastać na końcu, a nie być wplatana między stałe elementy — inaczej każdy nowy krok unieważnia cache dla wszystkiego, co po nim następuje.
Warto to zestawić z dwoma tekstami z serii o context engineeringu, bo domykają obraz z drugiej strony: okno kontekstu i context rot tłumaczy, dlaczego dłuższy kontekst bywa też gorszy jakościowo, a kompaktowanie kontekstu — co z tym robić przy długich przebiegach.
Razem daje to niewygodny wniosek: przy długim kontekście płacisz więcej za coś, co działa gorzej.
Jak to sprawdzić u siebie
Ćwiczenie pokazuje trafienie w cache. Dwa identyczne wywołania z tym samym długim prefiksem — drugie powinno zaraportować część tokenów jako odczytane z pamięci podręcznej.
from openai import OpenAI
client = OpenAI()
# prefiks musi byc odpowiednio dlugi, zeby cache w ogole sie wlaczyl
prefiks = "Jesteś asystentem technicznym. " + ("Odpowiadaj rzeczowo. " * 300)
def wywolaj(pytanie):
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": prefiks},
{"role": "user", "content": pytanie},
],
max_tokens=30,
)
u = r.usage
cache = getattr(u, "prompt_tokens_details", None)
z_cache = getattr(cache, "cached_tokens", 0) if cache else 0
return u.prompt_tokens, z_cache
for i, pytanie in enumerate(["Co to jest DNS?", "Co to jest DHCP?"], 1):
wejscie, z_cache = wywolaj(pytanie)
print(f"{i}. wejście {wejscie:5} z cache {z_cache:5}")
Pierwsze wywołanie zbuduje pamięć podręczną, drugie powinno z niej skorzystać mimo innego pytania — bo różnica jest na końcu, nie na początku.
Teraz najciekawsza część. Dopisz do prefiksu cokolwiek zmiennego na początku — choćby bieżącą godzinę — i uruchom ponownie. Trafienia znikną. To jest ten jeden wiersz kodu, który potrafi podnieść rachunek za agenta o kilkadziesiąt procent, nie powodując żadnej widocznej awarii.
Uwaga: mechanizm i nazwy pól różnią się między dostawcami — część włącza cache automatycznie powyżej pewnej długości prefiksu, część wymaga jawnego oznaczenia fragmentu do zapamiętania. Sprawdź dokumentację swojego, zanim uznasz, że nie działa.
Co dalej
Wiemy już, dlaczego duży model z długim kontekstem kosztuje tyle, ile kosztuje. Naturalne pytanie brzmi więc: czy zawsze potrzebny jest duży model. Okazuje się, że nie — i że istnieją dwa zupełnie różne sposoby na zmniejszenie wymagań, które bywają ze sobą mylone.
O tym jest artykuł dziesiąty: Rozmiar, kwantyzacja, destylacja — kiedy mniejszy model wygrywa.





















