← Назад в блог

Двухуровневый допрос: почему векторному поиску нужен кросс-энкодер

19 октября 2026 г. · 5 мин чтения
Двухуровневый допрос: почему векторному поиску нужен кросс-энкодер - Почему топ-50 кандидатов из векторной базы приводят к галлюцинациям LLM. Как кросс-энкодер с попарным вниманием выявляет скрытые животные компоненты.

Пайплайн автоматической сертификации проверяет документацию поставщиков на кондитерскую глазурь для европейского знака V-Label.

Система отправляет поисковый запрос по базе из тридцати тысяч технических спецификаций: «Подтвердить, что в натуральном ванильном ароматизаторе используются растворители исключительно растительного происхождения».

Гибридный поиск из пятой главы (BM25 плюс векторный поиск через Reciprocal Rank Fusion) находит пятьдесят кандидатов за 18 миллисекунд.

На первом месте с оценкой 0.94 оказывается следующий фрагмент:

«Технический паспорт №9104: Натуральный ароматизатор ванили (мадагаскарский экстракт). В качестве матрицы-носителя применен пищевой глицерин для кондитерских изделий».

А документ №38, затерявшийся внизу выборки, содержит ключевую деталь:

«Дополнение к паспорту №9104: Растворитель-носитель глицерин (CAS 56-81-5) синтезирован методом переэтерификации жирных кислот говяжьего жира под давлением».

Если передать все пятьдесят найденных фрагментов напрямую в контекст большой языковой модели, возникнет перегрузка контекста. Модель зафиксирует совпадение формулировок на первом месте («натуральный ароматизатор», «пищевой глицерин»), пропустит скрытое примечание на 38-й позиции и сделает вывод, что ингредиент соответствует требованиям. Партия уходит в производство, проваливает лабораторную проверку при инспекции ритейла и попадает под отзыв.

Причина этой ошибки кроется в архитектуре би-энкодеров.

Узкое горлышко би-энкодеров

Векторный поиск по умолчанию работает на би-энкодерах (bi-encoders). В этой схеме модель обрабатывает запрос и документ раздельно:

Запрос    ───► [Трансформер-энкодер] ───► Вектор u (1536 чисел)
                                                    │
                                            Косинусное сходство
                                                    │
Документ  ───► [Трансформер-энкодер] ───► Вектор v (1536 чисел)

Поскольку документы кодируются независимо от запроса, векторы для миллионов спецификаций можно рассчитать заранее, сохранить в векторном индексе (HNSW или IVFFlat) и опрашивать за считанные миллисекунды.

За эту скорость приходится платить потерей контекста.

Когда модель сжимает страницу химического паспорта в один массив чисел, она теряет связи между отдельными токенами. В би-энкодере токены запроса физически не взаимодействуют с токенами документа.

В запросе написано: «растворители растительного происхождения». В документе написано: «пищевой глицерин для кондитерских изделий».

В векторном пространстве слова растворитель, носитель, глицерин, ароматизатор и ваниль находятся рядом. Векторный индекс фиксирует близость и выдает сходство 0.94. Проверить, согласуется ли требование «растительного происхождения» с происхождением конкретной партии, би-энкодер не может: токены запроса и документа не встречаются в механизме внимания.

Кросс-энкодер: сквозное попарное внимание

Кросс-энкодер (cross-encoder) убирает эту изолированность. Вместо раздельных векторов он объединяет запрос и проверяемый документ в одну последовательность:

[CLS] Токены запроса [SEP] Токены документа [SEP]
                         │
        [12-24 слоев сквозного self-attention]
                         │
             Каждый токен взаимодействует
                  с каждым токеном
                         │
                   [Выход CLS]
                         │
            Оценка релевантности (от 0 до 1)

В каждом слое трансформера токены запроса напрямую взаимодействуют со всеми токенами документа.

Когда кросс-энкодер доходит до словосочетания «растительного происхождения», веса внимания направляются на слова «глицерин», «переэтерификация» и «говяжий жир». Сеть фиксирует логическое и фактическое противоречие между запросом и описанием сырья.

Результат работы кросс-энкодера — не абстрактный угол между векторами, а прямая оценка соответствия: отвечает ли этот конкретный фрагмент на поставленный вопрос.

