Przejdź do głównej zawartości

Strategie dla milionowych linii kodu

Bazy kodu liczące miliony linii przekraczają każde okno kontekstu, więc praca z AI opiera się w nich na strukturze, a nie na pojemności: warstwowa piramida kontekstu zamiast ładowania repozytorium, wyszukiwanie semantyczne zamiast grepa, raport promienia rażenia przed każdą edycją i zmiany przyrostowe weryfikowane moduł po module. Cursor, Claude Code i Codex wymagają innej konfiguracji indeksowania.

Odziedziczyłeś monolit liczący 1,8 miliona linii. Pierwotni architekci odeszli dwa lata temu, dokumentacja opisuje system, który już nie istnieje, a twój pierwszy ticket zmienia nazwę pola w User. Agent z pełnym przekonaniem edytuje trzy pliki w packages/web, ogłasza sukces, a dwie godziny po wdrożeniu usługa billing-worker wyrzuca błąd na produkcji, bo czytała to samo pole ze współdzielonego schematu, którego agent nigdy nie otworzył. Model nie pomylił się dlatego, że jest głupi. Pomylił się, bo nigdy nie zobaczył pliku, który miał znaczenie.

  • Konfigurację MCP do wyszukiwania semantycznego (Zilliz Claude Context) podłączoną do Cursora, Claude Code i Codeksa, dzięki czemu AI znajduje kod według intencji, a nie dopasowania ciągu znaków
  • Czterowarstwową piramidę kontekstu i pliki reguł dla każdego narzędzia, które trzymają warstwę architektoniczną załadowaną bez ładowania całego repozytorium
  • Wielokrotnego użytku prompt do rekonesansu architektury oraz prompt zamieniający jego wynik w żywy dokument architektoniczny
  • Dwa prompty do promienia rażenia — jeden dla zmiany nazwy przekraczającej pakiety, drugi dla pojedynczej klasy — działające w trybie tylko do odczytu przed jakąkolwiek edycją
  • Pętlę „dekompozycja, potem wykonanie”, która zamienia „zrefaktoryzuj system uwierzytelniania” w listę kontrolną gotową do przeglądu
  • Gotowe do skopiowania prompty do migracji masowych z poziomami ryzyka, opakowywania w stylu strangler fig, modernizacji kodu legacy względem modułu referencyjnego i zmian sygnatur zachowujących wsteczną zgodność
  • Konkretne kroki naprawcze na nieaktualne indeksy, zmyślone ścieżki i liczby, przepełnienie kontekstu i zderzenia równoległych refactoringów

Dlaczego większe okno kontekstu niczego nie rozwiązuje

Dział zatytułowany „Dlaczego większe okno kontekstu niczego nie rozwiązuje”

Obecne flagowe modele wspierają okna kontekstu o pojemności 1M tokenów (Claude Fable 5, Opus 5, Sonnet 5, Gemini 3.1 Pro), a baza kodu licząca wiele milionów linii i tak się w nich nie zmieści. Odpowiedzią nie jest większa pojemność, lecz lepszy dobór — i właśnie tam AI zarabia na swoje miejsce w tej skali.

Semantycznie, nie tekstowo

Dzięki indeksowi wektorowemu zapytanie „znajdź wszystkie przepływy uwierzytelniania” wydobywa kod OAuth, JWT i sesji, nawet gdy żaden z nich nie dzieli słowa kluczowego.

Śledzenie zależności

AI podąża za importami i miejscami wywołań przez granice modułów znacznie szybciej, niż zdołasz klikać przez „znajdź użycia”.

Testy charakteryzacyjne

Dla nieudokumentowanego kodu legacy AI tworzy testy, które przypinają obecne zachowanie, żebyś mógł refaktoryzować bez strachu.

To ty pozostajesz architektem

AI wykonuje mechaniczne skanowanie i boilerplate. To ty podejmujesz decyzje domenowe i architektoniczne, których ono podjąć nie potrafi.

