Головна / Статті

RAG у часі запиту з десятьма мільйонами документів: воронки, маршрутизатори, агенти та оцінки

RAG з десятьма мільйонами документів: механізм пошуку з часом відповіді менше 200 мс із фільтрами SQL, гібридним пошуком та переранжуванням, а також маршрутизацією запитів, оцінкою та скриптом Qdrant.

Видавець: REACTAPP.TOP8 хв читання1776 слів

Джерела: Scaling RAG to 10 Million Documents Part 2: Optimizing retrieval and generation

Завантаження десяти мільйонів документів у векторну базу даних — це лише половина роботи. У першій частині цієї теми йшлося про процес введення даних: маршрутизатори документів на кшталт Docling, Unstructured та Apache Tika, розбиття на частини з урахуванням формату, індексування за допомогою MinHash та концепція подвійного зберігання, яка дозволяє індексувати реальні файли без пошкодження даних чи збільшення витрат.

Друга частина присвятована часу виконання запитів. Мільйони вбудованих частин нічого не дають, якщо пошук займає вісім секунд, виникають проблеми через певні коди помилок, викриваються конфіденційні дані заробітної плати чи відбувається втрата інформації через вставку запиту в якийсь старий файл. У наступному розглядається механізм пошуку, який надає відповідь менш ніж за 200 мс, розумна сортувальна обробка запитів, оркестрація завдань, постійна оцінка ефективності та повний скрипт.

1. Багатоетапний механізм пошуку

У типових посібниках радять вбудувати запит користувача та провести пошук схожості за параметром top_k=5 у всій базі даних. При десяти мільйонах документів цей підхід створює дві серйозні проблеми.

  1. Щільні ембеддинги добре передають абстрактний зміст, але не ефективні для точних ключових слів, алфанумеричних номерів деталей та кодів помилок. Пошук за Error 504 Gateway Timeout може повернути загальні матеріали про тайм-аут, а не конкретну сторінку з вирішенням проблеми.
  2. Оцінка кожного кандидата за допомогою складної моделі релевантності є занадто повільною для інтерактивного використання.

Тому в продакшн-системах процес пошуку розглядається як воронка, яка поступово звужує список кандидатів:

[ 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

Етап 1: відносне попереднє фільтрування

Перш ніж починати будь-які обчислення високого розміру, фільтри метаданих за роллю користувача, відділом чи статусом документа зменшують простір пошуку. Зменшення кількості елементів з десяти мільйонів до, можливо, 50 000 скорочує затримку та водночас забезпечує контроль доступу:

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))
    ]
)

Етап 2: гібридний пошук

Реальні операції пошуку існують у двох взаємодоповнюючих формах, і варто зрозуміти обидві з них перед тим, як розглядати алгоритм їхнього поєднання.

Щільний або семантичний пошук перетворює текст на ембеддинги у векторній базі даних, розміщуючи слова з схожим значенням поруч одне з одним. Він добре справляється з парафразами, синонімами та розмовною формулюванням, хоча може пропустити точний ключовий слово та повернути документи, які лише поділяють певну концепцію.

Розріджений або ключовий пошук, за допомогою алгоритмів на кшталт BM25 чи старішого TF-IDF, оцінює термін за тим, наскільки часто він зустрічається в документі відносно того, наскільки рідкісним він є в усій корпусі даних. Він надійно виявляє точні слова, але не має жодної інформації про їхнє значення.

Привабливою відповіддю є запуск обох алгоритмів та підсумування їхніх оцінок, саме тут і входить у гру Рекурсивне злиття рангів. RRF об’єднує кілька рангованих списків у один єдиний список без необхідності нормалізації первинних оцінок. Документ, який знаходиться близько до верху обох рангівок, потрапляє на вершину об’єднаного списку.

Етап 3: переранжування за допомогою крос-кодеру

Гібридний пошук швидко дає близько сотні кандидатів, але їх порядок, визначений виключно за векторною відстанню, все одно може містити нерелевантні результати. Два способи кодування пояснюють, як це виправити.

Бі-кодер — це основа звичайного векторного пошуку. Документи перетворюються на вектори під час завантаження, запит користувача також стає вектором у момент пошуку, а прості геометричні методи, такі як косинусна схожість чи скалярний добуток, використовуються для їх порівняння. Цей підхід є швидким, але має лише середню точність.

Крос-енкодер читає запит та документ разом та виводить оцінку того, наскільки цей документ є релевантним для цього запиту. Він є ретельним, тому працює повільно, адже оцінка мільйона документів для одного пошуку може зайняти години. Рішенням є їх поєднання. Бі-енкодер знаходить 100 найкращих кандидатів, а потім крос-енкодер переупорядковує їх, залишаючи лише десять найкращих, що займає менше 100 мс.

