Strona główna / Artykuły

Cztery rodzaje pamięci, których potrzebuje twój agent programistyczny (a zbudowany jest tylko jeden z nich).

Grafy kodowe zapewniają agentom strukturę, jednak inżynierowie potrzebują również uzasadnień, historii i procedur. Przegląd czterech typów pamięci, poziomów pewności oraz sposobów ich łączenia.

Wydawca: REACTAPP.TOP11 min czytania2627 słów

Źródła: Your Coding Agent Is Always the New Guy on Monday.

Ostatnio pojawiła się grupa narzędzi, których celem jest uniemożliwienie agentom kodującym opartym na sztucznej inteligencji przeszukiwania repozytoriów. Analizują one kod za pomocą Tree-sitter, przechowują symbole i połączenia w SQLite i udostępniają je przez MCP, albo uruchamiają serwery językowe, zapewniając agentom prawdziwe semantyki IDE. Odnotowane korzyści są znaczne, a pomysł jest trafny.

Wszystkie one odpowiadają na ten sam rodzaj pytań: kto to wywołuje, jaki jest typ tego elementu, co może zostać uszkodzone przez zmianę.

Teraz obserwujmy doświadczonego inżyniera, który próbuje wprowadzić zmianę na nieznanym terenie. Znalezienie elementów, które to wywołują, zajmuje około trzydziestu sekund. Następnie przychodzi godzina na pytania, których nie porusza żaden z tych narzędzi – dlaczego to zostało zbudowane w taki sposób? Co się stało ostatnim razem, gdy ktoś próbował to zrobić? Jaka jest zwykła sekwencja działań, czy powinienem ponownie je wygenerować przed uruchomieniem testów?

Cztery pytania, cztery różne rodzaje pamięci. Niemal każda dzisiejsza konfiguracja agenta wykorzystuje jedną z nich i nazywa ją kontekstem.

Test, który je rozróżnia

Zanim pojawi się taksonomia, istnieje test, który ją tworzy. Dla każdej relacji, którą chcesz, aby agent znał, sprawdź, czy istnieje już formalny artefakt, który może być odczytany przez narzędzie deterministyczne.

Jeśli tak, przetwórz go. Artefaktem może być kompilator, sprawdzacz typów, odczytujnik bytecode’u lub rozproszony zapis działań. Użycie modelu językowego do wyodrębniania relacji, które już zna parser, jest wolniejsze, droższe i mniej dokładne, a ponadto marnuje informacje prawdziwe, które już posiadasz.

Jeśli nie, relacja istnieje tylko w formie tekstu i ocenie człowieka, więc wyodrębnianie jej za pomocą modelu jest jedynym sposobem na jej uzyskanie.

To nie jest podział na kod i dokumenty. Chodzi o to, czy istnieje jakaś gramatyka. Podział ten jest na tyle wyraźny, że różne rodzaje wiedzy wymagają różnych metod wydobywania informacji, różnego częstotliwości aktualizacji oraz różnych zasad oceny pewności. Przetwarzanie ich wszystkich w jednym procesie oznacza, że przynajmniej jeden z nich zostanie potraktowany w niewłaściwy sposób.

1. Pamięć strukturalna: wpływ zmiany

