본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 26. 04:45

다양한 청킹 (Chunking) 전략을 활용한 고급 RAG 시스템 구축 — 실전 가이드

요약

NVIDIA NIM, Qdrant를 활용하여 Apple의 10-K 보고서를 대상으로 4가지 청킹 전략을 비교한 고급 RAG 시스템 구축 가이드입니다. 실험 결과 의미론적 청킹(Semantic chunking)이 가장 우수한 성능을 보였습니다.

핵심 포인트

  • 4가지 청킹 전략(고정 크기, 재귀적, 의미론적, 계층적) 비교 분석
  • 의미론적 청킹이 0.86의 종합 점수로 가장 높은 성능 기록
  • NVIDIA 임베딩 모델 사용 시 input_type 파라미터 설정의 중요성
  • 데이터 수집부터 Streamlit UI까지 RAG 전체 파이프라인 구현

저는 NVIDIA NIM 모델, Qdrant, 그리고 커스텀 평가 지표를 사용하여 Apple의 10-K 보고서를 대상으로 4가지 청킹 (Chunking) 전략(고정 크기, 재귀적, 의미론적, 계층적)을 비교하는 고급 RAG 시스템을 구축했습니다. 의미론적 청킹 (Semantic chunking)이 0.86의 종합 점수로 승리했습니다. 제가 배운 모든 내용을 소개합니다.

서론 (Introduction)
검색 증강 생성 (Retrieval-Augmented Generation, RAG)은 오늘날 LLM (Large Language Models)의 가장 실용적인 응용 분야 중 하나입니다. RAG는 모델의 학습 데이터에 의존하는 대신, 사용자의 문서에서 관련 정보를 검색하여 정확하고 근거 있는 답변을 생성하는 데 사용합니다.

하지만 대부분의 RAG 튜토리얼이 생략하는 부분이 있습니다. 바로 문서를 어떻게 청킹 (Chunking)하느냐가 엄청나게 중요하다는 점입니다. 동일한 파이프라인이라도 청킹 (Chunking) 전략에 따라 완전히 다른 결과가 나올 수 있습니다. 저는 이를 제대로 테스트하고 싶었기에, 동일한 코퍼스 (Corpus)에서 4가지 청킹 (Chunking) 전략을 나란히 실행하고 실제 지표로 평가하는 시스템을 구축했습니다.

이 포스트에서는 데이터 수집 (Data ingestion), 청킹 (Chunking), 벡터 저장소 (Vector storage), 검색 (Retrieval), 생성 (Generation), 평가 (Evaluation), 그리고 Streamlit 챗봇 UI까지 모든 과정을 살펴보겠습니다.

기술 스택 (Tech Stack)

구성 요소도구 / 기술
임베딩 모델 (Embedding Model)NVIDIA llama-nemotron-embed-1b-v2
...
모든 NVIDIA 모델은 OpenAI와 호환되는 https://integrate.api.nvidia.com/v1을 통해 액세스할 수 있어 통합이 간편합니다.

llama-3.3-nemotron-super-49b-v1.5의 한 가지 중요한 특징은 명시적으로 비활성화해야 하는 사고 모드 (Thinking mode)가 있다는 점입니다. 또한 높은 max_tokens (8192 이상)가 필요한데, 그렇지 않으면 모델이 모든 토큰을 내부 추론에 소비하여 콘텐츠로 None을 반환하게 됩니다:

