ABOUT

-

Today
-
Yesterday
-
Total
-
  • Langchain Text Splitter
    LLM 2025. 6. 18. 11:00

    앞선 글에서 Chunking Strategy에 대해 설명했다.

     

    Chunk Optimization / 5 Levels Of Text Splitting

    필요성Langchain으로 RAG 구현하던 중, 적절한 Chunking Strategy를 선택해야했다.로드한 문서를 임베딩 하기 전에 크게 자를까, 작게 자를까, 뭘 기준으로 자를까 등을 결정하는 작업이다.이는 검색 정

    crayeji.tistory.com

    적절한 전략을 결정한 후에는 해당 전략에 맞는 Chunking 로직을 직접 구현할 수도 있고,

    LangChain을 사용하는 경우에는 TextSplitter의 하위클래스들을 활용할 수도 있겠다.

    그 중 몇 가지를 소개해보려고 한다.


    0. TextSplitter

    https://api.python.langchain.com/en/latest/base/langchain_text_splitters.base.TextSplitter.html

    앞으로 소개할 내용들은 TextSplitter라는 추상클래스에서 파생된 것들이다.

    (MarkdownHeaderTextSplitter과 HTMLHeaderTextSplitter 제외)

    이 추상클래스는 split_text라는 추상메서드를 포함하고있고, 저마다 이를 구현했다고 볼 수 있다.

    예를 들면 RecursiveCharacterTextSplitter는 아래와 같이 재귀적으로 splitting하도록 구현되어있다.

        def _split_text(self, text: str, separators: List[str]) -> List[str]:
            """Split incoming text and return chunks."""
            final_chunks = []
            # Get appropriate separator to use
            separator = separators[-1]
            new_separators = []
            for i, _s in enumerate(separators):
                _separator = _s if self._is_separator_regex else re.escape(_s)
                if _s == "":
                    separator = _s
                    break
                if re.search(_separator, text):
                    separator = _s
                    new_separators = separators[i + 1 :]
                    break
    
            _separator = separator if self._is_separator_regex else re.escape(separator)
            splits = _split_text_with_regex(text, _separator, self._keep_separator)
    
            # Now go merging things, recursively splitting longer texts.
            _good_splits = []
            _separator = "" if self._keep_separator else separator
            for s in splits:
                if self._length_function(s) < self._chunk_size:
                    _good_splits.append(s)
                else:
                    if _good_splits:
                        merged_text = self._merge_splits(_good_splits, _separator)
                        final_chunks.extend(merged_text)
                        _good_splits = []
                    if not new_separators:
                        final_chunks.append(s)
                    else:
                        other_info = self._split_text(s, new_separators)
                        final_chunks.extend(other_info)
            if _good_splits:
                merged_text = self._merge_splits(_good_splits, _separator)
                final_chunks.extend(merged_text)
            return final_chunks

     

     

    init함수가 아래와 같이 정의되어 있으므로, 알아뒀다가 하위클래스 활용시에 참고하자.

        def __init__(
            self,
            chunk_size: int = 4000,
            chunk_overlap: int = 200,
            length_function: Callable[[str], int] = len,
            keep_separator: Union[bool, Literal["start", "end"]] = False,
            add_start_index: bool = False,
            strip_whitespace: bool = True,
        ) -> None:
            """Create a new TextSplitter.
    • chunk_size: 각 청크(chunk)의 최대 길이
    • chunk_overlap: 청크 간 중첩(overlap)을 몇 글자까지 둘 것인지(문장 사이 맥락 연결성 확보를 위한 테크닉)
    • length_function: 텍스트 길이를 측정하는 함수
    • keep_separator: 분할 기준(separator) 문자를 청크에 포함할지 여부를 결정
    • add_start_index: True일 경우, 결과로 나오는 각 청크에 원본문 내 시작 인덱스 정보가 추가됨
    • strip_whitespace: 청크 앞뒤의 공백 문자(띄어쓰기, 줄바꿈 등)를 제거할지 여부 (True면 공백제거)

    1. character.CharacterTextSplitter

     하나의 separator를 기준으로 자른다. 기본적으로 \n\n을 만나면 자르도록 되어있다.

    class CharacterTextSplitter(
        _separator: str = '\n\n',
        _is_separator_regex: bool = False,
        **kwargs: Any,
    )
    • is_separator_regex: True일때는 정규표현식으로 해석, False일때는 단순 문자열로 해석(default)

    사용 예시는 아래와 같다. 청크는 500자씩 잘라지며, 그 중 50자는 겹쳐지게 했다. 

    from langchain_text_splitters import CharacterTextSplitter
    
    # 예시 원본문자열
    text = (
        "1. 첫 번째 문단입니다.\n"
        "이 문단은 설명을 포함합니다.\n\n"
        "2. 두 번째 문단입니다.\n"
        "여기에는 추가 정보가 포함되어 있습니다.\n\n"
        "3. 세 번째 문단입니다.\n"
        "이것은 테스트용 문장입니다.\n\n"
        "4. 네 번째 문단입니다.\n"
        "마지막 문장입니다."
    )
    
    # 분할기 설정
    splitter = CharacterTextSplitter(
        separator="\n",             # 줄바꿈 기준 분할
        chunk_size=50,             # 각 청크 최대 50자
        chunk_overlap=20,           # 20자 겹치게
        length_function=len
    )
    
    # 분할 수행
    chunks = splitter.split_text(text)
    
    # 결과 출력
    print("총 청크 개수:", len(chunks))
    for i, chunk in enumerate(chunks):
        print(f"\n== Chunk {i+1} ==\n{chunk}")

     

    한 청크가 50자를 넘지 않으면서, \n\n을 기준으로 쪼개졌다. 또한 chunk overlap에 의해 일부 내용이 겹쳐지게 쪼개졌다.

     

     

    CharacterTextSplitter — 🦜🔗 LangChain documentation

    CharacterTextSplitter class langchain_text_splitters.character.CharacterTextSplitter( separator: str = '\n\n', is_separator_regex: bool = False, **kwargs: Any, )[source] Splitting text that looks at characters. Create a new TextSplitter. Methods Parameters

    python.langchain.com


    2. character.RecursiveCharacterTextSplitter

    CharacterTextSplitter와 달리 separators가 여러 개(문자열 리스트)들어갈 수 있다.

    즉, 분할 기준을 여러가지 둔다는 것이다. 이 기준들을 순서대로 재귀적으로 자른다. 

        def __init__(
            self,
            separators: Optional[List[str]] = None,
            keep_separator: Union[bool, Literal["start", "end"]] = True,
            is_separator_regex: bool = False,
            **kwargs: Any,
        ) -> None:
            """Create a new TextSplitter."""
            super().__init__(keep_separator=keep_separator, **kwargs)
            self._separators = separators or ["\n\n", "\n", " ", ""]
            self._is_separator_regex = is_separator_regex

    이 또한 TextSplitter에서 파생되었으므로, chunk_size / chunk_overlap등을 지원한다. 

     

    사용 예시는 아래와 같다. 이번에는 \n\n뿐만 아니라, \n, " ", ""까지 분할 기준으로 삼고,

    chuck_size가 50이 넘는 동안은 재귀적으로 계속 잘라나갈 것이다. 

    from langchain_text_splitters import RecursiveCharacterTextSplitter
    
    # 예시 원본 텍스트
    text = (
        "제1장. 서론\n\n"
        "이 문서는 테스트용 텍스트입니다. 줄바꿈도 있고, 공백도 있습니다.\n"
        "내용이 계속 이어집니다.\n\n"
        "제2장. 본론\n\n"
        "여기에는 좀 더 긴 설명이 포함되어 있습니다. 여러 문장이 포함됩니다. "
        "길이가 길면 재귀적으로 더 잘게 나눌 것입니다."
    )
    
    # RecursiveCharacterTextSplitter 인스턴스 생성
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=50,
        chunk_overlap=20,
        separators=["\n\n", "\n", " ", ""],  # 문단 → 줄 → 단어 → 문자 순
        is_separator_regex=False
    )
    
    # 텍스트 분할
    chunks = splitter.split_text(text)
    
    # 결과 출력
    print(f"총 청크 수: {len(chunks)}")
    for i, chunk in enumerate(chunks):
        print(f"\n--- Chunk {i+1} ---\n{chunk}")

     

    여러 분할 기준에 의해 재귀적으로 잘라졌다.

    그 예로, 마지막 문장은 길이가 길었기 때문에(chunk_size 50보다) 공백을 기준으로도 쪼개진 것을 알 수 있다.

    chunk_overlap 또한 적용되었다. 


    3. python.PythonCodeTextSplitter

    지난 글에서 설명한 5 Levels Of Text Splitting의 Level 3 : Document Based Chunking에 해당한다.

    Python코드에서 클래스, 함수, import 등 특징적인 항목을 기준으로 분할한다.
    그 외에도 html, json, jsx, konlpy, latex, markdown 등 문서별로 제공된다.

     

    PythonCodeTextSplitter — 🦜🔗 LangChain documentation

    metadatas (list[dict[Any, Any]] | None)

    python.langchain.com


    4. html.HTMLHeaderTextSplitter

    Level 3 : Document Based Chunking의 예시이다.

    HTML 문서에서 헤더 태그(h1~h6 등)를 기준으로 chunk를 구성하는 Text Splitter다.

    그러므로 논리적 구조를 유지할 수 있다.

     

    참고로 문서에서 MarkdownHeaderTextSplitter HTMLHeaderTextSplitter TextSplitter에서 파생된 친구들은 아니라고 Note되어있다. 그러니 활용 방식에 약간 차이가 있다.

    [docs]    def __init__(
            self,
            headers_to_split_on: List[Tuple[str, str]],
            return_each_element: bool = False,
        ):
            """Create a new HTMLHeaderTextSplitter.
    
            Args:
                headers_to_split_on: list of tuples of headers we want to track mapped to
                    (arbitrary) keys for metadata. Allowed header values: h1, h2, h3, h4,
                    h5, h6 e.g. [("h1", "Header 1"), ("h2", "Header 2)].
                return_each_element: Return each element w/ associated headers.
            """
            # Output element-by-element or aggregated into chunks w/ common headers
            self.return_each_element = return_each_element
            self.headers_to_split_on = sorted(headers_to_split_on)
    • headers_to_split_on 분할에 사용할 HTML 태그 리스트 (예: `("h1", "Header 1")
    • return_each_line_as_document 각 헤더 섹션을 개별 문서로 반환할지 여부 (기본 False). true이면 모든 html요소가 각각 분리된 문서로 리턴된다.

    아래는 활용 예시코드이다.

    from langchain_text_splitters import HTMLHeaderTextSplitter
    
    headers_to_split_on = [("h1", "Main Topic"), ("h2", "Sub Topic")]
    
    splitter = HTMLHeaderTextSplitter(
        headers_to_split_on=headers_to_split_on,
        return_each_element=False
    )
    
    html_content = """
    <html>
      <body>
        <h1>Introduction</h1>
        <p>Welcome to the introduction section.</p>
        <h2>Background</h2>
        <p>Some background details here.</p>
        <h1>Conclusion</h1>
        <p>Final thoughts.</p>
      </body>
    </html>
    """
    
    documents = splitter.split_text(html_content)
    print(documents[4])
    
    # documents 배열의 각 요소를 줄바꿈하며 출력
    for document in documents:
        for line in str(document).splitlines():
            print(line)
        print()

     

    결과적으로 [("h1", "Header 1"), ("h2", "Header 2")]는 <h1> 및 <h2> 태그로 콘텐츠를 분할하고,

    해당 텍스트 콘텐츠를 문서 메타데이터에 할당한 것을 알 수 있다.

    심지어 결과 Line4를 보면 h1, h2 태그 계층도 드러나고 있다.


    5. SementicChunker

    Level 4: Semantic Chunking에 해당한다.

    그러나 Langchain에서는 Experimental 기능으로 분류되어있다. LlamaIndex에서는 정식 기능이므로 참고해도 좋을 것 같다.

     

    • Langchain sementic chunker
     

    SemanticChunker — 🦜🔗 LangChain documentation

    SemanticChunker class langchain_experimental.text_splitter.SemanticChunker( embeddings: Embeddings, buffer_size: int = 1, add_start_index: bool = False, breakpoint_threshold_type: Literal['percentile', 'standard_deviation', 'interquartile', 'gradient'] = '

    python.langchain.com

     

    • LlamaIndex sementic chunker
     

    Semantic Chunker - LlamaIndex

    Node ID: 68006b95-c06e-486c-bbb6-be54746aaf22 Similarity: 0.8465522042661249 Text: I couldn't figure out what to do with it. And in retrospect there's not much I could have done with it. The only form of input to programs was data stored on punched cards,

    docs.llamaindex.ai

     

     

     

    'LLM' 카테고리의 다른 글

    Qdrant 구성 요소  (0) 2025.06.23
    Chunk Optimization / 5 Levels Of Text Splitting  (1) 2025.06.18