2. Умовний маршрутизатор запитів

Більшість інтерфейсів чату є простими текстовими полями, тож користувачі можуть вводити що завгодно. Повідомлення на кшталт „Узагальніть нашу розмову до цього моменту“ чи „Перекладіть результати німецькою“ зовсім не потребують пошуку документів, а їх надсилання через повний процес марнує час та гроші.

Умовний маршрутизатор розташовується на вході як швидкий класифікатор, який вирішує, куди слід направити кожне повідомлення:

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

Концептуально, короткий запит просить модель класифікувати запитання за однією з кількох схем, таких як пошук у внутрішній документації чи пряма відповідь:

# 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. Більше, ніж просте пошук: оркестрація та зворотний зв’язок

Багато реальних запитань вимагають кількох кроків міркувань. Розгляньмо запит на порівняння показників продажів у регіоні APAC у 2025 році з правилами відповідності 2024 року та виявлення розбіжностей. Цей запит ділиться на три підзавдання: знаходження та вилучення даних про продажі у регіоні APAC за 2025 рік, отримання правил відповідності за 2024 рік та пошук суперечностей між ними.

Система одноразового оброблення тут не підходить, оскільки намагається відповісти на все в одному запиті. Агентні системи типу RAG вирішують цю проблему за допомогою фреймворків оркестрації, таких як LangGraph чи CrewAI, які розділяють завдання на координовані спеціалізовані кроки. Планувальник може створити кілька агентів, кожен з яких використовує ту саму систему пошуку для збору необхідних даних:

                  [ 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

Самокорекція

Коли система пошуку повертає фрагменти з низькими показниками схожості, або коли агент-синтезатор помічає суперечність, він повинен реагувати. Він відхиляє некоректний текст, переписує запит, розширюючи скорочення чи використовуючи синоніми, та знову звертається до двигуна пошуку, замість того щоб надавати вигадану універсальну відповідь. Агенти координують дії та приймають рішення, проте без міцної інфраструктури типу RAG у них немає нічого для роботи.

4. Постійна оцінка

Система для продакшну потребує постійної оцінки за допомогою автоматизованих фреймворків, таких як Ragas, TruLens чи DeepEval. Вони враховують кілька аспектів:

┌──► 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?)

Окрім операційних показників, кілька індикаторів рівня послуг вимірюють ефективність процесу отримання інформації:

  • „Вірність“ — це частка тверджень у відповіді, які підтримуються отриманим контекстом, і використовується для перевірки на наявність галюцинацій.
  • „Точність контексту“ — це частка отриманих фрагментів серед усіх, які були вказані у запиті, які є дійсно релевантними.
  • „Повернення контексту“ — це перевірка на те, чи були отримані всі факти, необхідні для відповіді на запитання.
  • „Затримка“ — як в цілому, так і на кожному етапі, враховує роботу маршрутизатора, фільтра, гібридного пошуку, системи переупорядкування та модуля генерації.

Кожен індикатор потребує визначеного базового рівня, щоб можна було перевіряти відповідність якості та продуктивності цим стандартам.

5. Повний скрипт отримання та генерації

Наведений нижче скрипт на Python відповідає за етап виконання. Він застосовує попередню фільтрацію метаданих, отримує вектори з Qdrant, формує контекст та звертається до локального сервера LM Studio для виконання обчислень:

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)

Форма архітектури виробництва

Перехід від прототипу до системи, яка обробляє мільйони корпоративних документів, означає розглядати RAG як цілісну інженерну дисципліну, а не просто як один виклик бібліотеки. Це можна узагальнити чотирма звичками:

  • Чисте отримання даних. Структура важливіша за винахідливість, тому використовуйте спеціалізовані парсери, зберігайте інформацію про структуру та уникайте повторного індексування незмінених файлів за допомогою дельта-хешування.
  • Фільтрація на ранній стадії. Критерії безпеки та реляційні метадані мають оброблятися на етапі SQL ще до початку дорогих векторних обчислень.
  • Поєднання стратегій пошуку. Поєднуйте щільні вектори зі збігом ключових слів BM25 та алгоритмом переупорядкування за допомогою крос-кодерів.
  • Захист меж. Умовно направляйте запити, щоб уникнути зайвих пошуків, застосовуйте правила контролю вхідних та вихідних даних та залучайте людей до дій із високим ризиком.

Пов’язана література

  • Чому RAG все ще дає неправильні відповіді: гібридний пошук, переранжування та Recall@K — Чисті фрагменти не гарантують якісних відповідей. Дізнайтеся, де зазнає невдачі векторний пошук, та як фузія, крос-енкодер для переранжування, HyDE та метод вимірювання Recall@K це виправляють.