response = client.chat.completions.create(
    model=LLM_MODEL,
    messages=[...],
...

마찬가지로, 임베딩 모델 (embedding model)은 비대칭적 (asymmetric)이며 input_type 파라미터가 필요합니다:

# 문서 청크 (document chunks)용
client.embeddings.create(model=EMBED_MODEL, input=text, extra_body={"input_type": "passage"})

...

데이터 수집 (Data Ingestion)
저는 Apple의 투자자 관계 (investor relations) 페이지에서 PDF로 직접 다운로드한 2022년 및 2023년 Apple 10-K 연례 보고서를 사용했습니다. 금융 문서는 밀집된 단락, 표, 번호가 매겨진 섹션, 그리고 상용구 (boilerplate) 등 혼합된 콘텐츠를 포함하고 있어 청킹 전략 (chunking strategy) 비교를 진정으로 의미 있게 만들어주므로 이러한 종류의 프로젝트에 이상적입니다.

pdfplumber를 이용한 추출:

import pdfplumber

def load_pdfs():
...

데이터 정제 후, 2022년 보고서에서 약 221k 자, 2023년 보고서에서 약 207k 자를 확보했습니다.

4가지 청킹 전략 (The 4 Chunking Strategies)
이 프로젝트의 핵심입니다. 각 전략은 동일한 문서로부터 서로 다른 수와 품질의 청크를 생성합니다.

1. 고정 크기 청킹 (Fixed-size Chunking)
가장 단순한 접근 방식으로, 콘텐츠 경계와 상관없이 일정 N개의 문자를 일정량의 중첩 (overlap)과 함께 분할합니다.

from langchain_text_splitters import CharacterTextSplitter

splitter = CharacterTextSplitter(chunk_size=512, chunk_overlap=50, separator="\n")
...

결과: 951개 청크
장점: 빠르고, 단순하며, 예측 가능함
단점: 문장과 단락을 가로질러 잘라내어 문맥 (context)을 상실함

2. 재귀적 문자 분할 (Recursive Character Splitting)
LangChain의 기본 방식입니다. 먼저 단락 구분, 그다음 문장, 그다음 단어 순으로 분할을 시도하여 가능한 한 의미 단위 (semantic units)를 보존합니다.

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
...

결과: 954개 청크
장점: 더 스마트한 분할, 자연어 경계를 존중함
단점: 여전히 고정 크기이며, 단지 어디서 자를지에 대해 더 지능적일 뿐임

3. Semantic Chunking (의미론적 청킹)
크기(size)에 따라 분할하는 대신, 이 방식은 모든 문장을 임베딩(embedding)하고 인접한 문장 간의 의미론적 유사도(semantic similarity)가 임계값(threshold) 미만으로 떨어지는 지점에서 분할합니다. 주제가 함께 유지되며, 주제의 경계가 곧 청크(chunk)의 경계가 됩니다.

def semantic_chunking(documents, threshold=0.6, min_chunk_size=200):
    # 모든 문장을 임베딩함
    # 코사인 유사도(cosine similarity)가 임계값 미만으로 떨어지는 지점에서 분할함
...

핵심 통찰: 임계값(threshold) 설정이 매우 중요합니다. 0.8로 설정했을 때는 질문에 답할 수 없는 아주 작은 청크가 3,281개 생성되었습니다. 이를 0.6으로 낮추자 훨씬 더 성능이 좋은 의미 있는 청크 1,123개가 생성되었습니다.

결과: 1,123개 청크 (튜닝 후)
장점: 주제별로 일관된 청크, 복잡한 문서에 매우 적합함
단점: 느림 (모든 문장을 임베딩해야 함), 임계값 선택에 민감함

4. Hierarchical Chunking (계층적 청킹)
정밀한 검색(retrieval)을 위해 작은 청크를 저장하되, LLM에는 풍부한 문맥(context)을 제공하기 위해 해당 청크의 더 큰 부모 청크(parent chunk)를 반환합니다. 두 방식의 장점을 모두 갖춘 방법입니다.

parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2048, chunk_overlap=50)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50)

...

검색 과정에서 자식 청크(child chunk)는 적절한 섹션을 찾는 데 사용되지만, LLM에는 부모 텍스트(parent text)가 반환됩니다:

if strategy == "hierarchical" and "parent_text" in result.payload:
    text = result.payload["parent_text"]  # 더 풍부한 문맥을 반환함
else:
...

결과: 1,070개 청크
장점: 정밀한 검색 + 풍부한 문맥, 완벽한 충실도(faithfulness) 점수
단점: 더 많은 저장 공간 필요, 부모 청크가 너무 광범위할 경우 문맥 회상(context recall) 성능이 저하될 수 있음

Advanced RAG Techniques (고급 RAG 기술)
청킹을 넘어, 검색 품질을 향상시키기 위해 세 가지 기술을 추가했습니다.

Query Rewriting (쿼리 재작성)
검색을 수행하기 전, LLM은 다양한 측면을 포착하기 위해 사용자의 질문을 3가지 변형으로 생성합니다:

# 원문: "What was Apple's total revenue in 2023?" (2023년 애플의 총 매출은 얼마였나요?)
# 재작성된 쿼리:
# 1. "What was Apple Inc.'s total revenue for fiscal year ending September 2023?" (2023년 9월에 종료된 회계연도 기준 Apple Inc.의 총 매출은 얼마였나요?)
...

