본문 바로가기
NLP/RAG

Enhancing RAG performance with smart chunking strategies

by 아르카눔 2025. 5. 10.

IBM에서 Enhancing RAG performance with smart chunking strategies란 글 (링크)을 올렸길래 유용해 보여서 러프하게 번역하고 간단한 코멘트를 남겨 본다. 그리고 여기서 Contextual Compression이란 개념이 나오는데 관련된 LangChain의 내용도 간략하게 정리한다. 

 

 

Chunking의 중요성

  • 토큰 길이 제한 내에서 컨텍스트 유지: LLM에는 토큰의 길이에 제약이 있으므로, 청킹을 통해 이러한 제한 내에서 관련성 있고 완전한 정보가 제공을 보장한다.
  • 컨텍스트 관계 유지: 잘 구성된 청크는 정보의 논리적 흐름을 유지하여 표현과 이해를 향상시킨다.
  • 확장성 향상: 청킹을 통해 대규모 데이터세트를 효율적으로 처리하여 인덱싱 및 검색을 더욱 쉽게 관리할 수 있다.
  • 검색 속도 향상: 최적화된 청크는 더 빠르고 정확한 검색 결과를 제공하여 전반적인 시스템 성능을 향상시킨다.

 

Common chunking strategies

1. Fixed-length chunking

텍스트를 미리 정의된 길이의 청크로 나눈다. 

 

한계점:

  • 문장이나 문구가 중간에 끊어져서 맥락을 잃을 수 있다. 

 

2. Section-based chunking

단락, 부제목, 문장 구분과 같은 자연스러운 경계를 기준으로 문서를 의미 있는 섹션으로 나눈다. 문서의 구조를 유지함으로써 각 청크 내의 맥락을 유지하는 데 도움이 된다.

 

한계점:

  • 섹션은 종종 문서 구조(예: 제목, 문단)에 따라 결정되는데, 항상 실제 semantic 의미적 경계와 일치하지 않을 수 있다.
  • 더 작고 정확한 청크를 얻는 것이 불가능할 수 있으며, 이로 인해 세분화된 쿼리의 검색 효과가 떨어질 수 있다.

 

3. Sentence chunking

텍스트를 개별 문장으로 분할한다. 이러한 접근 방식은 각 청크가 완전한 사고를 나타내도록 하여 검색 및 이해도를 향상시킨다.

NLTK, spaCy, LangChain의 SpaCyTextSplitter 등으로 문장 단위로 분할할 수 있다. 

 

한계점:

  • 문장의 길이가 다양할 수 있으므로 청크 크기에 일관성이 없을 수 있다.
  • 문장이 너무 짧으면 내용이 너무 작아져 효율성이 떨어질 수 있다.

 

4. Semantic chunking

텍스트의 실제 내용과 단어 또는 구문 간의 관계를 기반으로 텍스트를 의미 있는 세그먼트로 나눈다. 고정 길이 청킹이나 문장 기반 청킹과 달리 각 청크가 맥락과 관련성을 유지하도록 하여 검색 및 처리 정확도를 향상시킨다.

 

from langchain.embeddings import HuggingFaceEmbeddings
from langchain_experimental.text_splitter import SemanticChunker

# This is a long text document we can split up.
text_file_path = “file path”
with open(text_file_path) as f:
    text_data = f.read()


# Initialize the HuggingFace embeddings model
embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")

# Initialize the SemanticChunker with the embedding model
text_splitter = SemanticChunker(embedding_model=embedding_model)

docs = text_splitter.create_documents([text_data])
print(docs[0].page_content)

 

한계점:

  • 의미 있는 분할을 결정하기 위해 임베딩이나 언어 모델(LLM)에 의존하므로 계산 비용이 많이 든다.
  • 대용량 문서를 처리하는 데는 시간이 많이 걸릴 수 있다. 

 

기존 청킹의 함정

 

  • 맥락과 논리적 흐름의 상실 : 고정 길이와 문장 기반 청킹은 내용을 임의로 분할하여 문장을 가로지르고 논리적 흐름을 방해할 수 있다.
  • 구조화된 콘텐츠에 대한 종속성 : 섹션 기반 청킹은 헤더 및 하위 제목과 같은 명확한 구분 기호에 의존하는데, 이는 구조화되지 않은 데이터(예: 대화 로그, 음성 사본)에는 존재하지 않을 수 있다.
  • 일관되지 않은 청크 크기 : 고정 길이 청킹은 다양한 텍스트 길이에 대한 적용이 어려우며, 섹션 기반 청킹은 너무 크거나 작은 청크를 생성하여 불완전하거나 불균형한 맥락을 초래할 수 있다.
  • 의미적 청킹의 계산 오버헤드 : 자연어 처리 모델이나 임베딩이 필요하므로 리소스가 많이 필요하다. 임계값이 제대로 설정되지 않으면 모호하거나 중복되는 주제를 처리하는 데 어려움을 겪을 수 있다.
  • 비정형 데이터나 대화형 데이터에는 효과적이지 않음 : 기존의 청킹 방식은 정형화된 콘텐츠를 전제로 하기 때문에 명확한 형식이 없는 데이터 소스(예: 채팅 기록)에는 적합하지 않다. 대화는 여러 차례에 걸쳐 이루어지는 경우가 많아 검색이 어렵다.

 

