0Pricing
AI Engineering Academy · Lekcja

Strategie dzielenia tekstu: stały rozmiar, zdania i rekurencja

Uczestnicy zaimplementują i porównają dzielenie tekstu na fragmenty o stałym rozmiarze, według granic zdań oraz rekurencyjne, a także poznają wpływ rozmiaru i nakładania się fragmentów na jakość pobierania.

Strategie dzielenia tekstu: stały rozmiar, zdania i rekurencja to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Dlaczego jakość dzielenia na fragmenty ma znaczenie

Dzielenie na fragmenty to proces dzielenia wczytanych dokumentów na mniejsze części, które mieszczą się w oknie kontekstu LLM i mogą być osobno indeksowane oraz pobierane. Sposób podziału wpływa na jakość wyszukiwania bardziej niż niemal każdy inny czynnik. Jeśli fragment dzieli odpowiedź między dwie części, żadna z nich samodzielnie nie wystarczy do udzielenia odpowiedzi na pytanie. Fragment łączący dwa niezwiązane tematy będzie pobierany dla pytań dotyczących obu z nich, ale nie będzie przydatny dla żadnego.

Dzielenie na fragmenty o stałym rozmiarze

Dzielenie na fragmenty o stałym rozmiarze polega na dzieleniu tekstu na fragmenty zawierające dokładnie N znaków lub N tokenów, niezależnie od granic zdań i akapitów. To najprostsza strategia, łatwa do wdrożenia. Jej główną wadą jest częste przecinanie zdań na pół, przez co fragmenty zaczynają się lub kończą w połowie myśli. Jest to dopuszczalne w przypadku gęstego, jednolicie sformatowanego tekstu, takiego jak zrzuty eksportów baz danych, ale pogarsza jakość wyszukiwania w narracji lub dokumentacji technicznej.

def fixed_size_chunks(text, chunk_size=500, overlap=50):
    '''Split text into fixed-size character chunks with overlap'''
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)
        start += chunk_size - overlap  # overlap keeps context at boundaries
    return chunks

example = 'This is a long document. ' * 100
chunks = fixed_size_chunks(example, chunk_size=200, overlap=20)
print(f'Produced {len(chunks)} chunks, first: {chunks[0][:80]}...')

Dodawanie nakładania do fragmentów o stałym rozmiarze

Kluczowym ulepszeniem dzielenia na fragmenty o stałym rozmiarze jest nakładanie: każdy fragment powiela N znaków z poprzedniego fragmentu. Dzięki temu informacje znajdujące się w pobliżu granicy fragmentu występują w obu sąsiednich fragmentach. Przy nakładaniu wynoszącym 50–100 tokenów zdanie przekraczające granicę zostanie w całości ujęte w co najmniej jednym z dwóch fragmentów. Większe nakładanie oznacza lepsze pokrycie, ale także większy rozmiar indeksu i powtarzającą się treść w wynikach wyszukiwania.

# Overlap example with a sentence at the boundary
text = 'A B C D E F G H I J'
chunk_size = 6  # characters
overlap = 2

i = 0
while i < len(text):
    print(repr(text[i:i+chunk_size]))
    i += chunk_size - overlap

# Output shows overlapping content:
# 'A B C D'
# 'C D E F'
# 'E F G H'
# Each adjacent pair shares 2 chars

Dzielenie na granicach zdań

Dzielenie na granicach zdań wykorzystuje biblioteki NLP, takie jak nltk lub spacy, do wykrywania końców zdań przed podziałem. Gwarantuje to, że żadne zdanie nie zostanie przecięte na pół. Zdania są gromadzone do momentu, gdy dodanie kolejnego przekroczyłoby docelowy rozmiar, po czym rozpoczyna się nowy fragment. W rezultacie fragmenty zawsze zawierają kompletne myśli, co zapewnia znacznie lepszą jakość osadzeń niż dzielenie po stałej liczbie znaków.

import nltk
nltk.download('punkt', quiet=True)

def sentence_chunks(text, max_tokens=300):
    sentences = nltk.sent_tokenize(text)
    chunks = []
    current = []
    current_len = 0

    for sent in sentences:
        sent_tokens = len(sent.split())
        if current_len + sent_tokens > max_tokens and current:
            chunks.append(' '.join(current))
            current = []
            current_len = 0
        current.append(sent)
        current_len += sent_tokens

    if current:
        chunks.append(' '.join(current))
    return chunks

Rekursywne dzielenie tekstu po znakach

