Масштабирование RAG: от прототипа до продакшена в 2026 году | AiManual
AiManual Logo Ai / Manual.
17 Фев 2026 Гайд

Практический гайд: как масштабировать RAG-систему от MVP до продакшена на примере эксперта по охране труда

Разбор реального кейса: как превратить хрупкий RAG-прототип в надёжную систему. Замена LLM-реранкера, LangGraph, оптимизация чанков, метрики.

От прототипа, который стыдно показать, к системе, которая не подведёт

Вы сделали MVP RAG-системы для эксперта по охране труда. Он работает. Иногда. Когда не забывает про ТК РФ, не путает СНИПы с ГОСТами и не выдаёт рекомендации, за которые реально сажают. Поздравляю — у вас типичная проблема 2026 года: прототип есть, продакшена нет. Расскажу, как мы превращали нашу хрупкую конструкцию из LangChain и надежды в систему, которая реально помогает юристам не сесть в тюрьму.

Ключевая проблема 2026 года: большинство RAG-систем выходят в прод с точностью MVP. Они находят релевантные чанки, но контекст теряется между retrieval и generation. Особенно критично в регуляторных областях вроде охраны труда.

Архитектура, которая ломалась на каждом шагу

На старте было классически просто: документы → нарезка чанками → эмбеддинг в Pinecone → поиск по косинусной близости → промпт с контекстом → ответ GPT-4. Работало с точностью 68% на наших тестах. Проблемы начались, когда мы попытались обрабатывать сложные запросы вроде "Какая ответственность грозит директору, если на стройке нет журнала инструктажа, а работник получил травму?".

Система находила фрагменты про журналы, про ответственность, про травмы. Но связь между ними LLM устанавливала плохо. Ответы были общими, часто пропускали ключевые нюансы из ФСТЭК 117 и Указа 490.

💡
Первое открытие: наивный RAG хорошо работает на фактологических запросах ("статья 214 ТК РФ"), но проваливается на многоуровневых сценариях, где нужно соединить нормы из разных документов.

Шаг 1: Замена LLM-реранкера — почему Cohere Command R 2026 перестал быть панацеей

Мы начали с реранкера. В 2025 все использовали Cohere Command R или специализированные модели для re-ranking. К 2026 стало очевидно: они добавляют задержку в 200-400 мс, а выигрыш в точности часто не превышает 5-8%. Особенно в узких доменах, где терминология специфическая.

Вместо тяжёлого LLM-реранкера мы перешли на двухэтапный подход:

  1. Лёгкий бинарный классификатор (на основе BERT или DeBERTa): отсеивает заведомо нерелевантные чанки. Обучен на парах "запрос-чанк" с пометками 0/1.
  2. Cross-encoder с тонкой настройкой на доменных данных: ранжирует оставшиеся чанки. Ключевое отличие от 2025 года — мы не используем готовые API, а развернули свои модели, чтобы контролировать задержки.
# Пример конфигурации нашего пайплайна реранкинга (2026)
from sentence_transformers import CrossEncoder
import torch

# Лёгкая модель для бинарной фильтрации
binary_filter = load_model("microsoft/deberta-v3-small-binary")

# Cross-encoder, дообученный на 5000 пар "запрос-ответ" из охраны труда
cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2",
                             num_labels=1,
                             max_length=512)
cross_encoder.load_state_dict(torch.load("models/ot_cross_encoder_2026.pth"))

def rerank_chunks(query, chunks, top_k=10):
    # Этап 1: бинарная фильтрация
    filtered = []
    for chunk in chunks:
        score = binary_filter.predict([[query, chunk.text]])[0]
        if score > 0.3:  # Порог подобран на валидации
            filtered.append(chunk)
    
    # Этап 2: точное ранжирование
    pairs = [[query, chunk.text] for chunk in filtered]
    scores = cross_encoder.predict(pairs)
    
    # Сортировка и возврат top_k
    ranked = sorted(zip(filtered, scores), key=lambda x: x[1], reverse=True)
    return [chunk for chunk, _ in ranked[:top_k]]

Это дало прирост точности на 12% при увеличении задержки всего на 45 мс (вместо 200+ мс у LLM-реранкера).

Шаг 2: LangGraph против классического пайплайна — где он реально выигрывает

