Field Log · Entry

RAPTOR — 재귀 요약 트리로 긴 문맥을 검색하는 RAG 기법

rag-techniques 시리즈 1편 — RAPTOR 재귀 요약 트리 기반 Retrieval

평면 chunk RAG는 “이 문서 전체의 논지가 뭐야?” 같은 질문에서 거의 항상 무너집니다. top-k로 끌어온 인접 청크 몇 개에는 문서 전체에 흩어진 단서가 담겨 있지 않기 때문입니다. RAPTOR는 이 빈틈을 인덱싱 단계에서 메우는 기법입니다.

결론부터. RAPTOR는 원문 청크를 재귀적으로 클러스터링·요약해 추상화 레벨이 다른 트리를 bottom-up으로 쌓고, 검색할 때는 리프(원문)와 상위 요약을 같은 평면에 펼쳐 한 번에 뒤집니다. 그래서 세부 사실 질문과 통합형 질문에 동시에 대응합니다.

이 글은 테디노트 〈랭체인 노트〉의 RAPTOR 챕터(CH12 RAG · 04. 긴 문맥 요약)를 읽고 그 코드를 주재료로, 동작 원리(왜 GMM·UMAP인지)와 코드 워크스루를 한 편에 묶었습니다. LLM 위키처럼 길이가 들쭉날쭉한 문서 묶음을 인덱싱하려는 분을 독자로 가정합니다.

이 글이 답하는 질문

  • RAPTOR는 일반 chunk RAG와 무엇이 다른가?
  • 왜 k-means가 아니라 GMM이고, 왜 클러스터링 앞에 UMAP을 두는가?
  • 재귀 요약 트리는 코드로 어떻게 쌓는가?
  • collapsed tree 검색이란 무엇이고 왜 논문이 그걸 권장하는가?
  • 위키독스 예제 코드를 그대로 복붙하면 안 되는 곳은 어디인가?

평면 chunk RAG가 놓치는 것

반도체 장비 OEM에서 사내 RAG를 운영하던 시기, 답이 가장 자주 어긋난 질문이 두 종류였습니다. 하나는 “이 부품 교체 절차” 같은 세부 질문이고 — 이건 GraphRAG 시리즈에서 다룬 anchor 문제였습니다. 다른 하나는 “이 장비군 매뉴얼 전체에서 가장 자주 나오는 고장 패턴이 뭐야?” 같은 통합형 질문이었습니다.

통합형 질문이 어려운 이유는 단순합니다. 답에 필요한 단서가 문서 한 군데에 모여 있지 않습니다. 매뉴얼 3장, 정비로그 어딘가, 트러블슈팅 표 여기저기에 흩어져 있습니다. top-k=4로 인접 청크만 끌어오면 그중 무엇도 “전체 패턴”을 담고 있지 않습니다. 임베딩이 아무리 좋아도, 존재하지 않는 통합 표현을 검색해 올 수는 없습니다.

RAPTOR의 발상은 여기서 출발합니다. 검색이 통합 표현을 찾지 못한다면, 인덱싱 단계에서 통합 표현을 미리 만들어 두면 된다. 흩어진 청크를 묶어 LLM으로 요약해 두고, 그 요약을 검색 후보에 함께 넣는 것입니다.


RAPTOR 한 줄 정의와 트리 구조

RAPTOR는 Recursive Abstractive Processing for Tree-Organized Retrieval의 약어입니다. Stanford의 Sarthi et al.이 제안했고 ICLR 2024에 게재됐습니다 (arXiv:2401.18059).

논문 한 줄 정의를 풀면 이렇습니다.

원문을 리프 청크로 쪼개 임베딩 → soft clustering → 각 클러스터를 LLM으로 요약 → 그 요약들을 다시 임베딩·클러스터링·요약 … 을 재귀적으로 반복해, 추상화 레벨이 다른 트리를 bottom-up으로 쌓는다.

그림으로 그리면 이런 모양입니다.

Level 2          [ 문서 묶음 전체 요약 ]          ← 가장 추상적
                  /                  \