각 변형(variation)은 벡터 저장소(vector store)를 독립적으로 검색하며, 결과는 중복 제거(deduplicated) 과정을 거친 후 점수에 따라 순위가 매겨집니다.

하이브리드 검색 (Hybrid Search: Dense + BM25)
밀집 벡터 검색 (Dense vector search, 의미론적 의미)과 BM25 키워드 검색 (Exact term matching, 정확한 용어 매칭)을 결합합니다. 금융 문서는 정확한 매칭이 도움이 되는 특정 숫자와 전문 용어를 포함하고 있습니다.

# Dense search score * 0.7 + BM25 score * 0.3
combined_score = dense_score * 0.7 + bm25_score * 0.3

문맥 압축 (Contextual Compression)
청크(chunk)를 LLM에 전달하기 전에, 질의(query)와 관련된 문장만 추출합니다. 이는 노이즈와 토큰 사용량을 줄여줍니다:

# Apple의 제품 및 매출에 관한 500단어 분량의 청크에서,
# 매출 수치에 관한 2개의 문장만 추출

Qdrant를 이용한 벡터 저장 (Vector Storage with Qdrant)
더 나은 성능, 내장된 하이브리드 검색 지원, 그리고 프로덕션 준비성(production-readiness)을 고려하여 ChromaDB 대신 Qdrant를 선택했습니다. Docker를 통해 로컬에서 실행하는 방법:

docker run -p 6333:6333 qdrant/qdrant

각 청킹(chunking) 전략은 고유한 컬렉션(collection)을 가집니다 (NVIDIA 임베딩 모델로부터 생성된 2048차원 벡터):

