Projektowanie RAG dla 100 milionów dokumentów: shardowanie, wyszukiwanie hybrydowe, koszty
Jakie zmiany zachodzą, gdy system RAG rośnie do miliardów fragmentów: asynchroniczne pobieranie danych, kierowanie ruchem, dzielenie na fragmenty, hybrydowe wyszukiwanie, ponowna sortowanie wyników, autoryzacja, buforowanie oraz kontrola kosztów.
Uruchomienie demonstracji RAG zajmuje popołudnie. Podzielasz plik PDF na fragmenty, wstawiasz je, umieszczasz wektory w bazie danych, wybierasz pięć najbliższych sąsiadów, wklejasz je do promptu i uruchamiasz model.
Taki sposób jest w pełni wystarczający dla kilku tysięcy dokumentów. Gdy zbiór rozrasta się do 100 milionów dokumentów, cała struktura przestaje funkcjonować, a pytanie, które się stawiamy, zmienia się. Już nie brzmi ono „którą bazę wektorową powinniśmy wybrać?”, lecz: jak zmniejszyć miliardy potencjalnych dowodów do 10 lub 20 istotnych fragmentów, nie niszcząc przy tym opóźnień, jakości wyszukiwania, dostępności ani budżetu?
To zupełnie inny problem inżynieryjny. W takim skali RAG stanowi rozproszony system danych i wyszukiwania z modelem językowym połączonym na końcu.
Co musi zrobić system
Aby dyskusja była konkretna, załóżmy następujące wymagania:
- Ponad 100 milionów dokumentów w formatach takich jak PDF, HTML i DOCX
- Miliony użytkowników zadających pytania w języku naturalnym
- Cytaty dołączone do każdej odpowiedzi
- Dokumenty, które z czasem są aktualizowane i usuwane
- Szybkie wyszukiwanie – około 1 do 3 sekund przed rozpoczęciem generowania
- Wysoka dostępność
- Przetwarzanie dokumentów w trybie asynchronicznym
Wszystko sprowadza się do jednego trudnego wymagania: znalezienie odpowiednich 10 do 50 informacji wśród potencjalnie miliardów fragmentów.
Zacznijmy od arytmetyki
Pierwszym błędem w dużych projektach RAG jest traktowanie dokumentu i wektora jako tego samego. Załóżmy:
Documents = 100,000,000
Average chunks/document = 30
Embedding dimensions = 768
Storage/dimension = 4 bytes (float32)
Liczba wektorów wynosi zatem:
100,000,000 × 30
= 3,000,000,000 vectors
Trzy miliardy wektorów. Surowa pamięć typu float przeznaczona dla nich wynosi około:
3B × 768 × 4 bytes
≈ 9.2 TB
A to są tylko same liczby. Rzeczywiste wdrożenie obejmuje również:
ANN index structures
chunk text
document metadata
ACL metadata
IDs
lexical indexes
replicas
snapshots
WALs
temporary indexes
operational headroom
Mając dwie kopie indeksu wektorowego, już przekraczasz 18 TB surowych wektorów, zanim uwzględniono zostaną struktury wyszukiwania. Jeśli dokumenty składają się średnio z 50 fragmentów, a nie 30:
100M documents × 50 chunks
= 5 billion vectors
Dlatego stwierdzenie „umieścimy wszystkie fragmenty w bazie danych wektorowej i uruchomimy wyszukiwanie podobieństw” nie opisuje architektury na dużą skalę. Opisuje jedynie punkt wyjścia.
Dwa oddzielne poziomy
Najlepszym sposobem na zorganizowanie systemu jest podział go na dwa niezależne poziomy: jeden do przyjmowania danych, a drugi do ich wyszukiwania. Ten podział ma znaczenie – klient przesyłający 20 milionów nowych dokumentów nie powinien powodować wzrostu czasu odpowiedzi z 300 ms do 8 sekund. Proces przyjmowania danych jest optymalizowany pod kątem przepustowości, a proces wyszukiwania pod kątem szybkości reakcji; próba łączenia ich w jednym procesie to niepewny sposób na przyszłe problemy operacyjne.
Baza danych wektorowa nie jest twoim źródłem prawdy
Zachowaj oryginalne dokumenty w tanich usługach przechowywania obiektów, takich jak S3, GCS czy Azure Blob. Baza danych wektorowych to indeks, a indeksy można odbudować. Jeśli za sześć miesięcy przejdziesz z modelu embedding E1 na E2, chcesz zregenerować wektory na podstawie przechowywanych oryginałów, a nie próbować odtworzyć treść bezpośrednio z samej bazy wektorowej.
Tutaj również przydaje się wersjonowanie obiektów, ponieważ aktualizację można zapisać jako nową wersję zamiast niszczyć starszą. Na przykład Amazon S3 może przechowywać kilka wersji obiektu, a zasady cyklu życia mogą przenosić starsze wersje do tańszych poziomów przechowywania lub je wygaszać.
Proces pobierania musi być asynchroniczny
Załóżmy, że klient przesyła 500 000 plików PDF. Nie chcesz żadnego żądania POST /documents, które analizuje pliki, stosuje OCR, je oczyszcza, dzieli na fragmenty, wstawia treść, tworzy indeksy, a dopiero potem zwraca kod 200. Takie żądanie może trwać godzinami. Zamiast tego endpoint powinien wykonywać jak najmniej operacji:
POST /documents
│
▼
Store document
│
▼
Create ingestion event
│
▼
Return 202 Accepted
Wszystko inne dzieje się na boku, kierowane przez wydarzenia. Kafka jest jedną z opcji na podstawę takiego rozwiązania, ponieważ tematy są dzielone na partycje, którymi konsumenty mogą pracować równolegle, a kolejność jest zachowywana w każdej partycji, co jest przydatne, gdy wydarzenia dzielące ten sam dokument lub klucz potrzebują przewidywalnej sekwencji.
Konkretny narzędzie ma mniejsze znaczenie niż właściwość, którą zapewnia: proces pobierania danych wymaga bufora. Jeśli twój serwis do embeddingów obsługuje 30 000 fragmentów na sekundę, a nagle otrzymuje zadania w ilości 100 000 na sekundę, system powinien tworzyć kolejkę zaległych zadań, zamiast przestać funkcjonować.
Uczynienie każdego kroku pobierania danych idempotentnym
W takim skali zadania są wykonywane dwa razy. Komunikaty są dostarczane ponownie. Procesory umierają po zapisaniu danych, ale przed potwierdzeniem ukończenia zadania. Prośby wygasają mimo że strona zdalna zakończyła pracę. Należy zaplanować to wszystko od samego początku, tworząc deterministyczne identyfikatory na podstawie stabilnych danych wejściowych:
document_id
document_version
chunk_index
embedding_model_version
ID fragmentu może być wtedy haszem elementów, które go definiują:
hash(
tenant_id +
document_id +
document_version +
chunk_index
)
Otrzymanie tego samego zdarzenia indeksowania dwa razy już nie powoduje duplikatu treści, ponieważ
upsert(chunk_id, ...)
Można bezpiecznie spróbować ponownie. Realizacja wykonywania operacji dokładnie raz jest trudna. Celem bardziej realistycznym jest zazwyczaj wykonywanie operacji w sposób idempotentny.
Dzielenie na fragmenty to decyzja infrastrukturalna
W małym korpusie zmiana rozmiaru fragmentów wydaje się nieszkodliwa. Jednak przy miliardach wektorów wiąże się to z realnymi kosztami. Weźmy 100 milionów dokumentów – strategia dająca 20 fragmentów na dokument generuje 2 miliardy wektorów, natomiast ta dająca 60 fragmentów – 6 miliardów. Zmiana, która wydaje się niewinna, właśnie potroiła wszystkie te wartości:
embedding compute
vector storage
indexing work
retrieval fan-out
reindexing time
replication traffic
Dlatego dzielenie na fragmenty to nie tylko element do regulacji w NLP – jest to parametr planowania pojemności.
Każdy fragment powinien również zawierać wystarczająco dużo metadanych, aby można było go prześledzić do jego źródła:
{
"chunk_id": "c_982...",
"document_id": "doc_51...",
"document_version": 8,
"tenant_id": "tenant_17",
"page": 43,
"section": "Risk Factors",
"language": "en",
"created_at": "...",
"acl_groups": ["finance", "executives"],
"embedding_version": "embed_v4"
}
Sam tekst może być przechowywany gdzie indziej, w zależności od Twojej strategii przechowywania, ale jego ścieżka odniesienia musi przetrwać proces wyświetlania. W przeciwnym razie cytaty prędzej czy później staną się uciążliwe.
Nie przeszukuj całego korpusu, chyba że jest to konieczne
To może być najważniejsza koncepcja w całym tym projekcie. Weźmy pytanie: „Co zmieniło się w naszej europejskiej polityce zwrotów w tym kwartale?”. Proste rozwiązanie polegałoby na przeszukaniu wszystkich 3 000 000 000 wektorów. Jednak samo pytanie już wiele nam mówi:
organization = current tenant
region = Europe
topic = refund policy
time = current quarter
permissions = documents user may access
Architektura wyszukiwania dźwięków agresywnie wykorzystuje te ograniczenia, zmniejszając miliardy fragmentów do niewielkiej ich części przed rozpoczęciem jakiejkolwiek wyszukiwania podobieństwa. Dokładne liczby zależą od obciążenia, więc należy je traktować jako przykłady. Zasada pozostaje jednak ta sama: najpierw kierowanie zapytaniem, potem pobieranie danych.
Sharding sprawia, że warstwa wektorowa jest rozproszona
W pewnym momencie jeden węzeł nie jest w stanie przechować całego korpusu podlegającego wyszukiwaniu, więc indeks musi zostać podzielony. Rozproszone silniki wektorowe, takie jak Qdrant, skalują się horyzontalnie poprzez rozpraszanie punktów między fragmenty, a replikacja umieszcza kopie tych fragmentów na kilku węzłach w celu zapewnienia dostępności i większej przepustowości odczytu.
Jest jednak jeden haczyk. Jeśli każde zapytanie musi trafić do każdego fragmentu, dodawanie kolejnych fragmentów ostatecznie zwiększa pracę koordynacyjną zamiast ją zmniejszać. Lepszy projekt umożliwia kierowanie jak największej liczbie zapytań. Takie atrybuty mogą wskazać, które fragmenty faktycznie muszą brać udział:
tenant_id
region
document namespace
language
time partition
To przynosi największe korzyści w systemach wielu najemców. Zamiast
Query
↓
128 shards
zapytanie może zostać przetworzone
tenant_id = 429
↓
Shard group 18
↓
4 shards
Obecny rozproszony model Qdrant, jako przykład, umożliwia użytkownikowi definiowanie sposobu dzielenia danych na fragmenty, dzięki czemu operacje mogą być skierowane na wybrane klucze fragmentów i uniknąć modyfikacji całej kolekcji. Ta zasada jest ważna niezależnie od dostawcy – dobre podziałanie danych zmniejsza liczbę zapytań wysyłanych do różnych części systemu.
Replikacja odpowiada na inne pytanie
Ludzie mylą pojęcia dzielenia danych na fragmenty z replikacją, choć rozwiązują one odrębne problemy. Dzielenie danych na fragmenty dotyczy sposobu podziału zbyt dużych zbiorów danych na części dostępne dla pojedynczej maszyny. Replikacja natomiast dotyczy tego, co się dzieje, gdy jedna z tych maszyn przestaje funkcjonować. Wyobraźmy sobie taką strukturę:
Shard A → Node 1
Shard B → Node 2
Shard C → Node 3
Jeśli węzeł 2 przestanie działać, razem z nim znika część korpusu. Przy replikacji:
Shard A → Node 1 + Node 4
Shard B → Node 2 + Node 5
Shard C → Node 3 + Node 6
System nadal odpowiada na zapytania, nawet gdy poszczególne węzły przestają funkcjonować. Redundancja wiąże się z kosztami przechowywania, przepustowością do zapisu oraz wysiłkiem operacyjnym, ale klastr wyszukiwawczy w produkcji bez niej ma kilka kłopotliwych sposobów awarii.
Samo wyszukiwanie wektorowe jest niewystarczające
Załóżmy, że ktoś pyta: „Co mówi polityka HR-48291?”. Wyszukiwanie semantyczne może zwrócić materiały dotyczące ogólnych polityk HR, podczas gdy wyszukiwanie leksykalne doskonale radzi sobie z odnalezieniem dokładnego tokena HR-48291. Teraz spróbujmy inaczej: „Czy pracownicy mogą wziąć dodatkowy urlop po adoptacji dziecka?”. W tym przypadku wyszukiwanie semantyczne okazuje się przydatne. Obie metody się pokrywają, ale obejmują różne obszary, dlatego zwykle wybiera się hybrydową fazę wyszukiwania, która łączy obie metody.
Obecne wytyczne Elastic dotyczące hybrydowego wyszukiwania podążają tą samą drogą, łącząc wyszukiwanie leksykalne z wektorowym oraz proponując Reciprocal Rank Fusion jako sposób na połączenie wyników sortowania. Zaletą RRF jest to, że nie trzeba udawać, iż wynik BM25 oraz podobieństwo kosinusowe to porównywalne wartości – zamiast tego łączy się pozycje w rankingu. W uproszczonej formie:
RRF(d) = Σ 1 / (k + rank_i(d))
gdzie rank_i(d) to pozycja dokumentu d w systemie wyszukiwania i.
Uważaj przy sekwencyjnym prefiltrowaniu
Istnieje kusząca skrótka:
3B documents
↓
BM25 returns 10,000
↓
vector search only those
Może być skuteczny przy niektórych obciążeniach, ale może również zniszczyć semantyczne odzyskiwanie informacji. Wyobraź sobie użytkownika wpisującego „Jakie zasady obowiązują pracowników, którzy adoptują?”, a dokument zawierający stwierdzenie: „Pracownicy, którzy przyjmują dziecko poprzez adopcję, mają prawo do szesnastu tygodni urlopu rodzicielskiego”. Pokrycie słów kluczowych z sformułowaniem użytkownika jest niewielkie. Jeśli BM25 odrzuci ten dokument, zanim semantyczny mechanizm odzyskiwania go zdąży go przetworzyć, żaden model embeddingu nie będzie mógł mu pomóc.
Dlatego, po uwzględnieniu surowych ograniczeń routingu, lepszym rozwiązaniem jest generowanie kandydatów równolegle za pomocą obu mechanizmów odzyskiwania, a następnie ponowne rankowanie połączonej listy. Same surowe ograniczenia stanowią odrębną kategorię:
tenant
permissions
document status
Zazwyczaj należy to wdrożyć przed rozpoczęciem pobierania.
Uprawnienia są wymagane, zanim LLM cokolwiek zobaczy
Wyobraź sobie korporacyjny korpus zawierający:
Engineering documents
HR documents
Board documents
Legal documents
Executive compensation
Customer contracts
Relevancja semantyczna nie jest upoważnieniem. Dokument dyrektora finansowego może być matematycznie najbliższym odpowiednikiem pytania pracownika, ale to nadal nie daje pracownikowi prawa do jego przeczytania. Błędny projekt wygląda w ten sposób:
Vector Search
↓
Retrieve confidential chunk
↓
Send chunk to LLM
↓
Check permission
Zanim zostanie przeprowadzona weryfikacja uprawnień, tekst poufny został już przeniesiony do komponentu poniżej w łańcuchu przetwarzania. Poprawny tok działań najpierw określa, kim jest użytkownik:
User
↓
Identity
↓
Groups / roles / ACL
↓
Authorized search space
↓
Retrieval
To oznacza, że metadane kontrolujące dostęp muszą skutecznie uczestniczyć w procesie wyszukiwania. W bardzo dużych skali schemat ACL, który po wyszukiwaniu musi przeszukać miliony rekordów, staje się zarówno problemem bezpieczeństwa, jak i problemem wydajności.
Wyszukiwanie dostarcza kandydatów, a nie kontekstu
Załóżmy, że hybrydowe wyszukiwanie daje 400 kandydatów. Nie umieszczaj wszystkich 400 elementów w zapytaniu. Najpierw uporządkuj je według ważności:
Vector candidates: 200
Lexical candidates: 200
│
▼
Fusion
│
▼
~250 unique chunks
│
▼
Reranker
│
▼
top 20–40
Pierwszy etap wyszukiwania jest zaprojektowany tak, aby szybko znaleźć rzeczy prawdopodobnie istotne. Narzędzie do ponownego rankowania służy do zupełnie innego zadania: na podstawie znacznie mniejszego zbioru dokonuje dokładniejszej oceny istotności. Ten dwuetapowy model umożliwia stosowanie kosztownego modelu istotności do setek elementów zamiast miliardów.
Następnie usuwa się redundancje
Nawet dobre narzędzie do ponownego rankowania może dać coś w rodzaju:
Chunk 1 → page 14
Chunk 2 → page 14
Chunk 3 → page 15
Chunk 4 → page 14
Chunk 5 → page 15
Wszystkie pięć może przekazywać w zasadzie tę samą informację, a wysyłanie ich wszystkich marnuje tokeny kontekstowe. Zanim stworzysz ostateczne polecenie, zastosuj pewną kombinację następujących kroków:
deduplication
document diversity
section diversity
near-duplicate detection
adjacent-chunk merging
token budgeting
Czasami odpowiedź wymaga również swoich sąsiadów. Jeśli fragment 47 zawiera rozwiązanie, ale zaczyna się w środku wyjaśnienia, narzędzie do budowania kontekstu może je pobrać
chunk 46
chunk 47
chunk 48
Zamiast mieć nadzieję, że wyszukiwanie wektorowe samodzielnie odnajdzie każdy ciągły fragment.
Budowa kontekstu wymaga odrębnego warstwy
Zaskakująco powszechna implementacja traktuje kontekst jedynie w ten sposób:
context = "\n\n".join(retrieved_chunks)
Systemy produkcyjne funkcjonują lepiej przy użyciu dedykowanego budowniczego kontekstu. Jego zadaniem jest przyjęcie uporządkowanych dowodów i stworzenie efektywnego ciężaru danych promptu bez utraty informacji o cytatach. Koncepcyjnie:
Retrieved Evidence
↓
Deduplicate
↓
Merge related chunks
↓
Enforce token budget
↓
Preserve source IDs
↓
Order evidence
↓
Generate context
Model może wtedy otrzymać coś o strukturze podobnej do tej:
SOURCE S1
Document: Employee Handbook
Page: 42
Version: 18
Text: ...
SOURCE S2
Document: European Leave Addendum
Page: 7
Version: 4
Text: ...
QUESTION
...
i proszono o podanie źródeł w postaci S1, S2 itd. To o wiele bezpieczniejsze niż proszenie modelu o odtworzenie adresów URL lub identyfikatorów dokumentów z pamięci.
Rozumienie zapytań powinno być szybkie
Część zapytań odnosi korzyści ze standaryzacji przed wyszukiwaniem. Użytkownik wpisuje „O ile wzrosły przychody w Europie w zeszłym roku?”. Biorąc pod uwagę dzisiejszą datę oraz własną terminologię firmy, wyszukiwanie może przynieść lepsze wyniki przy użyciu strukturyzowanych danych wyekstrahowanych z tego zdania:
metric = revenue
region = Europe
year = 2025
intent = financial comparison
W rozmowie taki pytanie następcze jak „A co z Azją?” wymaga przepisania z uwzględnieniem wcześniejszego kontekstu:
How much did revenue grow in Asia in 2025?
Celem projektowania jest to, aby te kroki rzadko wymagały użycia najdroższego modelu analitycznego. Należy użyć najtańszego komponentu, który niezawodnie wykona zadanie. Dojrzała architektura AI zazwyczaj wygląda w ten sposób:
small model
↓
classification
small model
↓
query rewriting
embedding model
↓
retrieval
reranker
↓
relevance
large model
↓
final reasoning
a nie tak:
largest model everywhere
Rozbicie na części w przypadku pytań wieloetapowych
Część pytań nie może zostać odpowiedziana wiarygodnie na podstawie pojedynczego tekstu. Pytanie takie jak „Który europejski produkt odnotował największy spadek przychodów i jak zarząd to wytłumaczył?” dzieli się na dwa poszukiwania:
Question 1:
Which European product had the largest decline?
Question 2:
What explanation did management provide for that product?
Następnie system może zbierać dowody dla każdego z pytań podrzędnych osobno, a wyniki są łączone przed generowaniem odpowiedzi.
Complex Query
│
▼
Query Decomposer
/ \
/ \
▼ ▼
Subquery A Subquery B
│ │
▼ ▼
Retrieval Retrieval
\ /
\ /
▼ ▼
Evidence Join
│
▼
LLM
W tym momencie RAG na dużą skalę zaczyna przypominać system planowania zapytań raczej niż prosty wyszukiwacz najbliższych sąsiadów.
Szczegółowość wpływa na sposób indeksowania
Załóżmy, że korpus zawiera 100 milionów dokumentów, z których 2 miliony zmieniają się każdego dnia. Przeprowadzanie ponownego indeksowania co noc prawdopodobnie nie jest tym, czego chcesz, dlatego potrzebujesz indeksowania inkrementalnego:
Document updated
│
▼
new document version
│
▼
parse changed document
│
▼
generate new chunks
│
▼
embed
│
▼
write new index entries
│
▼
mark previous version inactive
Logiczne wersjonowanie jest lepsze od natychmiastowej destruktywnej zastąpienia. Na przykład w przypadku jednego dokumentu:
document_id = D17
version 41 → status=inactive
version 42 → status=active
Filtr zapytań według
status = active
Stare wpisy mogą być usuwane w tle za pomocą mechanizmu garbage collection. Z punktu widzenia aplikacji aktualizacja jest przeprowadzana atomowo, bez konieczności, by każdy system przechowywania zmienił się w tym samym momencie.
Wersjonuj również modele
Prędzej czy później zmienisz modele embeddingowe, więc nigdy nie powinieneś przechowywać wektorów, nie wiedząc, co je wygenerowało. Przynajmniej zachowaj informacje umożliwiające ich śledzenie:
embedding_model
embedding_version
embedding_dimensions
chunking_version
parser_version
W przeciwnym razie, po sześciu miesiącach, możesz odkryć, że 15% indeksu zostało utworzone przy użyciu starszego procesu, a nie ma łatwego sposobu, aby określić, które to 15%. Migracja może być przeprowadzana na indeksach równoległych:
Documents
│
┌─────────┴─────────┐
▼ ▼
embedding model V3 embedding model V4
│ │
▼ ▼
Index V3 Index V4
│
▼
shadow traffic
│
▼
evaluate
│
▼
switch alias
Nie przesuwaj miliardów wektorów i nie polegaj tylko na tym, że nowe wyszukiwanie będzie lepsze. Zmierz jego efektywność przed zmianą.
Pamięć cache na kilku poziomach
Nie każde zapytanie wymaga całego procesu. Jeśli tysiące pracowników pyta „Ile mamy dni urlopowych?”, uruchamianie
embedding
→ distributed retrieval
→ reranking
→ large LLM
Każdy taki przypadek jest marnotrawstwem. Bardziej sensowne jest użycie wielowarstwowego systemu pamięci podręcznych:
User Query
│
▼
Exact Response Cache
│ miss
▼
Semantic Cache
│ miss
▼
Retrieval Cache
│ miss
▼
Search
Oczywistym towarzyszem jest bufor embeddingów:
hash(normalized_query)
↓
query embedding
Sytuacja staje się skomplikowana, gdy treść ulega zmianie. Odpowiedź zlokalizowana w pamięci podręcznej
Policy version 17
może raz pójść nie tak
Policy version 18
Wchodzi w tryb działania. Klucz cache’u lub schemat unieważniania muszą zatem znać granice korpusu i wersji. Przechowywanie wyników modelu bez zasad aktualności jest niebezpieczne.
Planuj awarie, a nie tylko sukcesy
Wielkie systemy awariują stopniowo. Spodziewaj się takich sytuacji jak:
embedding provider unavailable
one vector shard unavailable
search cluster degraded
reranker timeout
LLM rate limited
LLM provider outage
queue backlog growing
parser crashing on malformed PDF
OCR worker running out of memory
Każda awaria wymaga wyraźnej polityki. Być może:
Vector search fails
↓
Can lexical search answer?
↓
yes
↓
return degraded retrieval mode
lub:
Primary LLM unavailable
↓
Fallback model
↓
Lower quality but service continues
lub:
Document parser fails 5 times
↓
Dead-letter queue
↓
record failure reason
↓
operator / automated remediation
Bezkresne próby ponownego wykonania nie są oznaką odporności. Czasami jest to po prostu nieskończony cykl z dodatkowymi kosztami w rachunku chmurowym.
Backpressure
Załóżmy, że proces pobierania danych odbywa się normalnie
20M chunks/hour
a pojemność wgrzewania może obsłużyć
25M chunks/hour
Wszystko w porządku. Następnie klient przeprowadza migrację i uzyskuje
200M chunks/hour
Bez ciśnienia zwrotnego awaria przebiega w następujący sposób:
embedding service overloaded
↓
timeouts
↓
retries
↓
more load
↓
more failures
↓
retry storm
Z buforem przed pracownikami:
incoming workload
↓
durable queue
↓
workers consume at safe rate
↓
backlog temporarily grows
Możliwość przetwarzania danych może teraz być skalowana niezależnie od tempa przychodzenia. To kolejny powód, dla którego warstwa przetwarzania danych wymaga własnej architektury.
Obserwowalność musi obejmować jakość pobierania danych
Zwykłe raporty monitoringu pokazują między innymi:
HTTP 200 rate
CPU
memory
disk
p95 latency
error rate
requests/second
Wszystko to jest przydatne, ale usługa typu RAG może zwrócić kod HTTP 200 w ciągu 800 ms i mimo to dostarczyć całkowicie błędną odpowiedź. Potrzebna jest również telemetria specyficzna dla AI. Przydatny zapis śledzenia za każdym żądaniem może wyglądać tak:
request_id: rq_912
query rewrite:
"parental policy?" → "parental leave policy"
vector retrieval:
200 candidates
145 ms
lexical retrieval:
200 candidates
91 ms
fusion:
278 unique candidates
reranking:
278 → 20
212 ms
context:
13 chunks
8,420 tokens
LLM:
input 9,104 tokens
output 681 tokens
1.8 sec
citations:
3 documents
total:
2.4 sec
Ocena offline wymaga zatem wskaźników, które odpowiadają na pytania takie jak: czy odzyskano właściwe dowody, czy miały one wystarczająco wysoką pozycję, czy odpowiedź była poparta tymi dowodami, czy ponowna sortacja wystarczająco poprawiła zapytania, by usprawiedliwić jej opóźnienie, oraz które kategorie zapytań najczęściej zawodzą? To jest bardziej praktyczne niż śledzenie halucynacji jako jednego globalnego procentu.
Oceniaj odzyskiwanie danych oddzielnie od generowania
Gdy ostateczna odpowiedź jest błędna, istnieją co najmniej dwie odrębne kategorie awarii. W przypadku awarii odzyskiwania danych potrzebne dowody nigdy nie trafiają do kontekstu:
Correct document exists
↓
retriever misses it
↓
LLM never sees it
↓
wrong answer
Żadna modyfikacja promptu tego nie naprawi. W przypadku niepowodzenia generacji odpowiednie dowody faktycznie się pojawiły:
correct evidence
↓
context
↓
LLM
↓
incorrect interpretation
Teraz znaczenie może mieć projekt promptu, wybór modelu, organizacja kontekstu lub strategia rozumowania. Jeśli oceniasz tylko ostateczne odpowiedzi, obie wyglądają identycznie, dlatego rzetelny proces oceny musi sprawdzić etapy pośrednie.
Przydziel budżet na opóźnienie
„System musi być szybki” nie jest wymogiem. Zamiast tego przydziel budżet każdemu etapowi. Dla ilustracji załóżmy, że celem jest pierwsza odpowiedź w ciągu około 2,5 sekundy, z takim podziałem:
API + auth 50 ms
query understanding 100 ms
retrieval 300 ms
fusion 20 ms
reranking 250 ms
context construction 30 ms
LLM time-to-first-token 1,200 ms
network / safety margin 550 ms
-----------------------------------
target ~2,500 ms
To nie są uniwersalne liczby. Ten test jest ważny, ponieważ pokazuje, dokąd faktycznie zmierza czas. Jeśli pobieranie danych zajmuje 1,8 sekundy, zmiana dostawcy LLM nie wpłynie na główny wąskie gardło. Jeśli LLM odpowiada za 90% opóźnienia, poświęcenie miesiąca na skrócenie o 40 ms czasu wyszukiwania wektorowego raczej nie będzie najlepszym wykorzystaniem wysiłku.
Koszty wymagają tej samej dyscypliny
Wielki system RAG ma wiele oddzielnych ośrodków kosztowych:
object storage
document parsing
OCR
embedding generation
vector storage
vector replicas
lexical search
metadata database
network transfer
reranking
LLM input tokens
LLM output tokens
observability
backups
Zatem oblicz koszt na jednostkę użyteczną, na przykład:
cost / 1,000 indexed documents
cost / million chunks
cost / query
cost / successful answer
cost / tenant
Ostatnia z tych kwestii ma największe znaczenie w SaaS. Możesz odkryć:
Tenant A
5% of revenue
42% of LLM spend
Bez pomiaru zużycia dla poszczególnych użytkowników widzi się jedynie nieoczekiwanie wysoką miesięczną rachunek za infrastrukturę.
Używaj drogiego przechowywania tylko tam, gdzie jest to konieczne
Przydatnym obrazem do zrozumienia jest gradient od drogiego i szybkiego do taniego i pojemnego:
expensive / fast
▲
│
vector indexes
search indexes
Redis caches
│
metadata DB
│
object storage
│
▼
cheap / large
Pełny plik PDF nie ma powodu, by znajdować się w indeksie wektorowym. Zachowaj drogie miejsce przechowywania tylko dla tych danych, które wymagają wyszukiwania – niech system przechowywania obiektów przechowuje oryginalne dokumenty, a infrastruktura wyszukiwania – ich zapisy nadające się do wyszukiwania. Gdy korpus osiągnie setki terabajtów, ta różnica staje się niezwykle ważna.
Dane gorące i zimne
Nie wszystkie 100 milionów dokumentów jest wyszukiwanych w równym stopniu. Być może:
Last 30 days → 70% of queries
Last 12 months → 25%
Older archive → 5%
Traktowanie wszystkiego tak samo marnuje infrastrukturę. Jedną z opcji jest podział w ten sposób:
Query
│
├────→ Hot index
│
└────→ Archive index when necessary
Routing oparty na czasie zapobiega również temu, by pytania dotyczące wydarzeń z tego tygodnia łączyły się z dziesięcioma latami historycznych danych. Duże systemy stają się wydajne poprzez unikanie pracy, a nie tylko poprzez szybsze wykonywanie niepotrzebnych zadań.
Multi-tenancy zmienia wszystko
Załóżmy, że twoja platforma obsługuje
10,000 organizations
Prawie na pewno nie chcesz tego
10,000 completely independent vector clusters
Jednak umieszczanie wszystkich najemców w jednym przestrzeni nazw powoduje problemy z hałaśliwymi sąsiadami oraz trudności z autoryzacją. Skutecznym rozwiązaniem jest grupowanie najemców według ich wielkości:
Small tenants
↓
shared shard groups
Medium tenants
↓
partitioned shared infrastructure
Very large tenants
↓
dedicated shard groups
To zapobiega sytuacji, w której jeden klient importuje 500 milionów fragmentów, przytłaczając tysiące mniejszych, a jednocześnie zapewnia drogę do izolacji dla klientów korporacyjnych, którzy tego potrzebują. Żadna strategia podziału na tenanty nie pasuje wszystkim.
LLM znajduje się pod koniec
Zobacz, ile tematów inżynieryjnych omówiliśmy przed dotarciem do modelu. To celowe. Zapytanie typu RAG w stanie dojrzałym przepływa w ten sposób:
User
↓
API Gateway
↓
Authentication
↓
Authorization
↓
Rate Limiter
↓
Query Understanding
↓
Shard Routing
↓
Hybrid Candidate Retrieval
↓
Rank Fusion
↓
Reranker
↓
Deduplication
↓
Context Builder
↓
LLM
↓
Citation Validation
↓
Response
LLM jest ważne, ale przy 100 milionach dokumentów stanowi tylko jeden z wielu elementów.
Pełny obraz produkcji
Łącząc to wszystko, oto ścieżka zapytania, następnie ścieżka pobierania danych, a potem systemy, które obsługują obie:
CLIENTS
│
▼
┌──────────────┐
│ API Gateway │
└──────┬───────┘
│
┌──────▼───────┐
│ Auth / ACL │
│ Rate Limits │
└──────┬───────┘
│
▼
┌────────────────────┐
│ Query Orchestrator │
└─────────┬──────────┘
│
┌─────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Cache Query Rewrite Metadata
Filters
│ │ │
└─────────────┼──────────────┘
▼
Shard Router
│
┌────────────┴────────────┐
▼ ▼
Lexical Search Vector Search
│ │
└───────────┬─────────────┘
▼
Rank Fusion
│
▼
Reranker
│
▼
Deduplicate
│
▼
Context Builder
│
▼
LLM
│
▼
Citation Validation
│
▼
RESPONSE
================================================================
INGESTION SYSTEM
Documents
│
▼
Object Store
│
▼
Event Queue
│
┌────────────┼────────────┐
▼ ▼ ▼
Parser Parser Parser
│ │ │
└────────────┼────────────┘
▼
Normalization
│
▼
Chunker
│
┌──────────────┴──────────────┐
▼ ▼
Embedding Queue Text Index Queue
│ │
┌─────┼─────┐ ▼
▼ ▼ ▼ Lexical Index
GPU GPU GPU
│ │ │
└─────┼─────┘
▼
Vector Index
================================================================
SUPPORTING SYSTEMS
Metadata DB → document state, versions, ownership
Object Storage → original source documents
Vector Cluster → semantic retrieval
Search Cluster → lexical retrieval
Redis → caches and rate limiting
Message Queue → asynchronous ingestion
Observability → traces, metrics, evaluation
Secrets / IAM → service authorization
Evaluation System → retrieval + answer-quality tests
Antypaternale, których należy unikać
Przy analizie takiej architektury następujące elementy wzbudzają natychmiastowe zaniepokojenie:
One giant vector collection with no routing strategy
Synchronous PDF ingestion
Only vector retrieval, no lexical search
ACL filtering after retrieval
No document versioning
No embedding model version
No reranking
Sending top-100 chunks directly to the LLM
Using the LLM for every trivial classification task
No dead-letter queue
No retrieval-level evaluation
No tenant-level cost attribution
Treating the vector DB as permanent document storage
Re-embedding the entire corpus for every small content change
Benchmarking only average latency instead of tail latency
Każdy z nich może stanowić prototyp. Razem, w skali miliardów wektorów, stają się niezwykle uciążliwe.
Zasada podstawowa
Najlepszym sposobem na rozumienie dużych systemów RAG nie jest ten:
question
→ vector search
→ LLM
To jest to:
billions of possible evidence units
↓
routing constraints
↓
relevant partitions
↓
cheap candidate search
↓
hundreds of items
↓
expensive reranking
↓
tens of passages
↓
context optimization
↓
LLM
↓
grounded answer
System funkcjonuje jak lejek. Na każdym etapie inwestujemy więcej zasobów obliczeniowych w coraz mniejszą liczbę kandydatów:
Cheap operation → huge search space
Moderate operation → smaller candidate set
Expensive operation → tiny candidate set
Most expensive model → final context only
To właśnie ta koncepcja utrzymuje architekturę w warunkach ekonomicznej wykonalności.
Co wynieść z tego
System RAG oparty na kilku tysiącach dokumentów to przede wszystkim ćwiczenie z inżynierii aplikacji. Przy 100 milionach dokumentów staje się to infrastrukturą wyszukiwania, systemami rozproszonymi, inżynierią danych, kwestiami bezpieczeństwa i kontrolą kosztów – wszystko to wymaga jednoczesnej uwagi. Baza wektorowa ma znaczenie, podobnie jak model embedding i LLM, ale architektura wokół nich jest ważniejsza, niż przyznają większość poradników.
Model mentalny, który się skaluje: przechowuj źródła tanio, przetwarzaj asynchronicznie, twórz wersje wszystkiego, celowo dziel dane, wcześnie wdrażaj mechanizmy autoryzacji, pobieraj informacje za pomocą kilku sygnałów, trudno oceniaj rankingi, dostarczaj modelowi wyłącznie wysokiej jakości dane i mierz każdy etap oddzielnie. Gdy zbiór danych osiągnie miliardy fragmentów, najlepszą optymalizacją nie jest już przyspieszenie wyszukiwania – chodzi o takie ułożenie elementów, aby większa część systemu nigdy nie miała kontaktu z większą częścią zbioru danych.