Myśl o kontekście jak o czterech warstwach ładowanych w malejącej trwałości:

  1. Warstwa architektoniczna (zawsze obecna): dokumentacja wysokopoziomowa, mapy zależności, granice modułów
  2. Warstwa domenowa (specyficzna dla zadania): podsystem, nad którym pracujesz, jego interfejsy i kontrakty
  3. Warstwa implementacyjna (specyficzna dla pliku): faktyczne pliki, które modyfikujesz
  4. Warstwa referencyjna (na żądanie): przykłady podobnych wzorców w innych częściach bazy kodu

Warstwa architektoniczna to ta, którą kodujesz raz i przestajesz za nią płacić. Każde narzędzie ma na nią swój plik, a kształt tego pliku decyduje o tym, czy agent rozumuje o całym systemie, czy zgaduje z trzech otwartych zakładek.

Indeksowanie bazy kodu w Cursorze dostarcza część warstwy architektonicznej automatycznie. Resztę przypnij w Project Rule, żeby obowiązywała bez przepisywania jej za każdym razem:

# .cursor/rules/architecture.mdc (alwaysApply: true)
Large-scale payment processing platform, 1.8M LOC.
Key modules:
- /src/payments/ - Payment processing (Stripe, PayPal, internal ledger)
- /src/accounts/ - User account management and KYC
- /src/notifications/ - Event-driven notification system
- /src/shared/ - Shared types, utilities, and base classes
When modifying any module, always check:
1. The module's public API in its index.ts barrel export
2. Integration tests in /tests/integration/{module-name}/
3. The event contracts in /src/shared/events/

Warstwę domenową zawężaj regułą przypiętą globem, leżącą obok kodu, którym zarządza:

# .cursor/rules/payment.mdc (glob: services/payment/**)
When working with payment code:
- All monetary amounts are integer cents — never floats
- Mutations require an idempotency key
- Never log full card numbers (PCI)
- Add audit logging for every state transition

Konkretny kontekst wciągaj oznaczeniami @file i @folder. Przy zmianach przekrojowych odwołaj się najpierw do współdzielonego kontraktu: @src/shared/types/payment.ts przed dotknięciem jakiegokolwiek modułu płatności. Dla prostych projektów AGENTS.md w katalogu głównym sprawdza się jako prostsza alternatywa dla strukturalnych reguł.

Wyszukiwanie tekstowe zawodzi w skali, bo powiązany kod rzadko dzieli słownictwo. Indeks semantyczny zbudowany na embeddingach wektorowych to naprawia: odpowiada na pytanie „gdzie obsługiwane jest przetwarzanie płatności?” bez otwierania pliku. Utrzymywany serwer to Zilliz Claude Context (@zilliz/claude-context-mcp — wcześniej publikowany jako code-context). Konfiguracja MCP jest niemal identyczna we wszystkich trzech narzędziach; różni się tylko polecenie rejestracji.

Dodaj do ~/.cursor/mcp.json:

{
"mcpServers": {
"claude-context": {
"command": "npx",
"args": ["-y", "@zilliz/claude-context-mcp@latest"],
"env": {
"EMBEDDING_PROVIDER": "OpenAI",
"OPENAI_API_KEY": "your-api-key",
"MILVUS_TOKEN": "your-zilliz-key"
}
}
}
}

Po zaindeksowaniu pytasz o koncepcje, a serwer zwraca odpowiednie pliki niezależnie od nazewnictwa. Dla wrażliwych baz kodu, które nie mogą sięgnąć do chmurowego API embeddingów, LuotoCompany/cursor-local-indexing uruchamia lokalny indeks ChromaDB i udostępnia go przez lokalny endpoint SSE:

Dodaj do ~/.cursor/mcp.json:

{
"mcpServers": {
"workspace-code-search": {
"url": "http://localhost:8978/sse"
}
}
}

To utrzymuje kod źródłowy na twojej własnej infrastrukturze — właściwy wybór dla usług finansowych, opieki zdrowotnej czy pracy w sektorze obronnym, gdzie kod nie może opuścić sieci.

Zewnętrzny indeks wektorowy to jedna połowa; drugą jest to, co samo narzędzie widzi natywnie — a to różni się między tą trójką ostro.

