Переранжировка в RAG: кросс-кодеры против судей на основе LLM и их задержка
Как второй этап ранжирования улучшает порядок кандидатов в RAG, сравнение кросс-кодеров с судьями на основе LLM, затраты на задержку, способы сбоев и способы их оценки.
Источники: Reranking for RAG: Cross-Encoders, LLM Rerankers, and Latency Tradeoffs
Подходящий фрагмент был в результатах, просто слишком низко
Представьте систему RAG, у которой гибридный поиск наконец начал корректно работать. Хранилище векторов запущено, индекс ключевых слов функционирует, логика объединения данных написана, и фрагмент с ответом теперь присутствует где-то в списке кандидатов. Тем не менее модель по-прежнему генерирует посредственный ответ.
В логах объясняется причина. Один из разработчиков спросил, как отладить ошибку PAYMENTS_API_TIMEOUT в среде тестирования. Гибридный поиск вернул двадцать кандидатов: общий обзор процедур аутентификации, устаревшую запись о инциденте прошлого года, общую страницу с информацией о логике повторных попыток, а на восьмом месте — фрагмент руководства для тестирования с точным решением проблемы. В запросе было место только для трех фрагментов. Подходящий фрагмент находился слишком низко, поэтому модель составила свой ответ на основе неправильных данных, так как подходящие данные не влезали в ограниченное пространство.
Метод извлечения находит доказательства, но не ставит их в начало списка. Основная задача извлечения — воспроизведение информации, тогда как переранжирование направлено на повышение точности при ограниченном объеме ресурсов.
Сам по себе хороший набор кандидатов недостаточен. То, что фактически читает модель, зависит от порядка элементов этого набора, и именно порядок является узким местом.
Что делает этап переранжирования
Первый проход предназначен для быстрого и массового обработания данных. Запрос к векторной базе данных или индексу по ключевым словам должен обрабатывать миллионы записей за доли секунды, поэтому такие системы опираются на приближения и заранее вычисленные индексы, намеренно извлекая больше данных, чем требуется.
Повторная классификация включает второй этап, на котором берется небольшой набор результатов и каждый из них изучается более тщательно. Этот процесс медленнее, но более точен, и он никогда не затрагивает базу данных. Те результаты, которые были получены при первом просмотре — обычно двадцать или пятьдесят лучших — снова оцениваются более мощной и точной моделью.
Первый просмотр выделяет кандидатов, а повторная классификация определяет, кто из них займет место в ограниченном окне контекста.
Почему первый просмотр не может выполнить работу полностью
Если модель вжимания хороша, разве лучший результат не должен автоматически оказаться на первом месте? Не всегда, поскольку процесс поиска при первом просмотре по своей природе сопряжен с шумом.
Оценка ключевых слов, такая как BM25, основана на точном совпадении терминов. Широкая справочная страница, в которой код ошибка упоминается пятьдесят раз, часто обгоняет короткий пошаговый руководство, в котором он упоминается дважды, поскольку алгоритм воспринимает частоту упоминаний как степень релевантности.
Поиск векторов имеет свои особенности, связанные с шумом. Эмбеддинги определяют семантическую близость путем размещения понятий в высокомерномензиональном пространстве. При поиске решений проблем они выделяют фрагменты, связанные с упомянутыми системами. Общие обзоры обычно находятся очень близко к конкретным процедурам в этом пространстве. Фрагмент, описывающий архитектуру оплаты, может иметь более высокий рейтинг, чем точная команда для перезапуска процесса обработки платежей. Модель понимает, что оба элемента связаны с оплатой, но не может отличить операционную инструкцию от теоретического описания.
Гибридный метод поиска объединяет лексические и семантические списки в единый полный набор кандидатов, включающий необходимые доказательства. Однако в этот набор также попадают дубликаты, устаревшие документы, общие краткие описания и фрагменты, имеющие лишь частичную релевантность.
Результаты первого этапа поиска никогда не следует считать окончательным уровнем ранжирования. Он действует как воронка, а повторное ранжирование выполняет роль фильтра в её нижней части.
Два способа переранжирования
Как только появляются кандидаты, необходимо как-то их отсортировать. Стандартом стали два подхода. Один из них использует кросс-энкодеры — специализированные модели, обученные оценивать степень релевантности документа к запросу. Другой подход основан на больших языковых моделях: модель вроде Claude или GPT получает запрос и документ и должна определить, насколько полезен этот документ.
Оба подхода значительно улучшают качество ранжирования по сравнению с первоначальной обработкой. Они отличаются по времени выполнения, стоимости, детерминизму и техническим требованиям к реализации.
Кросс-энкодеры
Кросс-энкодер представляет собой трансформер, хотя он работает иначе, чем модели эмбеддингов в векторной базе данных.
Стандартная модель вжимания пропускает запрос через сеть для получения одного вектора, а документ — для получения другого, затем сравнивает эти два вектора с помощью косинусного сходства. Это и есть дизайн двойного кодера, и он быстр, поскольку каждый вектор документа может быть вычислен заранее.
Кросс-кодер не генерирует отдельных векторов. Он объединяет запрос и документ в одну строку и пропускает всё это сразу через сеть. Благодаря самообратной связи на каждом слое можно одновременно рассматривать слова запроса и слова документа, чтобы понять, как термины связаны в контексте. Результатом является единый показатель релевантности.
Такой дизайн отлично подходит для принятия решений о высокоточной релевантности. Результаты предсказуемы и стабильны, с гораздо меньшей изменчивостью, чем при использовании языковой модели, и он надежно отдает предпочтение конкретному фрагменту информации перед общим резюме.
Вот функция переранжирования, созданная на основе API кросс-кодера, размещенного на сервере.
import cohere
import time
def rerank_cross_encoder(
query: str,
candidates: list[dict],
top_n: int = 5,
model: str = "rerank-v4.0-pro",
) -> list[dict]:
"""
The practical default for second-stage ranking.
Passes the query and candidate texts to a dedicated cross-encoder model.
"""
co = cohere.ClientV2()
# Extract just the text content for the API call
documents = [c["content"] for c in candidates]
resp = co.rerank(
model=model,
query=query,
documents=documents,
top_n=top_n,
)
# Reattach the original metadata and the new score
reranked = []
for r in resp.results:
original_chunk = candidates[r.index]
reranked.append({
**original_chunk,
"rerank_score": r.relevance_score
})
return reranked
В производственных условиях кросс-кодер подходит для десяти–пятидесяти лучших кандидатов. Команды по умолчанию используют его при поиске документации для инженеров, в базах знаний поддержки и во внутренних помощниках-копилотах.
Основной недостаток — скорость. Кросс-кодер не может просканировать всю базу данных. Он ждет первого прохода, получает кандидатов через API или локальную модель, затем ждет оценок, что добавляет этап сетевой передачи данных и вычислений.
Инструменты переранжирования LLM
Альтернативой является универсальная языковая модель, выполняющая роль судьи. Строгий промпт содержит запрос пользователя и один фрагмент кандидата, после чего модель возвращает оценку.
Инструменты для переранкинга LLM хорошо справляются с тонкостями. Кросс-кодер ищет глубокую семантическую связь, тогда как LLM может также проверять, завершена ли процедура, анализировать противоречивые доказательства и отделять фрагменты с конкретными шагами от тех, что содержат лишь фоновую информацию. В промпте можно указать, что необходимо наказывать нечеткость или отдавать предпочтение фрагментам с примерами кода.
Этот промпт для оценки использует клиент Anthropic.
import anthropic
JUDGE_PROMPT = """\
You are a strict relevance judge. Given a user query and a candidate document chunk,
rate how well the chunk answers the query on a scale of 0 to 10.
Respond with ONLY a JSON object in this exact format:
{"score": <int>, "reason": "<one short sentence>"}
Query: {query}
Chunk: {chunk}"""
async def rerank_llm(
query: str,
candidates: list[dict],
top_n: int = 5,
) -> list[dict]:
"""
Expensive special forces. Uses an LLM to reason about nuance and completeness.
"""
client = anthropic.AsyncAnthropic()
scored = []
for c in candidates:
resp = await client.messages.create(
model="claude-opus-4-6",
max_tokens=128,
messages=[{
"role": "user",
"content": JUDGE_PROMPT.format(query=query, chunk=c["content"]),
}],
)
import json
try:
result = json.loads(resp.content[0].text)
scored.append({
**c,
"rerank_score": result["score"],
"reason": result.get("reason", "")
})
except (json.JSONDecodeError, KeyError):
# Fallback if the model fails to follow JSON instructions
scored.append({**c, "rerank_score": 0, "reason": "parse_error"})
# Sort by the LLM-assigned score descending
scored.sort(key=lambda x: x["rerank_score"], reverse=True)
return scored[:top_n]
От модели требуется указывать причину вместе с оценкой. Сначала написание обоснования работает как метод последовательного мышления и способствует получению более стабильного итогового результата.
Они зарабатывают на запросах высокой ценности, в системах с низкой пропускной способностью, где точность ответа оправдывает ожидание, а также в сложных корпусах данных, где релевантность определяется тонкостями правил, а не простым совпадением темы.
Затраты велики. Для каждого кандидатского фрагмента выполняется полный вызов большой языковой модели, и даже при одновременном выполнении вызовов результат определяется самым медленным ответом. Расходы значительно превышают расходы кросс-кодеров, а результат менее детерминирован.
Как реранкинг влияет на задержку
Дополнительный этап меняет способ работы системы. Он влечет за собой дополнительные вызовы модели, дополнительную сериализацию текстовых данных и дополнительное время на передачу данных по сети.
Пользователям не важно, как организован процесс поиска, и они не ориентируются на показатели воспроизведения информации. Они обращают внимание на качество ответа и время реакции. Теоретически идеальный инструмент для переранжирования бесполезен, если из-за него ассистент работает слишком медленно, чтобы его можно было использовать.
Этот инструмент отслеживает время выполнения функции переранжирования.
def rerank_with_timing(
rerank_fn: callable,
query: str,
candidates: list[dict],
top_n: int = 5,
) -> tuple[list[dict], float]:
"""
Measure the exact cost of the reranking stage.
"""
t0 = time.perf_counter()
results = rerank_fn(query, candidates, top_n)
latency_ms = (time.perf_counter() - t0) * 1000
return results, latency_ms
Обработка двадцати кандидатов с помощью API быстрого кросс-кодера может увеличить время на 150–300 миллисекунд, что обычно является разумной ценой за лучший контекст. Обработка пятидесяти кандидатов с помощью модели LLM может занять от двух до четырех секунд. Плюс ещё три секунды на генерацию — в итоге пользователь ждёт семь секунд, что превышает допустимый срок в чат-интерфейсе.
Количество кандидатов, проходящих повторную классификацию, требует настройки. Топ-10 обрабатывается быстро, но может пропустить нужный фрагмент, если первая попытка даёт слабые результаты. Топ-50 покрывает практически всё, но исчерпывает весь лимит задержек. Для кросс-кодера в производственных условиях обычно оптимальным является топ-20.
Когда повторная классификация оправдана
Добавьте механизм повторной классификации, когда первая попытка поиска даёт много шума, но в целом удаётся найти необходимые данные.
Если следы указывают на то, что конкретный фрагмент доказательств часто встречается среди кандидатов, но занимает десятое или пятнадцатое место, переранжирование сразу же восстановит работу системы. Это оправдано, когда общие документы регулярно превосходят точные процедуры, а качество ответа в значительной степени зависит от наличия трех лучших фрагментов в запросе, поэтому точность имеет приоритет перед простым воспроизведением информации.
Руководства по эксплуатации, внутренние помощники для формирования политик и инструменты диагностики почти всегда требуют использования механизма переранжирования. Выбор правильной команды вместо близкой по результату может определить, будет ли инцидент решен или превратится в сбой.
Когда переранжирование — избыточно
При очень маленьком корпусе данных его можно пропустить. Базу знаний из пятидесяти страниц можно полностью встроить и обслуживать с помощью векторного поиска.
Также бессмысленно использовать дополнительную обработку, когда первый этап уже почти идеален. В системах с большим количеством поисковых запросов, где алгоритм BM25 надежно помещает нужную ссылку API на первое место, кросс-кодеры лишь замедляют работу, не меняя результата.
При очень строгих ограничениях по задержке, как в голосовых помощниках или системах автодополнения кода, нет места для дополнительных этапов обработки. Настройте первый этап и согласитесь на некоторый уровень шума. А когда нужный фрагмент обычно уже оказывается на первом или втором месте, ранжирование уже не является фактором, ограничивающим эффективность.
Виды сбоев, на которые следует обратить внимание
Инструменты для переранжирования исправляют порядок результатов, но приносят с собой собственные проблемы. Обращайте внимание на них после внедрения.
Склонность к общим обзорам. Некоторые кросс-кодеры обучаются на данных, где поощряются всесторонние ответы. Общий обзор может превзойти по оценке небольшой точный фрагмент кода, поскольку обзор имеет больше общего словарного запаса с запросом.
Повторное перегружение информацией. Если в корпусе имеются три версии одного руководства по развертыванию, при первом просмотре, скорее всего, будут загружены все три. Система переоценки считает, что каждая из них хорошо отвечает на запрос, и ставит им высокие оценки. В результате три лучших результата в окне контекста содержат одинаковую информацию, что мешает включению следующего уникального факта. Подобные инструменты помогают устранять почти идентичные записи.
def dedupe_candidates(
candidates: list[dict],
similarity_threshold: float = 0.85,
) -> list[dict]:
seen_tokens: list[set[str]] = []
deduped = []
for c in candidates:
tokens = set(c["content"].lower().split())
is_dup = False
for s in seen_tokens:
# Calculate simple Jaccard similarity
overlap = len(tokens & s) / max(len(tokens | s), 1)
if overlap >= similarity_threshold:
is_dup = True
break
if not is_dup:
deduped.append(c)
seen_tokens.append(tokens)
return deduped
Предвзятость в пользу устаревших документов. Межкодировщик читает только текст и не имеет ни малейшего представления о том, какой сейчас год. Тщательно подготовленный руководство за 2022 год может показать лучшие результаты, чем поспешно составленное руководство за 2026 год, даже если более старый документ полностью устарел. Метаданные, применяемые после переранжирования, корректируют оценки с учётом свежести и статуса документа.
from datetime import datetime, timezone
def apply_metadata_boost(
candidates: list[dict],
freshness_halflife_days: int = 90,
) -> list[dict]:
now = datetime.now(timezone.utc)
boosted = []
for c in candidates:
score = c.get("rerank_score", 0.0)
# Hard penalty for superseded documentation
if c.get("status") == "superseded":
score *= 0.4
# Gradual decay for older documents
updated = c.get("updated_at")
if updated:
age_days = (now - updated).days
decay_factor = max(0.5, 1 - age_days / (freshness_halflife_days * 2))
score *= decay_factor
boosted.append({**c, "rerank_score": score})
# Sort again based on the adjusted scores
boosted.sort(key=lambda x: x["rerank_score"], reverse=True)
return boosted
Взрыв задержек. Качество улучшается после внедрения, но время от времени система тормозит на пять секунд. Это обычно происходит, когда увеличивается количество кандидатов или снижается качество внешнего API для переранжирования. Всегда следует предусмотреть режим аварийного возврата с исчерпанием времени, который игнорирует процесс переранжирования и возвращает результаты первого этапа, если второй этап занимает слишком много времени.
Правильная оценка инструмента переранжирования
Команды часто измеряют не то, проверяя, присутствует ли правильный фрагмент информации среди двадцати лучших результатов. Поиск на первом этапе уже покрывает эту задачу.
Лучшим вопросом является то, поднял ли инструмент переранжирования правильный фрагмент на достаточно высокую позицию, чтобы он имел значение. Для этого необходимо измерять релевантность элементов на первой и третьей позициях, полноту ответа и эффективность удаления дубликатов, а также отслеживать нагрузку, связанную с задержками, и стоимость обработки каждого запроса.
Создайте инструмент оценки, который сравнивает базовый вариант первого прохождения с кросс-кодером и реранкером на основе больших языковых моделей, используя одинаковые наборы кандидатов.
from dataclasses import dataclass
@dataclass
class RerankEvalCase:
query: str
expected_substring: str
category: str # e.g., "identifier", "procedure", "troubleshooting"
def eval_reranking(
cases: list[RerankEvalCase],
retrieve_fn: callable,
rerank_fns: dict[str, callable | None],
top_n: int = 3,
) -> dict:
"""
Compare multiple reranking strategies against a baseline.
Measures hit rate at top-N and tracks latency overhead.
"""
results = {}
for name, rerank_fn in rerank_fns.items():
hits = 0
total_latency = 0.0
by_type: dict[str, dict] = {}
for case in cases:
# Get the exact same starting candidates for every strategy
candidates = retrieve_fn(case.query)
if rerank_fn is not None:
reranked, lat = rerank_with_timing(
rerank_fn, case.query, candidates, top_n
)
total_latency += lat
else:
# Baseline: just take the top-N from first-pass retrieval
reranked = candidates[:top_n]
# Check if the expected evidence made it into the final prompt window
top_contents = [r["content"] for r in reranked]
found = any(case.expected_substring in c for c in top_contents)
hits += int(found)
# Track metrics by query category
by_type.setdefault(case.category, {"hit": 0, "total": 0})
by_type[case.category]["total"] += 1
by_type[case.category]["hit"] += int(found)
total = len(cases)
results[name] = {
"hit_rate": hits / total if total else 0,
"avg_latency_ms": total_latency / total if total else 0,
"by_type": {
t: {**v, "rate": v["hit"] / v["total"]}
for t, v in by_type.items()
},
}
return results
Набор тестов должен включать несколько типов запросов: те, где важны идентификаторы и важна точность строк, те, где важен порядок выполнения шагов, и те, где симптомы помогают определить коренные причины проблем.
Цифры точно показывают, что получает алгоритм переранжирования. Если кросс-кодер повышает долю лучших результатов среди трех лидеров с 60% до 85% за 200 миллисекунд, его следует использовать. Если алгоритм переранжирования на основе больших языковых моделей достигает уровня 88% за три секунды, кросс-кодер, скорее всего, останется. Переранжирование должно оправдываться лучшими итоговыми ответами в рамках реалистичных временных ограничений.
Разумный стандартный набор компонентов
Надежная основа для производственной системы формируется с соблюдением определенной последовательности. Документы разделяются с использованием метода разбиения с учетом структуры. Для сбора от двадцати до тридцати кандидатов применяется гибридный метод поиска. Удаляются дубликаты, содержащие практически идентичный текст. Остальные кандидаты оцениваются с помощью кросс-кодера с учетом семантической релевантности. Применяется корректировка на основе метаданных, наказывающая устаревшие документы. Затем из окончательного отсортированного списка берутся три–пять лучших фрагментов для формирования запроса.
def two_stage_retrieve(
query: str,
retrieve_fn: callable,
top_k: int = 20,
top_n: int = 5,
) -> tuple[list[dict], dict]:
"""
The complete production pipeline for second-stage ranking.
"""
t0 = time.perf_counter()
# Stage 1: Fast hybrid retrieval
candidates = retrieve_fn(query)[:top_k]
# Clean up the candidate pool
candidates = dedupe_candidates(candidates)
# Stage 2: Cross-encoder rerank
# We score slightly more than top_n to allow metadata boosts to reorder the edges
score_limit = min(top_n * 2, len(candidates))
reranked = rerank_cross_encoder(query, candidates, top_n=score_limit)
# Apply business logic for freshness and status
final = apply_metadata_boost(reranked)[:top_n]
latency = (time.perf_counter() - t0) * 1000
trace = {
"query": query,
"first_pass_count": len(candidates),
"post_rerank_count": len(reranked),
"final_count": len(final),
"latency_ms": latency,
}
return final, trace
Стандартный подход работает, поскольку каждый инструмент выполняет ту задачу, для которой он предназначен. Механизм поиска обеспечивает восстановление всех подходящих кандидатов, механизм переранжирования определяет порядок кандидатов, а кросс-кодеры обеспечивают наилучшее соотношение качества и задержки на втором этапе.
Есть одно ограничение: механизм переранжирования не следует применять для устранения ошибок в данных. При некорректном разделении данных он просто будет правильно сортировать некорректные фрагменты. При отсутствии метаданных он не сможет определить, какой документ устарел. Сначала необходимо исправить процессы загрузки и извлечения данных.
Что остается нерешенным у механизма переранжирования
Механизм переранжирования гарантирует, что самые убедительные доказательства из полученного набора попадут в модель. Однако он не может сказать, был ли поиск по векторам вообще правильным решением. Возьмем вопрос о том, сколько пользователей активно находится в определенном регионе: направление такого вопроса на индекс документации неверно, поскольку эта цифра хранится в SQL-базе данных.
Качество поиска имеет несколько уровней. На первом этапе находится текст, на втором — он сортируется, но ещё до этого принимается решение о том, где искать. Распределение запросов между векторным поиском, SQL-запросами и онлайн-поиском в зависимости от намерений пользователя является уровнем, предшествующим всем вышеперечисленным процессам.