От прототипа, который стыдно показать, к системе, которая не подведёт
Вы сделали MVP RAG-системы для эксперта по охране труда. Он работает. Иногда. Когда не забывает про ТК РФ, не путает СНИПы с ГОСТами и не выдаёт рекомендации, за которые реально сажают. Поздравляю — у вас типичная проблема 2026 года: прототип есть, продакшена нет. Расскажу, как мы превращали нашу хрупкую конструкцию из LangChain и надежды в систему, которая реально помогает юристам не сесть в тюрьму.
Ключевая проблема 2026 года: большинство RAG-систем выходят в прод с точностью MVP. Они находят релевантные чанки, но контекст теряется между retrieval и generation. Особенно критично в регуляторных областях вроде охраны труда.
Архитектура, которая ломалась на каждом шагу
На старте было классически просто: документы → нарезка чанками → эмбеддинг в Pinecone → поиск по косинусной близости → промпт с контекстом → ответ GPT-4. Работало с точностью 68% на наших тестах. Проблемы начались, когда мы попытались обрабатывать сложные запросы вроде "Какая ответственность грозит директору, если на стройке нет журнала инструктажа, а работник получил травму?".
Система находила фрагменты про журналы, про ответственность, про травмы. Но связь между ними LLM устанавливала плохо. Ответы были общими, часто пропускали ключевые нюансы из ФСТЭК 117 и Указа 490.
Шаг 1: Замена LLM-реранкера — почему Cohere Command R 2026 перестал быть панацеей
Мы начали с реранкера. В 2025 все использовали Cohere Command R или специализированные модели для re-ranking. К 2026 стало очевидно: они добавляют задержку в 200-400 мс, а выигрыш в точности часто не превышает 5-8%. Особенно в узких доменах, где терминология специфическая.
Вместо тяжёлого LLM-реранкера мы перешли на двухэтапный подход:
- Лёгкий бинарный классификатор (на основе BERT или DeBERTa): отсеивает заведомо нерелевантные чанки. Обучен на парах "запрос-чанк" с пометками 0/1.
- 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 поняли: для регуляторных систем нужен не просто поиск, а поиск с учётом:
- Времени действия документа (старая редакция vs новая)
- Юрисдикции (федеральный закон vs региональное требование)
- Типа документа (закон → подзаконный акт → разъяснение)
Решение: кастомный индекс в 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 P95 | 95-й перцентиль времени ответа | <3.5s | Юристы терпеть не могут ждать |
Самый важный инсайт: мы добавили краудсорсинговую оценку ответов самими юристами. После каждого ответа появляется кнопка "Нашли ошибку?" — и эти данные идут напрямую в дообучение нашей системы.
Шаг 6: Безопасность и комплаенс — то, о чем забывают 90% команд
RAG-система по охране труда — это не просто поисковик. Это система, рекомендации которой могут привести к реальным судам. Мы внедрили:
- Валидацию ответов через правила (если система рекомендует что-то противоречащее ТК РФ — ответ блокируется)
- Логирование всех запросов/ответов с возможностью аудита (требование ФСТЭК 117)
- Человек в петле для сложных кейсов (система может сказать "запрос слишком сложный, передаю юристу")
Особенно критично было решить проблему галлюцинаций и нарушения правил. Мы используем не только prompt engineering, но и отдельную модель-валидатор, которая проверяет каждый ответ на соответствие законодательству.
Что в итоге получилось
Через 6 месяцев итераций наша система показывает:
- Точность на сложных запросах: 94% против исходных 68%
- Среднее время ответа: 2.8 секунды (было 4.5+)
- Снижение галлюцинаций: с 15% до 0.7%
- Возможность обрабатывать многошаговые сценарии с возвратами и уточнениями
Но главное — мы перестали бояться показывать систему клиентам. Она не идеальна, но её ошибки контролируемы, а архитектура позволяет быстро итерировать.
Что пробуем сейчас (2026)
Экспериментируем с RAG 2.0 подходами, где вместо поиска по эмбеддингам используем мелкие специализированные модели для прямого ответа на подзапросы. Также тестируем агентные подходы, где система сама решает, какие документы запросить для ответа на сложный вопрос.
Но это уже тема для отдельного разбора. Если хотите посмотреть на код — часть решений есть в нашем открытом репозитории по HR-агенту. Архитектурно проблемы очень похожи.
И последнее: не пытайтесь сразу построить идеальную систему. Начните с простого пайплайна, измеряйте реальные метрики, и улучшайте именно те части, которые горят. В 2026 году лучшая RAG-система — не та, что использует самые новые модели, а та, что лучше всего понимает свои слабые места и умеет с ними работать.