LLM RAG 파이프라인 청킹 전략 비교 분석과 프로덕션 적용 방법

LLM RAG 파이프라인 청킹 전략 비교 분석과 프로덕션 적용 방법

LLM RAG 파이프라인을 구축하고 나서 가장 먼저 마주치는 실망스러운 상황이 있습니다. 분명히 문서에 답이 있는데 LLM이 엉뚱한 말을 하거나, 검색된 컨텍스트가 질문과 동떨어진 내용이거나, 같은 정보가 여러 chunk에 분산되어 답변이 파편화되는 문제입니다.

이 문제들의 80% 이상은 청킹 전략에서 비롯됩니다. 아무리 좋은 임베딩 모델과 벡터 데이터베이스를 써도, 문서를 어떻게 자르느냐에 따라 검색 정확도(Retrieval Accuracy)가 크게 달라집니다. 실제로 같은 문서 컬렉션에 고정 크기 청킹과 시맨틱 청킹을 각각 적용해 비교 테스트했을 때, Hit Rate@5 기준으로 23% 차이가 났습니다.

RAG 파이프라인에서 청킹은 임베딩 전 단계에서 이루어지며, 한 번 잘못 설계하면 전체 파이프라인을 다시 구축해야 할 수 있습니다. 이 글에서는 4가지 청킹 전략의 원리와 장단점을 비교하고, 프로덕션에서 최적 chunk_size를 찾는 튜닝 방법을 실무 코드와 함께 설명합니다.

이 글을 읽고 나면: 문서 유형과 질의 패턴에 맞는 청킹 전략을 선택하고, chunk_size와 overlap을 데이터 기반으로 튜닝하며, 프로덕션 RAG 파이프라인의 검색 정확도를 체계적으로 개선할 수 있습니다.

RAG 파이프라인에서 청킹이 검색 정확도에 미치는 영향

RAG 파이프라인에서 청킹은 문서를 임베딩 가능한 단위로 분할하는 과정으로, 검색 결과의 품질을 결정하는 핵심 요소입니다. chunk가 너무 크면 관련 없는 정보가 섞여 임베딩 벡터의 의미적 집중도가 떨어지고, 너무 작으면 컨텍스트가 단절되어 LLM이 올바른 답변을 생성하지 못합니다.

청킹 전략이 검색에 영향을 주는 메커니즘은 이렇습니다. 벡터 검색은 질의(query) 임베딩 벡터와 chunk 임베딩 벡터의 코사인 유사도로 순위를 매깁니다. chunk 안에 질의와 관련 없는 내용이 많을수록 임베딩 벡터가 "평균화"되어 유사도 점수가 낮아집니다. 반대로 chunk가 너무 작으면 필요한 정보가 다른 chunk에 나뉘어 있어 상위 k개 검색 결과에 충분한 컨텍스트가 담기지 않습니다.

청킹 전략 선택은 다음 3가지 요인에 따라 달라집니다.

  1. 문서 구조: 비정형 텍스트(논문, 뉴스) vs. 구조화된 문서(API 문서, 매뉴얼)
  2. 질의 유형: 단일 사실 조회 vs. 다단계 추론이 필요한 복잡 질의
  3. 임베딩 모델의 최대 토큰 길이: 대부분 512~8192 토큰 범위

4가지 청킹 전략 비교 — 언제 무엇을 선택해야 하나

아래 비교표는 가장 많이 사용되는 4가지 청킹 전략의 특성을 정리한 것입니다.

전략 방법 장점 단점 적합한 문서 유형
고정 크기(Fixed-size) 토큰/문자 수 기준 분할 구현 단순, 예측 가능 문장/단락 중간에서 잘림 일반 텍스트, 빠른 프로토타입
재귀 문자(Recursive Character) 단락→문장→단어 순 재귀 분할 자연스러운 경계 보존 단락 구조 없으면 효과 제한 마크다운, 코드, 일반 문서
시맨틱(Semantic) 임베딩 유사도로 경계 결정 의미 단위 청킹 속도 느림, 비용 높음 비정형 텍스트, 학술 논문
문서 구조 기반(Document-aware) HTML/PDF/MD 구조 활용 섹션·표·코드 블록 보존 문서 파서 별도 필요 기술 문서, 매뉴얼, API 문서

LangChain의 RecursiveCharacterTextSplitter는 범용성이 가장 높아 처음 시작점으로 권장됩니다. 시맨틱 청킹은 정확도가 높지만 인덱싱 속도가 고정 크기 대비 5~10배 느리고 임베딩 API 비용도 증가하므로, 대용량 문서 컬렉션에 적용할 때는 비용-성능 트레이드오프를 사전에 계산해야 합니다.

