Как работает RAPTOR (иерархическое суммирование для длинного контекста)?

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

RAPTOR (Retrieval Augmented Processing with Tree‑based Organization) — это метод иерархической организации документов, при котором чанки текста кластеризуются по семантической близости, для каждого кластера LLM генерирует краткое саммари, а затем саммари кластеризуются снова, формируя многоуровневое дерево. При retrieval поиск начинается с корня дерева и спускается вниз по релевантным ветвям, что позволяет системе эффективно обрабатывать документы произвольной длины и давать LLM иерархическое представление контекста, избегая проблемы «lost in the middle».


1. Термин: RAPTOR и мотивация

RAPTOR — это аббревиатура от Retrieval Augmented Processing with Tree‑based Organization. Основная идея: вместо того чтобы хранить все чанки документа в плоском списке и искать по ним линейно, RAPTOR строит иерархическое дерево саммари. Каждый узел дерева — это краткое изложение (саммари) группы семантически связанных фрагментов текста.

Мотивация в классическом RAG при работе с очень длинными документами (книги, научные статьи, юридические контракты) возникает несколько проблем:

  • Lost in the middleLLM хуже обрабатывает информацию, расположенную в середине контекста.
  • Линейный retrieval не учитывает структуру документа: релевантные куски могут быть разбросаны, а их суммарный смысл теряется.
  • Ограничение контекстного окна — даже самые современные LLM имеют лимит токенов, поэтому все чанки в промпт не поместятся.

RAPTOR решает эти проблемы, создавая сжатое иерархическое представление, которое позволяет LLM «видеть» документ на разных уровнях детализации.


2. Этап 1: Чанкинг и эмбеддинги

Первый шаг — разбиение исходного документа на чанки (фрагменты текста). Обычно используются стратегии с перекрытием (overlap) для сохранения контекста на границах. Для каждого чанка вычисляется эмбеддинг — векторное представление, отражающее его семантику. Эмбеддинги могут быть получены с помощью моделей типа text-embedding-ada-002 или all-MiniLM-L6-v2.

# Пример: разбиение текста на чанки и получение эмбеддингов
from langchain.text_splitter import RecursiveCharacterTextSplitter
from sentence_transformers import SentenceTransformer

embedder = SentenceTransformer('all-MiniLM-L6-v2')
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
chunks = splitter.split_text(document)
chunk_embeddings = embedder.encode(chunks)

3. Этап 2: Кластеризация чанков

Все чанки группируются в кластеры на основе семантической близости их эмбеддингов. В оригинальной статье RAPTOR используется Gaussian Mixture Model (GMM) с автоматическим выбором числа кластеров (по критерию AIC или BIC). Альтернативно можно применить K‑means или иерархическую кластеризацию.

Почему кластеризация, а не просто k ближайших соседей? Кластеризация позволяет выявить естественные тематические группы в документе, а не просто ближайшие по расстоянию фрагменты. Это даёт более осмысленные группы для последующего суммирования.

from sklearn.mixture import GaussianMixture

# Предположим, embeddings — матрица (N_chunks, dim)
gmm = GaussianMixture(n_components=10, random_state=42)
cluster_labels = gmm.fit_predict(chunk_embeddings)

4. Этап 3: Генерация саммари кластеров

Для каждого кластера все входящие в него чанки подаются в LLM (например, GPT‑4 или Llama) с промптом: «Суммируй следующие фрагменты текста в одно краткое изложение, сохраняя ключевые факты». Результат — саммари кластера, которое становится узлом первого уровня дерева.

Важно саммари должно быть достаточно коротким (например, 100–200 токенов), чтобы на следующем уровне можно было кластеризовать уже сами саммари.

from openai import OpenAI

client = OpenAI()

def summarize_cluster(chunks_text):
    prompt = f"Суммируй следующие фрагменты текста:\n{chunks_text}\n\nКраткое изложение:"
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=200
    )
    return response.choices[0].message.content

5. Этап 4: Рекурсивное построение дерева

Полученные саммари первого уровня снова эмбеддятся, кластеризуются, и для каждого нового кластера генерируется саммари более высокого уровня. Процесс повторяется, пока не останется один кластер — корень дерева. В результате получается многоуровневое дерево, где:

  • Листья — исходные чанки.
  • Узлы промежуточных уровней — саммари групп чанков.
  • Корень — саммари всего документа.

Глубина дерева зависит от размера документа и параметров кластеризации (обычно 2–4 уровня).


6. Этап 5: Retrieval в дереве

При поступлении запроса пользователя:

  1. Вычисляется эмбеддинг запроса.
  2. Поиск начинается с корня дерева. Эмбеддинг корня сравнивается с эмбеддингом запроса (косинусная близость).
  3. Выбирается наиболее релевантный дочерний узел (среди детей корня). Сравнение может быть как по эмбеддингу саммари, так и по комбинации с эмбеддингами потомков.
  4. Процесс повторяется рекурсивно: спускаемся вниз по дереву, на каждом уровне выбирая самый релевантный дочерний узел.
  5. Когда достигаем листьев (чанков), возвращаем все чанки из выбранного поддерева (или только листья, в зависимости от реализации).