청크 크기가 검색 성능에 미치는 영향


청크의 크기는 검색 정확도와 시스템 효율성을 결정하는 데 중요한 역할을 합니다. 균형 잡힌 청킹은 검색 증강 생성(RAG) 시스템에서 관련성 있는 결과를 보장한다. 그러나 청크 크기가 불균일하면 편향이 발생할 수 있으며, 의미적으로 관련성이 낮더라도 작은 청크가 검색에서 우위를 차지할 수 있다. 이는 검색 시스템이 맥락이 풍부한 큰 청크보다 밀도가 높고 키워드와 일치하는 청크를 우선시하는 경우가 많기 때문이다.

 

주요 관찰 사항들


Small Chunks

  • 세분화된 검색과 키워드 매칭을 활성화한다.
  • 의미 있는 응답에 필요한 광범위한 맥락이 부족한 경우가 많다.

Large Chunks

  • 더 많은 맥락을 보존하여 일관성을 향상시킨다.
  • LLM 호출에서 specific matches 특정한 토큰 일치에 의한 결과를 희석하고 토큰 사용량을 늘릴 수 있다.


Uneven Chunks

  • 작은 청크가 지나치게 우선 순위가 매겨져 검색 불균형이 발생할 수 있다.
  • 더 크고 문맥상 관련성이 높은 부분은 간과될 수 있다.

 

Data-driven chunking strategies

데이터 기반 청킹은 문서 내 콘텐츠의 구조와 유형에 따라 텍스트 분할 프로세스를 조정한다. 코드, 표, 마크다운, 일반 ​​텍스트 등 다양한 콘텐츠 유형은 무결성과 의미를 유지하기 위해 전문적인 청킹 기법이 필요하다.

 

 

1. Adaptive document chunking

코드 스니펫, 마크다운, 표 등에 따라서 서로 다른 청킹 방법이 필요하다. 

 

 

Example 1: Chunking markdown documents

 

헤더를 기준으로 분할한다. 

from langchain_text_splitters import MarkdownHeaderTextSplitter

markdown_document = "Some markdown format"
# Splitting based on markdown headers
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
md_header_splits = markdown_splitter.split_text(markdown_document)

 

 

 

Example 2: Chunking python code

 

LangChain의 text_splitter에 코드 관련 syntax structure 구문 구조를 유지할 수 있는 옵션이 있나보다.

 

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.text_splitter import Language

PYTHON_CODE = """
def hello_world():
    print("Hello, World!")

# Call the function
hello_world()
"""

python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)
python_docs = python_splitter.create_documents([PYTHON_CODE])

 

 

 

Example 3: Extracting and chunking tables from PDFs

 

pdf에서 표가 나온다면 그대로 유지할 수 있도록 extract_tables()를 적용한다.

다만 pdfplumber로도 표가 제대로 나오지 않을 수 있으니 실제로 사용하기 위해서는 다양한 방법을 강구해야 한다. 

import pdfplumber

def extract_and_chunk_tables(pdf_path, chunk_size=5):
    with pdfplumber.open(pdf_path) as pdf:
        return [
            [table[i:i + chunk_size] for i in range(0, len(table), chunk_size)]
            for page in pdf.pages
            for table in page.extract_tables() if table
        ]

# Example usage
chunked_tables = extract_and_chunk_tables("sample_document.pdf")

 

 

 

 

2. Timestamp-based chunking

Slack 메시지, 시스템 로그, 채팅 기록 등의 소스에서 수집된 대화 데이터를 처리할 때, 기존의 청킹 방식은 자연스러운 상호작용 흐름을 포착하지 못하는 경우가 많다. 대화는 맥락에 따라 달라지기 때문에 메시지의 관련성은 이전 메시지와 해당 메시지의 타임스탬프에 따라 달라지는 경우가 많다.

타임스탬프 기반 청킹은 특정 시간 간격(예: 5분 간격, 세션당 또는 1시간 이내)을 기준으로 메시지를 그룹화한다. 이를 통해 청크의 맥락적 연속성을 유지하여 검색 및 분석 효율성을 높인다.

 

 

Example: Chunking slack conversations

다양한 간격으로 대화가 오고가는 슬랙 대화를 예시로 하여 5분 간격으로 청킹을 수행한다. 