Level 1   [ 주제 A 요약 ]        [ 주제 B 요약 ]    ← 중간 추상화
            /      \              /      \
Level 0  chunk  chunk  chunk   chunk  chunk  chunk  ← 리프 = 원문 청크

핵심은 리프(원문)와 모든 레벨의 요약 노드가 한 인덱스에 함께 들어간다는 점입니다. “전해질 열화의 원인은?” 같은 세부 질문은 리프 청크가, “이 문서의 전체 논지는?” 같은 질문은 상위 요약 노드가 답 후보가 됩니다. 검색이 질문에 맞는 추상화 수준을 알아서 고르게 됩니다.

논문은 복잡 추론 벤치마크에서 큰 향상을 보고합니다. 대표 수치로, 긴 문맥 QA 벤치마크인 QuALITY에서 GPT-4와 결합 시 절대 정확도가 약 20%p 향상됐습니다 (정확한 표는 논문 참조).


재료 준비 — 문서 로드와 토큰 분포 보기

원리는 그쯤 하고 코드로 내려갑니다. 먼저 인덱싱할 문서를 모으고, 길이 분포를 봅니다. 위키독스 예제는 LangChain의 LCEL 문서 웹페이지들을 대상으로 합니다 — LLM 위키를 인덱싱하는 상황과 거의 같은 구조입니다(웹 문서, 길이 들쭉날쭉).

from langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader
from bs4 import BeautifulSoup as Soup
import tiktoken

def num_tokens_from_string(string: str, encoding_name: str) -> int:
    encoding = tiktoken.get_encoding(encoding_name)
    return len(encoding.encode(string))

url = "https://python.langchain.com/docs/expression_language/"
loader = RecursiveUrlLoader(
    url=url, max_depth=20, extractor=lambda x: Soup(x, "html.parser").text
)
docs = loader.load()
docs_texts = [d.page_content for d in docs]

counts = [num_tokens_from_string(d, "cl100k_base") for d in docs_texts]

tiktoken으로 문서별 토큰 수를 세서 히스토그램으로 보면, 위키독스 예제 기준 콘텍스트가 2,000 토큰 미만에서 10,000 토큰 이상까지 퍼져 있습니다. 길이가 균질하지 않다는 건, 단순히 고정 크기로 잘라 임베딩하면 어떤 청크는 너무 잘게, 어떤 문서는 통째로 들어간다는 뜻입니다. 이 불균질함이 바로 트리가 정리해 줄 대상입니다.

여기서 한 가지 선택이 갈립니다. RAPTOR에서 트리의 리프 입자도는 고를 수 있습니다 — 단일 문서를 잘게 쪼갠 청크일 수도, 문서 한 편을 통째로 둔 것일 수도 있습니다. 긴 문맥 LLM으로 요약한다면 문서 전체를 리프로 두는 게 가능하고, 위키독스 예제가 택한 방식이 이쪽입니다. 더 잘게 쪼개고 싶을 때는 RecursiveCharacterTextSplitter로 2,000 토큰 청크를 만들면 됩니다.

from langchain_text_splitters import RecursiveCharacterTextSplitter

# concatenated_content: 앞서 문서들을 출처 기준 정렬·연결한 하나의 문자열
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=2000, chunk_overlap=0
)
texts_split = text_splitter.split_text(concatenated_content)

이 워크스루는 잘게 쪼개지 않고 문서 한 편을 통째로 리프로 둡니다(다음 절의 leaf_texts = docs_texts). 위 청크 분할은 “더 잘게 가고 싶으면 이렇게”의 시연이고, 트리 구축에는 쓰지 않습니다.


왜 GMM + UMAP인가

RAPTOR 트리의 한 단계는 “비슷한 청크끼리 묶어(clustering) → 묶음을 요약(summarize)“입니다. 이 묶는 방식이 RAPTOR에서 가장 흥미로운 부분이고, 두 가지 선택이 들어갑니다.

soft clustering(GMM) — 한 청크가 여러 주제에 걸칠 때

