Что такое hierarchical retrieval для long context RAG (когда контекст > 100k)?

Краткий тезис

Hierarchical retrieval (иерархический поиск) — это стратегия поиска релевантных фрагментов в документах, длина которых превышает 100k токенов. Вместо того чтобы искать сразу по всем мелким чанкам, система сначала определяет релевантные разделы документа (уровень 1), затем внутри них — релевантные чанки (уровень 2) и, при необходимости, конкретные предложения (уровень 3). Такой подход снижает вычислительную нагрузку, улучшает качество retrieval за счёт контекста раздела и позволяет подавать в LLM только действительно нужные части документа.


1. Термин: Hierarchical Retrieval

Hierarchical retrieval — это метод поиска информации, при котором документы предварительно структурируются в иерархию (например, документ → разделы → чанки → предложения). Поиск выполняется поэтапно: на каждом уровне отбираются кандидаты для следующего, более детального уровня.

Long context RAG — это вариант RAG, где в качестве источника знаний выступают документы очень большого объёма (сотни тысяч токенов). Прямая подача всего документа в LLM невозможна из-за ограничения контекстного окна (даже у GPT-4 оно ~128k токенов) и эффекта «lost in the middle».

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


2. Проблема Long Context RAG

При работе с документами >100k токенов возникают три основные проблемы:

ПроблемаОписание
Ограничение контекстного окнаДаже самые современные LLM (Claude 3, Gemini 1.5) имеют окно 200k–1M токенов, но подача всего документа дорога и часто неэффективна.
Lost in the middleLLM хуже обрабатывает информацию из середины длинного контекста.
Высокая стоимость и latencyОбработка 100k+ токенов на каждый запрос резко увеличивает время и затраты.
Шум в retrievalПри плоском поиске по миллиону мелких чанков легко получить нерелевантные фрагменты, не имеющие общего контекста.

Hierarchical retrieval решает эти проблемы, предоставляя LLM только компактные, контекстно-связанные блоки.


3. Архитектура: три уровня

Типичная иерархия включает три уровня:

Уровень 1: Разделы (Sections)

  • Документ разбивается на логические разделы (главы, параграфы, секции).
  • Каждый раздел имеет заголовок и краткое саммари (или эмбеддинг заголовка).
  • При поиске сначала сравнивается запрос с эмбеддингами разделов (или их саммари).
  • Отбираются top-k разделов (например, k=3).

Уровень 2: Чанки (Chunks)

  • Внутри каждого отобранного раздела выполняется поиск по мелким чанкам (например, по 512 токенов).
  • Чанки могут перекрываться (overlap) для сохранения контекста.
  • Отбираются top-m чанков из каждого раздела (например, m=5).

Уровень 3: Предложения (Sentences) — опционально

  • Если нужна высокая точность, внутри отобранных чанков можно искать конкретные предложения.
  • Используется sentence-level retrieval (например, с помощью Sentence-BERT).
  • Позволяет получить ответ буквально из одного предложения.

Итоговый контекст для LLM объединение отобранных чанков (или предложений) с указанием их раздела. Общий объём обычно не превышает 4–8k токенов.


4. Индексация: как построить иерархию

Процесс подготовки документов:

  1. Разбиение на разделы — по заголовкам (Markdown, HTML, PDF-структура) или с помощью LLM-парсера.
  2. Генерация эмбеддингов для разделов — можно использовать эмбеддинг заголовка + первые 100 токенов раздела.
  3. Разбиение разделов на чанки — с помощью рекурсивного character splitter (например, RecursiveCharacterTextSplitter из LangChain).
  4. Генерация эмбеддингов для чанков — обычный эмбеддинг текста чанка.
  5. Создание метаданных — каждый чанк хранит ID раздела, позицию в разделе, заголовок раздела.
  6. Индексация в векторной БД — два индекса: один для разделов, другой для чанков (с фильтром по разделу).