COLLECTION_NAMES = {
    "fixed_size": "fixed_size_collection",
    "recursive": "recursive_collection",
...

배운 점 하나는, 데이터를 한꺼번에 넣지 말고 100개 단위로 배치(batch) 업서트(upsert)를 해야 한다는 것입니다. 단일 요청에 1000개 이상의 포인트를 보내면 연결 시간 초과(connection timeout)가 발생합니다.

평가 프레임워크 (Evaluation Framework)
원래 RAGAS를 사용할 계획이었으나 최신 버전과의 의존성 충돌(dependency conflicts) 문제가 발생했습니다. 패키지 버전을 해결하는 데 몇 시간을 허비하는 대신, 커스텀 LLM-as-judge 메트릭을 구축했습니다. 이는 실제로 더 많은 제어권과 투명성을 제공합니다.

4가지 메트릭 (The 4 Metrics)
충실도 (Faithfulness) — 답변이 검색된 문맥(context)을 준수하는가, 아니면 모델이 환각(hallucinate)을 일으키는가?

답변 관련성 (Answer Relevance) — 응답이 실제로 질문된 내용을 다루고 있는가?

문맥 정밀도 (Context Precision) — 검색된 내용 중 실제로 얼마나 관련이 있는가?

문맥 재현율 (Context Recall) — 문맥에 질문에 답하기 위한 충분한 정보가 포함되어 있는가?

각 메트릭은 LLM이 0.0에서 1.0 사이의 점수를 반환하도록 프롬프트(prompt)를 구성합니다:

def faithfulness(answer, contexts):
    context = "\n\n".join([c[:300] for c in contexts])
    prompt = f"""Given this context: {context}
..."""

LangSmith Tracing
모든 파이프라인 실행 — 쿼리(query), 전략(strategy), 응답(response), 컨텍스트(contexts), 그리고 4가지 메트릭 점수 전체 — 는 LangSmith에 자동으로 기록됩니다. 이는 백그라운드에서 조용히 실행되며, 모든 평가 실행에 대한 완전한 감사 추적(audit trail)을 제공합니다.

결과 (Results)
5개의 금융 질문에 대해 4가지 전략을 모두 평가한 결과:

전략 (Strategy)충실도 (Faithfulness)답변 관련성 (Ans. Relevance)컨텍스트 정밀도 (Ctx. Precision)컨텍스트 재현율 (Ctx. Recall)종합 (Overall)
고정 크기 (Fixed-size)0.701.000.620.600.73
...

주요 발견 사항 (Key Findings)

시맨틱 청킹 (Semantic chunking)이 종합적으로 승리 (0.86) — 임계값(threshold)을 0.8에서 0.6으로 조정한 후, 시맨틱 청킹은 가장 높은 충실도 (0.90)와 컨텍스트 재현율 (0.80)을 기록했습니다. 주제적으로 일관된 청크(chunks)는 LLM이 집중적이고 관련성 높은 컨텍스트를 얻을 수 있음을 의미합니다.

계층적 청킹 (Hierarchical)은 완벽한 충실도 (1.00)를 기록 — LLM에 부모 텍스트(parent text)를 반환함으로써, LLM이 작업에 사용할 풍부하고 완전한 컨텍스트를 항상 보유하게 됩니다. 환각(hallucination)이 발생하지 않습니다.

재귀적 청킹 (Recursive)은 최고의 컨텍스트 정밀도 (0.89)를 기록 — 스마트한 분할(splitting)을 통해 검색된 청크들이 쿼리에 매우 관련성이 높음을 의미합니다.

고정 크기(Fixed-size)는 가장 약하지만 가장 단순함 — 베이스라인(baseline)으로서는 괜찮지만, 성능을 충분히 끌어올리지 못합니다.

Streamlit 챗봇
프로젝트를 대화형으로 만들기 위해, 실시간으로 청킹 전략을 전환하고 검색된 컨텍스트를 확인할 수 있는 Streamlit UI를 구축했습니다:

strategy = st.selectbox("Chunking Strategy", 
    ["fixed_size", "recursive", "semantic", "hierarchical"])

...

streamlit run app.py로 실행하세요. "2022년과 2023년 사이에 iPhone 매출이 어떻게 변했나요?"와 같은 비교 질문을 던져보며, 서로 다른 전략들이 멀티 문서 검색(multi-document retrieval)을 어떻게 처리하는지 확인해 보세요.

Lessons Learned (교훈)

1. NVIDIA Nemotron 모델은 특별한 처리가 필요합니다
이 모델에는 내장된 사고 모드(thinking mode)가 있습니다. 항상 max_tokens=8192로 설정하고 chat_template_kwargs: {"thinking": False}를 지정해야 합니다. 그렇지 않으면 모델이 내부 추론(internal reasoning)에 토큰 예산을 모두 소진하여 None 응답을 반환하게 됩니다.

2. 의미론적 청킹(Semantic chunking) 임계값은 매우 중요합니다
임계값(Threshold) 0.8 설정 시 → 3,281개의 작고 쓸모없는 청크가 생성됩니다. 임계값 0.6 설정 시 → 1,123개의 의미 있는 청크가 생성됩니다. 항상 방어책으로 최소 청크 크기(minimum chunk size)를 설정하세요.

3. 계층적 청킹(Hierarchical chunking)은 검색을 위해 부모 텍스트(parent text)가 필요합니다
자식 청크(child chunks)를 검색하고 LLM에 자식 텍스트를 전달하면, 문맥 회상(context recall) 성능이 저하됩니다. 검색에는 자식 청크를 사용하되, LLM에는 항상 부모 텍스트를 반환하세요.

4. Qdrant 업서트(upserts)는 배치(Batch)로 처리하세요
모든 벡터를 한 번에 보내면 연결 시간 초과(connection timeout)가 발생합니다. 100개 단위로 그룹화하여 배치로 처리하세요.

5. RAGAS가 제대로 작동하지 않을 때는 커스텀 평가 지표(eval metrics)를 구축하세요
의존성 충돌(Dependency conflicts)은 실제로 발생합니다. 커스텀 LLM-as-judge 지표는 투명하고 유연하며 어떤 모델과도 함께 사용할 수 있습니다.

6. 평가는 튜닝이 숨기는 것을 드러냅니다
평가가 없었다면, 의미론적 청킹이 작고 쓸모없는 청크를 생성하고 있다는 사실이나, 계층적 청킹이 부모 텍스트를 무시하고 있다는 사실을 결코 알아채지 못했을 것입니다. 평가를 조기에, 그리고 자주 실행하세요.

GitHub

전체 소스 코드는 다음에서 확인할 수 있습니다: https://github.com/IsaacNatarajan/Advanced-RAG/

NVIDIA NIM, Qdrant, LangChain, LangSmith, 그리고 Streamlit으로 구축되었습니다. 이 내용이 유용했다면 ❤️를 눌러주시고, 댓글로 자유롭게 질문해 주세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0