Почему наивный векторный RAG гарантированно деградирует на прайс-листах и ГОСТах — и как гибридный поиск с RRF, визуальные эмбеддинги ColPali и локальный BGE-reranker снижают галлюцинации по ценам с 14.8% до 0.38% при p95 = 240 мс.
1. Наивный RAG умирает на первом прайс-листе
Классический пайплайн (чанк 512 токенов + косинус по text-embedding-3-large или bge-m3 + top-5 в промпт) уверенно проходит демо и разваливается на первом боевом запросе вида «цена на подшипник SKF 6205-2RSH при поставке от 500 шт. по договору №14/2025». Причины системные:
В боевом контуре First Touch — платформы ИИ-рассылок в Telegram, где бот пишет по вашей базе знаний с ваших аккаунтов, отвечает на вопросы и передаёт менеджеру заинтересованных клиентов — ровно это давало 14.8% галлюцинаций по ценам и условиям поставки. Ниже — архитектура, которая заменила наивный пайплайн.
2. Анатомия гибридного поиска: Dense + Sparse
Разделение ответственности
| Компонент | Что решает | Слабость |
|---|---|---|
| Dense (bge-m3, 1024d) | Семантика, синонимы: «подшипник» ≈ «bearing» ≈ «опора вала» | Не видит разницы в один символ артикула |
| Sparse (BM25Okapi / SPLADE-v3) | Точные токены: артикулы, ID договоров, ГОСТы, единицы | Не обобщает перефразировки |
В enterprise нужны оба: dense находит документ, sparse доказывает, что это тот самый артикул. Отключение любой половины стоит 17–20 п.п. Hit Rate@3.
RRF против Weighted Sum
Складывать скоры напрямую нельзя: BM25 не ограничен сверху и зависит от длины документа, косинус лежит в [0,1]. Min-max нормировка нестабильна на выбросах. Рабочее решение — Reciprocal Rank Fusion, устойчивый к масштабу:
score(d) = Σ_i 1 / (k + rank_i(d)), где k = 60 (сглаживание, Cormack et al., 2009).
Weighted Sum (α·dense + (1-α)·sparse) даёт +1–2% NDCG только после тюнинга α под конкретный домен и ломается при смене коллекции. RRF не требует обучения и работает из коробки.
Качество на внутреннем бенчмарке (2 400 запросов, 180k чанков)
| Стратегия | Hit Rate@3 | NDCG@5 | MRR | Recall@50 |
|---|---|---|---|---|
| Dense only | 71.4% | 0.62 | 0.58 | 0.81 |
| Sparse only | 68.9% | 0.59 | 0.61 | 0.74 |
| Hybrid + RRF | 88.2% | 0.79 | 0.76 | 0.93 |
| Hybrid + RRF + reranker | 94.8% | 0.89 | 0.86 | 0.95 |
3. ColPali: мультимодальный RAG для сложных PDF
PyPDF, Tesseract и Unstructured теряют ровно то, что в B2B-документе самое ценное: многоколоночную верстку, объединённые ячейки смет, сноски с условиями, графики и чертежи. Типичный сбой: таблица 6×14 после text extraction превращается в плоский список чисел без привязки к столбцам — модель складывает цену из одной колонки с количеством из другой.
ColPali решает это без OCR: страница режется на визуальные патчи, PaliGemma-энкодер отдаёт по эмбеддингу на патч, а сопоставление запроса и страницы идёт по late interaction (механизм ColBERT) — максимум по каждому токену запроса. Пространственные связи «строка ↔ столбец ↔ сноска» сохраняются физически, потому что документ не покидал пиксельное представление. Для PDF-смет, спецификаций и чертежей это даёт +11–14 п.п. точности извлечения табличных значений относительно OCR-пайплайна.
4. Локальные реранкеры: Cross-Encoder обязателен
Би-энкодер кодирует запрос и документ независимо — он физически не видит их взаимодействия, поэтому top-50 по косинусу содержит документы «про то же», но не «про этот пункт договора». Cross-Encoder читает пару (query, passage) целиком и различает нюансы вроде «цена при поставке от 500 шт.» против «цена при поставке от 50 шт.».
Стек на собственном GPU-контуре: BGE-Reranker-v2-m3 (или Qwen2-VL-Reranker для страниц ColPali), развёрнутый через Text Embeddings Inference (TEI) или vLLM с continuous batching. Никакого внешнего API — данные клиента не покидают периметр.
Эффект на боевом контуре: Hit Rate@3 71.4% → 94.8%, NDCG@5 0.62 → 0.89, p50 инференса реранкера по 40 кандидатам — 31 мс, p95 — 45 мс на GPU L4.
Отдельный слой — машиночитаемая спецификация llms.txt: она описывает доменную онтологию базы знаний (разделы, сущности, правила навигации), и агент-роутер не тратит токены на слепой перебор коллекций, а сразу выбирает нужный индекс. Это же снижает долю запросов, уходящих в ColPali-индекс вместо текстового, — экономия 60% GPU-времени на визуальных патчах.
5. Сжатие контекста и защита от Lost-in-the-Middle
6. Воспроизводимый пайплайн на Python
from qdrant_client import QdrantClient
from rank_bm25 import BM25Okapi
from sentence_transformers import CrossEncoder
def reciprocal_rank_fusion(rankings, k: int = 60) -> dict:
scores = {}
for ranking in rankings:
for pos, doc_id in enumerate(ranking):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + pos + 1)
return dict(sorted(scores.items(), key=lambda kv: -kv[1]))
class EnterpriseRAGPipeline:
def __init__(self, client: QdrantClient, collection: str,
corpus: list[dict], encoder, reranker_path: str):
self.client, self.collection = client, collection
self.corpus = {d['id']: d for d in corpus}
self.ids = [d['id'] for d in corpus]
self.bm25 = BM25Okapi([d['text'].lower().split() for d in corpus])
self.encoder = encoder
self.reranker = CrossEncoder(reranker_path, max_length=512)
def retrieve(self, query: str, top_k: int = 40) -> list[str]:
dense = self.client.search(self.collection, self.encoder.encode(query), limit=top_k)
dense_ids = [h.payload['doc_id'] for h in dense]
sparse_ids = self.bm25.get_top_n(query.lower().split(), self.ids, n=top_k)
fused = reciprocal_rank_fusion([dense_ids, sparse_ids])
return list(fused)[:top_k]
def build_context(self, query: str, threshold: float = 0.35, limit: int = 6):
cand = self.retrieve(query)
pairs = [(query, self.corpus[c]['text']) for c in cand]
scores = self.reranker.predict(pairs)
ranked = sorted(zip(cand, scores), key=lambda kv: -kv[1])
return [self.corpus[c]['text'] for c, s in ranked if s >= threshold][:limit]
build_context возвращает пустой список, если уверенности нет — и это корректный ответ, а не сбой.
7. Продакшен-кейс First Touch: метрики боевого контура
Инфраструктура First Touch после миграции:
8. Чеклист и CTA
Четыре правила production-ready RAG в 2026 году:
1. Никогда не полагайтесь на один тип поиска — dense без sparse не различает артикулы.
2. Слияние через RRF, а не через сырые скоры: BM25 и косинус несопоставимы.
3. Реранкер обязателен, и он должен стоять в вашем контуре: cross-encoder на API — это утечка и задержка.
4. Отсутствие ответа — валидный результат. Порог по score важнее, чем top-k.
Если ваш бот уже врёт по ценам — начните с аудита базы знаний и развёртывания RAG-контура без галлюцинаций: First Touch собирает гибридный индекс, поднимает локальный реранкер и подключает ИИ-рассылку в Telegram, которая передаёт менеджеру только заинтересованных клиентов.
🚀 Проект автора: First Touch (https://aifirsttouch.ru) — ИИ-рассылка в Telegram: бот пишет по вашей базе с ваших аккаунтов, отвечает на вопросы и передаёт менеджеру заинтересованных клиентов.
Автор: Alexander Bond — AI-разработчик и архитектор автономных систем
Теги: rag, гибридный поиск, colpali, реранкеры, векторные базы, llm, first touch