To jest wiedza agenta na temat samego kodu. To pamięć, którą buduje każdy, a typowym błędem jest dostarczanie do niej informacji wyłącznie z jednego źródła.

  • Tree-sitter zapewnia szerokość analizy: wykaz węzłów i obszarów tekstu, obsługę ponad 40 języków, tolerancję na błędy oraz informacje o czasie przetwarzania w milisekundach na plik. Nie rozwiązuje problemu typów. Może stwierdzić, że w linii 3 znajduje się wywołanie funkcji o nazwie execute, ale nie określi, która to konkretna wersja execute. Dopasowywanie nazw nie stanowi analizy na poziomie kompilatora, a ta luka jest szczególnie widoczna w kodzie polimorficznym, warstwowym z użyciem iniekcji zależności.
  • LSP zapewnia poprawność dzięki elementom takim jak textDocument/definition, references i implementation, wspieranym przez rzeczywisty sprawdzacz typów. Uruchamianie go jest powolne, wymaga gotowego projektu i musi być wyłączone z ścieżki zapytań, ponieważ niesprawny serwer językowy niszczy cały budżet czasu reakcji. Należy go uruchamiać jako tło w celu wzbogacania danych.
  • Bytecode ujawnia kod, który istnieje tylko po zakończeniu procesu budowania: zapytania pochodne Spring Data, proxy’i CGLIB, mapery MapStruct – wszystko to, co zostało napisane przez procesor anotacji. W pliku źródłowym nic z tego nie jest widoczne. W katalogu target/classes znajduje się to wszystko.
  • Wynik AOT lub plik /actuator/beans pokazują, w jaki sposób realizowana jest iniekcja zależności, innymi słowy, która konkretna klasa wypełnia którą interfejs w danym profilu. Przeczytanie pliku źródłowego nie pozwala na uzyskanie tej informacji, ponieważ odpowiedź tam nie znajduje się.
  • Ślady rozproszone pokazują przepływy pomiędzy usługami i granicami asynchronicznymi: od kontrolera przez protokół HTTP do innej usługi, od producenta do tematu wiadomości do konsumenta. Każdy serwer językowy zatrzymuje się przy granicy modułu, a systemy rozproszone składają się głównie z takich elementów. Jeśli używasz OpenTelemetry, już teraz zbierasz graf połączeń między usługami, a potem go usuwasz.
  • Najważniejszą decyzją projektową jest to, że każda krawędź otrzymuje określony poziom:

    • resolved: sprawdzacz typów to potwierdził, więc można bezpiecznie na tym polegać.
    • observed: powstało to podczas rzeczywistego uruchomienia, co stanowi prawdziwą informację dla objętych ścieżek.
    • inferred: dopasowanie oparte wyłącznie na nazwie. Prawdopodobnie poprawne, ale niesprawdzone.
    • ambiguous: istnieje kilka kandydatów. Należy je wszystkie zwrócić i żaden nie wybrać.
    • dynamic: obliczane w czasie wykonywania i niemożliwe do rozstrzygnięcia, więc należy je zarejestrować jako takie i nie odrzucać.

    Jeśli dać agencie graf bez poziomów, będzie traktować każdą krawędź jako fakt, a następnie dokonać modyfikacji na podstawie domysłu. Jeśli poinformuje się ją, że krawędź ma status inferred, sprawdzi to przed podjęciem działań. To właśnie ta kolumna stanowi różnicę między systemem, który cicho wprowadza w błąd, a tym, który w sposób uprzejmy radzi sobie z problemami.

    Jedną z konsekwencji, o której niewiele implementacji mówi wprost, jest to, że w warstwowym kodzie opartym na Spring lub Django analiza wpływu daje jedynie dolną granicę, a nie pełny obraz sytuacji. Stwierdzenie „17 wywołujących funkcji, 3 nierozwiązane dyspozycje interfejsu, 1 wygenerowana metoda” jest bezpieczne. Stwierdzenie jedynie „17 wywołujących funkcji” jest mylące.

    Proces przerywania działania odbywa się co godzinę, a ponowna konfiguracja trwa kilka sekund.

    2. Pamięć wiedzy: powody stojące za kodem

    Ta pamięć przechowuje uzasadnienia i znajduje się w dokumentach, których nie może odczytać żaden parser: zapisy decyzji, analizy incydentów, wątki pull requestów, bilety, RFC-y, dokumenty projektowe oraz komunikaty z commitów zawierające rzeczywisty treść.

    Rozważmy liczbę prób ponownych w pliku konfiguracyjnym wynoszącą 3. Jej wyjaśnienie znajduje się w dokumencie ADR z 2023 roku, w analizie przypadku podwójnych instrukcji rozliczeniowych oraz w wątku, w którym ktoś opisał SLA partnera handlowego. Związek pomiędzy nimi – polityka powstała w wyniku incydentu i ograniczająca integrację – jest przedstawiony w trzech dokumentach, z których każdy formułuje to inaczej.

    GraphRAG jest tu dobrym narzędziem, a warto znać jego funkcjonowanie. Istnieje sześć kroków:

    1. Chunking dzieli dokumenty na jednostki tekstowe w sposób deterministyczny.
    2. Extraction to praca modeli LLM: model czyta każdy fragment i wydaje informacje o entytetach oraz relacjach.
    3. Opis i podsumowanie również jest zadaniem modeli LLM – łączy te same entytety opisane na pięćdziesiąt różnych sposobów w jeden opis.
    4. Klastrowanie Leidena nie wymaga modelu. Dzieli graf na nawarstwione społeczności na podstawie modularności, grupując te, w których liczba wewnętrznych krawędzi znacznie przewyższa liczbę krawędzi wychodzących.
    5. Raporty społeczności to streszczenia przygotowane przez modele językowe dla każdej społeczności, tworzone od dołu, przy czym wyższe poziomy budowane są na podstawie raportów poniżej.
    6. Embeddingi generują wektory, które służą jako punkty wejścia dla zapytań.

    Dwa terminy wymagają bliższego przyjrzenia się. Krawędź w tym kontekście to nie wskaźnik typowany – zawiera ona opis prozatorski dostarczony przez model wraz z wagą i odniesieniami do źródeł, dzięki czemu każda krawędź rejestruje interpretację zdania przez model. Społeczność również jest czymś więcej niż arbitralnym podgrafem, ponieważ losowy zbiór węzłów może w ogóle nie mieć żadnych krawędzi, pozostawiając izolowane punkty bez niczego, co można by podsumować. Leiden wybiera klastry, których wywołane krawędzie są gęste, co umożliwia stworzenie spójnego raportu.

    Zaletą jest możliwość wyszukiwania globalnego. Zamiast pobierania fragmentów najbliższych zapytaniu, system zadaje pytanie każdemu osobnemu raportowi z danej społeczności i łączy częściowe odpowiedzi. W ten sposób stają się dostępne informacje dotyczące całego korpusu – na pytanie o często występujące przyczyny awarii w procesie rozliczeń istnieje odpowiedź, która nie znajduje się w żadnym pojedynczym raporcie postmortem. Zwykły RAG wektorowy nie jest w stanie tego zapewnić, bez względu na to, jak precyzyjnie dostosowane są fragmenty tekstu.

    Proces aktualizacji odbywa się co miesiąc, a jego realizacja jest kosztowna – trzy z sześciu kroków polegają na wywoływaniu modeli LLM, więc koszt indeksowania rośnie wraz z rozmiarem korpusu, liczbą przeprowadzanych analiz oraz ceną modelu. Jest to akceptowalne, ponieważ korpus prawie się nie zmienia.

    3. Pamięć epizodyczna: co wydarzyło się podczas wcześniejszych prób

    To zapisuje własną historię agenta w tym konkretnym repozytorium. Ostatnie dotknięcie SettlementService spowodowało awarię trzech testów alokacji. Zmiana tej interfejsu bez ponownego utworzenia mapowników psuje proces budowania. Dwie najnowsze propozycje zmian, które pominęły warstwę walidacji, zostały odrzucone.

    Ma również właściwość, której żaden indeks semantyczny nie może sobie przypisać: jego prawda podstawowa jest deterministyczna. Analiza post mortem zawiera ludzkie stwierdzenie wyciągnięte z modelu. „Agent zmienił X i CI stało się czerwone” to oceniony wynik, ustalony przez system budowania. W odniesieniu do pytań, które rozwiązuje, to sprawia, że pamięć epizodyczna jest najbardziej wiarygodnym źródłem w tym układzie.

    Trzy aspekty projektowe są podatne na błędy:

    • Pomij tutaj GraphRAG. Odcinki są uporządkowane chronologicznie i mają określone wyniki. Grupowanie według gęstości połączeń to niewłaściwa struktura – konieczne jest ważenie według aktualności, częstotliwości oraz sukcesu lub porażki.
    • Wymagana jest eksmisja. Indeksy semantyczne mogą funkcjonować bez niej, ten jednak nie może. Lekcja z refaktoryzacji dwóch architektur dwa lata temu nie tylko jest przestarzała, ale także kieruje agencją w niewłaściwym kierunku.
    • Błędy się gromadzą. Załóżmy, że agencja wyciągnęła błędne wnioski i zapisała wynik. Staje się to zapamiętanym faktem, który jest przenoszony we wszystkich kolejnych sesjach. Zachowaj wynik (CI failed, PR rejected) z wysokim poziomem pewności, a własny wniosek agencji z znacznie niższym poziomem pewności; następnie pozwól, by wniosek wygasł, podczas gdy wynik pozostanie.

    Odchodzenie użytkowników odbywa się w ramach każdej sesji.

    4. Pamięć proceduralna: w ogóle nie jest to indeks

    Pamięć proceduralna dotyczy sposobu wykonywania zadań: polecenia budowania, listy kontrolnej przeglądu, kolejności migracji, stylu projektu. Pierwszym odruchem ludzi jest uczynienie jej dostępną do wyszukiwania, ale należy temu odruchowi się oprzeć.

    Wszystko, co jest zawsze istotne, źle funkcjonuje w systemie wyszukiwania. Powinno znajdować się w instrukcjach systemu lub w pliku AGENTS.md, ładowanym przy każdej sesji bez konieczności wyszukiwania. Umieszczenie go poza systemem wyszukiwania powoduje problem, gdy agent nie wie, gdzie powinien szukać, a poleganie na tym, że agent coś sam znajdzie, nie stanowi skutecznego mechanizmu.

    Wszystko, co ma znaczenie tylko w określonych sytuacjach, powinno być narzędziem z wyzwalaczem. Na przykład uruchomienie zestawu integracji powinno być czymś, do czego agent może się odwołać, a nie dokumentem, o który musi sam pomyśleć, by go znaleźć.

    Przykładem wartym obserwacji jest wyrafinowany przepływ pracy. Załóżmy, że agent odkryje, iż w tym repozytorium mapery muszą być ponownie generowane przed uruchomieniem testów, i że ten porządek nadal działa. To procedura wywodząca się z epizodów. Zapisz ją jako kandydatową zasadę z liczbą udanych przypadków, awansuj ją do zasad zawsze ładowanych, gdy udowodni swoją skuteczność, i przechowuj oryginalne epizody, aby można było przebadać ten awans.

    Zatem pamięć proceduralna nie jest czwartym indeksem. To to, co pozostaje po trzecim, po wyeliminowaniu elementów niepotrzebnych i zapisaniu ich w instrukcjach.

    Łączenie: jeden przestrzeń nazw symboli

    Trzy odzyskane wspomnienia łączą się wokół tożsamości symbolu, a nikt nie rozwiązał jeszcze tego problemu. Węzły strukturalne używają w pełni określonych nazw. Wpisy epizodyczne wskazują na symbol, który faktycznie został obsłużony, dzięki czemu łączą się one dokładnie. Entytety w indeksie wiedzy to ciągi znaków wybrane przez model z tekstu prozatorskiego, takie jak „SettlementService”, „the settlement service”, „the settlement reconciliation path” czy „the counterparty SLA constraint”, a ich przyporządkowanie do rzeczywistych symboli stanowi zadanie dopasowania nieprecyzyjnego.

    Udawanie czegoś innego to dokładnie ten błąd, którego mają zapobiec poszczególne poziomy, dlatego łączenie to ma swój własny system klasyfikacji:

    • exact: nazwa występuje dosłownie i wskazuje dokładnie na jeden symbol.
    • strong: znormalizowana fraza wyraźnie wskazuje na jedną klasę.
  • weak: pojęcie obejmujące kilka klas, których granica jest niejasna. Zwraca się zbiór symboli, a agent je sprawdza.
  • unresolved: rzeczywisty ograniczenie, dla którego nie ma odpowiadającego mu symbolu nigdzie indziej. „SLA strony kontraktowej” to prawdziwe ograniczenie architektoniczne, którego kod nie reprezentuje.
  • W przypadku tej ostatniej kategorii należy ją zachować, zwrócić i nie wymuszać dopasowania. Dobrze czytelne, nierozwiązane ograniczenie jest lepsze od błędnego powiązania symbolu, a wymuszanie dopasowań sprawia, że agent może z powodzeniem edytować złą klasę.

    Co tak naprawdę pyta agent

    Powierzchnia zapytań może wyglądać w ten sposób:

    what_breaks(symbol)        → structural: resolved callers, tiered
    why_is_this(symbol)        → knowledge: decisions, incidents, constraints
    what_happened_before(sym)  → episodic: past attempts and their outcomes
    related_by_intent(concept) → resolve a phrase to a symbol set, tiered
    

    Następnie pojawia się pytanie, na które żaden z tych trzech nie może odpowiedzieć samodzielnie: „Zamierzam zmienić politykę ponawiania prób w procesie rozliczania. Co się zepsuje, dlaczego jest to trzy razy i czy ktoś próbował tego już wcześniej?”

    Mapy wiedzy łączą „politykę ponawiania prób” z SettlementService.retry na poziomie strong i zwracają informacje o ADR oraz analizę postmortem. Mechanizm strukturalny bierze ten symbol i zwraca elementy, które go wywołują, w tym dwa usługi widoczne tylko w śladach działania. Mechanizm epizodyczny dodaje, że podobna zmiana w zeszłym kwartale spowodowała awarię testów alokacji.

    To jest jedna odpowiedź stworzona na podstawie trzech rodzajów dowodów, z których każdy pochodzi od mechanizmu zdolnego go wygenerować, a każdy jest oznaczony stopniem wiarygodności.

    Jakie to kosztuje

    • Większość zbiorów dokumentów jest zbyt skromna. Dziewięć zapisów ADR oraz komentarzy typu „naprawić błąd” nie dostarczają indeksowi wiedzy żadnych materiałów do pracy. Sprawdź, co masz, zanim cokolwiek budujesz.
    • Rozwiązywanie problemów z identyfikacją entytetów nigdy nie jest doskonałe. Narzędzie typu ladder pomaga sobie z tym radzić, ale nie eliminuje problemu. Poziom weak będzie prawdopodobnie najbardziej zatłoczony, więc planuj przede wszystkim weryfikację, a nie poleganie na zaufaniu.
    • Zacznij od ograniczonego zakresu. Rozpocznij od tree-sitter w połączeniu z LSP. Dodaj bytecode, gdy generowany kod zacznie dawać błędne odpowiedzi, oraz ślady działania, gdy pytania przechodzą między różnymi usługami. Indeks wiedzy dodawaj tylko wtedy, gdy korpus jest wart indeksowania.
  • Nikt nie stworzył tego wszystkiego – to może być zarówno szansa, jak i ostrzeżenie. Poszczególne elementy znajdują się w różnych społecznościach: specjaliści od analizy kodu nie czytają prac na temat GraphRAG, ludzie zajmujący się GraphRAG nie myślą o sprawdzaczach typów, a specjaliści od architektury pamięci pracują głównie nad asystentami do rozmów, a nie nad bazami kodu.
  • Pomyśl o włączeniu kogoś do zespołu

    Załóżmy, że w poniedziałek dołącza starszy inżynier i otrzymuje dostęp do repozytorium, nic więcej. Może czytać każdą linię kodu, śledzić każde wywołanie i uruchamiać testy. Do piątku nadal pracuje wolniej niż kolega zatrudniony rok wcześniej, a nie chodzi tu o różnice w umiejętnościach.

    Kolega z zeszłego roku posiada trzy rzeczy, których nigdy nie było w repozytorium, a nowo przybyły ich nie ma.

    Jednym z powodów dziwnych decyzji są okoliczności, które je napędzają. Próba ponowna odbywa się trzy razy z powodu incydentu w marcu, a rozwiązanie tymczasowe istnieje, ponieważ API partnera zachowuje się niewłaściwie w piątki. Żaden komentarz tego nie mówi – znajduje się to w dokumencie ADR, analizie postmortem oraz rozmowach.

    Kolejnym powodem jest historia. Ktoś próbował uproszczyć tę interfejs w drugim kwartale, w wyniku czego testy alokacji przestały działać przez tydzień, więc ludzie zostawiają to w spokoju.

    Trzecim powodem jest lokalny sposób pracy. Mapery należy regenerować przed uruchomieniem testów. PR-y, które pomijają weryfikację, są odrzucane. Nikt nie korzysta z żadnego dokumentu w tym celu – po prostu tak pracuje zespół.

    Pracownik zatrudniony w poniedziałek, który ma tylko diagram kodu, to osoba, która nie pamięta całego repozytorium, nie rozumie, dlaczego coś jest tak, jak jest, nie pamięta wcześniejszych prób ani nie zna lokalnych zwyczajów. Każda sesja to nowy poniedziałek.

    To właśnie jest ta luka, i żaden narzędzie nie jest za to winne. Graf kodu dobrze odpowiada na własne pytania, a dzisiejsze implementacje są solidne. Brakuje jednak tego, że trzy z czterech elementów, od których zależy kompetentny inżynier, w ogóle nie znajdują się w tym zestawie narzędzi, więc czwarty musi sam dźwigać cały ciężar.

    Wersja jednej linijki

    Krótko mówiąc: kompilator kontroluje strukturę, ponieważ struktura jest deterministyczna. Model reprezentuje intencję, ponieważ intencja to niestrukturyzowana proza. Zasady systemu budowania opierają się na doświadczeniu. Procedury są wytwarzane na podstawie doświadczenia i ładowane z góry, nigdy nie pobierane później.

    Żadne narzędzie z tej kategorii nie popełnia błędu w zakresie pamięci. Każde z nich buduje jedną i przestaje, a nic ich ze sobą nie łączy. Lepszy graf nie jest rozwiązaniem. Rozwiązaniem są cztery pamięci, z których każda szczycie się informacją o swojej niezawodności, mapowane na jeden wspólny przestrzeń nazw symboli.

    Terminologia GraphRAG oraz etapy procesu odpowiadają implementacji opracowanej przez Microsoft Research. Koszty znacznie się zmieniają w zależności od rozmiaru korpusu, ustawień pozyskiwania danych oraz wyboru modelu, dlatego przed podjęciem decyzji przeprowadź testy na własnych danych.

    Kontrola trwająca piętnaście minut

    Wybierz w swojej bazie kodu interfejs, który ma co najmniej pięciu znanych implementatorów, na przykład PaymentProcessor lub NotificationChannel, z których jeden jest powiązany za pomocą @Autowired a nie zbudowany bezpośrednio. Zapytaj swój graf kodu, kto go wywołuje, i policz wyniki.

    Opublikowane benchmarki w tej kategorii mierzą liczbę tokenów, liczba wywołań narzędzi oraz opóźnienia. Jak dotąd nie ma takich pomiarów, które sprawdzałyby, czy zwrócony graf wywołań jest poprawny. W bazie kodu z deklarowanymi interfejsami i znanych implementatorów kontrola ta zajmuje pięć minut i opiera się na danych rzeczywistych, które już posiadasz.

    Podobne materiały