Преимущество вместо сравнения запроса со всеми чанками (O(N)) мы проходим по дереву за O(log N) шагов, и каждый шаг использует сжатое саммари, что ускоряет поиск и улучшает релевантность.

def retrieve(query, tree, embedder):
    query_emb = embedder.encode([query])
    node = tree.root
    while node.children:
        # выбираем ребёнка с максимальной косинусной близостью
        child_scores = [cosine_similarity(query_emb, child.embedding) for child in node.children]
        node = node.children[argmax(child_scores)]
    return node.chunks  # листья

7. Преимущества RAPTOR

ПреимуществоОписание
Иерархическое представлениеLLM получает не плоский список чанков, а структурированное дерево, что улучшает понимание контекста.
Сжатие контекстаВместо сотен чанков в промпт можно подать саммари верхних уровней, экономя токены.
Устойчивость к lost in the middleВажные факты не теряются в середине, так как они агрегированы в саммари.
МасштабируемостьДерево можно строить для документов любого размера; retrieval остаётся быстрым.
ГибкостьМожно комбинировать с другими техниками (HyDE, query rewriting).

8. Недостатки и ограничения

НедостатокОписание
Вычислительные затратыПостроение дерева требует множества вызовов LLM для генерации саммари (особенно для больших документов).
Зависимость от качества LLMПлохое саммари на нижних уровнях приведёт к потере информации на верхних.
Потеря деталейПри агрегации мелкие, но важные детали могут быть утеряны, если они не попали в саммари.
Сложность настройкиПараметры кластеризации (число кластеров, порог) требуют экспериментов.
Не подходит для потоковых данныхДерево статично; обновление документа требует перестроения.

9. Сравнение с другими методами

МетодПодходКогда лучше
Обычный RAGПлоский retrieval по чанкамКороткие документы, простые запросы
Parent Document RetrievalЧанки + родительские документыКогда нужен полный контекст вокруг чанка
HyDEГенерация гипотетического ответа для поискаЗапросы, плохо совпадающие с текстом
RAPTORИерархическое дерево саммариОчень длинные документы, сложные многоуровневые запросы
Self‑RAGДинамический выбор retrieval + рефлексияКогда нужно контролировать, когда искать

10. Реализация: псевдокод построения дерева

def build_raptor_tree(chunks, embedder, llm, max_depth=3):
    # Шаг 1: эмбеддинги чанков
    embeddings = embedder.encode(chunks)
    nodes = [Node(content=chunk, embedding=emb, is_leaf=True) for chunk, emb in zip(chunks, embeddings)]
    
    current_level = nodes
    for level in range(max_depth):
        if len(current_level) <= 1:
            break
        # Шаг 2: кластеризация
        cluster_labels = cluster_embeddings([n.embedding for n in current_level])
        clusters = group_by_label(current_level, cluster_labels)
        # Шаг 3: генерация саммари для каждого кластера
        new_nodes = []
        for cluster in clusters:
            texts = [n.content for n in cluster]
            summary = llm.summarize(texts)
            summary_emb = embedder.encode([summary])[0]
            parent = Node(content=summary, embedding=summary_emb, children=cluster)
            new_nodes.append(parent)
        current_level = new_nodes
    return current_level[0]  # корень

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

Задача Реализовать RAPTOR на небольшом датасете (например, 10–20 статей из Wikipedia по одной теме) и сравнить его с обычным RAG по метрикам hit rate@5 и faithfulness (доля ответов, не содержащих галлюцинаций).

Инструменты Python, LangChain, OpenAI API (или локальная LLM), scikit-learn (GMM), sentence-transformers.

Шаги:

  1. Загрузить статьи, разбить на чанки по 500 токенов.
  2. Построить дерево RAPTOR (глубина 2–3).
  3. Реализовать retrieval: для каждого тестового запроса получить чанки через RAPTOR и через обычный косинусный поиск.
  4. Подать чанки в LLM и получить ответы.
  5. Оценить hit rate (есть ли релевантный чанк в top-5) и faithfulness (вручную или через LLM-as-judge).

Ожидаемый результат RAPTOR покажет более высокий hit rate для запросов, требующих обобщения информации из разных частей документа, и меньше галлюцинаций за счёт лучшего контекста.


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

ВопросТема
5Оценка качества retrieval (метрики для RAPTOR)
3Стратегии chunking (влияют на качество кластеризации)
10Self‑RAG (альтернативный подход к улучшению retrieval)
2Lost in the middle (RAPTOR как решение)
1Проектирование RAG‑системы для 10 000 документов (масштабирование RAPTOR)
7Уменьшение latency (RAPTOR ускоряет retrieval за счёт дерева)

Навигация