Uwaga w transformerze to pamięć asocjacyjna — co z tego wynika dla RAG i okna kontekstu
Skoro attention to miękkie wyszukiwanie po kluczach i wartościach, to RAG, kolejność promptu i okno kontekstu przestają być magią — stają się projektowaniem pamięci.

Kiedy w code review pojawia się zdanie „wrzućmy całą dokumentację do promptu, przecież model ma dwieście tysięcy tokenów kontekstu”, zwykle oznacza to, że nikt nie zastanowił się, w jaki sposób model korzysta z tego, co mu wklejasz. Transformer nie czyta promptu jak człowiek dokumentu — nie przechodzi go linearnie i nie „zapamiętuje” rozdziałów. Mechanizm, który decyduje o tym, co model faktycznie widzi w danym kroku, to attention, a działa on zaskakująco podobnie do pamięci asocjacyjnej: dopasowuje zapytanie do kluczy i zwraca ważoną mieszankę wartości.
Warto przejść tę konstrukcję krok po kroku, bo z prostego modelu „dopasuj i zwróć” wynikają konkretne decyzje inżynierskie: jak chunkować dokumentację, czy dokładać treść do okna kontekstu, czy raczej budować RAG, dlaczego duplikaty w prompcie szkodzą i jak testować zmiany w kontekście, żeby nie zgadywać. Punktem wyjścia jest dla mnie tekst You could have invented transformers from memory, który pokazuje, że attention da się wyprowadzić z pamięci asocjacyjnej bez magii i bez wzorów wyciągniętych z nikąd.
Zaznaczam od razu: to model mentalny, nie opis implementacji. Ale model mentalny, który pozwala podejmować decyzje, działa lepiej niż traktowanie modelu jak czarnej skrzynki, do której wrzuca się tekst i观察uje, co wyjdzie.
Od słownika do pamięci asocjacyjnej
Najprostsza pamięć asocjacyjna to zwykły słownik: zapytanie trafia w klucz, klucz zwraca wartość.
pamiec = {
"zwrot": "formularz zwrotu: /zwroty",
"reklamacja": "formularz RMA: /reklamacje",
}
pamiec["zwrot"] # "formularz zwrotu: /zwroty"
pamiec.get("zwrot pieniędzy") # None — zapytanie nie trafiło w klucz
Działa świetnie, dopóki zapytania są dokładnie takie jak klucze. W rzeczywistym ruchu nigdy nie są: użytkownik pisze „jak oddać buty”, „chcę kasę z powrotem”, „zwrot pieniędzy”. Twarde dopasowanie zwraca None przy każdym wariancie i cała pamięć na nic.
Pierwszy odruch to policzyć podobieństwo zapytania do wszystkich kluczy i wybrać najbliższy. To już połowa drogi, ale wciąż wersja twarda: jeden klucz wygrywa, reszta znika. Jeśli dwa klucze mają podobieństwo 0,51 i 0,49, wynik skacze w zależności od drobnego szumu w zapytaniu. Do tego taka operacja nie jest różniczkowalna, więc nie da się jej wytrenować gradientem.
Miękkie dopasowanie: rozkład zamiast zwycięzcy
Zamiast wybierać jeden klucz, policzmy podobieństwo do wszystkich i zamieńmy je na rozkład prawdopodobieństwa. To dokładnie robi softmax, a wynik jest mieszanką wartości ważoną tym rozkładem.
import numpy as np
def softmax(x):
e = np.exp(x - x.max()) # stabilizacja numeryczna
return e / e.sum()
def attention(q, K, V):
scores = K @ q # podobieństwo do każdego klucza
weights = softmax(scores / np.sqrt(q.shape[0]))
return weights @ V, weights # mieszanka wartości + wagi
To jest cały szkic mechanizmu. Dzielenie przez pierwiastek z wymiaru utrzymuje wyniki w sensownym zakresie, gdy wektory są długie — bez tego softmax nasyca się i zwraca prawie one-hot.
Zauważ dwie konsekwencje. Po pierwsze, wynik jest kombinacją wielu wartości, więc reprezentacja tokenu po warstwie attention jest „rozmyta” — zawiera domieszkę wszystkiego, co było w kontekście. Po drugie, nie ma progu odcięcia: nawet słabo dopasowany klucz dostaje niezerową wagę. Praktycznie oznacza to, że nie da się powiedzieć modelowi „ten fragment zignoruj”, po prostu umieszczając go w prompcie.
Query, key, value: trzy role tego samego wektora
W prawdziwym transformerze nie mamy osobnej bazy kluczy i wartości. Każdy token jest rzutowany trzema wyuczonymi macierzami na trzy role: query („czego szukam”), key („po czym mnie znajdziesz”) i value („co przekazuję, gdy mnie znajdziesz”).
Q = X @ W_q # X: (n, d) — reprezentacje tokenów w kontekście
K = X @ W_k
V = X @ W_v
Dalej liczy się to samo podobieństwo, tylko macierzowo dla wszystkich pozycji naraz. Multi-head attention to kilka takich kompletów macierzy działających równolegle — kilka pamięci asocjacyjnych przeglądających ten sam kontekst pod różnym kątem. Analizy interpretowalności pokazują, że część głów specializing się w dość konkretnych wzorcach, np. wiązaniu zaimka z rzeczownikiem, ale nie da się tego przypisać z góry do konkretnego indeksu głowy.
Warstwy się stackują, więc to nie jest jedno wyszukanie, a kilka poziomów pośrednich: wyższe warstwy operują na reprezentacjach, które są już mieszankami. To ważne, bo znaczy, że model może „dociągnąć” informację pośrednio — przez powiązanie z czymś innym w kontekście — a nie tylko przez bezpośrednie podobieństwo do pytania.
Konsekwencja pierwsza: okno kontekstu to pamięć robocza
Wagi po softmax sumują się do jedynki. To budżet, który rozkłada się na wszystkie pozycje w kontekście — im więcej wpisów, tym mniejsza średnio część dla każdego z nich. Dołożenie trzydziestu dodatkowych akapitów nie „dodaje wiedzy”, tylko zmienia rozkład uwagi.
Z tego wynikają dwa praktyczne wnioski. Jakość kontekstu zależy od trafności wpisów, nie od ich liczby. I kolejność ma znaczenie: praktycy regularnie raportują, że modele lepiej korzystają z informacji na początku i na końcu kontekstu niż w środku (zjawisko bywa nazywane lost in the middle). Nie traktuj tego jak twardej reguły, ale jak sygnał, żeby najważniejszy fragment — instrukcję, definicję, dane — umieszczać blisko brzegów, a nie w środku dwustustronicowego wklejonego PDF-a.
Konsekwencja druga: RAG jako zarządzanie pamięcią
Skoro attention to pamięć robocza, to RAG nie jest „wyszukiwaniem do promptu”, tylko świadomym zarządzaniem tym, co trafia do pamięci. Trzy elementy robią największą różnicę.
- Chunkowanie po jednostkach sensu. Fragment powinien być zrozumiały bez sąsiadów: jedna procedura, jedna sekcja, jedno FAQ. Pocięcie po 512 tokenach w połowie tabeli gwarantuje, że model dostanie ochłap.
- Metadane i filtrowanie. Wersja dokumentu, data, produkt, najemca. Filtr działa przed podobieństwem wektorowym i potrafi wyciąć połowę szumu taniej niż reranker.
- Reranking jako drugi etap. Tani retrieval po wektorach zwraca dwadzieścia kandydatów, drobniejszy model sortuje je i wybierasz trzy do promptu. To dwa poziomy dopasowania — odpowiednik dwóch warstw attention, tylko po stronie infrastruktury.
Osobna sprawa: embeddingi słabo radzą sobie z dokładnymi identyfikatorami — nazwami funkcji, kodami błędów, numerami wersji. Warto łączyć wyszukiwanie wektorowe z leksykalnym (BM25), bo zapytanie „błąd E_STRIPE_409” to nie jest problem semantyczny.
Konsekwencja trzecia: szum i koszt
Każdy element kontekstu konkuruje o uwagę z pozostałymi. Trzy typy śmieci psują odpowiedzi najbardziej: duplikaty (ten sam akapit z dwóch wersji dokumentacji), treści nieaktualne (stary endpoint obok nowego) i boilerplate (menu nawigacyjne zeskrapowane razem z treścią). Wszystkie trzy mają wysokie podobieństwo do zapytania, więc dostają wysoką wagę i realnie obniżają jakość odpowiedzi — model nie „wybierze sobie lepszej wersji”, tylko zmiksuje obie.
Do tego dochodzi koszt obliczeniowy. Standardowa self-attention liczy macierz podobieństw n × n, więc podwojenie długości kontekstu to około czterokrotności pracy w fazie prefill. Przy generowaniu, z pamięcią podręczną kluczy i wartości, koszt na token rośnie liniowo z długością kontekstu. To nie są abstrakcyjne liczby — to bezpośrednio opóźnienie pierwszego tokenu i rachunek za tokeny wejściowe.
Praktyczny checklist dla zespołu
- Zestaw pytań kontrolnych. Dwadzieścia–trzydzieści realnych pytań użytkowników z oczekiwanymi odpowiedziami i wskazaniem, z którego dokumentu mają pochodzić. Bez tego każda zmiana w kontekście to zgadywanie.
- A/B na kontekście, nie na modelu. Ta sama wersja modelu, dwa warianty doboru fragmentów. Zmiana modelu i kontekstu naraz daje wynik, którego nie zinterpretujesz.
- Logowanie użytych fragmentów. ID chunka, wersja dokumentu, wynik z retrievera i z rerankera, w każdym śladzie (trace) odpowiedzi. Bez tego nie odpowiesz na pytanie „dlaczego model tak powiedział”.
- Mierzenie trafności, nie tylko płynności. Czy odpowiedź jest pokryta przez wstawione fragmenty, czy linkuje do właściwej sekcji, czy nie zmyśla. Płynna odpowiedź z błędnym endpointem to najgorszy możliwy wynik.
- Test na pusty retrieval. Co się dzieje, gdy wyszukiwanie nic nie zwraca? Model powinien powiedzieć „nie wiem”, a nie zbudować odpowiedź z ogólnej wiedzy.
- Budżet kontekstu jako parametr. Ustal limit tokenów na prompt i pilnuj go w CI, tak samo jak rozmiaru bundle’a.
Czego z tego modelu nie wynika
Przede wszystkim: attention nie jest wyjaśnieniem. Wysoka waga na danym fragmencie nie dowodzi, że model się na nim oparł — wagi przechodzą przez kolejne warstwy i splatają się z MLP, rezyduami i pozycjonowaniem. Nie buduj na tym reguł biznesowych ani automatycznych audytów.
Po drugie, pamięć asocjacyjna nie daje przewidywalności. Nie masz gwarancji, że model użyje fragmentu A, bo jest „najbardziej podobny”. Może pominąć go, bo w kontekście jest sprzeczna informacja, bo pytanie zostało sformułowane dwuznacznie, bo instrukcja systemowa przykryła resztę. Ten model mówi, gdzie szukać przyczyn, a nie co dokładnie się stanie.
Po trzecie wreszcie, cała konstrukcja pomija sporo: MLP, które robią większość „myślenia”, rezydualny strumień, kodowanie pozycyjne, dynamikę treningu. To nie jest uproszczenie bez strat — to uproszczenie, które akurat dobrze podpowiada, jak projektować system wokół modelu. Traktuj je jako narzędzie do podejmowania decyzji: mniej, ale trafniejszych fragmentów; RAG z rerankingiem zamiast wklejania wszystkiego; porządek w kontekście i pomiar zamiast wiary w to, że większy prompt jest z definicji lepszy.