RAG w czasie zapytania przy dziesięciu milionach dokumentów: lejki, routery, agenci i oceny
RAG w dziesięciu milionach dokumentów: mechanizm wyszukiwania działający w mniej niż 200 ms z filtrami SQL, hybrydowym wyszukiwaniem i ponownym rankowaniem, a także routowaniem zapytań, ich oceną oraz skryptem Qdrant.
Źródła: Scaling RAG to 10 Million Documents Part 2: Optimizing retrieval and generation
Umieszczenie dziesięciu milionów dokumentów w bazie danych wektorowej to zaledwie połowa pracy. Pierwsza część tego tematu dotyczyła procesu wprowadzania danych: routerów dokumentów takich jak Docling, Unstructured i Apache Tika, dzielenia plików z uwzględnieniem ich struktury, indeksowania typu delta przy użyciu MinHash oraz projektu z podwójnym przechowywaniem, który umożliwia indeksowanie rzeczywistych plików bez uszkadzania danych ani zwiększania kosztów.
Druga część dotyczy czasu wykonywania zapytań. Miliony fragmentów danych nie przyniosą żadnych rezultatów, jeśli wyszukiwanie zajmie osiem sekund, będzie blokowane przez określone kody błędów, ujawni poufne dane płacowe lub pozwoli na infekcję pliku złośliwym kodem ukrytym w starym pliku. Dalej omówiono mechanizm wyszukiwania, który odpowiada w mniej niż 200 ms, inteligentne triaging zapytań, orkiestrację z użyciem agentów, ciągłą ocenę oraz kompletny skrypt.
1. Wieloetapowy mechanizm wyszukiwania
Typowy przewodnik radzi wgranie zapytania użytkownika i przeprowadzenie wyszukiwania podobieństwa z użyciem top_k=5 w całej bazie danych. Przy dziesięciu milionach dokumentów takie podejście stwarza dwa poważne problemy.
- Gęste embeddingi dobrze oddają abstrakcyjne znaczenia, ale zawodzą przy dokładnych słowach kluczowych, numerach części alfanumerycznych oraz kodach błędów. Wyszukiwanie
Error 504 Gateway Timeoutmoże zwrócić ogólne informacje na temat czasu oczekiwania, a nie dokładną stronę z rozwiązaniami problemów. - Ocena każdego kandydata za pomocą skomplikowanego modelu relewantności jest zbyt wolna do użycia w interaktywnych aplikacjach.
Dlatego systemy produkcyjne traktują proces wydobywania informacji jako lejek, który krok po kroku zawęża listę kandydatów:
[ 10,000,000 Total Document Chunks ]
│
▼
[ Step 1: SQL Pre-Filter ] ────────► Filter by Tenant / Dept / Role / Region
│
▼
[ ~50,000 Candidates ]
│
▼
[ Step 2: Hybrid Search ] ─────────► Dense Vectors (Qdrant) + Sparse BM25
│
▼
[ Top 100 Candidates ]
│
▼
[ Step 3: Cross-Encoder ] ───────► Cohere Rerank / BGE-Reranker
│
▼
[ Final Top 5 Chunks ] ──────────► Passed to LLM Context Window
Etap 1: wstępne filtrowanie relacyjne
Zanim zostaną zastosowane jakiekolwiek operacje matematyczne o wysokiej wymiarowości, filtry metadanych oparte na roli użytkownika, dziale lub stanie dokumentu zmniejszają przestrzeń wyszukiwania. Spadek liczby elementów z dziesięciu milionów do około 50 000 zmniejsza opóźnienia i jednocześnie zapewnia kontrolę dostępu:
from qdrant_client.models import Filter, FieldCondition, MatchValue
# Restrict search space by user session permissions before distance scoring
user_access_filter = Filter(
must=[
FieldCondition(key="department", match=MatchValue(value="Engineering")),
FieldCondition(key="is_active", match=MatchValue(value=True))
]
)
Etap 2: wyszukiwanie hybrydowe
Rzeczywiste wyszukiwania występują w dwóch uzupełniających się formach, a zrozumienie obu przed przyjrzeniem się algorytmowi fuzji jest pomocne.
Wyszukiwanie gęste lub semantyczne przekształca tekst w wektory w bazie danych wektorowej, umieszczając słowa o podobnym znaczeniu blisko siebie. Dobrze radzi sobie z parafrazami, synonimami i konwersacyjnym stylem wypowiedzi, choć może przeoczyć dokładne słowo kluczowe i zwrócić dokumenty, które jedynie dzielą wspólne pojęcie.
Wyszukiwanie rzadkie lub oparte na słowach kluczowych, wykorzystujące algorytmy takie jak BM25 lub starszy TF-IDF, ocenia termin według częstotliwości jego występowania w dokumencie w stosunku do tego, jak rzadki jest on w całym korpusie. Skutecznie wykrywa dokładne słowa, ale nie wie nic o ich znaczeniu.
Kuszącą odpowiedzią jest uruchomienie obu metod i dodanie ich wyników, co prowadzi do konieczności zastosowania techniki Reciprocal Rank Fusion. RRF łączy kilka uporządkowanych list w jedną spójną listę bez konieczności normalizacji surowych wyników. Dokument, który znajduje się blisko góry obu rankingów, trafia na czoło połączonej listy.
Etap 3: ponowne uporządkowanie za pomocą cross-encodera
Hybrydowe wyszukiwanie szybko dostarcza około stu kandydatów, ale uporządkowanie ich wyłącznie na podstawie odległości wektorowej może nadal doprowadzić do pojawienia się nieistotnych wyników. Dwa sposoby kodowania wyjaśniają, jak to naprawić.
Bi-encoder to sposób, w jaki działa zwykłe wyszukiwanie wektorowe. Dokumenty przekształcają się w wektory podczas przetwarzania danych, a zapytanie użytkownika staje się kolejnym wektorem w momencie wyszukiwania; proste metody geometryczne, takie jak podobieństwo kosinowe lub iloczyn skalarny, służą do porównywania tych wektorów. Jest to szybkie, ale tylko średnio dokładne.
Kodownik krzyżowy czyta zapytanie i dokument razem i wyprowadza wynik określający, na ile ten dokument jest istotny dla danego zapytania. Jest on dokładny, ale dlatego wolny – ocena miliona dokumentów przy jednym wyszukiwaniu mogłaby zająć godziny. Rozwiązaniem jest połączenie obu metod. Kodownik dwukierunkowy wybiera 100 najlepszych kandydatów, a następnie kodownik krzyżowy przestawia je na dziesięć najlepszych, co zajmuje mniej niż 100 ms.
2. Warunkowy router zapytań
Większość interfejsów czatowych to zwykłe pole tekstowe, więc użytkownicy mogą wpisywać cokolwiek. Wiadomości takie jak „streszcz naszą dotychczasową rozmowę” lub „przetłumacz wyniki na niemiecki” w ogóle nie wymagają wyszukiwania w dokumentach, a przesyłanie ich przez cały proces marnuje czas i pieniądze.
Warunkowy router znajduje się na wejściu jako szybki klasyfikator, który decyduje, dokąd trafi każda wiadomość:
User Query
│
▼
[ Intent Classifier / Router ]
│
├── Simple Math / Logic ──────────► Direct Calculator / Python REPL
├── Conversational / Follow-up ───► Direct LLM Memory Context
└── Domain Knowledge Request ─────► Full Hybrid RAG Pipeline
Koncepcyjnie mały prompt prosi model o sklasyfikowanie zapytania do jednej z kilku możliwych ścieżek, takich jak wyszukiwanie w wewnętrznej dokumentacji lub udzielenie bezpośredniej odpowiedzi:
# Conceptual Router Pattern
def route_query(user_query: str) -> str:
prompt = f"""Classify the user query into one of these routes:
- RETRIEVE: Needs internal company documentation/database lookup.
- COMPUTE: Pure math, calculation, or logic.
- DIRECT: Conversational, greetings, or basic language rewrites.
Query: {user_query}
Classification:"""
# Run a fast, lightweight classifier (or small local SLM)
decision = fast_classifier(prompt).strip()
return decision
3. Coś więcej niż pojedyncze zapytanie: orkiestracja i informacje zwrotne
Wiele rzeczywistych pytań wymaga kilku kroków rozumowania. Weźmy na przykład prośbę o porównanie danych sprzedaży w regionie APAC za rok 2025 z wytycznymi dotyczącymi zgodności z roku 2024 oraz wskazanie niezgodności. Zadanie to dzieli się na trzy podzadania: znalezienie i wydobycie danych sprzedaży z regionu APAC za rok 2025, pobranie wytycznych dotyczących zgodności z roku 2024 oraz sprawdzenie istnienia konfliktów pomiędzy nimi.
System jednorazowego zapytania zawodzi w takich przypadkach, ponieważ próbuje odpowiedzieć na wszystko w jednej prośbie. Agentic RAG rozwiązuje ten problem za pomocą frameworków orkiestracyjnych takich jak LangGraph lub CrewAI, które dzielą zadanie na skoordynowane zadania specjalistów. Planer może utworzyć kilku agentów, z których każdy wykorzystuje ten sam proces pobierania danych, aby zebrać swoją część dowodów:
[ Orchestrator / Planner ]
│
┌─────────────────┴─────────────────┐
▼ ▼
[ Researcher Agent ] [ Compliance Agent ]
• Retrieves 2025 Sales Data • Retrieves 2024 Regulations
• Extracts regional tables • Parses policy constraints
│ │
└─────────────────┬─────────────────┘
▼
[ Synthesis Agent ]
• Reconciles numbers
• Validates output consistency
│
Confidence Score Check
│ │
[ Low Confidence ] [ High Confidence ]
│ │
▼ ▼
Loop back & refine Final Guardrail Validation
Korekta samodzielna
Gdy system wyszukiwania zwraca fragmenty o niskich wynikach podobieństwa lub gdy agent syntetyzujący dostrzega sprzeczność, powinien on zareagować. Odrzuca słaby tekst, przepisuje zapytanie poprzez rozwinięcie skrótów lub wypróbowanie synonimów, a następnie ponownie wysyła zapytanie do silnika wyszukiwania, zamiast zwracać wymyśloną, ogólną odpowiedź. Agenci koordynują i podejmują decyzje, lecz bez solidnego pipeline’u RAG nie mają nic, z czym mogliby pracować.
4. Ciągła ocena
System produkcyjny wymaga ciągłego oceniania za pomocą zautomatyzowanych frameworków takich jak Ragas, TruLens czy DeepEval. Biorą one pod uwagę kilka wymiarów:
┌──► Faithfulness (Is the answer grounded in the retrieved chunks?)
RAG Evaluation ─┼──► Answer Relevance (Did it actually answer the user's prompt?)
├──► Context Recall (Did retrieval find all necessary reference chunks?)
└──► System Latency & Token Cost (Is it cost-effective at scale?)
Poza metrykami operacyjnymi, kilka wskaźników poziomu usługi mierzy efektywność procesu wydobywania informacji:
- Wierność to udział twierdzeń w odpowiedzi, które są poparte wydobytym kontekstem, i służy do sprawdzania możliwości halucynacji.
- Dokładność kontekstu to udział wydobytych fragmentów wśród wszystkich umieszczonych w zapytaniu, które są rzeczywiście istotne.
- Pamięć kontekstu sprawdza, czy wszystkie fakty niezbędne do odpowiedzi na pytanie zostały wydobyte.
- Opóźnienie, zarówno łączne, jak i na poszczególnych etapach, obejmuje router, filtr, wyszukiwanie hybrydowe, ponowną sortację oraz generowanie.
Każdy z tych wskaźników wymaga ustalonego standardu, aby można było sprawdzać zgodność jakości i wydajności.
5. Kompletny skrypt wydobywania i generowania
Poniższy skrypt w Pythonie zajmuje się fazą wykonywania. Zastosowuje wstępne filtrowanie metadanych, pobiera wektory z Qdrant, komponuje kontekst i wywołuje lokalny serwer LM Studio w celu przeprowadzenia inferencji:
from openai import OpenAI
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue
# 1. Initialize Clients
# Pointing to local LM Studio running on port 8080
ai_client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="lm-studio")
qdrant_client = QdrantClient(url="http://localhost:6333")
COLLECTION_NAME = "enterprise_knowledge_base"
EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5"
def retrieve_and_generate(user_query: str, user_department: str) -> str:
print(f"\n🔍 Processing query: \"{user_query}\" for department: [{user_department}]")
# 2. Vectorize the User Query
query_resp = ai_client.embeddings.create(
input=[user_query],
model=EMBEDDING_MODEL
)
query_vector = query_resp.data[0].embedding
# 3. Stage 1 & 2: SQL Pre-Filter + Vector Search
# Filter by user department and active document status
access_filter = Filter(
must=[
FieldCondition(key="department", match=MatchValue(value=user_department))
]
)
search_results = qdrant_client.search(
collection_name=COLLECTION_NAME,
query_vector=query_vector,
query_filter=access_filter,
limit=3
)
if not search_results:
return "No relevant or authorized documents found."
# 4. Context Assembly with Breadcrumbs
context_blocks = []
for hit in search_results:
breadcrumb = hit.payload.get("breadcrumb", "General")
text = hit.payload.get("text", "")
context_blocks.append(f"[{breadcrumb}]\n{text}")
full_context = "\n\n---\n\n".join(context_blocks)
# 5. Generation via Local LLM
system_prompt = (
"You are an enterprise technical assistant. "
"Answer the user query strictly using the provided context. "
"If the context does not contain the answer, explicitly state that you do not know.\n\n"
f"Context:\n{full_context}"
)
completion = ai_client.chat.completions.create(
model="local-model",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
],
temperature=0.1
)
return completion.choices[0].message.content
# Example Execution
if __name__ == "__main__":
response = retrieve_and_generate(
user_query="How do I enable TLS 1.3 in config.yaml?",
user_department="Engineering"
)
print("\n🤖 Final Answer:\n", response)
Kształt architektury produkcyjnej
Przejście od prototypu do systemu obsługującego miliony dokumentów korporacyjnych oznacza traktowanie RAG jako całościowej dyscypliny inżynieryjnej, a nie jako pojedynczego wywołania biblioteki. Cztery nawyki podsumowują to zjawisko:
- Przyjmuj dane w czystej formie. Struktura jest ważniejsza od pomysłowości, dlatego należy używać specjalistycznych parserów, zachowywać informacje o strukturze i unikać ponownego indeksowania niezmienionych plików za pomocą hashowania delta.
- Filtruj wcześnie. Zakresy bezpieczeństwa oraz metadane relacyjne powinny być przetwarzane w kroku SQL, który wykonywany jest przed kosztownymi obliczeniami wektorowymi.
- Mieszaj strategie wyszukiwania. łącz gęste wektory z dopasowywaniem słów kluczowych BM25 oraz narzędziem ponownego rankowania typu cross-encoder.
- Ochroniaj granice. kieruj dane warunkowo, aby uniknąć niepotrzebnych zapytań, stosuj zasady bezpieczeństwa dla danych wejściowych i wyjściowych oraz angażuj ludzi w działania o wysokim ryzyku.
Pozycje pokrewne
- Projektowanie RAG dla 100 milionów dokumentów: sharding, wyszukiwanie hybrydowe, koszty — Jakie zmiany zachodzą, gdy system RAG rośnie do miliardów fragmentów: asynchroniczne pobieranie danych, kierowanie ruchem, sharding, hybrydowe odzyskiwanie informacji, ponowna klasyfikacja, autoryzacja, buforowanie i kontrola kosztów.
- Jak debugować puste wyniki wyszukiwania, gdy system RAG nie zwraca żadnych danych — Lista kontrolna krok po kroku dla systemów RAG, które nie zwracają użytecznych fragmentów: pobieranie danych, progi oceny, filtry, przepisywanie zapytań, wyszukiwanie hybrydowe, ponowna klasyfikacja, rozwiązania awaryjne i telemetryka.