Cross-encoder в роли судьи: фильтруем RAG-пайплайн по оценкам релевантности
Как перекрёстное внимание позволяет cross-encoder оценивать релевантность пары «запрос — документ» от 0 до 1 и как порог не даёт слабым результатам поиска вообще дойти до LLM.
Большинство руководств по RAG считают cross-encoder просто шагом переранжирования. Но он способен на более интересную работу. В роли судьи он решает, достаточно ли хорош найденный текст, чтобы вообще передавать его генератору.
Распространённая альтернатива — LLM в роли судьи, но это всё та же генеративная модель со всеми обычными причудами. Судья на базе cross-encoder оказывается в удачной точке пересечения трёх забот: как развивается архитектура, сколько стоит пайплайн и как долго ждут пользователи.
Почему cross-encoder — хороший судья
Базовые RAG-системы целиком опираются на bi-encoder — семейство sentence-transformer, которым строят эмбеддинги. Bi-encoder сжимает каждый текст в отдельный самостоятельный вектор. Он измеряет, насколько близки два вектора в пространстве, и упускает более тонкую логику вроде условий, отрицаний и структурных переворотов.
Есть и более глубокое ограничение. Bi-encoder настроены на полноту, а не на точность. Они находят документы на ту же тему, но не могут подтвердить, что документ отвечает на тот вопрос, который действительно задали.
Сегодня поиск обычно оценивают так: просят LLM вроде GPT или Claude прочитать найденные чанки и выставить им оценку. В продакшене у этого подхода три серьёзных недостатка:
- Задержка. Ожидание тяжёлого API-вызова ещё до начала генерации портит впечатление пользователю, который ждёт быстрого ответа.
- Стоимость. Отправка тысяч токенов найденного контекста модели-судье на каждый запрос раздувает счёт за API.
- Нестабильность. Промпты дрейфуют, а вывод модели недетерминирован, что ломает любой жёстко заданный порог. Возможны и галлюцинации, так что фрагмент, признанный релевантным в понедельник, во вторник может оказаться нерелевантным или наполовину релевантным.
Cross-encoder снимает все три проблемы. Он читает запрос и документ вместе за один проход трансформера, так что каждый токен может обращать внимание на любой другой. На выходе — точная, воспроизводимая оценка от 0.0 до 1.0.
Что происходит внутри: перекрёстное внимание
Проще всего понять, почему он судит лучше, если сравнить, как он читает текст, с тем, как это делает bi-encoder.
Bi-encoder кодирует запрос (Q) и документ (D) по отдельности. Получается один вектор для Q и другой для D. Внутри трансформера токены запроса обращают внимание только на токены запроса, а токены документа — только на токены документа. Две стороны встречаются в самом конце, через скалярное произведение или косинусное сходство.
Cross-encoder работает иначе. Он собирает одну входную последовательность, в которой лежат оба текста:
[CLS] User Query [SEP] Retrieved Document Chunk [SEP]
Поскольку оба текста находятся в одной последовательности, модель применяет полное перекрёстное внимание. Стандартная формула внимания по-прежнему действует:
Attention(Q, K, V) = softmax( (Q * Kᵀ) / √dₖ ) * V
Разница в том, что каждый токен запроса вычисляет вес внимания относительно каждого токена чанка документа, причём всё сразу. Слова вопроса напрямую связываются со словами, которые на него отвечают или ему противоречат.
Для контраста — схема bi-encoder, где две стороны не видят друг друга до финального сравнения:
Bi-Encoder Architecture:
Query Tokens ──► [ Encoder A ] ──► [ Vector Q ] ──┐
├──► Cosine Distance ──► Score
Doc Tokens ──► [ Encoder B ] ──► [ Vector D ] ──┘
(No token-to-token interaction)
Cross-Encoder Architecture:
Query Tokens ──┐
├──► [ Full Self-Attention Matrix ] ──► [ [CLS] Token ] ──► Sigmoid ──► Relevance
Doc Tokens ──┘ (Every token attends to every) (Dense Output) Probability
Разбор на примере
Вот случай, где bi-encoder ломается, а cross-encoder справляется.
Допустим, в базе знаний три документа:
- Документ A: «Сильные грозы и проливной дождь нарушили движение по всему региону, и аэропорт отменил все рейсы, назначенные на вечер».
- Документ B: «Несмотря на ливень и тёмное небо, аэропорт работал в обычном режиме, и ни один рейс не задержали».
- Документ C: «Сбой в программном обеспечении управления воздушным движением привёл к крупным задержкам, и дневной дождь тут был ни при чём».
Пользователь спрашивает: «Дождь стал причиной задержек рейсов?»
Где ошибается bi-encoder
Стандартная модель эмбеддингов воспринимает запрос целиком и оценивает каждый документ сам по себе. Во всех трёх текстах есть сильно взвешенные термины «дождь», «рейс» и «задержки». Bi-encoder измеряет лишь общую близость векторов, поэтому документы B и C получают очень высокое косинусное сходство. Они часто оказываются рядом с документом A, а иногда и выше него. Ретривер видит совпадающие слова, решает, что совпадает и тема, и считает их релевантными.
К чему приходит cross-encoder
Теперь передадим тех же кандидатов судье на базе cross-encoder. Матрица перекрёстного внимания заставляет токены запроса выстроиться напротив текста документа и образовать настоящие связи. Результат совсем другой:
[Query] + [Doc A] ---> Score: 0.94 (High Confidence)
Mechanics: The model explicitly links "rain" -> "caused" -> "cancel/delays".
[Query] + [Doc B] ---> Score: 0.21 (Irrelevant Noise)
Mechanics: The cross-attention catches the modifier "Despite the heavy rain... continued smoothly without a single flight delay". It recognizes a structural negation.
[Query] + [Doc C] ---> Score: 0.42 (Topical Context Only)
Mechanics: The model notes that while both events happened, the text explicitly declares them "unrelated".
Если считать эти оценки настоящими вероятностями, документы B и C можно сразу выкинуть из промпта. Тогда генератор получает один чанк с сильным сигналом — документ A. Это исключает шанс, что модель выдумает противоречивое или ложное утверждение о том, как повлияла погода.
Как получается оценка
Bi-encoder сравнивает расстояние в пространстве между двумя отдельными векторами. Cross-encoder вообще не создаёт независимых эмбеддингов. Он приходит к оценке, анализируя всю последовательность совместно:
- Обработка токенов. Запрос и найденный документ упаковываются в одну последовательность и разделяются специальным токеном:
[CLS] User Query [SEP] Document Chunk [SEP]. - Матрица внимания. Пока последовательность проходит через трансформер, каждый токен обоих текстов вычисляет своё отношение к каждому другому токену. В итоге внутри модели получается плотная матрица взаимодействий.
- Скрытое состояние. Итоговое понимание модель собирает в токене
[CLS]в начале последовательности, который служит единой сводкой того, как связаны запрос и документ. - Отображение в вероятность. Финальное скрытое состояние
[CLS]проходит через линейный классификационный слой, который сворачивает сырые скрытые измерения в одно число. Затем его преобразует сигмоида,Score = 1 / (1 + e^-x).
Сигмоида сжимает любой сырой выход в диапазон от 0.0 до 1.0. Результат читается как откалиброванная вероятность релевантности — мера того, насколько модель уверена, что документ действительно содержит ответ на вопрос.
Как читать оценку
Поскольку значение ограничено и абсолютно, его можно разложить по уровням и действовать по каждому во время выполнения:
- От 0.80 до 1.00 — сигнал высокой уверенности. Документ закрывает суть вопроса. Ответ прямо есть в чанке, и структура утверждений совпадает с запросом.
- От 0.50 до 0.80 — только тематический контекст. Документ из нужной области и упоминает нужные понятия, но определённого ответа в нём нет или в нём много фонового шума.
- От 0.00 до 0.50 — структурное несоответствие или шум. У текста много общих с запросом ключевых слов, но противоположная логика, отрицания или посторонняя тема.
Превращаем судью в автоматический выключатель
В обычном RAG cross-encoder — сортировщик. Он переставляет блоки текста, прежде чем они пойдут дальше. Настоящая новизна роли судьи в том, чтобы превратить его вероятность в жёсткий шлюз, который может сработать как автоматический выключатель.
Вместо того чтобы позволять генератору читать то, что случайно оказалось первым, вы задаёте пайплайну твёрдую нижнюю планку качества, например 0.75.
Допустим, ретривер вернул двадцать документов, и судья оценил каждый из них. Лучшая оценка во всей пачке — 0.42. Выключатель срабатывает, и пайплайн тут же отменяет вызов генерации.
Огромной дорогой языковой модели нельзя оставлять гадать по нерелевантному материалу, потому что именно это и провоцирует галлюцинации. Вместо этого система отдаёт пользователю заранее подготовленный запасной ответ, что-то вроде: «Извините, нет проверенной внутренней документации, чтобы надёжно ответить на этот вопрос».
Откалиброванные вероятности, используемые как жёсткая программная граница, превращают непредсказуемый цикл генерации текста в предсказуемую программу. Генератор запускается, только когда доказательства хороши, а судья обходится дёшево, потому что это небольшая модель-энкодер, а не ещё один раунд дорогой генерации.