k-means는 한 점을 정확히 한 클러스터에만 넣습니다(hard clustering). 문제는 텍스트 청크가 보통 여러 주제를 동시에 건드린다는 점입니다. “전해질 열화” 청크는 ‘재료’ 주제에도 ‘수명예측’ 주제에도 속합니다. 하드 배정은 이걸 한 군집에 강제로 밀어넣어 연결을 잃습니다.

가우시안 혼합 모델(GMM)은 각 점이 여러 클러스터에 확률적으로 속하게 합니다(soft clustering). 임계 확률을 넘으면 한 청크가 두 클러스터에 동시에 들어가, 두 상위 요약 모두에 기여합니다. 트리가 주제 간 연결을 보존하는 거죠. RAPTOR가 k-means 대신 GMM을 택한 이유입니다.

클러스터 개수 k는 미리 모릅니다. 그래서 k를 1부터 늘려가며 GMM을 적합하고, **BIC(베이지안 정보 기준)가 최소인 k**를 고릅니다. BIC는 적합도와 복잡도(파라미터 수)를 균형 잡아 과적합을 억제합니다.

from sklearn.mixture import GaussianMixture
import numpy as np

def get_optimal_clusters(embeddings, max_clusters=50, random_state=42):
    max_clusters = min(max_clusters, len(embeddings))
    n_clusters = np.arange(1, max_clusters)
    bics = []
    for n in n_clusters:
        gm = GaussianMixture(n_components=n, random_state=random_state)
        gm.fit(embeddings)
        bics.append(gm.bic(embeddings))      # ← BIC 계산
    return n_clusters[np.argmin(bics)]       # ← BIC 최소인 k

UMAP — 클러스터링 앞에 차원을 줄이는 이유

임베딩은 보통 수백~수천 차원입니다. 고차원에서는 점들 간 거리가 균질해져서(“차원의 저주”) “가까움/멈”의 대비가 약해지고, 거리·밀도 기반인 GMM이 군집을 잘 못 잡습니다. 그래서 클러스터링 전에 저차원으로 줄여 군집 구조를 또렷하게 만듭니다. UMAP 공식 문서도 클러스터링 전처리로서의 차원 축소를 이 동기로 설명합니다.

RAPTOR는 UMAP+GMM을 2단계로 적용합니다.

단계이웃 수(n_neighbors)잡는 것
Global큼 (전역)문서 전체의 굵직한 대주제
Local작음 (sqrt(N-1) 휴리스틱)각 대주제 안의 세부 하위 군집

global이 큰 그림(coarse)을, local이 그 안의 결(fine)을 잡습니다. 단일 단계로는 전역 대주제와 국소 세부 주제를 동시에 잡기 어렵기 때문입니다. 두 단계를 거치면 다양한 입자도의 요약 노드가 생기고, 뒤에서 볼 collapsed tree 검색이 고를 수 있는 후보의 추상화 스펙트럼이 넓어집니다.

import umap

def global_cluster_embeddings(embeddings, dim, n_neighbors=None, metric="cosine"):
    if n_neighbors is None:
        n_neighbors = int((len(embeddings) - 1) ** 0.5)
    return umap.UMAP(
        n_neighbors=n_neighbors, n_components=dim, metric=metric
    ).fit_transform(embeddings)

트리를 쌓는다 — 재귀 임베딩·클러스터·요약

클러스터링이 준비되면, 한 레벨을 처리하는 함수는 “임베딩 → 클러스터 → 클러스터별 LLM 요약”입니다. 각 클러스터의 텍스트를 모아 LLM에 넣고 요약 하나를 받습니다.

def embed_cluster_summarize_texts(texts, level):
    df_clusters = embed_cluster_texts(texts)        # 임베딩 + GMM 클러스터링
    # ... 클러스터별로 텍스트를 모아 ...
    summaries = []
    for i in all_clusters:
        formatted_txt = fmt_txt(expanded_df[expanded_df["cluster"] == i])
        summaries.append(chain.invoke({"context": formatted_txt}))   # ← LLM 요약
    # df_summary: 클러스터별 요약 + level + cluster id
    return df_clusters, df_summary

