효율적인 검색 증강 생성(RAG) 구축을 위한 세 가지 핵심 전략 탐구
검색 증강 생성 (RAG)은 AI 기반 Chatbot에서 자체 데이터를 활용하는 데 유용한 기법입니다. 이 블로그 게시물에서는 RAG를 최대한 활용하기 위한 세 가지 핵심 전략을 안내하겠습니다:
스마트 텍스트 청킹 📦:
- 첫 번째 단계는 텍스트 데이터를 의미 있고 관리하기 쉬운 청크로 나누는 것입니다. 이 단계는 Vector Database가 가장 관련성 높은 정보를 빠르고 정확하게 검색할 수 있도록 보장합니다.
다양한 Embedding Model 반복 실험 🔍:
- embedding model을 반복적으로 실험하는 것은 매우 중요합니다. embedding model은 데이터가 vectors로 어떻게 표현되는지를 결정합니다. 벡터는 AI의 공통 언어로, Vector Database가 올바른 정보 조각을 검색하는 방식을 향상합니다.
다양한 LLM 또는 Generative Model 실험 🧪:
- 각 Language Model(LLM) API는 비용, 지연 시간, 정확도가 다릅니다. 이를 테스트하면 워크로드에 가장 잘 맞는 것을 선택할 수 있습니다.
이제 이러한 전략이 어떻게 작동하는지, 그리고 평가를 통해 실제 RAG 애플리케이션에 가장 뛰어난 성능을 보이는 구성을 어떻게 식별할 수 있는지 살펴보겠습니다! 🚀📚
스마트 텍스트 청킹
텍스트 청킹은 긴 이야기를 더 작고 한입 크기의 조각으로 자르는 것과 같아서, 컴퓨터가 질문에 답하거나 작업을 도울 때 가장 중요한 부분을 쉽게 찾고 사용할 수 있습니다.
아래에서는 몇 가지 다른 기법을 설명하겠습니다. 이러한 기법은 Greg Kamradt의 이 원문 기사에서 심층적으로 잘 설명되어 있습니다.
재귀적 문자 텍스트 분할 🔄:
- 문자 수를 기준으로 텍스트를 청크로 나누면 각 조각을 관리하기 쉽고 일관되게 만들 수 있습니다.
작은 것에서 큰 것으로 텍스트 분할 📏:
- 더 큰 청크로 시작해 점진적으로 더 작은 청크로 나눕니다. 작은 단위로 검색하되, 큰 단위로 검색 결과를 가져옵니다.
의미 기반 텍스트 분할 🧠:
- 의미를 기준으로 텍스트를 나누어 각 청크가 완전한 아이디어나 주제를 나타내도록 하며, 문맥이 보존되도록 합니다.
이러한 방법은 다양한 애플리케이션에서 텍스트를 효과적으로 정리하고 검색하는 데 도움이 됩니다. 각 기법이 어떻게 작동하는지 알아보세요!
재귀적 문자 텍스트 분할 🔄
LangChain의 RecursiveCharacterTextSplitter를 사용하여 고정 크기 오버랩이 있는 고정 크기 청크로 텍스트를 분할하는 것부터 시작합니다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
CHUNK_SIZE = 512
chunk_overlap = np.round(CHUNK_SIZE * 0.10, 0)
print(f"chunk_size: {CHUNK_SIZE}, chunk_overlap: {chunk_overlap}")
# The splitter to use to create smaller (child) chunks.
child_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=chunk_overlap,
length_function = len, # use built-in Python len function
separators = ["\n\n", "\n", " ", ". ", ""], # defaults
)
# Child docs directly from raw docs
sub_docs = child_text_splitter.split_documents(docs)
# Inspect chunk lengths
print(f"{len(docs)} docs split into {len(sub_docs)} child documents.")
plot_chunk_lengths(sub_docs, 'Recursive Character')
저자 제공 이미지: RecursiveCharacterTextSplitter 청크 길이에 대한 Matplotlib 플롯, 전체 코드는 GitHub에서 확인 가능.
작은 것에서 큰 것으로 텍스트 분할 📏
이 기법은 작은(자식) 청크를 사용해 검색하지만 큰(부모) 텍스트 청크를 사용해 가져옵니다. 두 개의 메모리 저장소가 사용됩니다: 1) 문서 저장소 및 2) 벡터 저장소. 아래 코드는 LangChain의 MultiVectorRetriever를 사용합니다.
from langchain_milvus import Milvus
from langchain.text_splitter import RecursiveCharacterTextSplitter
import uuid
from langchain.storage import InMemoryByteStore
from langchain.retrievers.multi_vector import MultiVectorRetriever
# Create doc storage for the parent documents
store = InMemoryByteStore()
id_key = "doc_id"
# Create vectorstore for vector index and retrieval.
COLLECTION_NAME = "MilvusDocs"
vectorstore = Milvus(
collection_name=COLLECTION_NAME,
embedding_function=embed_model,
connection_args={"uri": "./milvus_demo.db"},
auto_id=True,
# Set to True to drop the existing collection if it exists.
drop_old=True,
)
# The MultiVectorRetriever (empty to start)
retriever = MultiVectorRetriever(
vectorstore=vectorstore,
byte_store=store,
id_key=id_key,
)
PARENT_CHUNK_SIZE = 1586
# The splitter to use to create bigger (parent) chunks
parent_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=PARENT_CHUNK_SIZE,
length_function = len, # use built-in Python len function
# separators=["\n\n"], # split at end of paragraphs
)
# Parent docs directly from raw docs
parent_docs = parent_text_splitter.split_documents(docs)
doc_ids = [str(uuid.uuid4()) for _ in parent_docs]
# Inspect chunk lengths
print(f"{len(docs)} docs split into {len(parent_docs)} parent documents.")
plot_chunk_lengths(parent_docs, 'Parent')
CHUNK_SIZE = 512
chunk_overlap = np.round(CHUNK_SIZE * 0.10, 0)
print(f"chunk_size: {CHUNK_SIZE}, chunk_overlap: {chunk_overlap}")
# The splitter to use to create smaller (child) chunks.
child_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=chunk_overlap,
length_function = len, # use built-in Python len function
separators = ["\n\n", "\n", " ", ". ", ""], # defaults
)
# Child docs directly from parent docs
sub_docs = child_text_splitter.split_documents(parent_docs)
# Inspect chunk lengths
print(f"{len(docs)} docs split into {len(sub_docs)} child documents.")
plot_chunk_lengths(sub_docs, 'Small-to-big')
small-to-big chunking.png
작성자 이미지: 작은 청크에서 큰 청크로의 길이를 나타낸 Matplotlib 플롯; 전체 코드는 github에서 확인할 수 있습니다.
의미론적 텍스트 분할 🧠
이 청커는 문장을 언제 "분리"할지 결정하는 방식으로 작동합니다. 이 작업은 인접한 문장들 사이의 코사인 거리를 계산하여 수행합니다. 이러한 모든 코사인 거리를 살펴보고, 어떤 임계값을 넘는 이상치 거리를 찾습니다. 이 이상치 거리들이 청크가 분할되는 시점을 결정합니다.
해당 임계값을 결정하는 방법은 몇 가지가 있으며, 이는 breakpoint_threshold_type kwarg에 의해 제어됩니다.
from langchain_experimental.text_splitter import SemanticChunker
semantic_docs = []
for doc in docs:
# Extract and clean document content.
cleaned_content = clean_text(doc.page_content)
# Initialize the SemanticChunker with the embedding model.
text_splitter = SemanticChunker(embed_model)
semantic_list = text_splitter.create_documents([cleaned_content])
# Append the list of semantic chunks to semantic_docs.
semantic_docs.extend(semantic_list)
# Inspect chunk lengths
print(f"Created {len(semantic_docs)} semantic documents from {len(docs)}.")
plot_chunk_lengths(semantic_docs, 'Semantic')
우리는 Milvus 문서를 데이터로 사용하고, Ragas를 RAG의 평가 방법으로 사용할 것입니다. RAGAS 사용 방법에 대한 제 블로그를 읽어보세요.
결과는 다음과 같았습니다:
- Chunking Method = top_k=2인 Recursive Character Text Splitter가 가장 좋았습니다.
다양한 Embedding 모델
청킹 방법을 top_k=2인 Recursive Character Text Splitter로 고정한 뒤, 두 가지 다른 Embedding 모델을 시도했습니다.
BAAI/bge-large-en-v1.5
Text-embedding-3-small with embedding-dim = 512
Milvus 문서와 평가 방법 Ragas를 사용한 결과는 다음과 같았습니다:
- Embedding model = BAAI/bge-large-en-v1.5가 가장 좋았습니다.
다양한 LLM
청킹 방법을 top_k=2인 Recursive Character Text Splitter로, Embedding 모델을 BAAI/bge-large-en-v1.5로 고정한 후, 여섯 가지 서로 다른 LLM API 엔드포인트를 시도했습니다.
Milvus 문서와 평가 방법 Ragas를 사용한 결과는 다음과 같았습니다:
- LLM = Anyscale Endpoints를 사용하는 MistralAI mixtral_8x7b_instruct가 가장 좋았습니다.
결론
RAG 파이프라인 평가는 특정 데이터와 사용 사례에 따라 달라질 것입니다. 개인적인 경험과 문헌에서 얻은 한 가지 핵심 교훈은 가장 큰 개선이 대개 검색 전략을 정교화하는 데서 나온다는 것입니다. 🛠️
Milvus 문서 데이터와 Ragas 평가를 사용하여 이 블로그에서 관찰한 내용은 다음과 같습니다:
Chunking Strategy 변경으로 35% 개선 📦
Embedding Model 변경으로 27% 개선 🔍
LLM Model 변경으로 6% 개선 🤖
이러한 요소들을 반복적으로 조정하면 더 나은 결과를 위해 RAG 파이프라인을 최적화하는 데 도움이 될 수 있습니다!
참고 자료
Greg Kamradt Tutorial on 5 different Levels of Text Splitting: https://github.com/FullStackRetrieval-com/RetrievalTutorials/blob/main/tutorials/LevelsOfTextSplitting/5_Levels_Of_Text_Splitting.ipynb
LangChain Recursive Character Text Splitter: https://python.langchain.com/v0.2/docs/how_to/recursive_text_splitter/
LangChain Multivector Retriever: https://python.langchain.com/v0.2/docs/how_to/multi_vector/#smaller-chunks
LangChain Semantic Chunker: https://python.langchain.com/v0.2/docs/how_to/semantic-chunker/#standard-deviation
RAGAS를 사용하여 RAG 파이프라인을 평가하는 방법: https://medium.com/towards-data-science/rag-evaluation-using-ragas-4645a4c6c477
계속 읽기

Notion's Vector Search Is Excellent. Their Next Problem Is Harder.
Notion solved vector search scaling in two years. The next bottleneck — offline context engineering, unified data, and the real-time/offline gap — is harder.

Optimizing Embedding Model Selection with TDA Clustering: A Strategic Guide for Vector Databases
Discover how Topological Data Analysis (TDA) reveals hidden embedding model weaknesses and helps optimize vector database performance.

What is the K-Nearest Neighbors (KNN) Algorithm in Machine Learning?
KNN is a supervised machine learning technique and algorithm for classification and regression. This post is the ultimate guide to KNN.



