Что такое 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 middle | LLM хуже обрабатывает информацию из середины длинного контекста. |
| Высокая стоимость и 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. Индексация: как построить иерархию
Процесс подготовки документов:
- Разбиение на разделы — по заголовкам (Markdown, HTML, PDF-структура) или с помощью LLM-парсера.
- Генерация эмбеддингов для разделов — можно использовать эмбеддинг заголовка + первые 100 токенов раздела.
- Разбиение разделов на чанки — с помощью рекурсивного character splitter (например, RecursiveCharacterTextSplitter из LangChain).
- Генерация эмбеддингов для чанков — обычный эмбеддинг текста чанка.
- Создание метаданных — каждый чанк хранит ID раздела, позицию в разделе, заголовок раздела.
- Индексация в векторной БД — два индекса: один для разделов, другой для чанков (с фильтром по разделу).
Пример структуры метаданных чанка
{
"chunk_id": "doc1_section3_chunk7",
"section_id": "doc1_section3",
"section_title": "Методы оптимизации",
"chunk_text": "Градиентный спуск — это итеративный метод...",
"embedding": [0.12, -0.45, ...]
}
5. Retrieval pipeline: от запроса к ответу
Пошаговый процесс для одного запроса:
- Эмбеддинг запроса — получаем вектор запроса.
- Поиск по разделам — ищем top-k разделов в индексе разделов (по косинусной близости).
- Фильтрация чанков — для каждого отобранного раздела загружаем все его чанки (или используем предварительно построенный индекс с фильтром).
- Поиск по чанкам — внутри каждого раздела ищем top-m чанков по близости к запросу.
- Объединение результатов — собираем все найденные чанки (максимум k*m), удаляем дубликаты.
- Re-ranking (опционально) — используем кросс-энкодер (например, Cohere rerank) для пересортировки чанков по релевантности.
- Формирование контекста — собираем текст чанков, добавляем заголовки разделов для контекста.
- Генерация ответа — подаём контекст + запрос в 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 и времени ответа.
Инструменты
- Python, LangChain, ChromaDB (или FAISS)
- OpenAIEmbeddings (или sentence-transformers/all-MiniLM-L6-v2)
- PyMuPDF (для извлечения текста и структуры)
- RAGAS (для оценки faithfulness и answer relevance)
Шаги:
- Скачать 10–20 PDF-статей из arXiv (например, по теме NLP).
- Извлечь текст и заголовки разделов (по
\n##или по стилям PDF). - Построить иерархический индекс (разделы + чанки по 512 токенов).
- Построить плоский индекс (все чанки без структуры).
- Для 50 вопросов по статьям выполнить retrieval обоими методами.
- Измерить: время поиска, Recall@10, MRR, контекстную связность (оценить LLM).
- Визуализировать результаты (графики, таблица).
Ожидаемый результат
- Hierarchical retrieval покажет на 10–20% выше Recall@10 при сопоставимом времени.
- Контекст ответов будет более связным (LLM будет реже путать темы).
- Вы получите готовый модуль для интеграции в RAG-систему.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 1 | Проектирование RAG-системы для 10 000 документов |
| 3 | Стратегии chunking'а |
| 5 | Оценка качества retrieval |
| 7 | Уменьшение latency RAG-системы |
| 10 | Self-RAG |
| 20 | Multi-hop RAG |
| 30 | Agentic RAG |
| 644 | Что такое Agentic RAG |
Навигация
- Предыдущий: 644
- Следующий: 646
- Индекс: 00. Индекс разборов