그리고 이걸 재귀로 감쌉니다. 이번 레벨에서 만든 요약들을 다음 레벨의 입력 텍스트로 넘기고, 클러스터가 하나로 수렴하거나 최대 레벨에 닿을 때까지 반복합니다.

def recursive_embed_cluster_summarize(texts, level=1, n_levels=3):
    results = {}
    df_clusters, df_summary = embed_cluster_summarize_texts(texts, level)
    results[level] = (df_clusters, df_summary)

    unique_clusters = df_summary["cluster"].nunique()
    if level < n_levels and unique_clusters > 1:
        new_texts = df_summary["summaries"].tolist()    # ← 요약을 다음 레벨 입력으로
        results.update(
            recursive_embed_cluster_summarize(new_texts, level + 1, n_levels)
        )
    return results

leaf_texts = docs_texts
results = recursive_embed_cluster_summarize(leaf_texts, level=1, n_levels=3)

이게 RAPTOR의 심장입니다. level=1에서 리프 청크를 묶어 요약하고, level=2에서 그 요약들을 다시 묶어 더 추상적인 요약을 만들고… 멈출 때까지 올라갑니다. 위키독스 예제에서는 33개 리프 문서가 6개 클러스터로 묶이고, 다음 레벨에서 1개로 수렴하며 트리가 닫힙니다.

요약 비용 주의: 트리의 모든 클러스터마다 LLM 요약 호출이 한 번씩 듭니다. 클러스터 수 × 레벨 수만큼 호출이 쌓이므로, 단순 임베딩 인덱싱보다 토큰 비용과 인덱싱 시간이 큽니다. 이게 RAPTOR의 가장 큰 트레이드오프입니다.


collapsed tree로 검색한다

트리를 다 쌓았으면 이제 검색입니다. 논문은 두 가지 방식을 비교합니다.

  • Tree traversal(트리 순회): 루트에서 시작해 쿼리와 유사한 top-k 노드를 고르고, 그 자식으로 내려가며 레벨별로 다시 top-k를 고르는 계층 탐색.
  • Collapsed tree(평탄화 트리): 트리의 모든 레벨 노드(리프 + 모든 요약)를 한 풀에 펼쳐 놓고, 레벨 구분 없이 전체에 대해 한 번에 kNN(top-k) 검색.

논문은 collapsed tree를 권장합니다. 질문마다 필요한 추상화 수준이 다른데, collapsed tree는 세부와 고수준 요약을 같은 평면에 두고 검색이 알아서 가장 맞는 입자도를 고르게 합니다. 트리 순회는 매 레벨 top-k 같은 하이퍼파라미터에 민감하고, 상위에서 잘못 가지치기하면 하위 정답 노드에 영영 도달하지 못합니다.

구현은 의외로 단순합니다. 리프 텍스트 + 모든 레벨의 요약을 한 리스트로 합쳐 벡터스토어를 만들면 끝입니다.

from langchain_community.vectorstores import FAISS

all_texts = leaf_texts.copy()
for level in sorted(results.keys()):
    summaries = results[level][1]["summaries"].tolist()
    all_texts.extend(summaries)        # ← 리프 + 모든 레벨 요약을 한 풀에

vectorstore = FAISS.from_texts(texts=all_texts, embedding=embd)
retriever = vectorstore.as_retriever()

all_texts에 리프와 모든 요약 노드가 평탄하게 섞여 있다는 점이 collapsed tree의 전부입니다. 트리 구조는 인덱싱할 때만 쓰였고, 검색 시점에는 “노드들의 평평한 집합”으로 펼쳐집니다. 나머지는 평범한 RAG 체인입니다.

from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt | model | StrOutputParser()
)

rag_chain.invoke("전체 문서의 핵심 주제에 대해 설명해주세요.")   # 통합형 → 상위 요약 노드가 답
rag_chain.invoke("PydanticOutputParser 예시 코드를 작성해 주세요.")  # 세부형 → 리프 청크가 답

