올바른 RAG 전략 선택하기: Chunking, Agentic RAG, 그리고 GraphRAG를 위한 완전한 결정 가이드
요약
RAG 시스템의 성능 저하 원인이 임베딩이나 LLM이 아닌, 부적절한 청킹(Chunking)과 검색 아키텍처의 불일치에 있음을 지적합니다. 효과적인 RAG 구축을 위해 의미를 보존하는 청킹 전략의 중요성을 강조하며, Agentic RAG와 GraphRAG를 포함한 최적의 아키텍처 선택 가이드를 제공합니다.
핵심 포인트
- RAG 실패의 주요 원인은 문서 분할 방식(Chunking)과 쿼리 복잡성 간의 불일치임
- 훌륭한 청킹은 단순한 텍스트 분할이 아니라 의미론적 가치, 구조, 관계를 보존하는 과정임
- 청킹의 목표는 검색 정밀도 향상, 환각 감소, 컨텍스트 윈도우 최적화 및 비용 효율성 달성임
- 유스케이스에 따라 적절한 청킹 전략과 Agentic RAG 또는 GraphRAG 아키텍처를 선택해야 함
서론
많은 RAG 빌더들이 잘 알고 있는 시나리오가 있습니다. 파이프라인을 연결하고, 문서를 로드하고, 질문을 던졌는데 답변이 틀리거나, 모호하거나, 혹은 아주 자신 있게 환각 (Hallucination)을 일으키는 상황입니다. 정보는 지식 베이스 (Knowledge Base)에 분명히 있었습니다. 그렇다면 무엇이 잘못된 것일까요? 대부분의 경우 문제는 임베딩 모델 (Embedding Model)이 아닙니다. LLM의 문제도 아닙니다. 문제는 문서를 저장하기 전에 어떻게 나누었는지, 즉 '청킹 (Chunking)'이라 불리는 과소평가된 기술과, 당신이 선택한 검색 (Retrieval) 아키텍처가 실제 쿼리 (Query)의 복잡성과 일치하는지 여부입니다. 이 블로그에서는 모든 주요 청킹 전략을 살펴보고, 이러한 청크 (Chunks) 위에서 검색과 증강 (Augmentation)이 어떻게 작동하는지 설명하며, 두 가지 고급 아키텍처인 Agentic RAG와 GraphRAG를 다룹니다. 그리고 가장 중요한 것은, 당신의 유스케이스 (Use Case)에 정확히 어떤 조합이 적합한지 알고 마칠 수 있도록 완전한 결정 프레임워크를 제공한다는 점입니다.
🐘 코끼리와 레고 조각들
당신의 문서는 코끼리입니다. 200페이지가 넘는 법률 계약서, 밀도 높은 연구 논문, 방대한 제품 매뉴얼, 또는 수년간 쌓인 기업 지식 등은 크고, 복잡하며, 서로 연결되어 있고, 가치 있는 정보로 가득 차 있습니다. 대규모 언어 모델 (Large Language Model, LLM)은 다음과 같은 이유로 코끼리 전체를 한 번에 효과적으로 소비할 수 없습니다:
- 컨텍스트 윈도우 (Context Window) 제한
- 검색 정밀도 (Retrieval Precision) 제약
- 지연 시간 (Latency) 고려 사항
- 토큰 비용 (Token Cost) 최적화
- 컨텍스트 희석 (Context Dilution) 및 검색 노이즈
따라서 코끼리는 더 작은 조각으로 나누어져야 합니다. 하지만 바로 이 지점에서 대부분의 RAG 시스템이 실패합니다. 만약 코끼리를 무작위로 자른다면, 의미를 파괴하게 됩니다. 문장은 문맥을 잃고, 아이디어는 파편화되며, 관계는 사라집니다. 검색 품질은 무너집니다. 좋은 청킹이란 단순히 텍스트를 작게 만드는 것이 아닙니다. 검색을 효율적으로 만들면서 의미를 보존하는 것입니다. 이것이 바로 청킹을 코끼리를 레고 (LEGO) 조각으로 만드는 과정으로 이해해야 하는 이유입니다.
LEGO 조각의 특징은 다음과 같습니다:
- 모듈형 (Modular) — 각 조각이 독립적으로 존재할 수 있음
- 구조적 (Structured) — 조각들이 관련 있는 조각들과 깔끔하게 연결됨
- 일관적 (Consistent) — 신뢰할 수 있는 검색 (Retrieval)을 위해 충분히 표준화됨
- 의미론적 (Meaningful) — 각 조각이 의미론적 가치 (Semantic value)를 보존함
- 조합 가능 (Composable) — 작업에 필요한 조각들만 조립할 수 있음
훌륭한 청킹 (Chunking)도 이와 동일하게 작동합니다. 잘 설계된 청크는 효율적인 검색 (Retrieval)과 생성 (Generation)을 위해 충분히 작게 유지되면서도, 구조 (Structure), 의미 (Semantics), 관계 (Relationships), 그리고 주변 문맥 (Surrounding context)을 보존해야 합니다. RAG 시스템에서 청킹의 진정한 목표는 단순히 문서를 분할하는 것이 아닙니다. 청킹은 단순히 문서를 작게 만드는 것이 아닙니다. 실제 목표는 다음과 같습니다:
- 의미론적 의미 (Semantic meaning) 보존
- 검색 정밀도 (Retrieval precision) 향상
- 환각 (Hallucinations) 감소
- 컨텍스트 윈도우 (Context windows) 최적화
- 근거 제시 품질 (Grounding quality) 개선
- 지연 시간 (Latency)과 비용의 균형 조절
실무적으로는: 더 나은 청크가 더 나은 검색, 더 나은 프롬프트 (Prompts), 그리고 더 나은 답변으로 이어집니다. 목표는 적절한 시점에, 적절한 섹션에서, 적절한 문맥과 함께, 적절한 조각을 검색하는 것입니다. 이것이 효과적인 검색 증강 생성 (Retrieval Augmented Generation, RAG)의 토대입니다.
RAG 파이프라인 (RAG Pipeline): 엔드 투 엔드 (End to End)
복잡성에 관계없이 모든 RAG 시스템은 동일한 4단계 흐름을 따릅니다. 각 단계를 이해하면 청킹 및 아키텍처 결정이 임의적인 것이 아니라 명확해집니다.
1단계: 문서화 (Document)
PDF, Word 파일, 웹 페이지, 전사 데이터 (Transcripts), 데이터베이스 내보내기 파일 등 원본 소스 자료를 준비합니다. 이는 LLM (Large Language Model)에 직접 전달하기에는 너무 큽니다. 인덱싱 (Indexing)하거나 검색하기 전에 청크로 나누어야 합니다.
2단계: 청킹 및 임베딩 (Chunking and Embedding)
문서를 단위로 자르고, 각 단위를 의미의 수치적 표현인 벡터 임베딩 (Vector embedding)으로 변환합니다. 이 임베딩들은 벡터 데이터베이스 (Vector database)에 저장되어 검색 가능한 인덱스를 형성합니다. 여기서의 청킹 전략이 이후의 모든 과정을 결정합니다.
3단계: 검색 (Retrieval)
사용자가 질문을 하면, 질의 (Query) 또한 임베딩됩니다. 벡터 데이터베이스는 질의의 의미와 가장 유사한 임베딩을 가진 청크들을 반환합니다. 이것들이 바로 여러분이 검색한 LEGO 조각들입니다.
4단계: 증강(Augmentation) 및 생성(Generation) 검색된 청크(chunks)는 주변의 부모 컨텍스트(parent context)와 함께 프롬프트(prompt)로 조립되어 LLM(Large Language Model)으로 전송됩니다. 모델은 수신한 자료를 바탕으로 정확하고 근거에 기반한(grounded) 답변을 생성합니다. 핵심 통찰: 답변의 품질은 검색(retrieval) 품질에 의해 제한되며, 검색 품질은 다시 청크(chunk) 품질에 의해 제한됩니다. 더 나은 청크 → 더 나은 검색 → 더 나은 답변. 이후의 모든 아키텍처 결정은 이 토대 위에 구축됩니다.
- 고정 크기 청킹 (Fixed-Size Chunking)
가장 단순하고 가장 널리 사용되는 전략입니다. 문서는 의미, 문장 경계 또는 문서 구조를 고려하지 않고 토큰 수(token count), 문자 수(character count) 또는 단어 수(word count)에 따라 동일한 크기의 블록으로 분할됩니다.
LangChain 메서드
CharacterTextSplitter: 단일 구분자(기본값
)를 기준으로 분할한 다음, 문자 수를 기준으로 chunk_size를 강제합니다.
TokenTextSplitter: 토크나이저(예: OpenAI 모델용 tiktoken)를 사용하여 토큰 수로 분할합니다. 문자 기반 분할보다 LLM 컨텍스트 예산(context budgets) 측면에서 더 정확합니다.
from langchain.text_splitter import CharacterTextSplitter, TokenTextSplitter
# 문자 기반 분할기 (Character-based splitter)
= CharacterTextSplitter (
chunk_size = 1000, # 청크당 최대 문자 수
chunk_overlap = 200, # 청크 경계에서 반복되는 문자 수
separator = " \n\n "
)
# 토큰 기반 분할기 (Token-based splitter)
= TokenTextSplitter (
chunk_size = 512, # 청크당 최대 토큰 수
chunk_overlap = 50 # 청크 경계에서 반복되는 토큰 수
)
중첩(Overlap) 가이드: 일반적으로 1020%의 중첩이 일반적입니다. chunk_size가 1000인 경우, chunk_overlap을 100200 사이로 설정하십시오. 중첩은 약간의 중복을 대가로, 관련 있는 답변이 두 개의 청크로 나뉘어 버릴 위험을 줄여줍니다.
장점: 구현이 간단하고 빠르며, 예측 가능하고 확장이 용이합니다.
단점: 문장을 중간에 끊는 경우가 빈번하여, 복잡한 문서의 의미적 연속성(semantic continuity)과 검색 품질을 저하시킵니다.
가장 적합한 용도: 로그(Logs), 원격 측정(telemetry), JSON, CSV 및 기타 균일한 구조화된 콘텐츠.
- 재귀적 청킹 (Recursive Chunking)
맹목적으로 분할하는 대신, 재귀적 청킹은 자연스러운 문서 구조를 존중합니다.
이는 우선순위가 지정된 구분자 목록 — \n\n, 그 다음 \n, 그 다음 . / ! / ?, 그 다음 공백 —을 따라 작동하며, 청크(Chunk)가 여전히 크기 제한을 초과할 때만 더 세밀한 구분자로 넘어갑니다. 이는 LangChain에서 대부분의 문서 유형에 대해 권장하는 기본 전략입니다.
LangChain 메서드
RecursiveCharacterTextSplitter: 주요 구현체로, 다음 단계로 넘어가기 전에 목록에 있는 각 구분자를 시도합니다.
RecursiveCharacterTextSplitter.from_language(): 특정 프로그래밍 언어(Python, JS, Markdown, HTML 등)를 위해 사전 구성된 구분자 목록을 제공합니다.
from langchain.text_splitter import RecursiveCharacterTextSplitter, Language
# 일반 산문 분할기 (General prose splitter)
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=150,
separators=["\n\n", "\n", ".", "!", "?", " ", ""]
)
# 언어 인식형 (예: Python 소스 코드) 분할기
splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=1000,
chunk_overlap=100
)
중첩(Overlap) 가이드: 대부분의 산문에는 1015%의 중첩이 효과적입니다. 코드의 경우, 청크 간에 함수 시그니처(Function signatures)가 중복되는 것을 방지하기 위해 중첩을 낮게(50100 토큰) 유지하세요.
장점: 고정 크기 청킹 (Fixed size chunking)보다 의미론적 보존력이 뛰어남; 우수한 범용 전략; 검색 일관성(Retrieval coherence) 향상.
단점: 의미(Meaning)를 인식하기보다는 구조(Structure)를 인식함; 성능이 문서 포맷팅 품질에 따라 달라짐.
적합한 용도: 문서(Documentation), PDF, 기사, 지식 베이스(Knowledge bases), 웹 페이지.
- 의미론적 청킹 (Semantic Chunking)
청크의 크기를 얼마로 해야 하는지 묻는 대신, 의미론적 청킹은 어떤 문장들이 서로 함께 속하는지를 묻습니다. 문장들은 벡터 임베딩 (Vector embeddings)으로 변환되고, 인접한 문장 간의 유사도가 측정되며, 유사도가 임계값(Threshold) 아래로 떨어지는 지점 — 즉 주제가 전환되는 지점 —에 청크 경계가 설정됩니다.
LangChain 메서드
SemanticChunker (langchain_experimental 제공) — 세 가지 중단점 탐지 (Breakpoint detection) 전략을 지원합니다: 백분위수 (Percentile), 표준 편차 (Standard deviation), 사분위수 범위 (Interquartile).
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
splitter = SemanticChunker(
embeddings = OpenAIEmbeddings(),
breakpoint_threshold_type = "percentile", # 또는 "standard_deviation", "interquartile"
breakpoint_threshold_amount = 95 # 유사도 하락이 상위 5%에 해당할 때 경계 생성
)
중첩 가이드 (Overlap guidance): 의미론적 청킹 (Semantic chunking)은 고정된 청크 중첩 (chunk_overlap)을 사용하지 않습니다. 경계가 의미를 기반으로 설정되므로, 중첩을 사용하는 것은 이 접근 방식의 효과를 저해할 수 있습니다. 만약 경계 부분에서 연속성이 필요하다면, 이전 청크의 마지막 문장을 수동으로 추가하는 것을 고려하십시오.
장점: 높은 검색 관련성 (retrieval relevance); 강력한 의미론적 연속성 (semantic continuity); 정밀도가 중요한 시스템에 적합함.
단점: 계산 비용이 높음; 청킹 시점에 임베딩 모델 (embedding model)이 필요함; 데이터셋마다 유사도 임계값 (similarity thresholds)을 조정해야 함.
가장 적합한 분야: 기업 지식 시스템, 연구 플랫폼, 정책 문서, 그리고 문맥적 정밀도가 요구되는 AI 어시스턴트.
- 계층적 청킹 (Hierarchical Chunking)
문맥을 위한 큰 부모 청크 (parent chunks)와 정밀도를 위한 작은 자식 청크 (child chunks)라는 두 가지 수준의 청크를 생성합니다. 검색 시에는 자식 수준을 대상으로 관련 구절을 찾은 다음, 부모 수준으로 확장하여 주변 문맥을 함께 반환합니다. 이는 RAG의 핵심 트레이드오프 (trade-off)를 직접적으로 해결합니다: 즉, 작은 청크는 정밀도를 높이고, 큰 청크는 문맥을 보존합니다.
LangChain 메서드
ParentDocumentRetriever: 부모 청크는 문서 저장소 (document store)에, 자식 청크는 벡터 저장소 (vector store)에 저장한 다음, 검색 시점에 이들을 연결합니다.
from langchain.retrievers import ParentDocumentRetriever
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.storage import InMemoryStore
from langchain_community.vectorstores import Chroma
parent_splitter = RecursiveCharacterTextSplitter(
chunk_size = 2000
) # large context chunks
child_splitter = RecursiveCharacterTextSplitter(
chunk_size = 400
) # precise retrieval chunks
retriever = ParentDocumentRetriever(
vectorstore = Chroma(embedding_function = embeddings),
docstore = InMemoryStore(),
child_splitter = child_splitter,
parent_splitter = parent_splitter
)
Overlap guidance: Apply overlap only on the child splitter (typically 10–15%). Parent chunks are retrieved wholesale for context, so overlap there adds noise rather than value.
Strengths: Strong retrieval precision without sacrificing context; effective for long documents.
Weaknesses: More complex to index and retrieve; requires additional storage and orchestration.
Best for: Legal documents, technical manuals, books, enterprise documentation, and compliance systems.
5. Structure and Metadata Aware Chunking
Uses the document's own structure titles, headers, sections, tables, and page layout as natural chunk boundaries rather than treating the document as plain text. Especially important for enterprise PDFs and structured reports, where layout carries semantic meaning that arbitrary splits would destroy.
LangChain Methods
MarkdownHeaderTextSplitter: splits on Markdown heading levels and attaches header text as metadata to each chunk.
HTMLHeaderTextSplitter: same pattern for HTML documents, splitting on '<h1>-<h4>' tags.
from langchain.text_splitter import MarkdownHeaderTextSplitter, HTMLHeaderTextSplitter
Markdown
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on = [
( " # " , " h1 " ),
( " ## " , " h2 " ),
( " ### " , " h3 " )
]
)
chunks = md_splitter.
split_text ( markdown_text ) # 각 청크 (chunk)는 다음과 같은 메타데이터를 포함합니다: {"h1": "섹션 제목", "h2": "하위 섹션"} # HTML
html_splitter = HTMLHeaderTextSplitter ( headers_to_split_on = [( " h1 " , " h1 " ), ( " h2 " , " h2 " )] )
중첩 (Overlap) 가이드: 이 스플리터 (splitters)들은 크기 기반이 아닌 구조적 경계 기반의 청크를 생성합니다. 만약 후속 청크 (downstream chunks)가 여전히 너무 크다면, 출력물을 적절한 중첩 (100–150자)을 가진 RecursiveCharacterTextSplitter로 전달하여 두 번째 패스 (second pass)를 수행하십시오.
장점: 레이아웃 의미론 (layout semantics)을 보존합니다; 표 (tables)를 온전하게 유지합니다; 구조화된 기업용 문서에 대한 검색 품질 (retrieval quality)을 향상시킵니다.
단점: 유능한 문서 파서 (document parser)가 필요합니다; 파서의 품질이 성능을 직접적으로 제한합니다.
적합한 용도: 재무 보고서, 컴플라이언스 (compliance) 문서, 기술 PDF, 의료 문서 및 기업 기록.
- 하이브리드 청킹 (Hybrid Chunking)
동일한 코퍼스 (corpus) 내에서 콘텐츠 유형에 따라 서로 다른 청킹 (chunking) 전략을 적용합니다: 로그 (logs)에는 고정 크기 (fixed-size), 문서화 (documentation)에는 재귀적 (recursive), 연구 논문 (research papers)에는 의미론적 (semantic), Markdown 또는 HTML에는 구조 인식 (structure aware) 방식을 사용합니다. LangChain에는 전용 하이브리드 스플리터 (hybrid splitter)가 없습니다. 하이브리드 파이프라인 (pipelines)은 위의 빌딩 블록 (building blocks)을 사용하여 수동으로 구성됩니다.
from langchain.text_splitter import (
TokenTextSplitter ,
RecursiveCharacterTextSplitter ,
MarkdownHeaderTextSplitter ,
)
from langchain_experimental.text_splitter import SemanticChunker
def hybrid_chunk ( doc ):
content_type = doc . metadata . get ( " type " )
if content_type ===
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기