LangChain을 통한 LLM 앱을 위한 다양한 청킹 전략 실험하기
청킹은 retrieval-augmented generation (RAG) 애플리케이션을 구축할 때 가장 어려운 문제 중 하나입니다. 청킹은 다운스트림 처리를 위해 문장을 더 작고 관리하기 쉬운 조각으로 분할하는 과정입니다. 단순하게 들리지만, 악마는 디테일에 있습니다. 잘못 선택된 청킹 전략은 관련성이 낮거나 불완전한 결과로 이어져, AI 시스템이 정확한 응답을 제공하기 어렵게 만들 수 있습니다. 예를 들어, 지나치게 작은 청크는 문맥을 놓칠 수 있고, 지나치게 큰 청크는 관련 없는 정보를 반환할 수 있습니다.
이 튜토리얼에서는 동일한 데이터셋에 대해 서로 다른 청킹 전략이 검색 성능에 어떤 영향을 미치는지 살펴보며, 특히 Langchain 청킹을 사용하는 사례에 초점을 맞춥니다. 이 가이드를 마치면 청크 크기와 오버랩이 검색 품질에 어떤 영향을 미치는지 이해하고, 특정 사용 사례에 적합한 매개변수를 선택하는 데 실용적인 인사이트를 얻을 수 있습니다. 이 게시물의 코드는 이 LLM Experimentation GitHub Repo에서 확인할 수 있습니다.
LangChain 개요와 청킹이 중요한 이유
LangChain은 문서 로딩과 텍스트 분할을 위한 내장 도구를 갖춘 Large Language Model 오케스트레이션 프레임워크입니다. 그 유연성 덕분에 RAG 애플리케이션 구축에 널리 사용되며, 여기서 청킹은 검색된 정보의 관련성과 완전성을 결정하는 데 중요한 역할을 합니다.
청킹에는 두 가지 주요 매개변수, 즉 주어진 청크, 크기와 오버랩을 선택하는 것이 포함됩니다.
청크 크기는 각 청크에 포함되는 문자 또는 토큰 수를 정의합니다. 더 큰 청크는 더 많은 문맥을 포착하지만 관련 없는 정보를 반환할 위험이 있으며, 더 작은 청크는 정확성을 보장하지만 필요한 문맥을 잃을 수 있습니다. 오버랩은 연속된 청크 간에 공유되는 텍스트의 양을 결정합니다. 이는 특히 단락을 가로지르는 연결된 아이디어의 경우 연속성을 보존하는 데 도움이 되지만, 과도한 오버랩은 처리 오버헤드를 증가시킵니다. 이러한 매개변수를 최적화하면 텍스트를 분할할 때 문맥을 포착하는 것과 초점을 유지하는 것 사이의 균형을 보장할 수 있으며, 둘 다 정확한 검색에 매우 중요합니다.
청킹 전략은 RAG 워크플로를 넘어 폭넓게 적용됩니다. 청킹된 텍스트에 의존해 사용자 질의에 간결하면서도 포괄적인 응답을 제공하는 문맥 인식 챗봇에서 중요합니다. 예를 들어, 지원 봇은 청킹을 사용해 문제 해결 가이드를 사용자를 압도하지 않는 실행 가능한 단계로 나눌 수 있습니다. 마찬가지로, 지식 그래프 구축에서 청킹은 원시 텍스트에서 엔티티와 관계를 추출하여 비정형 문서를 구조화된 데이터로 변환하는 데 도움을 줍니다. 이러한 예시는 올바른 청킹 전략이 다운스트림 AI 작업을 어떻게 향상시킬 수 있는지 보여줍니다.
잘못된 청킹의 문제 이해하기
제대로 최적화되지 않은 청킹 전략은 검색에서 심각한 문제를 일으킬 수 있습니다. 매우 작은 청크 크기는 충분한 문맥이 부족한 경우가 많아 단편적이거나 불완전한 결과를 생성합니다. 예를 들어, 제품 매뉴얼을 질의하면 이후 단계에 대한 동반 정보 없이 “1단계: 전원을 켜십시오”와 같은 고립된 조각이 반환될 수 있습니다. 반대로, 애플리케이션이 텍스트를 지나치게 큰 청크로 분할하면 검색된 정보의 구체성이 희석될 수 있습니다. 간결한 문제 해결 지침을 기대하는 의미적 유사도 질의가 전체 섹션을 반환할 수 있어, 사용자가 관련 세부 정보를 정확히 찾아내기 더 어려워질 수 있습니다.
오버랩은 또 다른 복잡성을 추가합니다. 오버랩이 없으면 시스템은 연속된 청크 간 사고의 연속성을 잃을 수 있으며, 이는 연결된 서사나 프로세스에서 매우 중요합니다. 그러나 과도한 오버랩은 중복을 초래하여 저장 및 처리 비용을 증가시킬 수 있습니다. 이러한 트레이드오프의 균형을 맞추는 것은 검색 시스템이 정확하고 실행 가능한 응답을 제공하도록 보장하는 데 필수적입니다.
LangChain 코드 가져오기 및 설정
이 첫 번째 섹션은 imports와 기타 설정 도구에 초점을 맞춥니다. 아마 아래 코드에서 가장 먼저 눈에 띄는 것은 imports가 아주 많다는 점일 것입니다. 더 많이 사용되는 것은 os와 dotenv이므로 다루지 않겠습니다. 이들은 단순히 환경 변수에 사용됩니다. python과 pymilvus 클라이언트를 사용해 LangChain에서 텍스트 분할을 단계별로 살펴보겠습니다.
맨 위에는 문서를 가져오기 위한 세 가지 import가 보입니다. 먼저 markdown/Notion 문서가 있는 디렉터리를 로드하는 NotionDirectoryLoader가 있습니다. 그런 다음 Markdown Header와 Recursive Character 텍스트 스플리터가 있습니다. 이들은 markdown 문서 내의 텍스트를 헤더(header splitter)나 미리 선택된 문자 구분 집합(recursive splitter)을 기준으로 분할합니다.
다음으로 retriever imports가 있습니다. Milvus는 우리의 벡터 데이터베이스이고, OpenAIEmbeddings는 우리의 임베딩 모델이며, OpenAI는 우리의 LLM입니다. SelfQueryRetriever는 벡터 데이터베이스가 “자기 자신을 query할 수 있게 해주는” LangChain 네이티브 retriever입니다. 이 글에서 LangChain을 사용해 벡터 데이터베이스를 query하는 방법에 대해 더 자세히 썼습니다.
마지막 LangChain import는 AttributeInfo로, 이름에서 짐작할 수 있듯이 정보가 포함된 attribute를 self-query retriever에 전달합니다. 마지막으로 pymilvus imports에 대해 짚고 싶습니다. 이것들은 순전히 유틸리티 목적입니다. LangChain에서 벡터 데이터베이스로 작업하는 데 이것들이 필요하지는 않습니다. 저는 마지막에 데이터베이스를 정리하기 위해 이 imports를 사용합니다.
함수를 작성하기 전에 마지막으로 하는 일은 환경 변수를 로드하고 몇 가지 상수를 선언하는 것입니다. headers_to_split_on 변수는 필수적입니다 - markdown에서 볼 것으로 예상하고 분할 기준으로 삼고 싶은 모든 헤더를 나열합니다. path는 LangChain에게 Notion 문서를 어디에서 찾을지 알려줄 뿐입니다.
import os
from langchain.document_loaders import NotionDirectoryLoader
from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
from langchain.vectorstores import Milvus
from langchain.embeddings import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from pymilvus import connections, utility
from dotenv import load_dotenv
load_dotenv()
zilliz_uri = os.getenv("ZILLIZ_CLUSTER_01_URI")
zilliz_token = os.getenv("ZILLIZ_CLUSTER_01_TOKEN")
headers_to_split_on = [
("##", "Section"),
]
path='./notion_docs'
청킹 실험 함수 만들기
실험 함수를 만드는 것은 이 튜토리얼에서 가장 중요한 부분입니다. 앞서 언급했듯이, 이 함수는 문서 수집 및 실험을 위한 몇 가지 매개변수를 받습니다. 문서 경로, 분할할 헤더(splitters), 청크 크기, 최대 청크 크기 오버랩, 그리고 마지막에 collections를 삭제하여 정리할지 여부를 제공해야 합니다. collection 삭제는 기본값이 true입니다
가능하다면 collections를 생성하고 삭제하는 것을 최대한 드물게 하고 싶습니다. 방지할 수 있는 오버헤드가 있기 때문입니다. 좋은 우회 방법을 찾으면서 스크립트가 바뀌는 것을 볼 수도 있습니다.
이 함수는 위에서 링크한 Notion을 LangChain과 함께 사용하는 방법에 관한 함수와 매우 비슷합니다. 첫 번째 섹션은 Notion Directory Loader를 사용해 경로에서 문서를 로드합니다. 첫 번째 웹 페이지의 html 콘텐츠만 가져온다는 점에 주목하세요(그리고 페이지도 하나뿐입니다).
다음으로, splitters를 가져옵니다. 먼저 markdown splitter를 사용해 위에서 전달한 헤더를 기준으로 분할합니다. 그런 다음 recursive splitter 메서드를 사용해 청크 크기와 오버랩을 기준으로 분할합니다.
필요한 분할은 이것이 전부입니다. 분할이 완료되면 컬렉션 이름을 지정하고 기본 환경 변수, OpenAI embeddings, splits, 컬렉션 이름을 사용해 LangChain Milvus 인스턴스를 초기화합니다. 또한 AttributeInfo 객체를 통해 메타데이터 필드 목록을 만들어 self-query retriever에게 “sections”가 있다는 것을 알려줍니다.
이 모든 설정을 마치면 LLM을 가져와 python self-query retriever에 전달합니다. 그다음부터는 문서에 대해 질문할 때 retriever가 알아서 마법을 부립니다. 또한 우리가 테스트 중인 chunk strategy를 알려주도록 설정해두었습니다. 마지막으로 원한다면 컬렉션을 삭제할 수 있습니다.
def test_langchain_chunking(docs_path, splitters, chunk_size, chunk_overlap, drop_collection=True):
path=docs_path
loader = NotionDirectoryLoader(path)
docs = loader.load()
md_file=docs[0].page_content
# Let's create groups based on the section headers in our page
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=splitters)
md_header_splits = markdown_splitter.split_text(md_file)
# Define our text splitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
all_splits = text_splitter.split_documents(md_header_splits)
test_collection_name = f"EngineeringNotionDoc_{chunk_size}_{chunk_overlap}"
vectordb = Milvus.from_documents(documents=all_splits,
embedding=OpenAIEmbeddings(),
connection_args={"uri": zilliz_uri,
"token": zilliz_token},
collection_name=test_collection_name)
metadata_fields_info = [
AttributeInfo(
name="Section",
description="Part of the document that the text comes from",
type="string or list[string]"
),
]
document_content_description = "Major sections of the document"
llm = OpenAI(temperature=0)
retriever = SelfQueryRetriever.from_llm(llm, vectordb, document_content_description, metadata_fields_info, verbose=True)
res = retriever.get_relevant_documents("What makes a distinguished engineer?")
print(f"""Responses from chunking strategy:
{chunk_size}, {chunk_overlap}""")
for doc in res:
print(doc)
# this is just for rough cleanup, we can improve this
# lots of user considerations to understand for real experimentation use cases though
if drop_collection:
connections.connect(uri=zilliz_uri, token=zilliz_token)
utility.drop_collection(test_collection_name)
LangChain 테스트와 결과
좋습니다, 이제 흥미로운 부분입니다! 테스트와 결과를 살펴보겠습니다.
LangChain 청크를 테스트하는 코드
아래의 짧은 코드 블록은 실험을 위해 함수를 실행하는 방법입니다. 저는 다섯 가지 실험을 추가했습니다. 이 튜토리얼은 길이를 32부터 512까지 2의 거듭제곱 단위로, 오버랩을 4부터 64까지 역시 2의 거듭제곱 단위로 설정해 chunk strategies를 테스트합니다. 테스트를 위해 튜플 목록을 반복하면서 위에서 작성한 함수를 호출합니다.
chunking_tests = [(32, 4), (64, 8), (128, 16), (256, 32), (512, 64)]
for test in chunking_tests:
test_langchain_chunking(path, headers_to_split_on, test[0], test[1])
전체 출력은 다음과 같습니다. 이제 개별 출력들을 살짝 들여다보겠습니다. 우리가 선택한 예시 질문이 "What makes a distinguished engineer?"라는 점을 기억하세요.
길이 32, 오버랩 4
좋습니다. 여기서 32는 너무 짧다는 것을 명확히 알 수 있습니다. 이 문장은 완전히 쓸모가 없습니다. “Is a Distinguished Engineer”는 가능한 한 가장 순환적인 논리입니다.
길이 64, 오버랩 8
64와 8도 시작부터 크게 더 낫지는 않습니다. 그래도 distinguished engineer의 예시는 제공합니다. Amazon의 CTO인 Werner Vogels입니다.
길이 128, 오버랩 16
128에서는 더 완전한 문장들이 보이기 시작합니다. “engineer.” 같은 단어와 응답이 줄어듭니다. 나쁘지 않습니다. Werner Vogel에 대한 부분과 “Has achieved noteworthy technical, professional accomplishments while working as an engineer.”를 추출해 냅니다. 마지막 항목은 실제로 principal engineer 섹션에서 온 것입니다.
여기서 한 가지 단점은 \xa0 및 \n 같은 특수 문자의 예가 이미 나타난다는 점입니다. 이는 어쩌면 청크 길이를 너무 길게 잡고 있다는 것을 알려줍니다.
길이 256, 오버랩 32
이 청크 길이는 확실히 너무 길다고 생각합니다. 필요한 항목들을 가져오긴 하지만, “Fellow”, “Principal Engineer”, “Senior Staff Engineer”의 항목들도 함께 가져옵니다. 그래도 첫 번째 항목은 Distinguished Engineer에서 온 것이며, 그에 대한 세 가지 포인트를 다룹니다.
청크 길이 512, 오버랩 64
우리는 이미 256이 아마도 너무 길다는 것을 확인했습니다. 하지만 이 512의 첫 번째 추출 결과는 실제로 distinguished engineers에 대한 전체 섹션입니다. 이제 딜레마가 생깁니다. 개별 “줄”이나 “메모”를 원할까요, 아니면 전체 섹션을 가져오고 싶을까요? 이는 사용 사례에 따라 달라집니다.
다양한 청킹 전략 실험 요약
좋습니다. 이 튜토리얼에서는 langchain chunking python을 사용해 청크 크기와 청크 오버랩 전략을 강조하는 파라미터화된 접근 방식으로 다섯 가지 서로 다른 텍스트 분할 전략을 살펴보았습니다. 이 간단한 5가지 청크 전략만 수행해 보면서 확인한 딜레마 중 하나는 청크 크기에 따라 개별 정보 조각을 얻을 것인지, 전체 섹션을 되돌려 받을 것인지의 문제였습니다. 128은 distinguished engineers에 대한 개별 “줄”이나 “메모”를 얻는 데 꽤 좋았지만, 512는 전체 섹션을 되돌려 받을 수 있다는 것을 보았습니다.
하지만 256은 그다지 좋지 않았습니다.
이 세 가지 데이터 포인트는 텍스트 분할기에 대해 무언가를 알려줍니다. 이상적인 청크 크기를 찾는 것이 어렵다는 것만이 아닙니다. 청크 크기를 설계할 때 응답에서 무엇을 원하는지도 함께 생각해야 한다는 신호이기도 합니다.
우리는 아직 서로 다른 오버랩을 테스트하는 단계까지도 가지 않았다는 점에 유의하세요. 좋은 청크 전략을 배우고 마련한 뒤에는 오버랩을 확인하는 것이 논리적인 다음 단계입니다. 아마 향후 튜토리얼에서 다룰 수도 있고, 어쩌면 다른 라이브러리로 다룰 수도 있습니다. 기대해 주세요!
실험에서 얻은 인사이트
이 실험들은 청크 크기와 오버랩 사이의 미묘한 관계를 보여줍니다. 더 작은 청크는 정확한 지점의 정밀성이 필요한 작업에 뛰어나며, 더 큰 청크는 광범위한 문맥을 요구하는 질문에 더 적합합니다. 반면 오버랩은 이러한 목표들의 균형을 맞추는 데 핵심적인 역할을 합니다.
여기서 관찰된 트레이드오프가 모든 경우에 동일하게 적용되는 것은 아니라는 점을 강조할 가치가 있습니다. 이상적인 청킹 파라미터는 특정 사용 사례에 크게 좌우됩니다. 대화형 에이전트나 빠른 조회 도구와 같은 애플리케이션은 정확한 답변을 위해 더 작은 청크의 이점을 얻을 수 있습니다. 반면, 복잡한 법률 문서를 요약하는 것과 같은 연구 집약적인 워크플로에는 포괄적인 문맥을 보장하기 위해 더 큰 청크가 필요합니다.
실제 시나리오에서는 하이브리드 접근 방식이 종종 최상의 결과를 제공할 수 있습니다. 사용자의 쿼리와 의도에 따라 청크 크기를 동적으로 조정함으로써, 개발자는 효율성과 검색 품질 사이의 균형을 맞출 수 있습니다. 이러한 접근 방식에는 사용자 요구에 맞춘 지능형 쿼리 라우팅 계층이나 적응형 청킹 파이프라인을 추가하는 것이 포함될 수 있습니다.
향후 고려 사항
이 튜토리얼에서 수행한 실험은 단지 시작일 뿐입니다. 향후 탐구에서는 다단계 세분화를 위해 서로 다른 청크 크기를 결합하는 계층적 청킹 전략을 검토할 수 있습니다. 또한 텍스트와 이미지 임베딩을 결합하는 것과 같은 멀티모달 데이터 검색으로 확장하면 더 복잡한 사용 사례의 가능성이 열립니다.
다양한 데이터셋에서 검색에 어떤 영향을 미치는지 더 잘 이해하기 위해 오버랩을 체계적으로 실험해 볼 가능성도 있습니다. LangChain과 함께 다른 프레임워크를 통합하면 청킹 접근 방식을 더욱 개선하고 새로운 최적화 기법을 발견할 수 있습니다. 예를 들어, Haystack과 같은 프레임워크는 LangChain의 워크플로를 보완할 수 있는 추가 검색 기능을 제공합니다.
마지막으로, 사용자 피드백을 검색 프로세스에 통합하면 시간이 지남에 따라 청킹 전략을 학습하고 최적화하는 적응형 시스템으로 이어질 수 있습니다. 더 세밀하게 조정된 실험을 통해, 견고하고 사용자 중심적인 RAG 파이프라인을 구축할 수 있습니다.
결론
청킹은 RAG 애플리케이션의 검색 워크플로에서 필수적인 구성 요소입니다. 이 튜토리얼은 청크 크기와 오버랩을 신중하게 조정하는 것의 중요성을 보여주었으며, 다양한 전략이 검색 결과에 어떤 영향을 미치는지 보여주었습니다.
컨텍스트와 정밀도 사이의 트레이드오프를 균형 있게 조정함으로써, 개발자는 다양한 사용 사례에 맞게 시스템을 최적화할 수 있습니다. 이 튜토리얼에서는 기초적인 실험을 다루었지만, 추가 탐구의 가능성은 매우 방대합니다. AI 기반 검색 시스템 최적화에 대한 더 고급 튜토리얼과 인사이트를 기대해 주세요.
청킹 참고 자료
청킹, 또는 텍스트 분할 전략은 계속 발전하고 있으므로, 이러한 다양한 전략을 살펴보고 애플리케이션에 잠재적으로 구현할 수 있도록 모음을 구축하기 시작했습니다. 즐겨보세요!
Retrieval Augmented Generation (RAG)을 위한 청킹 전략 가이드. 이 가이드에서는 Retrieval-Augmented Generation (RAG) 시스템 내 청킹 전략의 다양한 측면을 살펴보았습니다.
RAG 애플리케이션을 위한 웹사이트 청킹 및 임베딩 초보자 가이드. 이 글에서는 웹사이트에서 콘텐츠를 추출하고 RAG 애플리케이션에서 LLM의 컨텍스트로 사용하는 방법을 설명합니다. 하지만 그렇게 하기 전에 웹사이트의 기본 사항을 이해해야 합니다.
효율적인 Retrieval Augmented Generation (RAG) 구축을 위한 세 가지 핵심 전략 살펴보기. Retrieval Augmented Generation (RAG)은 AI 기반 Chatbot에서 자체 데이터를 사용하는 데 유용한 기술입니다. 이 블로그 게시물에서는 RAG를 최대한 활용하기 위한 세 가지 핵심 전략을 안내합니다.
Pandas DataFrame: Milvus를 사용한 청킹 및 벡터화. 청크 텍스트와 임베딩을 포함한 모든 데이터를 Pandas DataFrame 안에 저장하면, 이를 Milvus 벡터 데이터베이스에 쉽게 통합하고 가져올 수 있습니다.
계속 읽기

Zilliz Cloud BYOC Now Available Across AWS, GCP, and Azure
Zilliz Cloud BYOC is now generally available on all three major clouds. Deploy fully managed vector search in your own AWS, GCP, or Azure account — your data never leaves your VPC.

Zilliz Cloud Update: Tiered Storage, Business Critical Plan, Cross-Region Backup, and Pricing Changes
This release offers a rebuilt tiered storage with lower costs, a new Business Critical plan for enhanced security, and pricing updates, among other features.

Selecting the Right ETL Tools for Unstructured Data to Prepare for AI
Learn the right ETL tools for unstructured data to power AI. Explore key challenges, tool comparisons, and integrations with Milvus for vector search.