LangChain 기반 청킹 전략 구현 코드

아래는 LangChain을 사용해 재귀 문자 청킹과 시맨틱 청킹을 구현하는 실무 코드입니다.

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

# --- 재귀 문자 청킹 (범용 기본 전략) ---
recursive_splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,        # 토큰 기준 (실제로는 문자 수, 모델별 토큰화 차이 존재)
    chunk_overlap=64,      # 청크 간 중첩 — 경계에서 컨텍스트 단절 방지
    length_function=len,
    separators=["\n\n", "\n", ". ", " ", ""],  # 우선순위 순으로 분할 시도
)

# 사용 예시
with open("document.md", "r", encoding="utf-8") as f:
    text = f.read()

recursive_chunks = recursive_splitter.create_documents([text])
print(f"재귀 청킹: {len(recursive_chunks)}개 chunk 생성")

# --- 시맨틱 청킹 (정확도 우선 전략) ---
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
semantic_splitter = SemanticChunker(
    embeddings=embeddings,
    breakpoint_threshold_type="percentile",  # 유사도 변화율 상위 95%를 경계로 처리
    breakpoint_threshold_amount=95,
)

semantic_chunks = semantic_splitter.create_documents([text])
print(f"시맨틱 청킹: {len(semantic_chunks)}개 chunk 생성")

시맨틱 청킹에서 breakpoint_threshold_typepercentile(전체 유사도 분포 기준), standard_deviation(표준편차 기준), interquartile(사분위수 기준) 중 선택할 수 있습니다. 문서가 명확한 섹션 구분을 가질 때는 interquartile이 안정적인 결과를 보여줍니다.

프로덕션 환경에서 chunk_size 튜닝하는 방법

chunk_size는 "정답"이 없는 하이퍼파라미터입니다. 올바른 접근은 실제 질의-응답 쌍으로 평가 세트를 구성하고, 다양한 chunk_size를 실험해 검색 지표를 측정하는 것입니다.

import json
from typing import Any

def evaluate_chunking_strategy(
    documents: list[str],
    eval_qa_pairs: list[dict],  # [{"question": "...", "answer": "..."}]
    chunk_sizes: list[int] = [256, 512, 1024],
    chunk_overlaps: list[int] = [32, 64, 128],
) -> list[dict]:
    """다양한 chunk_size·overlap 조합의 Hit Rate@5를 측정"""
    results = []

    for chunk_size in chunk_sizes:
        for overlap in chunk_overlaps:
            if overlap >= chunk_size:
                continue  # overlap이 chunk_size보다 크면 무의미

            splitter = RecursiveCharacterTextSplitter(
                chunk_size=chunk_size,
                chunk_overlap=overlap,
            )
            chunks = splitter.create_documents(documents)

            # 벡터 스토어 구축 및 검색 평가 (간략화)
            hit_count = 0
            for qa in eval_qa_pairs:
                # 실제 구현에서는 벡터 검색 후 답변 포함 여부 확인
                retrieved = search_top_k(chunks, qa["question"], k=5)
                if any(qa["answer"] in chunk.page_content for chunk in retrieved):
                    hit_count += 1

            results.append({
                "chunk_size": chunk_size,
                "overlap": overlap,
                "num_chunks": len(chunks),
                "hit_rate_at_5": hit_count / len(eval_qa_pairs),
            })

    return sorted(results, key=lambda x: x["hit_rate_at_5"], reverse=True)

평가 세트는 최소 50개 이상의 질의-응답 쌍으로 구성하는 것이 권장됩니다. 평가 지표로는 Hit Rate@K(상위 K개 결과에 정답 포함 비율)와 MRR(Mean Reciprocal Rank)을 함께 사용합니다.

경험상 한국어 문서에서는 chunk_size=512~768 범위가 대부분의 시나리오에서 가장 균형 잡힌 결과를 보입니다. 영어 문서는 512~1024가 일반적인 시작점입니다. 오픈소스 RAG 시스템 전체 아키텍처가 궁금하다면 오픈소스 로컬 RAG 시스템 아키텍처 가이드를 함께 참고하면 좋습니다.

프로덕션 RAG 청킹 파이프라인 설계 시 주의사항

RAG 파이프라인을 프로덕션에 배포할 때 청킹 관련으로 자주 놓치는 주의사항들입니다.

