Query rewriting and multi-hop retrieval
Rewrite ambiguous questions, decompose multi-hop requests, and route queries without multiplying latency and cost without bounds.
Lesson content is currently available in Russian.
What you will learn
- →Разворачивать контекстно-зависимые и разговорные запросы в самодостаточные поисковые.
- →Применять decomposition, HyDE и step-back prompting и знать, когда каждая техника оправдана.
- →Строить multi-hop retrieval, где результат первого поиска порождает второй запрос.
Запрос пользователя — не запрос к индексу
Retrieval ищет то, что спросили, а пользователи не формулируют запросы под индекс. Четыре типовых разрыва. Разговорная зависимость: «а в про-тарифе?» — без предыдущей реплики диалога запрос бессмыслен; перед поиском его нужно развернуть в «какие лимиты хранилища в про-тарифе» (contextualization / condensation — обязательный шаг любого RAG-чата). Множественность интентов: «сравни X и Y по цене и производительности» — это четыре поиска, а не один. Разрыв лексики: пользователь говорит «слетела оплата», документация — «ошибка обработки платежа». Ошибочные предпосылки: «почему функция Z удалена?» — а она переименована.
Поэтому зрелые системы вставляют между пользователем и индексом слой query understanding: дешёвая LLM переписывает, разворачивает и размножает запрос. Базовая техника — multi-query: сгенерировать 3–5 переформулировок («как вернуть деньги» → «политика возврата средств», «refund policy», «отмена платежа и компенсация»), выполнить все поиски параллельно и слить результаты через уже знакомый RRF. Это дёшево (один вызов маленькой модели, поиски параллельны) и стабильно поднимает recall на запросах с нестандартной лексикой.
Важный продакшен-нюанс: rewriting добавляет латентность и точку отказа, поэтому его делают на быстрой модели (gpt-4o-mini, haiku-класс) со strict-таймаутом и fallback’ом на исходный запрос — если переписывание упало или заняло больше 200–400 мс, ищем по оригиналу.
Decomposition, HyDE и step-back
Три техники посильнее multi-query, каждая под свой класс запросов:
- Decomposition — сложный вопрос разбивается на подвопросы, каждый ищется отдельно, ответ собирается из всех результатов. «Чем тариф Pro отличается от Enterprise по SLA и цене?» → «SLA тарифа Pro», «SLA тарифа Enterprise», «цена Pro», «цена Enterprise». Обязательна для сравнительных и агрегирующих вопросов.
- HyDE (Hypothetical Document Embeddings) — LLM пишет гипотетический ответ на вопрос, и эмбеддится этот ответ, а не вопрос. Идея: вопрос и документ находятся в разных «регионах» векторного пространства, а выдуманный ответ по форме и лексике ближе к реальному документу. Факты в гипотезе могут быть неверными — важна форма. Работает на коротких вопросах к длинным документам; на запросах с точными ID бесполезен и даже вреден.
- Step-back prompting — по конкретному вопросу генерируется более общий: «упадёт ли скидка при даунгрейде с годового Pro?» → «как работают скидки при смене тарифа». Общий запрос достаёт раздел с правилами, конкретный — исключения; ищут по обоим.
SUBQ_PROMPT = (
'Разбей вопрос на 2-4 самостоятельных поисковых подвопроса, '
'по одному на строку. Вопрос: {q}'
)
async def decompose_and_search(q: str) -> list[Doc]:
subqs = (await fast_llm.generate(SUBQ_PROMPT.format(q=q))).splitlines()
results = await asyncio.gather(*[hybrid_search(s, limit=20) for s in subqs])
return rrf(results)[:30]
Правило выбора: multi-query — дефолт для всех; decomposition — если в вопросе союз «и»/«или»/«сравни»; HyDE — если eval показывает разрыв лексики вопрос↔документ; step-back — для вопросов об исключениях из правил.
Multi-hop: когда ответа нет ни в одном чанке
Некоторые вопросы принципиально не решаются одним поиском: «кто руководит отделом, в котором работает автор документа X?» — сначала нужно найти документ X и извлечь автора, потом найти его отдел, потом руководителя. Это multi-hop retrieval: результат первого хопа становится параметром запроса второго.
Наивный RAG на таких вопросах галлюцинирует особенно охотно: retrieval приносит чанки, похожие на вопрос целиком, но не содержащие цепочку фактов, и модель «достраивает» недостающие звенья. Классические датасеты этого класса — HotpotQA и MuSiQue (2–4 хопа); на них single-shot retrieval теряет десятки пунктов точности против итеративных схем.
Базовая реализация — управляемый цикл: (1) поиск по исходному вопросу; (2) LLM смотрит на найденное и отвечает на вопрос «достаточно ли фактов для ответа? если нет — какой следующий поисковый запрос?»; (3) новый поиск, накопление контекста; (4) стоп по условию «достаточно» или по лимиту хопов (обычно 3–4, иначе латентность и стоимость уходят в небо). Схема известна в литературе как self-ask / IRCoT (interleaved retrieval + chain-of-thought).
Два инженерных предохранителя обязательны. Бюджет: каждый хоп — это вызов LLM плюс поиск, поэтому multi-hop включается классификатором сложности запроса, а не для всех подряд. Накопление контекста: промежуточные находки суммируются в компактные факты («автор X — Иванова, отдел — аналитика»), а не конкатенируются полными чанками, иначе к третьему хопу контекст распухает и качество рассуждения падает.
Роутинг запросов: не всё — retrieval
Продакшен-система обслуживает поток разнородных запросов, и прогонять все через полный конвейер «rewrite → hybrid → rerank → multi-hop» расточительно. Перед конвейером ставят router — быструю LLM или классификатор, который решает: (a) простой фактический вопрос — прямой гибридный поиск без переписывания; (b) сравнение/агрегация — decomposition; (c) цепочка фактов — multi-hop; (d) чит-чат или вопрос вне базы знаний — вообще без retrieval, чтобы не подмешивать нерелевантный контекст (частая причина галлюцинаций: модель чувствует себя обязанной использовать приложенные чанки); (e) запрос к структурированным данным — SQL/API вместо векторного поиска.
Роутер — это одна классификация на дешёвой модели (20–50 мс, доли цента), которая экономит секунды и центы на каждом сложном маршруте и заметно чистит качество простых. Ошибки роутера дешевле недо-инженерии: неверно направленный в полный конвейер простой запрос просто медленнее; сложный запрос, ушедший по простому пути, ловится eval’ом и правкой правил роутинга.
Практический минимум для внедрения из этого урока: (1) condensation разговорных запросов — всегда; (2) multi-query с RRF — дефолт; (3) роутер на три маршрута: no-retrieval / simple / complex; (4) decomposition и multi-hop — по показаниям eval на вашем распределении запросов. Каждый слой добавляйте по одному и замеряйте recall и латентность до/после — сложность конвейера сама по себе не ценность.
Practice
Слой query understanding
Соберите 15 «плохих» запросов трёх типов: 5 контекстно-зависимых из диалога, 5 сравнительных, 5 с лексикой, не совпадающей с документами. Реализуйте condensation + multi-query(3) с RRF-слиянием и decomposition для сравнительных. Замерьте recall@10 до и после для каждого типа.
Deliverable
Скрипт со слоем rewriting (fallback на исходный запрос по таймауту) и таблица recall@10 «до/после» по трём типам запросов с выводом, какая техника дала наибольший прирост.
Checkpoint
Пользователь в чате спрашивает: «а чем это отличается от того, что было в прошлой версии?» Разберите вслух, какие преобразования пройдёт этот запрос до попадания в индекс и почему один поиск здесь не сработает.
- ✓ Назван шаг condensation: разворачивание анафоры «это»/«прошлой версии» в самодостаточный запрос по истории диалога.
- ✓ Показана decomposition сравнительного вопроса на подзапросы по каждой версии и сущности со слиянием через RRF.
- ✓ Упомянуты предохранители: быстрая модель для rewriting, таймаут с fallback, лимит хопов и роутер, решающий, нужен ли сложный маршрут.
Check yourself
Sources