При оценке кросс-энкодером:

  • Фрагмент №1 (размытое упоминание пищевого глицерина) падает с 0.94 до 0.12.
  • Фрагмент №38 (указание на животный жир) получает максимальный балл по проверке сырья и сразу попадает в отчет инспектора.

Архитектура воронки

Если кросс-энкодер работает точнее, почему не проверять им всю базу?

Из-за вычислительной сложности.

Для поиска по базе в сто тысяч документов би-энкодер кодирует запрос один раз (5 миллисекунд) и сканирует индекс (10 миллисекунд). Суммарное время — 15 миллисекунд.

Кросс-энкодер ничего не может рассчитать заранее. Поскольку запрос и текст склеиваются перед подачей в сеть, для ста тысяч документов потребуется провести сто тысяч прямых проходов через трансформер. При времени 20 миллисекунд на документ на графическом процессоре один запрос выполнялся бы больше получаса.

Поэтому в инженерной практике используют воронку:

  1. Этап 1: Быстрый отбор кандидатов (Би-энкодер + BM25) Гибридный поиск отбирает топ-50 кандидатов из 100 000 документов за 15 мс через Reciprocal Rank Fusion.
  2. Этап 2: Точечная проверка (Кросс-энкодер) Только эти 50 пар отправляются в кросс-энкодер (bge-reranker-large или ms-marco-MiniLM-L-6-v2) одним батчем на GPU (32 мс).
  3. Этап 3: Генерация ответа в LLM В контекст языковой модели передаются только 5 лучших фрагментов с подтвержденным соответствием.
from sentence_transformers import CrossEncoder

class ComplianceReranker:
    def __init__(self, model_name: str = "BAAI/bge-reranker-large"):
        # Загружаем кросс-энкодер для попарного анализа запрос-документ
        self.model = CrossEncoder(model_name)

    def rerank(self, query: str, candidates: list[dict], top_k: int = 5) -> list[dict]:
        """
        Переранжирование кандидатов первичного поиска через механизм cross-attention.
        candidates: список словарей с ключами 'doc_id', 'text', 'retrieval_score'
        """
        if not candidates:
            return []

        # Формируем пары [запрос, текст документа] для совместной подачи в сеть
        pairs = [[query, doc["text"]] for doc in candidates]

        # Батчевый проход через слои трансформера
        cross_scores = self.model.predict(pairs)

        # Прикрепляем полученные оценки к метаданным
        reranked = []
        for doc, score in zip(candidates, cross_scores):
            reranked.append({
                "doc_id": doc["doc_id"],
                "text": doc["text"],
                "initial_rank_score": doc.get("retrieval_score", 0.0),
                "cross_encoder_score": float(score)
            })

        # Сортируем документы строго по оценке попарного внимания
        reranked.sort(key=lambda item: item["cross_encoder_score"], reverse=True)
        return reranked[:top_k]

Практический эффект

При анализе спецификаций на пищевое сырье добавление кросс-энкодера меняет структуру выдачи:

  • Пул кандидатов: Топ-50 фрагментов из гибридного поиска (BM25 + Dense).
  • Время ответа: Первичный поиск занимает 18 мс, переранжирование 50 фрагментов — 32 мс. Общее время пайплайна остается в пределах 55 мс.
  • Точность топ-3: Доля релевантных фактов в первых трех позициях вырастает на 34% по сравнению с прямой передачей кандидатов из векторной базы в LLM.

Отсекая внешне похожие, но фактологически пустые формулировки, система подает в языковую модель очищенный контекст, исключая галлюцинации на этапе аудита.

В среду, в седьмой главе, мы разберем Двухэтапный RAG (сначала стандарты, затем лабораторные факты): как на первом этапе извлечь требования стандарта (ISO 23662), а на втором — использовать эти критерии для проверки паспортов качества (CoA) и лабораторных тестов.


Используете ли вы кросс-энкодеры в своих поисковых пайплайнах, и какой лимит задержки заложен в вашей архитектуре на этап переранжирования?

Есть проект на прицеле?

Давайте обсудим, как мы можем помочь.

Есть идея проекта? →