Cursor indeksuje przestrzeń roboczą automatycznie i wylicza embeddingi do wyszukiwania semantycznego. Utrzymuj indeks szczupły dzięki plikom ignorowania:

  • .cursorignore blokuje pliki przed indeksowaniem oraz przed dostępem agenta (używaj go do sekretów, node_modules/, wyników budowania).
  • .cursorindexingignore wyklucza pliki tylko z indeksu — pozostają dostępne przez jawne @-wskazanie. Używaj go do dużych plików generowanych (lockfile’i, dist/, snapshoty), które zaśmiecają wyniki wyszukiwania.
.cursorindexingignore
dist/
**/*.snap
pnpm-lock.yaml

Następnie zawężaj kontekst symbolami, a nie całymi plikami: @accountType (symbol) daje ciaśniejszy, mniej zaszumiony kontekst niż @user-service.ts (plik z 2000 linii).

Od czego zacząć? Z góry na dół. Skłoń AI do zbudowania modelu mentalnego, zanim czegokolwiek dotkniesz, a potem zagłęb się w obszar, którego naprawdę dotyczy twój ticket.

Agent Cursora sam zbiera kontekst z zaindeksowanej bazy kodu — wystarczy opisać, czego chcesz. Użyj @Folders, aby zawęzić pytanie do jednego obszaru, i @Code, aby wskazać konkretny fragment:

@Folders services/auth
Explain the authentication and authorization architecture: where tokens
are issued, how refresh works, and which services validate them.

Aby uzyskać precyzyjne odniesienie, zaznacz funkcję w edytorze i dodaj ją przez @Code, zanim poprosisz agenta o prześledzenie miejsc jej wywołań.

Rekonesans, który wyrzucasz, to rekonesans, za który zapłacisz ponownie za tydzień. Zapisz go do pliku, który narzędzie odczyta w każdej przyszłej sesji — ten plik jest twoją warstwą architektoniczną, a utrzymywanie go w aktualności kosztuje mniej niż wyprowadzanie go od nowa.

Miejsce docelowe różni się w zależności od narzędzia, a to właśnie ono sprawia, że plik działa automatycznie, a nie tylko wtedy, gdy pamiętasz o @-wskazaniu:

Zapisz go do .cursor/architecture.md i odwołuj się przez @.cursor/architecture.md albo wskaż go zawsze aktywną Project Rule, żeby każdy czat startował z nim w kontekście.

Przybliżanie zamiast ładowania: hierarchia kontekstu

Dział zatytułowany „Przybliżanie zamiast ładowania: hierarchia kontekstu”

Największy błąd przy dużych bazach kodu to ładowanie wszystkiego naraz. Twój asystent nie potrzebuje wszystkich 1,8 miliona linii — potrzebuje właściwego wycinka we właściwym momencie. Pomyśl o tym jak o przybliżaniu na mapie: kontynent, kraj, miasto, ulica.

  1. Poziom domeny (widok z 10 000 stóp)

    What are the main bounded contexts in this system, and how do the payment,
    user, and inventory domains interact?
  2. Poziom usługi (widok z 1000 stóp)

    Within the payment domain, explain the service architecture and the main
    APIs each service exposes.
  3. Poziom komponentu (widok ze 100 stóp)

    Show me how PaymentProcessor handles credit-card transactions and what its
    retry strategy is for failed charges.
  4. Poziom implementacji (poziom gruntu)

    In PaymentProcessor.processCard(), why is there a 30-second timeout, and is
    the synchronized block safe to remove?

Ruchem o największej dźwigni w dużym repozytorium jest zmuszenie agenta, by znalazł i zaraportował pliki dotknięte zmianą, zanim cokolwiek wyedytuje. To wyłapuje zależność między pakietami, którą agent w innym wypadku by przeoczył, a kosztuje grosze: przebieg w trybie tylko do odczytu to ułamek ceny spartaczonej refaktoryzacji.

Ten przebieg ma dwa kształty, dla dwóch różnych zmian. Pierwszy jest dla zmiany rozchodzącej się po nazwie przez pakiety — zmiany nazwy, pola, klucza konfiguracyjnego — a jego wynikiem jest uszeregowana tabela, względem której prowadzisz przegląd:

Jeśli raport pomija usługę, o której wiesz, że istnieje, twój indeks jest niekompletny albo jego reguły ignorowania są zbyt agresywne — napraw to, zanim ruszysz dalej. Trzymaj raport otwarty jako artefakt przeglądu i odhaczaj pliki w miarę wprowadzania zmiany.

Drugi jest dla zmiany skupionej na jednej klasie lub module. Prosi o graf, a nie o listę, i to on wyłapuje konsumentów, których wyszukiwanie po nazwie nie zobaczy: nasłuchy zdarzeń i subskrybentów kolejek komunikatów, którzy w ogóle nie importują symbolu.

Nigdy nie dawaj dużemu repozytorium otwartego zadania w stylu „zrefaktoryzuj cały system uwierzytelniania”. Model rozprasza się, gubi wątek w połowie, a ty dostajesz diff na 40 plików, którego nie da się przejrzeć. Zmuś go najpierw do wypisania listy kontrolnej, a potem wykonuj po jednym elemencie na turę.

  1. Wypisz każdy plik, który musi się zmienić

    Zdobądź tę listę, zanim powstanie choć jedna linia kodu, i zweryfikuj ją z własnym zrozumieniem systemu.

  2. Modyfikuj najpierw współdzielone interfejsy

    Zacznij od definicji typów, interfejsów i kontraktów. Te zmiany propagują błędy kompilacji, które ujawniają ukryte zależności pominięte przez raport.

  3. Aktualizuj implementacje moduł po module

    Modyfikuj każdy konsumujący moduł niezależnie i uruchom testy tego modułu przed przejściem do następnego.

  4. Uruchamiaj testy integracyjne po każdym module

    Nie czekaj, aż wszystkie moduły zostaną zaktualizowane. Wyłapuj problemy integracyjne, póki diff jest jeszcze na tyle mały, że da się go przeczytać.

  5. Zweryfikuj w poprzek całości

    Uruchom pełny zestaw testów, sprawdź typy w całej bazie kodu i przejrzyj kompletnego diffa przed zatwierdzeniem.

Gdy plan zostanie zatwierdzony, realizuj go po jednym elemencie i nazywaj konkretny symbol, żeby agent trzymał się celu, zamiast od nowa wyprowadzać zakres:

Gdy fazy wynikają wprost z kształtu zmiany — najpierw interfejs, potem jego konsumenci — możesz pominąć osobną turę planowania i wbudować je w jeden prompt z jawnymi przystankami. To zwięzła forma tej samej dyscypliny:

Po pełną dyscyplinę planowania stojącą za tą pętlą zajrzyj do PRD → plan → todo.

Refactoring milionowej bazy kodu to jak remont szpitala podczas trwającej operacji — nie możesz wszystkiego wyłączyć. Wzorzec, który działa: odkryj, ustanów szablon, migruj małymi partiami, zweryfikuj.

Weźmy bazę kodu Node.js wciąż naszpikowaną callbackami error-first. Ręczna migracja do async/await zajęłaby miesiące. Zamiast tego skłoń AI do skategoryzowania pracy według ryzyka, a potem wygenerowania jednej wielokrotnego użytku transformacji na kategorię:

// Before — error-first callback
function loadUser(id, callback) {
db.query('SELECT * FROM users WHERE id = ?', [id], (err, rows) => {
if (err) return callback(err);
callback(null, rows[0]);
});
}
// After — async, with a backward-compatible callback shim
async function loadUser(id, callback) {
try {
const rows = await db.query('SELECT * FROM users WHERE id = ?', [id]);
if (callback) return callback(null, rows[0]);
return rows[0];
} catch (err) {
if (callback) return callback(err);
throw err;
}
}

Shim pozwala wywołującym migrować we własnym tempie. Stosuj transformację katalog po katalogu, uruchamiaj istniejące testy po każdej partii i śledź postęp — nigdy nie transformuj całego drzewa w jednym przebiegu.

Przy dużym przedsięwzięciu rozłożonym na zespół skłoń AI do podzielenia pracy tak, by zminimalizować konflikty między zespołami, a potem pilnuj uczciwości gałęzi:

  1. Podziel według granic zależności

    Analyze module dependencies and propose how to split this refactor across
    four developers so their territories barely overlap. Flag any shared files
    that two teams would both need to edit.
  2. Gałąź na terytorium

    Okno terminala
    git checkout -b refactor/user-services
    git checkout -b refactor/payment-services
    git checkout -b refactor/shared-utils
  3. Wykrywaj kolizje wcześnie

    Review the diffs across all refactor/* branches and identify conflicting
    or breaking changes between teams before we attempt to merge.

Każda duża baza kodu ma warstwy archeologiczne — kod z różnych epok i filozofii, część z niego sprzed czasów obecnego zespołu. Klasyczny koszmar: 15 000-liniowa procedura składowana, której nikt nie rozumie, a która wciąż codziennie przetwarza prawdziwe pieniądze.

Wzorzec strangler fig pozwala modernizować bez przepisywania: opakuj kod legacy za czystym interfejsem, a potem wyodrębniaj fragmenty po jednym, uruchamiając stary i nowy równolegle, aż zaufasz nowej ścieżce.

Nie każda zmiana w kodzie legacy zasługuje na fasadę. Gdy musisz tylko coś do starego kodu dodać, daj AI Kamień z Rosetty: najnowszy, dobrze napisany moduł stosujący aktualne konwencje jako wzorzec do naśladowania. To utrzymuje diff małym tam, gdzie prompt strangler fig celowo buduje nową powierzchnię.

Gdy dokumentacja nie istnieje, testy stają się dokumentacją. Poproś AI o napisanie testów charakteryzacyjnych, które przypną obecne zachowanie — łącznie z dziwnymi fragmentami — tak by każda przyszła zmiana zmieniająca wyjście zawodziła głośno:

describe('Legacy OrderProcessor — current behavior', () => {
it('returns status code 1 on a standard single-item order', async () => {
const result = await processOrder({
customerId: 123,
items: [{ sku: 'WIDGET-1', quantity: 1 }],
});
expect(result.status).toBe(1); // 1 = success (undocumented magic number)
expect(result.orderId).toMatch(/^ORD-\d{8}$/);
});
it('returns -99 when inventory is unavailable', async () => {
const result = await processOrder({
customerId: 123,
items: [{ sku: 'OUT-OF-STOCK', quantity: 1 }],
});
expect(result.status).toBe(-99); // -99 = inventory error
});
});

Koordynacja między zespołami przy zmianach łamiących

Dział zatytułowany „Koordynacja między zespołami przy zmianach łamiących”

W milionowej bazie kodu różne zespoły posiadają różne terytoria. Najtrudniejsze jest wprowadzenie zmiany przekraczającej granicę bez zepsucia czegoś komuś innemu. Powyższe prompty do promienia rażenia kończą się na raporcie; ten idzie dalej i prosi o ścieżkę migracji, bo zmiana łamiąca, której nie da się rozłożyć na etapy, to zmiana, której nie da się wdrożyć.

Połącz to z automatycznie generowanymi kontraktami. Poproś AI o wytworzenie specyfikacji OpenAPI i schematów zdarzeń dla usługi, z której korzysta inny zespół — to zamienia „idź przeczytaj nasz kod” w stabilną granicę, względem której mogą się integrować bez grzebania w twoich wnętrznościach.

W długiej sesji sama historia rozmowy staje się nieaktualnym kontekstem — agent wciąż „pamięta” błąd, który naprawiłeś godzinę temu. Resetuj świadomie, gdy zmieniasz zadanie. Mechanika różni się w zależności od narzędzia:

Rozpoczynaj nowy czat dla każdego odrębnego zadania (nowa funkcja, nowy błąd). Cursor trzyma kontekst każdego czatu osobno, więc świeży czat oznacza, że agent rozumuje wyłącznie o zadaniu, które ma przed sobą. Używaj punktów kontrolnych, by cofnąć czat, jeśli eksploracyjna edycja pójdzie nie tak.

Przepływy pracy AI w dużych bazach kodu zawodzą na konkretne, rozpoznawalne sposoby. Poznaj sposób naprawy każdego z nich.