Пример структуры метаданных чанка

{
  "chunk_id": "doc1_section3_chunk7",
  "section_id": "doc1_section3",
  "section_title": "Методы оптимизации",
  "chunk_text": "Градиентный спуск — это итеративный метод...",
  "embedding": [0.12, -0.45, ...]
}

5. Retrieval pipeline: от запроса к ответу

Пошаговый процесс для одного запроса:

  1. Эмбеддинг запроса — получаем вектор запроса.
  2. Поиск по разделам — ищем top-k разделов в индексе разделов (по косинусной близости).
  3. Фильтрация чанков — для каждого отобранного раздела загружаем все его чанки (или используем предварительно построенный индекс с фильтром).
  4. Поиск по чанкам — внутри каждого раздела ищем top-m чанков по близости к запросу.
  5. Объединение результатов — собираем все найденные чанки (максимум k*m), удаляем дубликаты.
  6. Re-ranking (опционально) — используем кросс-энкодер (например, Cohere rerank) для пересортировки чанков по релевантности.
  7. Формирование контекста — собираем текст чанков, добавляем заголовки разделов для контекста.
  8. Генерация ответа — подаём контекст + запрос в LLM.

Псевдокод

def hierarchical_retrieve(query, section_index, chunk_index, k=3, m=5):
    query_emb = embed(query)
    # Уровень 1
    section_results = section_index.similarity_search(query_emb, k=k)
    chunks = []
    for section in section_results:
        # Уровень 2
        section_chunks = chunk_index.search(
            query_emb, 
            filter={"section_id": section.id},
            k=m
        )
        chunks.extend(section_chunks)
    # Уровень 3 (опционально)
    # sentences = sentence_search(chunks, query)
    return chunks

6. Преимущества и недостатки

ПреимуществаНедостатки
Скорость — поиск по разделам отсекает 90% данных, дальше работаем только с малым объёмом.Сложность индексации — нужно правильно разбить документ на разделы, что не всегда тривиально.
Качество — чанки из релевантного раздела имеют контекст, меньше шума.Зависимость от структуры — если документ плохо структурирован (нет заголовков), иерархия строится хуже.
Экономия токенов — в LLM подаётся только ~5% от всего документа.Дополнительные вызовы эмбеддинга — нужно два поиска вместо одного.
Масштабируемость — можно обрабатывать документы до 10M+ токенов.Риск пропуска — если релевантный раздел не попал в top-k, теряются все его чанки.

7. Сравнение с плоским retrieval

ХарактеристикаПлоский retrievalИерархический retrieval
Размер корпусаДо 100k чанков (на одной машине)Миллионы чанков (за счёт фильтрации)
Скорость на документе 200k токенов~500 мс (поиск по 400 чанкам)~200 мс (поиск по 10 разделам + 50 чанкам)
Контекстная связностьНизкая (чанки из разных мест)Высокая (чанки из одного раздела)
Точность (Recall@10)70–80%85–95% (при хорошей структуре)
Сложность реализацииНизкаяСредняя

8. Пример реализации (Python + LangChain)

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.schema import Document

# 1. Загрузка документа
with open("big_doc.txt") as f:
    text = f.read()

# 2. Разбиение на разделы (по заголовкам ##)
sections = text.split("\n## ")
section_docs = []
for i, sec in enumerate(sections):
    lines = sec.split("\n", 1)
    title = lines[0].strip()
    content = lines[1] if len(lines) > 1 else ""
    section_docs.append(Document(
        page_content=content,
        metadata={"section_id": f"sec_{i}", "title": title}
    ))

# 3. Индексация разделов (эмбеддинги заголовков)
embeddings = OpenAIEmbeddings()
section_index = Chroma.from_documents(
    documents=[Document(page_content=d.metadata["title"]) for d in section_docs],
    embedding=embeddings,
    collection_name="sections"
)