LangChain — отличный инструмент для прототипирования. Но когда нужно обрабатывать сложные многошаговые запросы ("найди нарушения → сопоставь со статьями КоАП → оцени риски для директора"), его линейный пайплайн ломается.

Мы перешли на LangGraph, но не для создания "автономных агентов", а для оркестрации RAG-процесса. Вот как выглядит наш граф:

УзелЗадачаПочему не в LangChain
QueryAnalyzerОпределяет тип запроса: факт, сравнение, сценарий, расчётТребует условной логики, которую сложно впихнуть в последовательную цепь
ParallelRetrieverПараллельный поиск по разным индексам: законы, судебная практика, разъясненияLangChain делает это последовательно, теряя время
ConflictCheckerПроверяет противоречия между найденными документамиТребует состояния и возврата назад в графе
AnswerGeneratorГенерирует ответ с учётом проверенных и отсортированных чанковСтандартный LCEL здесь ещё работает

Ключевое преимущество: граф позволяет обрабатывать сценарии, где нужно вернуться на предыдущий шаг. Например, если ConflictFinder обнаруживает противоречие между ТК РФ и местным нормативным актом — система не тупо выбирает один источник, а запускает дополнительный поиск судебной практики по этому противоречию.

# Упрощённая структура графа
from langgraph.graph import StateGraph, END
from typing import TypedDict, List

class RAGState(TypedDict):
    query: str
    query_type: str
    retrieved_chunks: List
    filtered_chunks: List
    conflicts: List
    final_answer: str

graph = StateGraph(RAGState)

# Добавляем узлы
graph.add_node("analyze_query", query_analyzer)
graph.add_node("retrieve_parallel", parallel_retriever)
graph.add_node("check_conflicts", conflict_checker)
graph.add_node("generate_answer", answer_generator)

# Определяем edges
graph.add_edge("analyze_query", "retrieve_parallel")
graph.add_edge("retrieve_parallel", "check_conflicts")

def route_conflicts(state):
    if state["conflicts"]:
        return "retrieve_parallel"  # Возвращаемся за дополнительным контекстом
    return "generate_answer"

graph.add_conditional_edges(
    "check_conflicts",
    route_conflicts,
    {"retrieve_parallel": "check_conflicts",  # Цикл для повторной проверки
     "generate_answer": "generate_answer"}
)

graph.add_edge("generate_answer", END)
graph.set_entry_point("analyze_query")

app = graph.compile()

Шаг 3: Оптимизация нарезки чанков — почему semantic chunking 2026 года не решает всех проблем

В 2025 все перешли с fixed-size chunking на semantic chunking (разбиение по смысловым границам). К 2026 выяснилось: в юридических документах это работает плохо. Статья закона может быть длинной, но разрывать её посередине абзаца — преступление против смысла.

Мы разработали гибридный подход:

  • Структурное разбиение по заголовкам, статьям, пунктам (для нормативных документов)
  • Recursive chunking с перекрытием внутри структурных единиц
  • Метаданные каждого чанка: не только источник, но и иерархия (Глава 3 → Статья 214 → Пункт 2)

Но главное — мы добавили чунки-связки (relationship chunks). Это искусственно созданные чанки, которые описывают связи между документами. Например: "Статья 214 ТК РФ ссылается на Постановление Правительства № 390. Основное отличие: ТК требует проведения инструктажа, а Постановление уточняет форму журнала."

Важное наблюдение: большинство RAG-систем страдают от "потери контекста между документами". Чанки-связки решают эту проблему, явно кодируя отношения, которые LLM должна выводить самостоятельно.

Шаг 4: Векторный поиск 2026 — когда Pinecone и Weaviate уже недостаточно

Мы начали с Pinecone. Потом перешли на Weaviate из-за гибридного поиска. К 2026 поняли: для регуляторных систем нужен не просто поиск, а поиск с учётом:

  1. Времени действия документа (старая редакция vs новая)
  2. Юрисдикции (федеральный закон vs региональное требование)
  3. Типа документа (закон → подзаконный акт → разъяснение)

Решение: кастомный индекс в PostgreSQL + pgvector с дополнительными метаданными фильтрами. Да, скорость немного ниже, чем у специализированных векторных БД, но контроль над фильтрацией того стоит.