주의사항 1: 메타데이터 보존

chunk에 원본 문서 출처(파일명, 페이지 번호, 섹션 제목)를 메타데이터로 반드시 포함해야 합니다. LLM이 답변을 생성한 뒤 출처를 인용하려면 이 정보가 필수입니다. LangChain의 Document 객체는 metadata 딕셔너리를 지원합니다.

from langchain.schema import Document

# 메타데이터를 포함한 청킹
chunks_with_metadata = []
for page_num, page_text in enumerate(pages):
    chunks = recursive_splitter.create_documents(
        [page_text],
        metadatas=[{"source": "manual.pdf", "page": page_num + 1}]
    )
    chunks_with_metadata.extend(chunks)

주의사항 2: 청킹 파이프라인 버전 관리

청킹 전략을 변경하면 기존 벡터 인덱스 전체를 재구축해야 합니다. 청킹 설정을 코드로 관리하고, 변경 시 벡터 스토어를 완전히 재인덱싱하는 파이프라인을 CI/CD에 통합하는 것을 권장합니다. 부분 업데이트만 하면 일관성 없는 검색 결과로 이어집니다.

주의사항 3: 표와 코드 블록 처리

마크다운이나 HTML 문서에서 표(<table>)나 코드 블록(```)은 일반 텍스트 분할 방식으로 자르면 구조가 깨집니다. LangChain의 MarkdownHeaderTextSplitterHTMLHeaderTextSplitter를 사용하면 문서 구조를 인식해 올바르게 청킹합니다.

자주 묻는 질문 (FAQ)

Q. RAG 파이프라인에서 chunk_size는 얼마로 설정하는 것이 적당한가요?

일반적인 시작점은 512토큰(약 400~600자)이며, 이후 실제 질의 평가로 튜닝합니다. 사실 조회형 질의에는 256~512토큰의 작은 chunk가 유리하고, 다단계 추론이 필요한 복잡 질의에는 1024토큰 이상이 더 나은 결과를 줍니다. 고정된 정답이 없으므로 평가 세트 기반 실험이 필수입니다.

Q. chunk_overlap은 왜 필요하고 얼마나 설정해야 하나요?

chunk_overlap은 인접한 두 chunk가 일부 내용을 공유하게 만들어, chunk 경계에서 중요한 정보가 잘리는 문제를 완화합니다. 일반적으로 chunk_size의 10~15% 수준을 권장합니다. 512 chunk_size 기준으로 50~70토큰 overlap이 적당합니다. overlap이 너무 크면 중복 컨텍스트가 검색 결과에 반복 노출되어 LLM에 전달되는 정보 밀도가 낮아집니다.

Q. 시맨틱 청킹이 항상 고정 크기 청킹보다 좋은 결과를 내나요?

항상 그렇지는 않습니다. 명확한 섹션 구분이 있는 기술 문서나 마크다운에서는 재귀 문자 청킹이 시맨틱 청킹과 유사한 정확도를 내면서도 훨씬 빠르고 저렴합니다. 시맨틱 청킹이 가장 효과적인 경우는 섹션 구분이 모호한 긴 비정형 텍스트입니다. 두 방식을 동일한 평가 세트로 비교 측정한 뒤 선택하는 것이 원칙입니다.

Q. 다국어 문서(한국어+영어 혼합)에서 청킹 시 주의사항이 있나요?

한국어는 영어와 토큰화 방식이 달라, 동일한 chunk_size 설정에서 한국어 문서가 영어 문서보다 더 많은 토큰을 소모하는 경향이 있습니다. 혼합 언어 문서에서는 tiktoken 같은 실제 토크나이저로 chunk 토큰 수를 직접 측정해 임베딩 모델의 최대 토큰 한도를 초과하지 않도록 검증해야 합니다.

LLM RAG 파이프라인의 청킹 전략은 프로젝트 초기에 시간을 투자해 올바르게 설계하는 것이 나중에 전체 파이프라인을 재구축하는 것보다 훨씬 효율적입니다. 지금 당장 시작할 수 있는 첫 번째 액션은 현재 사용 중인 청킹 전략의 chunk_size와 overlap 설정을 확인하고, 10개 이상의 실제 질의-응답 쌍으로 간단한 Hit Rate를 측정해보는 것입니다. 수치를 한 번이라도 확인하는 순간, LLM RAG 파이프라인 청킹 전략을 어떻게 개선해야 할지 방향이 보이기 시작합니다.