Rekursywne dzielenie po znakach jest najczęściej stosowaną strategią w produkcyjnych systemach RAG. Próbuje kolejno separatorów w hierarchii: najpierw przerw akapitowych (\n\n), następnie znaków nowego wiersza (\n), potem kropek kończących zdania, spacji, a na końcu pojedynczych znaków. Dzieli tekst przy największej znaczącej granicy, która pozwala utrzymać fragment poniżej docelowego rozmiaru, dzięki czemu fragmenty możliwie dobrze respektują strukturę dokumentu.

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,       # target size in characters
    chunk_overlap=200,     # overlap between consecutive chunks
    separators=['\n\n', '\n', '. ', '! ', '? ', ' ', ''],
    length_function=len
)

with open('document.txt') as f:
    text = f.read()

chunks = splitter.split_text(text)
print(f'Split into {len(chunks)} chunks')
for i, chunk in enumerate(chunks[:3]):
    print(f'Chunk {i}: {len(chunk)} chars — {chunk[:60]}...')

Dzielenie uwzględniające tokeny

Liczba znaków jest niedokładnym przybliżeniem liczby tokenów. Fragment o długości 1000 znaków może mieć 200 lub 400 tokenów, zależnie od długości słów i języka. Aby precyzyjnie kontrolować rozmiar, należy dzielić tekst według liczby tokenów za pomocą tiktoken. Ma to znaczenie przy dopasowywaniu fragmentów do okna kontekstu modelu i dokładnym szacowaniu kosztów. TokenTextSplitter z LangChain opakowuje tiktoken na potrzeby dzielenia uwzględniającego tokeny.

from langchain_text_splitters import TokenTextSplitter
import tiktoken

# Split by actual token count, not characters
splitter = TokenTextSplitter(
    encoding_name='cl100k_base',  # GPT-4 tokenizer
    chunk_size=256,               # target tokens per chunk
    chunk_overlap=32              # overlap in tokens
)

chunks = splitter.split_text(text)

# Verify token count
enc = tiktoken.get_encoding('cl100k_base')
for chunk in chunks[:3]:
    tokens = len(enc.encode(chunk))
    print(f'Chunk: {tokens} tokens')

Wybór odpowiedniego rozmiaru fragmentu

Rozmiar fragmentu jest krytycznym hiperparametrem. Małe fragmenty (100–200 tokenów) są precyzyjne — zawierają ściśle skoncentrowane informacje, które dobrze się osadzają. Tracą jednak kontekst otoczenia, więc LLM może nie mieć wystarczających informacji, aby udzielić odpowiedzi. Duże fragmenty (500–1000 tokenów) zapewniają więcej kontekstu, ale ich osadzenia uśredniają szerszy temat, przez co trudniej je pobrać dla konkretnych zapytań. Większość systemów produkcyjnych wykorzystuje 256–512 tokenów, przeprowadzając testy empiryczne na własnych danych.

Dodawanie kontekstu do fragmentów

Skutecznym ulepszeniem są kontekstowe nagłówki fragmentów: przed osadzeniem należy dodać do tekstu każdego fragmentu tytuł dokumentu i nagłówek sekcji. Dzięki temu osadzenie obejmuje nie tylko treść fragmentu, lecz także jego położenie w dokumencie. Fragment o treści Świadczenia i polityka urlopowa: Pracownicy nabywają 15 dni urlopu rocznie... jest pobierany znacznie dokładniej dla pytań o politykę urlopową niż ten sam tekst bez nagłówka.

def create_contextual_chunks(doc, splitter):
    title = doc['metadata'].get('title', '')
    section = doc['metadata'].get('section', '')
    text = doc['text']

    raw_chunks = splitter.split_text(text)
    contextual_chunks = []
    for chunk in raw_chunks:
        # Prepend document context to each chunk
        context_header = f'{title}\n{section}\n\n' if title else ''
        contextual_chunks.append({
            'text': context_header + chunk,
            'metadata': doc['metadata']
        })
    return contextual_chunks

Porównywanie strategii na własnych danych

Żadna strategia dzielenia na fragmenty nie jest uniwersalnie najlepsza. Należy przygotować szybką ewaluację: wybrać 20 pytań, na które znają Państwo odpowiedzi, wykonać wyszukiwanie z użyciem każdej strategii i zmierzyć, jak często właściwy fragment znajduje się wśród 5 najwyżej sklasyfikowanych wyników (hit rate@5). Przygotowanie zajmuje godzinę i pozwala uniknąć tygodni zgadywania. Często okaże się, że konkretny format dokumentów (gęsty tekst prawniczy w porównaniu z uporządkowaną dokumentacją techniczną) zdecydowanie faworyzuje jedną strategię nad innymi.