같은 retriever인데, 통합형 질문에는 상위 요약 노드가, 세부 질문에는 리프 청크가 끌려옵니다. 인덱스 하나로 두 종류 질문을 모두 받는 것 — 이게 도입부에서 말한 “평면 chunk RAG가 놓치는 것”의 해법입니다.


위키독스 코드를 그대로 쓰면 안 되는 곳

위키독스 예제는 동작 원리를 익히기엔 훌륭하지만, 2024년 초 버전이라 지금 그대로 복붙하면 걸리는 데가 몇 군데 있습니다. LLM 위키에 실제로 적용할 거라면 아래를 손봐야 합니다.

위키독스 원문문제손볼 것
설명은 “Chroma 벡터 저장소”, 코드는 FAISS.from_texts서술과 코드 불일치둘 중 하나로 통일. 둘 다 동작하나 코드 기준 FAISS로 읽으면 됨. 원저자 공식 노트북은 Chroma 사용
from langchain_core.pydantic_v1 import BaseModelpydantic_v1 shim은 LangChain 0.3+에서 제거from pydantic import BaseModel (Pydantic v2)
from langchain.embeddings import ...통합이 별도 패키지로 분리됨langchain_openai, langchain_chroma 등 분리 패키지 경로
FAISS.load_local(DB_INDEX, embd)최신 FAISS는 역직렬화 플래그 필요allow_dangerous_deserialization=True 인자 추가
claude-3-opus-20240229 / gpt-4-turbo-preview구버전 모델명예시일 뿐. 최신 모델로 교체 가능(특정 모델명을 권장 사실로 받지 말 것)

RANDOM_SEED(위키독스는 42)와 local UMAP의 n_neighbors = int((N-1)**0.5) 휴리스틱은 부정확한 게 아니라 재현성·기본값 설계입니다. 워크스루에서는 그대로 두고 “이 숫자가 왜 여기 있는지”를 이해하는 편이 낫습니다.

라이브러리 import 경로는 LangChain 버전마다 자주 바뀝니다. 실제 적용 시점의 정확한 경로는 공식 문서로 한 번 더 확인하세요.


언제 RAPTOR를 쓰고 언제 과한가

사내 RAG를 운영하며 배운 건, RAPTOR가 만능이 아니라 질문 분포와 코퍼스 성격에 따라 켜고 끄는 옵션이라는 점입니다.

RAPTOR가 값을 하는 경우

  • 질문의 상당수가 통합형·요지형이다 (“전체에서 가장 흔한 패턴”, “A와 B의 관계”).
  • 단서가 여러 문서·여러 청크에 흩어지는 multi-hop 질의가 많다.
  • 코퍼스가 비교적 정적이다 — 한 번 트리를 쌓아 오래 쓴다.

RAPTOR가 과한 경우

  • 질문이 대부분 단일 사실 조회다 (“이 부품 토크 스펙은?”). 평면 chunk RAG로 충분하고, 요약 비용만 더 든다.
  • 코퍼스가 자주 바뀐다. 문서가 갱신될 때마다 트리를 (부분)재구축해야 해서 인덱싱 부담이 누적된다.
  • 요약 환각이 치명적인 도메인이다. 상위 노드는 LLM 요약물이라, 요약 단계에서 끼어든 오류가 상위 레벨로 전파될 수 있습니다. 검색이 그 요약 노드를 집으면 환각이 답변에 그대로 반영됩니다. 정비·안전 절차처럼 한 줄 틀리면 안 되는 도메인에서는 상위 요약 노드에 출처 청크를 함께 묶어 검증 경로를 남기는 게 안전합니다.

제 경우 정비 절차 질의(세부·정확성 우선)에는 GraphRAG의 multi-anchor 검색이, 매뉴얼 전체를 아우르는 통합 질의에는 RAPTOR식 요약 트리가 맞았습니다. 두 기법은 경쟁이 아니라 질문 유형별 분업에 가깝습니다.


RAPTOR를 LLM Wiki에 적용하기