5분 단위로 그룹화하여 대화의 흐름을 유지한다.

 

from datetime import datetime, timedelta

# Sample message data with timestamps
messages = [
    {"timestamp": "2024-12-14 08:01:00", "message": "Hi there! How can I help?"},
    {"timestamp": "2024-12-14 08:02:45", "message": "I need assistance with my account."},
    {"timestamp": "2024-12-14 08:07:30", "message": "Sure, I can help. What's the issue?"}
]

# Convert string timestamps to datetime objects
for msg in messages:
    msg["timestamp"] = datetime.strptime(msg["timestamp"], "%Y-%m-%d %H:%M:%S")

# Function to chunk messages based on a time interval
def chunk_by_timestamp(messages, interval_minutes=5):
    chunks, current_chunk, chunk_start_time = [], [], None

    for msg in messages:
        if not current_chunk:
            current_chunk.append(msg)
            chunk_start_time = msg["timestamp"]
        else:
            # Check if the message falls within the specified time interval
            if (msg["timestamp"] - chunk_start_time).seconds <= interval_minutes * 60:
                current_chunk.append(msg)
            else:
                chunks.append(current_chunk)
                current_chunk = [msg]
                chunk_start_time = msg["timestamp"]

    # Add the last chunk
    if current_chunk:
        chunks.append(current_chunk)

    return chunks

# Apply timestamp-based chunking
chunked_messages = chunk_by_timestamp(messages, interval_minutes=5)

 

 

 

Optimizing chunks post-retrieval: Contextual compression

Support ticket conversations, system logs, or chat transcripts 지원 티켓 대화, 시스템 로그, 채팅 기록과 같이 규모가 크고 밀도가 높은 데이터 세트를 처리할 때 관련 정보를 효율적으로 검색하는 것은 어려울 수 있다. 이러한 데이터 소스에는 중복되거나 장황한 내용이 포함되어 있는 경우가 많아 검색 및 처리 효율이 떨어진다.

어떤 청크화 전략(섹션 기반, 문장 기반 또는 의미 기반)을 사용하든, 청크를 과도하게 생성하면 복잡하고 관련성 없는 결과가 나올 수 있다.

 

Contextual compression 문맥적 압축이 어떻게 도움이 되나요?

 

문맥적 압축은 검색된 청크를 쿼리 관련성을 기반으로 필터링하고 요약하여 최적화한다. 검색된 모든 청크를 LLM으로 전달하는 대신, 이 방식은 정보를 동적으로 결합하고 압축하여 가장 관련성 높은 콘텐츠만 사용되도록 한다.

 


Benefits:

  • LLM에 전달되는 청크 수를 줄여 비용과 지연 시간을 줄인다.
  • 불필요하거나 중복된 데이터를 제거하여 응답 품질을 개선한다.
  • 더 나은 검색 성능을 위해 간결하고 맥락에 맞는 입력을 보장한다.

아래 Figure를 통해 위 과정의 시각적 과정을 참고하시오.

 

Figure 1. Contextual Compression

 

Note: For code and more details about contextual compression, see LangChain documentation.

 

 

 

LangChain의 Contextual Compression 관련 클래스은 아래의 5가지다.

 

  1. ContextualCompressionRetriever
  2. LLMChainExtractor
  3. LLMChainFilter
  4. LLMListwiseRerank
  5. EmbeddingsFilter

 

ContextualCompressionRetriever와 LLMChainExtractor을 활용해서 관련된 문서를 필터링하고 쿼리와 관련된 문서의 내용만을  압축해서 반환한다.

 

LLMChainFilter는 LLMChainExtractor와 다르게 내용을 요약하지 않고 쿼리와 관련이 있는지 필터링만 수행한다.

 

LLMListwiseRerank는 논문 Zero-Shot Listwise Document Reranking with a Large Language Model (링크)에 기반한 re-rank 방법이다. LLMChainFilter보다 로버스트하지만 더 비용이 많이 드는 선택지다. 

 

EmbeddingsFilter는 LLM의 호출보다 빠르고 저렴한 방법으로 쿼리와 문서의 relevancy를 임베딩으로 계산해서 반환하는 방법이다. 

 

 

 

 

 

References:

https://developer.ibm.com/articles/awb-enhancing-rag-performance-chunking-strategies

https://python.langchain.com/docs/how_to/contextual_compression

https://wikidocs.net/234097

 

 

'NLP > RAG' 카테고리의 다른 글

TAG (2024) 논문 리뷰  (1) 2025.04.27
Self-RAG (2023) 논문 리뷰  (0) 2025.04.17
Self-RAG, RAGAS 그리고 RAG Evaluation by LLM  (0) 2025.04.02
RAGAS의 metric별 required columns  (0) 2025.03.28
RAG에서의 평가 지표  (0) 2025.03.26