# 4. Разбиение разделов на чанки
chunk_splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50)
all_chunks = []
for sec_doc in section_docs:
    chunks = chunk_splitter.split_documents([sec_doc])
    for chunk in chunks:
        chunk.metadata["section_id"] = sec_doc.metadata["section_id"]
        chunk.metadata["section_title"] = sec_doc.metadata["title"]
    all_chunks.extend(chunks)

# 5. Индексация чанков
chunk_index = Chroma.from_documents(
    documents=all_chunks,
    embedding=embeddings,
    collection_name="chunks"
)

# 6. Иерархический поиск
def hierarchical_search(query, k_sections=3, k_chunks_per_section=5):
    # Уровень 1
    sec_results = section_index.similarity_search(query, k=k_sections)
    selected_ids = [r.metadata["section_id"] for r in sec_results]
    
    # Уровень 2
    final_chunks = []
    for sec_id in selected_ids:
        chunks_in_sec = chunk_index.similarity_search(
            query, 
            k=k_chunks_per_section,
            filter={"section_id": sec_id}
        )
        final_chunks.extend(chunks_in_sec)
    return final_chunks

# Пример использования
query = "Как работает градиентный спуск?"
results = hierarchical_search(query)
context = "\n\n".join([f"[{c.metadata['section_title']}] {c.page_content}" for c in results])

9. Метрики оценки

Для оценки hierarchical retrieval используются те же метрики, что и для обычного retrieval, но с учётом иерархии:

  • Section Recall@k — доля релевантных разделов, попавших в top-k разделов.
  • Chunk Recall@k — доля релевантных чанков среди отобранных (с учётом фильтрации по разделам).
  • Hierarchical Hit Rate — доля запросов, для которых хотя бы один релевантный чанк найден на любом уровне.
  • Context Precision — точность итогового контекста (оценивается LLM-judge).

Важно метрики должны учитывать, что чанк может быть релевантен только в контексте своего раздела. Поэтому gold standard лучше размечать с указанием раздела.


10. Когда использовать, а когда нет

СценарийРекомендация
Документ >100k токенов, чёткая структура (книги, статьи, отчёты)Hierarchical retrieval — лучший выбор
Документ >100k токенов, но без структуры (сырой лог, транскрипт)Лучше использовать sliding window + re-ranking
Короткие документы (<10k токенов)Иерархия избыточна, достаточно плоского поиска
Высокие требования к latency (<100 мс)Иерархия может быть медленнее из-за двух запросов (но можно кэшировать)
Нужна максимальная точность (юридические/медицинские кейсы)Иерархия + re-ranking дают наилучший результат

Пет-проект для закрепления

Задача Реализовать hierarchical retrieval для набора научных статей (PDF, каждая ~50–200 страниц) и сравнить с плоским retrieval по метрикам Recall@10 и времени ответа.

Инструменты

Шаги:

  1. Скачать 10–20 PDF-статей из arXiv (например, по теме NLP).
  2. Извлечь текст и заголовки разделов (по \n## или по стилям PDF).
  3. Построить иерархический индекс (разделы + чанки по 512 токенов).
  4. Построить плоский индекс (все чанки без структуры).
  5. Для 50 вопросов по статьям выполнить retrieval обоими методами.
  6. Измерить: время поиска, Recall@10, MRR, контекстную связность (оценить LLM).
  7. Визуализировать результаты (графики, таблица).

Ожидаемый результат

  • Hierarchical retrieval покажет на 10–20% выше Recall@10 при сопоставимом времени.
  • Контекст ответов будет более связным (LLM будет реже путать темы).
  • Вы получите готовый модуль для интеграции в RAG-систему.

Связь с другими вопросами

ВопросТема
1Проектирование RAG-системы для 10 000 документов
3Стратегии chunking
5Оценка качества retrieval
7Уменьшение latency RAG-системы
10Self-RAG
20Multi-hop RAG
30Agentic RAG
644Что такое Agentic RAG

Навигация