def evaluate_chunking_strategy(questions_and_answers, retriever):
    hits = 0
    for qa in questions_and_answers:
        results = retriever.retrieve(qa['question'], top_k=5)
        result_texts = [r['text'] for r in results]
        # Check if answer text appears in any retrieved chunk
        if any(qa['answer'] in text for text in result_texts):
            hits += 1
    hit_rate = hits / len(questions_and_answers)
    print(f'Hit rate@5: {hit_rate:.1%} ({hits}/{len(questions_and_answers)})')
    return hit_rate

Obsługa specjalnych typów dokumentów

Niektóre typy dokumentów wymagają dzielenia do konkretnych zastosowań. Pliki z kodem należy dzielić według granic funkcji lub klas, a nie liczby znaków. Pliki Markdown należy dzielić na granicach nagłówków, aby każdy fragment odpowiadał jednej sekcji. Tabele należy zachowywać w całości jako jeden fragment (dzielenie tabeli w środku sprawia, że treść staje się niezrozumiała). Strategię dzielenia należy planować na podstawie typów dokumentów w korpusie, zamiast stosować jeden uniwersalny splitter.

from langchain_text_splitters import MarkdownHeaderTextSplitter

# Split Markdown by header hierarchy
md_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[
        ('#', 'h1'),
        ('##', 'h2'),
        ('###', 'h3')
    ]
)

with open('documentation.md') as f:
    md_text = f.read()

# Each chunk gets metadata from its heading hierarchy
chunks = md_splitter.split_text(md_text)
for chunk in chunks[:3]:
    print('Section:', chunk.metadata)
    print('Text:', chunk.page_content[:80])
    print()

Śledzenie pochodzenia fragmentów

Każdy fragment potrzebuje stabilnego, unikalnego identyfikatora utworzonego na podstawie dokumentu źródłowego i pozycji fragmentu. Identyfikator ten służy do aktualizowania konkretnych fragmentów po zmianie dokumentów, bez ponownego indeksowania całego korpusu. Dobrze sprawdza się deterministyczny skrót ścieżki źródłowej wraz z indeksem fragmentu. W metadanych należy również zapisywać pozycję fragmentu (fragment 3 z 12 z dokumentu X) — ułatwia to ponowne składanie pełnych sekcji, gdy pobranych zostanie kilka sąsiednich fragmentów.

import hashlib

def assign_chunk_ids(chunks, source_path):
    for i, chunk in enumerate(chunks):
        key = f'{source_path}::chunk_{i}'
        chunk_id = hashlib.sha256(key.encode()).hexdigest()[:16]
        chunk['id'] = chunk_id
        chunk['metadata']['chunk_index'] = i
        chunk['metadata']['total_chunks'] = len(chunks)
    return chunks

# IDs are stable across re-runs if source and position match
chunks = sentence_chunks(text)
chunks = [{'text': c, 'metadata': {}} for c in chunks]
assign_chunk_ids(chunks, 'docs/policy_v3.pdf')

Szybki test

Sprawdź swoją wiedzę na temat koncepcji inżynierii AI z tej lekcji.

Podsumowanie lekcji

W tej lekcji poznali Państwo: dzielenie na fragmenty o stałym rozmiarze, które jest proste, ale przecina zdania na pół, dzielenie na granicach zdań, zachowujące kompletne myśli, rekursywne dzielenie po znakach, respektujące strukturę dokumentu i najczęściej wybierane w środowisku produkcyjnym, a także zaawansowane techniki, w tym dzielenie uwzględniające tokeny, kontekstowe nagłówki, splittery do Markdown i kodu oraz stabilne identyfikatory fragmentów na potrzeby przyrostowych aktualizacji. W następnej części osadzimy te fragmenty i zapiszemy je w bazie wektorowej.

Często zadawane pytania

Czy lekcja „Strategie dzielenia tekstu: stały rozmiar, zdania i rekurencja” jest bezpłatna?

Tak — pełny tekst „Strategie dzielenia tekstu: stały rozmiar, zdania i rekurencja” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Strategie dzielenia tekstu: stały rozmiar, zdania i rekurencja”?

Uczestnicy zaimplementują i porównają dzielenie tekstu na fragmenty o stałym rozmiarze, według granic zdań oraz rekurencyjne, a także poznają wpływ rozmiaru i nakładania się fragmentów na jakość pobi… Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?

Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.

Ile czasu zajmuje lekcja „Strategie dzielenia tekstu: stały rozmiar, zdania i rekurencja”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?

Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Wczytywanie dokumentów i ekstrakcja tekstu
  2. Strategie dzielenia tekstu: stały rozmiar, zdania i rekurencja
  3. Indeksowanie: tworzenie embeddingów i przechowywanie fragmentów
  4. Zapytanie, pobieranie i generowanie
← Powrót do AI Engineering Academy