이 글을 쓴 실제 동기가 여기 있습니다. 사내 지식을 거창한 벡터DB 없이 markdown LLM Wiki(index.md + frontmatter + alias + 문서 간 링크)로 정리하다 보면, RAPTOR를 그대로 얹고 싶어집니다. 그런데 한 가지 오해를 먼저 풀어야 합니다.

RAPTOR는 관계 그래프를 만드는 기법이 아닙니다. wiki를 구조화하면 그 자체로 이미 그래프입니다 — frontmatter의 related_* 링크가 곧 edge입니다. RAPTOR가 더하는 건 그 그래프 위에 얹는 수직 요약 층입니다. 세 층으로 보면 이렇습니다.

요약 layer (RAPTOR)   summaries/*.md          ← 비슷한 노드를 묶은 상위 요약
관계 graph layer       method ↔ case ↔ failure  ← related_* 링크가 edge
leaf layer             SOP·정비로그·원문 청크    ← 실제 문서

관계 그래프 자체를 어떻게 설계하는지는 GraphRAG 시리즈에서 다뤘습니다. 여기서는 그 위에 RAPTOR 요약 층을 어떻게 얹는지만 봅니다.

embedding 없이 RAPTOR-like 만들기

원본 RAPTOR는 임베딩 → UMAP → GMM으로 묶습니다. 그런데 LLM Wiki는 이미 사람이 tags·related_*묶음 신호를 명시해 뒀습니다. 그러면 임베딩 클러스터링을 메타데이터 기반 묶음으로 대체할 수 있습니다 — GMM의 soft assignment를, 한 문서가 여러 tag·여러 링크에 속하는 구조가 대신합니다.

단계원본 RAPTORembedding-less Wiki 변형
묶기임베딩 → UMAP → GMM soft clustertags/related_* 교집합으로 묶음
요약클러스터 텍스트를 LLM 요약같은 묶음 문서를 LLM 요약 → summaries/
재귀요약을 다시 임베딩·클러스터summary를 다시 상위 index로 묶음
검색collapsed tree kNNindexsummaryleafrelated 탐색

요약 노드는 그냥 또 하나의 markdown 문서입니다. frontmatter에 무엇을 묶었는지(contains)와 — 중요하게 — 유효 기간을 적습니다.

---
id: summary.vacuum-pump-replace-cluster
type: summary
contains:
  - method.vacuum-pump-replace
  - case.2026-05-pumpdown-fail
  - failure.oring-seat-damage
valid_period: { from: 2026-01, to: 2026-06 }   # 시간 민감 도메인 주의
---

검색은 위 → 아래 → 옆

collapsed tree의 “모든 노드를 한 평면에”가 wiki에서는 요약으로 넓게 잡고, leaf로 내려가고, 링크로 옆으로 확장하는 흐름이 됩니다.

질문: "이 펌프 계열에서 자주 나는 교체 실패는?"
→ summary.vacuum-pump-replace-cluster   (요약으로 넓게)
→ method.vacuum-pump-replace            (leaf로 내려가기)
→ failure.oring-seat-damage             (related로 옆 확장)
→ case.2026-05-pumpdown-fail            (현장 근거)

요약 노드 하나가 도입부에서 말한 “검색이 못 찾는 통합 표현”을 미리 만들어 둔 것입니다. 임베딩이 없어도, 사람이 related_*로 그 통합 경로를 박아 두면 됩니다.

시간 민감 도메인에서의 함정

여기서 원본 RAPTOR의 한계가 wiki에서 더 날카로워집니다. 클러스터링이 오래된 사례와 최신 사례를 한 요약에 섞으면 위험합니다. 정비·금융처럼 시점이 답을 바꾸는 도메인에서는 2024년 사례와 2026년 사례가 한 “교체 실패 요약”에 뭉치면 안 됩니다. 그래서 위 frontmatter의 valid_period·freshness 같은 시간 메타를 묶음 기준에 함께 넣어, 시간 축으로도 한 번 잘라 요약해야 합니다.

정리하면, LLM Wiki에 RAPTOR를 적용한다는 건 GMM+UMAP을 옮겨 심는 게 아니라, 요약 층이라는 발상을 옮겨 심는 것입니다. 묶음 신호는 메타데이터가, soft clustering은 다중 링크가, collapsed 검색은 index 탐색이 대신합니다.


FAQ

Q. RAPTOR와 일반 chunk RAG의 차이는? 일반 RAG는 원문을 청크로 쪼개 평면적으로 임베딩하고 인접 top-k만 검색합니다. RAPTOR는 청크를 클러스터링·요약해 추상화 레벨이 다른 트리를 추가로 쌓고, 리프 청크와 상위 요약을 함께 인덱싱합니다. 그래서 세부 질문과 통합형 질문에 동시에 대응합니다.

Q. collapsed tree가 뭔가? tree traversal과 뭐가 다른가? tree traversal은 루트부터 레벨별로 top-k를 따라 내려가는 계층 탐색이고, collapsed tree는 모든 레벨 노드를 한 풀에 펼쳐 전체에 대해 한 번에 kNN 검색합니다. 논문은 더 유연하고 성능 좋은 collapsed tree를 권장합니다.

Q. RAPTOR 인덱싱 비용은 얼마나 드나? 트리의 모든 클러스터마다 LLM 요약 호출이 필요해, 단순 임베딩 인덱싱보다 토큰 비용과 시간이 더 듭니다. 정적이고 깊은 추론이 필요한 코퍼스에 유리하고, 자주 바뀌는 코퍼스엔 재구축 부담이 있습니다.

Q. 왜 k-means가 아니라 GMM을 쓰나? 한 청크가 여러 주제에 걸치는 경우가 많은데, GMM의 soft clustering은 그 청크를 여러 상위 요약에 동시에 기여시켜 주제 연결을 보존합니다. 클러스터 수는 BIC로 자동 선택합니다.

Q. RAPTOR와 GraphRAG는 무엇이 다른가? 둘 다 평면 청크 RAG를 넘어서는 구조적 인덱스지만, RAPTOR는 **요약 트리(수직 추상화)**를, GraphRAG는 **엔티티-관계 지식 그래프(수평 관계망)**를 만듭니다. 대체로 통합·요지형 질문엔 요약 트리가, 관계 추론·정확 조회엔 관계 그래프가 더 어울리는 것으로 정리되지만, 둘은 경쟁이 아니라 함께 쓸 수 있는 층위입니다(자세한 비교는 GraphRAG 시리즈에서 다룹니다).

Q. RAPTOR는 어떤 질문에서 특히 강한가? 여러 청크에 흩어진 단서를 모아야 하는 multi-hop 질문, 그리고 문서 전체의 논지·주제를 묻는 통합형 질문입니다. 논문은 QuALITY에서 GPT-4 결합 시 +20%p 향상을 보고했습니다.


마무리

RAPTOR를 한 문장으로 줄이면 — “검색이 통합 표현을 못 찾으면, 인덱싱이 미리 만들어 둔다” 입니다. 재귀 클러스터링·요약으로 추상화 레벨이 다른 트리를 쌓고, collapsed tree로 리프와 요약을 같은 평면에서 한 번에 검색합니다.

핵심 세 가지로 정리하면 —

  1. 트리는 인덱싱 시간에만 존재한다. 검색 시점엔 모든 노드를 평탄화(collapsed)해 한 번에 kNN.
  2. GMM soft clustering이 주제 연결을 보존한다. 한 청크가 여러 요약에 기여하고, 클러스터 수는 BIC로 정한다.
  3. 요약 비용과 환각 전파 가능성이 트레이드오프다. 정적·통합형 코퍼스에 쓰고, 정확성이 생명인 도메인은 상위 노드에 출처 청크를 묶어 검증 경로를 남긴다.

다음 편에서는 RAPTOR 같은 요약 트리와 GraphRAG 같은 관계 그래프를 언제 어떻게 섞을지 — 질문 유형별 라우팅을 다룰 예정입니다. 평면 RAG 한 장으로 모든 질문을 받던 시절은 지났으니까요.


참고자료