-- Наша схема в PostgreSQL (2026)
CREATE TABLE document_chunks (
    id UUID PRIMARY KEY,
    content TEXT NOT NULL,
    embedding vector(768),  -- Для нашего tuned embedding model
    metadata JSONB NOT NULL,
    -- Критичные для фильтрации поля выделены отдельно:
    doc_type VARCHAR(50), -- 'law', 'court_decision', 'explanation'
    jurisdiction VARCHAR(100), -- 'federal', 'regional_77' (Москва)
    effective_date DATE,
    expiry_date DATE,
    hierarchy_path TEXT[] -- ['ТК РФ', 'Раздел X', 'Глава 35']
);

-- Гибридный поиск с фильтрацией по метаданным
SELECT 
    id,
    content,
    0.7 * (1 - (embedding <=> query_embedding)) + 
    0.3 * ts_rank(to_tsvector('russian', content), query) AS score
FROM document_chunks
WHERE 
    doc_type = 'law' 
    AND jurisdiction = 'federal'
    AND effective_date <= CURRENT_DATE 
    AND (expiry_date IS NULL OR expiry_date > CURRENT_DATE)
ORDER BY score DESC
LIMIT 20;

Шаг 5: Метрики, которые имеют значение (а не просто "точность")

Мы перестали мерить accuracy на тестовом наборе. Вместо этого отслеживаем:

МетрикаКак считаемЦелевое значениеПочему важно
Context Utilization Score% релевантных чанков, реально использованных в ответе>85%Показывает, не теряем ли мы найденный контекст
Hallucination Rate% ответов с выдуманными ссылками/нормами<1%Критично для юридических систем
Multi-doc RecallМожет ли система находить все нужные документы для сложного запроса>90%Показывает качество навигации по связанным нормам
Latency P9595-й перцентиль времени ответа<3.5sЮристы терпеть не могут ждать

Самый важный инсайт: мы добавили краудсорсинговую оценку ответов самими юристами. После каждого ответа появляется кнопка "Нашли ошибку?" — и эти данные идут напрямую в дообучение нашей системы.

Шаг 6: Безопасность и комплаенс — то, о чем забывают 90% команд

RAG-система по охране труда — это не просто поисковик. Это система, рекомендации которой могут привести к реальным судам. Мы внедрили:

  1. Валидацию ответов через правила (если система рекомендует что-то противоречащее ТК РФ — ответ блокируется)
  2. Логирование всех запросов/ответов с возможностью аудита (требование ФСТЭК 117)
  3. Человек в петле для сложных кейсов (система может сказать "запрос слишком сложный, передаю юристу")

Особенно критично было решить проблему галлюцинаций и нарушения правил. Мы используем не только prompt engineering, но и отдельную модель-валидатор, которая проверяет каждый ответ на соответствие законодательству.

Что в итоге получилось

Через 6 месяцев итераций наша система показывает:

  • Точность на сложных запросах: 94% против исходных 68%
  • Среднее время ответа: 2.8 секунды (было 4.5+)
  • Снижение галлюцинаций: с 15% до 0.7%
  • Возможность обрабатывать многошаговые сценарии с возвратами и уточнениями

Но главное — мы перестали бояться показывать систему клиентам. Она не идеальна, но её ошибки контролируемы, а архитектура позволяет быстро итерировать.

💡
Ключевой урок: масштабирование RAG — это не про добавление больше GPU или переход на более крупную модель. Это про архитектурные решения, которые сохраняют контекст, контролируют качество и позволяют системе развиваться без полного переписывания.

Что пробуем сейчас (2026)

Экспериментируем с RAG 2.0 подходами, где вместо поиска по эмбеддингам используем мелкие специализированные модели для прямого ответа на подзапросы. Также тестируем агентные подходы, где система сама решает, какие документы запросить для ответа на сложный вопрос.

Но это уже тема для отдельного разбора. Если хотите посмотреть на код — часть решений есть в нашем открытом репозитории по HR-агенту. Архитектурно проблемы очень похожи.

И последнее: не пытайтесь сразу построить идеальную систему. Начните с простого пайплайна, измеряйте реальные метрики, и улучшайте именно те части, которые горят. В 2026 году лучшая RAG-система — не та, что использует самые новые модели, а та, что лучше всего понимает свои слабые места и умеет с ними работать.