Strategie podziału w porównaniu RAG: alternatywy,权衡 i przykłady

Porównanie strategii chunkowania w RAG

Page content

Chunking to najbardziej niedoceniany hiperparametr w Retrieval ‑ Augmented Generation (RAG): czynnie określa, co LLM “widzi”, jak drogie staje się przetwarzanie, i ile miejsca w oknie kontekstu LLM zużywa się na odpowiedź.

W tym artykule traktujemy chunking jako problem optymalizacji inżynierskiej: zdefiniuj cele, wybierz strategię, zmierz, a następnie iteruj.

Jeśli jesteś nowy w architekturze RAG, zacznij od głównej Instrukcji do Retrieval-Augmented Generation (RAG): architektura, implementacja i przewodnik po produkcji.

kolorowy tetris na stole

TL;DR (Podsumowanie dla menedżerów)

Systemy RAG pobierają chunki, a nie dokumenty. Chunking więc definiuje jednostkę pobierania, jednostkę kosztu osadzania i jednostkę dowodów, które możesz pokazać lub cytować. W oryginalnej formule RAG, pobieranie dostarcza fragmentów do generowania; granice fragmentów są skutecznie granicami chunków.

Dobra strategia chunkinga dąży do punktu Pareto w zakresie: jakości pobierania (odzyskiwanie / precyzja dowodów), spójności (chunki muszą być interpretowalne), i kosztów (osadzanie, przechowywanie i opóźnienie zapytań). Nie ma globalnie optymalnego rozmiaru chunka ani metody, a systemy produkcyjne zazwyczaj mieszają strategie (np. chunking świadomy struktury dla PDFów + chunking świadomy semantyki dla tekstu + chunking AST dla kodu).

Dla większości przypadków “QA dokumentacji” i wewnętrznych baz wiedzy, bezpieczna domyślna wartość to rekursywny splitter szanujący strukturę z umiarkowanym nakładem (aby zmniejszyć utratę granic), wspierany przez bazę wektorów z filtrowaniem metadanych i opcjonalnym ponownym rangowaniem. LangChain’s RecursiveCharacterTextSplitter to typowa implementacja tej idei hierarchicznego separatora; nakład istnieje w celu ograniczenia utraty informacji, gdy istotny kontekst jest cięty na granicach.

Gdy dokumenty mają silną strukturę (PDFy z nagłówkami, tabelami, listami, opisami), chunking oparty na elementach / świadomy struktury może przewyższać podział na tokeny, produkując mniej chunków. Badanie z 2024 roku nad dokumentami SEC wykazało, że chunking oparty na typach elementów poprawił wyniki RAG i zmniejszył liczbę chunków (i zatem wektorów) o około połowę w porównaniu do metod nieświadomych struktury – zmniejszając koszt indeksowania i potencjalnie poprawiając opóźnienie zapytań.

Jeśli możesz pozwolić sobie na więcej obliczeń wstępnych, chunking semantyczny (podział na zmiany tematu za pomocą podobieństwa osadzania) może znacząco poprawić wiarygodność pobierania dla tekstu narracyjnego i stron z mieszanką tematów. Starsze algorytmy segmentacji tematów, takie jak TextTiling, pokazują ogólny zasada: silne zmiany słownictwa / semantyki są dobrymi kandydatami na granice.

Dla bardzo długich, wewnętrznie wzajemnie odnoszących się materiałów (zasady, RFCy, standardy, duże podręczniki), hierarchiczny chunking + hierarchiczne pobieranie / łączenie (węzły nadrzędne / podrzędne) może odzyskać większy spójny kontekst po żądaniu. Hierarchiczny parser LlamaIndex tworzy hierarchie chunków od grubej do drobnej, a AutoMergingRetriever może łączyć węzły podrzędne w węzły nadrzędne w czasie pobierania, gdy zostanie odzyskanych wystarczająco dużo powiązanych podrzędnych węzłów.

Cele chunkinga i kompromisy

Chunking to nie tylko “podziel tekst tak, by pasował do modelu osadzania”. Kontroluje wiele zachowań operacyjnych i dalszych.

Jakość pobierania vs. szum pobierania. Mniejsze chunki zwiększają szansę, że dokładne zdanie zawierające odpowiedź zostanie odzyskane (wyższy potencjalny odzysk przy stałym top-k). Ale tworzą więcej wektorów, zwiększając rozmiar indeksu i czasem powodując “bliskie dopasowania”, które są semantycznie podobne, ale nie faktycznie dowodowe (niższa precyzja). Gęste pobieracze, takie jak DPR, zostały stworzone wokół skutecznego pobierania fragmentów dla QA, podkreślając, że granice fragmentów mają znaczenie dla wydajności QA end-to-end.

Spójność kontekstu vs. utrata granic. Spójne chunki pomagają LLM w poprawnym rozumowaniu i zmniejszają halucynacje, dostarczając pełnego lokalnego kontekstu (definicje, ograniczenia, wymagania). Nakład zmniejsza utratę granic, ale tworzy duplikat tekstu, który może prowadzić do nadmiernych wyników pobierania i powiększenia długości promptu, jeśli nie zduplikujesz / połączysz.

Koszt osadzania i indeksowania. Koszt osadzania jest zwykle proporcjonalny do tokenów osadzonych, a czas indeksowania skaluje się z liczbą chunków (plus nadmiar kosztu zapisu w bazie wektorów). Dla osadzania OpenAI, żądania mają maksymalny limit tokenów na wejściu (8192 tokenów dla wszystkich modeli osadzania) i maksymalną sumę tokenów na żądanie (300 000 tokenów). Dla dużych korpusów, API w partii może zmniejszyć koszt o około 50% z asynchroniczną, 24-godzinną odpowiedzią – przydatną do uzupełniania danych i okresowego ponownego indeksowania.

Rozmiar indeksu wektorów, RAM i opóźnienie. Więcej chunków oznacza więcej wektorów i potencjalnie więcej pamięci i wolniejsze zapytania (w zależności od typu indeksu). FAISS jawnie formułuje projekt indeksu jako zestaw kompromisów między czasem wyszukiwania, jakością wyszukiwania, a pamięcią na wektorze indeksowanym; oferuje również implementacje GPU dla szybkiego dokładnego i przybliżonego wyszukiwania.

Długość promptu LLM / użycie okna kontekstu. Wyjście pobieracza staje się budżetem promptu. Strategia chunkinga, która spójnie odzyskuje “dostatecznie dużo” kontekstu, może poprawić jakość odpowiedzi i zmniejszyć koszt. Odwrotnie, nakład i zbyt duże chunki powiększają długość promptu. W praktyce często dostosowujesz: (rozmiar chunka, nakład, top-k, ponowne rangowanie / łączenie) razem.

Koszt aktualizacji / indeksowania i deduplikacja. Chunking wpływa na to, jak drogie jest odświeżanie danych. Mniejsze chunki czynią częściowe aktualizacje tańsze (możesz ponownie osadzić tylko zmieniony fragment), ale utrudniają deduplikację, jeśli proliferują duplikaty lub zbliżone chunki.

Gdzie chunking znajduje się w przepływie pracy RAG

chunking w przepływie pracy RAG

Strategie chunkinga i alternatywy

Poniżej znajdują się główne rodziny chunkinga, które spotkasz w modernym RAG. W praktyce często łączysz dwa: chunking pierwszeństwa struktury (szanuje granice dokumentu) + enforcement budżetu tokenów (zapewnia, że chunki pasują do Twojego modelu osadzania i budżetu promptu).

Chunking o stałym rozmiarze

Co to jest. Podziel tekst na bloki o równym rozmiarze w znakach lub tokenach.

Dlaczego istnieje. Jest prosty, szybki, przewidywalny i łatwy do równoległego przetwarzania. Jest również najprostszym sposobem na przetwarzanie strumieniowe, gdzie nie masz pełnego kontekstu dokumentu.

Gdzie zawodzi. Ignoruje granice (zdania, sekcje, bloki kodu), więc może rozerwać definicje lub podzielić “pary pytanie/odpowiedź” na chunki, zwiększając błąd pobierania.

Profil operacyjny. Najniższa złożoność indeksowania; przewidywalna liczba chunków; najłatwiejsze przechowywanie w pamięci. Ale zazwyczaj potrzebujesz nakładu (poniżej), aby uniknąć utraty granic.

Chunking z nakładem

Co to jest. Dowolna strategia, gdzie kolejne chunki dzielą wspólny region nakładu (np. 10–20% rozmiaru chunka). Nakład jest standardowy w wielu ramach, ponieważ zmniejsza utratę informacji, gdy kontekst jest dzielony.

Dlaczego ma znaczenie. Nakład to w zasadzie “miękka granica” – pozwala pobieraniu na odzyskanie faktu, który przekracza granicę.

Koszty i pułapki. Więcej tokenów osadzonych; więcej duplikatów tekstu w indeksie; większy ryzyko odzyskania wielu niemal identycznych chunków, chyba że deduplikujesz w czasie pobierania (np. łącząc według offsetów źródła lub używając MMR).

Chunking oparty na zdaniach i akapitach

Co to jest. Podziel tekst na granice zdania lub akapitu, a następnie spakuj zdania/akapity w chunki do budżetu tokenów.

Dlaczego inżynierowie lubią to. Poprawia spójność dla języka naturalnego i jest odporny na dokumenty z konwencjonalnymi znakami interpunkcyjnymi i odstępami.

Narzędzia. NLTK’s sent_tokenize() domyślnie używa detekcji granic zdania Punkt, a spaCy oferuje narzędzia oparte na regułach do granic zdania, takie jak Sentencizer (korzystne, gdy chcesz podziały zdania bez pełnego modelu zależności).

Tryby awaryjne. Niekonwencjonalna interpunkcja (logi, transkrypcje czatu), tabele, kod i listy kropek mogą zniszczyć założenia segmentacji zdania.

Chunking z suwakiem

Co to jest. Tworzenie chunków za pomocą stałego rozmiaru okna i kroku (przesunięcia). Jest to wersja “systematycznego nakładu” chunkinga.

Kiedy jest dobre. Szeregi czasowe tekstu, transkrypcje, logi czatu, notatki z spotkań – wszystko, gdzie istotne fakty mogą pojawiać się w lokalnych sąsiedztwach i chcesz solidną odzyskiwalność.

Kiedy jest złe. Wzmacnia redundancję i może być drogie w dużej skali. Tendencja do pobierania nadmiernych okien, chyba że deduplikujesz.

Rekursywny / hierarchiczny chunking

Co to jest. Zacznij od dużych “naturalnych” separatorów (np. \n\n dla akapitów) i rekursywnie podziel na mniejsze jednostki (zdania, przestrzenie), tylko wtedy, gdy jest to konieczne, aby pozostać pod budżetem rozmiaru. LangChain jawnie opisuje to zachowanie: rekursywny splitter stara się zachować większe jednostki nienaruszone i zwraca się do mniejszych separatorów tylko wtedy, gdy jednostka nadal przekracza rozmiar chunka. Dlaczego to silny domyślny wybór. Szanuje strukturę bez konieczności skomplikowanego analizowania dokumentu. To praktyczny kompromis dla Markdown, HTML jako tekstu i dokumentacji.

Główne węzły dostosowania. chunk_size, chunk_overlap, oraz length_function (znaki vs tokeny), plus niestandardowe separatorzy dla repozytoriów kodu wielojęzycznych.

Semanticzny (świadomy osadzania) chunking

Co to jest. Wykrywanie zmian tematu za pomocą reprezentacji semantycznych (osadzeń) i dzielenie tam, gdzie podobieństwo spada. To odbiega klasycznych idei segmentacji, takich jak TextTiling, które wykorzystują zmiany spójności leksykalnej do znalezienia granic podtematów.

Dlaczego może przewyższać chunking oparty na rozmiarze. Zatrzymujesz się od dzielenia na arbitralne liczby tokenów i zamiast tego wyrównujesz chunki z granicami tematu – często poprawiając precyzję pobierania dla dokumentów wielostrukturalnych (blogi, dokumenty projektowe, bilety, raporty o incydentach).

Koszty. Może być konieczne dodatkowe osadzanie w trakcie chunkinga (osadzenia na poziomie zdania lub akapitu) przed końcowymi osadzeniami chunków. To może podwoić lub potroić wywołania osadzania, chyba że ponownie użyjesz osadzeń pośrednich.

Praktyczny trik. “Semanticzne pakowanie”: oblicz osadzenia zdania raz, grupuj zdania w sekcje spójne tematowo, a następnie osadzaj każdą końcową sekcję.

Hierarchiczny chunking (nadrzędny / podrzędny)

Co to jest. Tworzenie reprezentacji wielu poziomów szczegółowości: grube nadrzędne chunki (np. rozmiar sekcji) z bardziej szczegółowymi podrzędnymi chunkami (np. rozmiar akapitu). Hierarchiczne parsowanie LlamaIndex tworzy “od grubej do drobnej” hierarchie domyślnie (np. 2048 → 512 → 128 tokenów), a AutoMergingRetriever może łączyć podrzędne węzły w nadrzędne w czasie pobierania, gdy zostanie odzyskanych wystarczająco dużo powiązanych podrzędnych węzłów.

Dlaczego pomaga. Unika wyboru między “małymi chunkami dla odzyskiwania” a “dużymi chunkami dla spójności”, przechowując oba i wybierając w czasie zapytania.

Koszty. Bardziej złożona logika indeksowania i pobierania, plus potencjalnie więcej miejsca (ponieważ przechowujesz wiele poziomów szczegółowości).

Adaptacyjny / oparty na LLM chunking

Co to jest. Użyj LLM, aby określić granice chunków (opcjonalnie generując podsumowania lub kontekstowe nagłówki). Weaviate jawnie opisuje chunking oparty na LLM jako tworzenie semantycznie spójnych chunków, zamiast polegania na stałych regułach lub podobieństwie osadzania.

Kiedy jest wart. Wysokiej wartości korpusy, gdzie poprawność dominuje koszt (prawo, zgodność, podręczniki wsparcia), a dokumenty są chaotyczne, heterogeniczne i słabo podzielone.

Ryzyka. Koszt, opóźnienie i niedeterminizm. Chcesz caching, deterministyczne dekodowanie i testy regresyjne (patrz sekcja oceny).

Chunking oparty na strukturze i elementach (dokumenty nie są zwykłym tekstem)

Co to jest. Parsuj dokument na elementy (tytuły, akapity, listy, tabele, opisy) za pomocą warstwy zrozumienia dokumentu, a następnie chunkuj za pomocą tych elementów. Funkcje chunkowania Unstructured jawnie używają metadanych i elementów dokumentu (wygenerowanych przez podział) do tworzenia chunków dla RAG. Docling’s HierarchicalChunker tworzy chunki na podstawie wykrytych elementów dokumentu i dołącza strukturalne metadane, takie jak nagłówki / opisy.

Dowody z ostatnich badań. Badanie z 2024 roku nad dokumentami SEC argumentuje, że chunking tylko na akapitach pomija strukturę dokumentu i proponuje chunking na podstawie elementów strukturalnych; raportuje poprawione wyniki RAG i mniej chunków / wektorów niż metody nieświadome struktury.

Dlaczego to ważne dla multimodalnego. Tabele, rysunki i opisy często zawierają prawdziwy stan rzeczy. “Spłaszczanie” ich do zwykłego tekstu może zniszczyć sygnały, które inaczej wykorzystałby pobieranie.

Chunking świadomy kodu (AST / struktura)

Co to jest. Chunkuj kod w jednostki syntaktyczne (funkcje, klasy, moduły), opcjonalnie włączając dokumentację i komentarze.

Dlaczego to ważne. Stałe podziały tokenów tendencja do przycinania funkcji na pół i oddzielania dokumentacji od implementacji – złe dla wyszukiwania kodu i przypadków RAG typu “wyjaśnij tę funkcję”.

Opcje implementacji. Dla Pythona, wbudowany moduł ast często wystarcza. Dla repozytoriów wielojęzycznych, chunkery oparte na tree-sitter są typowe.

Wymiar oceny i sposób porównywania strategii chunkinga

Chunking powinien być testowany jako komponent systemu.

Metryki jakości pobierania

Użyj standardowych metryk IR dla warstwy pobierania:

  • Recall@k / Precision@k: Czy top-k zawierał dowód?
  • MRR / nDCG: Czy dowód był wysokiej rangi?

BEIR to szeroko używany heterogeniczny benchmark dla oceny IR na zadaniach i domenach, który podkreśla kompromisy między podejściami rzadkimi, gęstymi, późnym interakcjom i ponownym rangowaniem.

Chunking wpływa na te metryki, ponieważ definiuje, co liczy się za “relevantny pobrany element”.

Metryki jakości odpowiedzi RAG end-to-end

Jeśli tworzysz QA lub asystentów, metryki pobierania są konieczne, ale nie wystarczające. Potrzebujesz również:

  • Recall / precision kontekstu: czy pobrane konteksty zawierają istotne dowody i unikają szumu.
  • Faithfulness: czy wygenerowana odpowiedź jest wspierana przez pobrany kontekst.

RAGAS dostarcza konkretne definicje i implementacje dla “faithfulness” i innych metryk orientowanych na RAG.

Wymiar kosztu i wydajności systemu

Chunking zmienia te ustawienia:

Opóźnienie (p50/p95). Opóźnienie zapytania zwykle rośnie z większą liczbą wektorów i większą post-processorową przetwarzaniem. Twoja baza wektorów również ma znaczenie: typy indeksów FAISS wymieniają czas wyszukiwania, jakość, pamięć i czas treningu/dodawania. [^faiss]

Koszt osadzania i przepustowość. Osadzanie OpenAI jest opłacane za tokeny; API osadzania ma jawne limity na wejściu i żądaniu. [^openai_embed_create] Dla offline indeksowania, API w partii zmniejsza koszt i oferuje wyższy limit w zamian za nie rzeczywisty czas odpowiedzi. [^openai_batch]

Rozmiar indeksu i pamięć. W przybliżeniu, przechowywanie N wektorów typu float32 o wymiarze d kosztuje ~4 * N * d bajtów tylko dla surowych wektorów (plus metadane + nadmiar indeksu). Chunking wpływa na N. Wymiarowość osadzania wpływa na d, a API osadzania OpenAI umożliwia kontrolowanie wymiarowości wyjścia poprzez parametr dimensions. [^openai_embed_create]

Budżet promptu LLM. Większe chunki i nakład powiększają tokeny promptu. Może to zwiększyć opóźnienie i koszt, a także zwiększyć tryby awaryjne typu “straciliśmy w środku”, gdzie modele mniej zwracają uwagę na pewien kontekst. W praktyce często:

  1. pobierasz małe chunki,
  2. łączysz / deduplikujesz,
  3. opcjonalnie podsumowujesz,
  4. wysyłasz kompaktowy zestaw dowodów do LLM.

Koszt aktualizacji / indeksowania. Mniejsze chunki pozwalają na częściowe ponowne osadzanie, ale zwiększają księgowanie. Dla przetwarzania strumieniowego, preferuj deterministyczne, inkrementalne chunking (stałe lub suwakowe) i dołącz stałe ID (document_id, zakresy offsetów, hash).

Projekt eksperymentalny: praktyczna pętla benchmarku

Typowy reprodukowalny benchmark chunkinga ma:

  • Stały zrzut korpusu + stały zestaw zapytań z dowodami (lub przynajmniej oczekiwanymi fragmentami odpowiedzi).
  • Stały model osadzania i konfigurację indeksu wektorów.
  • Oceny “tylko pobierania” (recall@k, nDCG) i “RAG” (faithfulness, odpowiednie odniesienie).
  • Telemetrię kosztów: liczba chunków, osadzone tokeny, $/miesiąc przechowywania, p95 opóźnienia zapytania, tokeny promptu.

Artykuł Unstructured SEC-filings to dobre przykładowe oceny strategii chunkinga z metrykami skupionymi na pobieraniu i miarami dokładności QA.

Praktyczne wytyczne, macierz decyzyjna i zalecane domyślne

Zalecane domyślne, które w niezwykły sposób działają

Jeśli potrzebujesz solidnej strategii “dzień 1” dla ogólnego QA dokumentacji:

  1. Lekko parsuj: zachowaj nagłówki i podstawowe metadane (źródło, tytuł sekcji, URL / ścieżka, znacznik czasu).
  2. Chunkuj z rekursywnym splitterem separatora (akapit → zdanie → słowo), z umiarkowanym nakładem.
  3. Osadzaj z silnym ogólnym modelem osadzania.
  4. Indeksuj z metadanymi (ID dokumentu, sekcja, ACL) i deduplikuj w czasie pobierania.
  5. Dodaj ponowne rangowanie lub łączenie hierarchiczne tylko wtedy, gdy Twoja ocena pokazuje lukę.

To zgadza się z tym, jak typowe ramy RAG opisują nakład i podział szanujący strukturę.

Która metoda chunkinga użyć - macierz decyzyjna

Która metoda chunkinga użyć - Diagram

Przypadek użycia Zalecana domyślna metoda chunkinga Kluczowe parametry do dostosowania Typowe tryb awaryjny Ścieżka upgrade
QA krótkich form (FAQ, wewnętrzny wiki) Rekursywny / separator chunking + nakład chunk_size, nakład, top-k Brak dowodów w granicach Dodaj chunking semantyczny lub ponowne rangowanie
QA długich form (zasady, standardy, podręczniki) Hierarchiczny chunking + łączący pobieracz rozmiary nadrzędne / podrzędne, próg łączenia Pobiera małe fragmenty; LLM brakuje pełnego kontekstu Auto-łączenie / hierarchiczne pobieranie
Podsumowanie (na dokument / sekcję) Chunking świadomy struktury (sekcje) wykrywanie sekcji, maksymalne tokeny Podsumowania pomijają odniesienia między sekcjami Hierarchiczne podsumowanie + graf sekcji
Wyszukiwanie kodu & “wyjaśnij tę funkcję” Chunking na poziomie AST / funkcji włącz dokumentację / komentarze, maksymalne tokeny Funkcja podzielona; utracone podpis / użycie Hierarchia świadoma repozytorium (moduł → klasa → funkcja)
Multimodalne PDFy (tabele / rysunki) Chunking oparty na elementach (świadomy tytułu / tabeli / opisu) serializacja tabel, łączenie opisów Strata treści tabeli lub zniekształcenie Użyj Docling / Unstructured + strukturalne serializatory
Przetwarzanie strumieniowe (logi, czaty, bilety) Suwak lub stałe tokeny okno, krok, deduplikacja Nadmierne pobieranie nadmiernych okien Dodaj wykrywanie granic semantycznych na partiiach

chunking - Porównanie jakościowe wydajności

Traktuj to jako “przewidywany kierunek zmiany” (weryfikuj na własnych danych).

Strategia Potencjalna dokładność pobierania Spójność pobranych kontekstów Złożoność indeksowania Liczba wektorów / rozmiar indeksu Koszt osadzania Wpływ opóźnienia zapytania Najlepsze do
Stały rozmiar (bez nakładu) Średni Niski Niski Średni Niski Średni szybkie prototypy, tekst jednorodny
Stały rozmiar + nakład Średni–Wysoki Średni Niski Wysoki Średni–Wysoki Średni–Wysoki QA, gdzie utrata granic szkodzi
Pakowanie zdania / akapitu Wysoki (proza) Wysoki Średni Średni Średni Średni dokumenty, artykuły, czysta proza
Suwak Wysoki odzyskiwanie Średni Średni Bardzo wysoki Bardzo wysoki Wysoki transkrypcje, logi, czaty
Rekursywny / separator Wysoki Wysoki Średni Średni Średni Średni “domyślny” RAG dokumentów
Chunking semantyczny Wysoki–Bardzo wysoki Wysoki Wysoki Średni Wysoki Średni strony wielostrukturalne, tekst narracyjny
Hierarchiczny (nadrzędny / podrzędny) Bardzo wysoki Bardzo wysoki Wysoki Wysoki Wysoki Średni długie podręczniki / standardy
LLM / adaptacyjny Bardzo wysoki Bardzo wysoki Bardzo wysoki Średni Bardzo wysoki Średni–Wysoki korpusy wysokiej wartości
Oparty na strukturze / elementach Wysoki–Bardzo wysoki Wysoki Wysoki Niski–Średni Średni Średni PDFy, raporty, tabele, mieszane układu
Świadomy kodu (AST) Wysoki (kod) Wysoki Średni Średni Średni Średni wyszukiwanie kodu, asystenci repozytorium

Uwagi DevOps i sprzętowe (często pominięte)

Wybory chunkinga wpływają na to, ile infrastruktury potrzebujesz:

  • Mniejsze chunki → więcej wektorów → większe indeksy i więcej RAM / dysku. Dla samowystarczalnego FAISS, to może wymusić podział lub indeksy oparte na dysku.
  • Jeśli osadzasz lokalnie, przepustowość osadzania staje się problemem harmonogramowania GPU; jeśli osadzasz przez API, objętość tokenów staje się problemem FinOps (API w partii to Twój przyjaciel dla uzupełnień).
  • Niektóre silniki (FAISS) oferują przyspieszenie wyszukiwania przez GPU; to może przenieść koszt z RAM-bound CPU do pamięci GPU i przepustowości PCIe.
  • Parsowanie świadome struktury (layout PDF, OCR, ekstrakcja tabel) często jest CPU-bound i może przewyższać koszt osadzania dla skanowanych dokumentów; budżetuj je oddzielnie.

Chunking - Implementacje referencyjne w Pythonie

Wszystkie przykłady zostały zaprojektowane tak, aby były czytelne i uruchamialne. Jeśli potrzebujesz klucza API lub działającej bazy danych, jest to jasne z kodu.

Użyteczne narzędzia: liczenie tokenów i stabilne identyfikatory fragmentów

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from typing import Any, Iterable, Optional

from transformers import AutoTokenizer  # pip install transformers

@dataclass(frozen=True)
class Chunk:
    text: str
    meta: dict[str, Any]

def sha1_id(*parts: str) -> str:
    h = hashlib.sha1()
    for p in parts:
        h.update(p.encode("utf-8"))
        h.update(b"\x1e")
    return h.hexdigest()

# Użyj dowolnego tokenizatora, który w przybliżeniu odpowiada tokenizacji Twojego LLM/embedding.
TOKENIZER = AutoTokenizer.from_pretrained("bert-base-uncased")

def token_len(text: str) -> int:
    return len(TOKENIZER.encode(text, add_special_tokens=False))

Fragmentacja o stałym rozmiarze tokenów

def chunk_fixed_tokens(
    text: str,
    *,
    chunk_size: int = 512,
) -> list[Chunk]:
    token_ids = TOKENIZER.encode(text, add_special_tokens=False)
    out: list[Chunk] = []

    for i in range(0, len(token_ids), chunk_size):
        window = token_ids[i : i + chunk_size]
        chunk_text = TOKENIZER.decode(window)
        out.append(
            Chunk(
                text=chunk_text,
                meta={"strategy": "fixed_tokens", "start_token": i, "end_token": i + len(window)},
            )
        )
    return out

Fragmentacja o stałym rozmiarze z nakładaniem się i oknem przesuwnym

def chunk_sliding_window(
    text: str,
    *,
    window_tokens: int = 512,
    stride_tokens: int = 384,  # mniejszy przeskok = większa nakładka
) -> list[Chunk]:
    assert 1 <= stride_tokens <= window_tokens, "przeskok musi być w zakresie (0, okno]"
    token_ids = TOKENIZER.encode(text, add_special_tokens=False)
    out: list[Chunk] = []

    start = 0
    while start < len(token_ids):
        end = min(start + window_tokens, len(token_ids))
        window = token_ids[start:end]
        out.append(
            Chunk(
                text=TOKENIZER.decode(window),
                meta={"strategy": "sliding_window", "start_token": start, "end_token": end},
            )
        )
        if end == len(token_ids):
            break
        start += stride_tokens

    return out

Fragmentacja oparta na zdaniach (NLTK) z pakowaniem w budżecie tokenów

# pip install nltk
import nltk
from nltk.tokenize import sent_tokenize

nltk.download("punkt", quiet=True)

def chunk_by_sentences_nltk(
    text: str,
    *,
    max_tokens: int = 512,
    overlap_sentences: int = 1,
) -> list[Chunk]:
    sents = [s.strip() for s in sent_tokenize(text) if s.strip()]
    out: list[Chunk] = []

    buf: list[str] = []
    buf_tokens = 0

    def flush():
        nonlocal buf, buf_tokens
        if not buf:
            return
        chunk_text = " ".join(buf).strip()
        out.append(
            Chunk(
                text=chunk_text,
                meta={"strategy": "sentences_nltk", "sent_count": len(buf)},
            )
        )
        # Nakładka przez zachowanie ostatnich N zdań
        if overlap_sentences > 0:
            buf = buf[-overlap_sentences:]
            buf_tokens = token_len(" ".join(buf))
        else:
            buf, buf_tokens = [], 0

    for s in sents:
        s_tokens = token_len(s)
        # Jeśli pojedyncze zdanie przekracza budżet, wycofaj się do fragmentacji o stałym rozmiarze na tym fragmencie
        if s_tokens > max_tokens:
            flush()
            out.extend(chunk_fixed_tokens(s, chunk_size=max_tokens))
            continue

        if buf_tokens + s_tokens > max_tokens and buf:
            flush()

        buf.append(s)
        buf_tokens += s_tokens

    flush()
    return out

Fragmentacja oparta na zdaniach (spaCy) w przypadku, gdy chcesz użyć reguł lub modelu do wykrywania granic zdaniowych

# pip install spacy
# python -m spacy download en_core_web_sm
import spacy

def chunk_by_sentences_spacy(
    text: str,
    *,
    max_tokens: int = 512,
) -> list[Chunk]:
    # Dla lekkiego wykrywania granic zdaniowych na podstawie reguł (bez analizy zależności), użyj sentencizera.
    nlp = spacy.blank("en")
    nlp.add_pipe("sentencizer")  # regułowe wykrywanie granic zdaniowych
    doc = nlp(text)

    sents = [sent.text.strip() for sent in doc.sents if sent.text.strip()]
    return chunk_by_sentences_nltk(" ".join(sents), max_tokens=max_tokens, overlap_sentences=1)

Rekursywna fragmentacja na podstawie separatorów (LangChain)

# pip install langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter

def chunk_recursive_langchain(
    text: str,
    *,
    chunk_size: int = 1200,
    chunk_overlap: int = 150,
) -> list[Chunk]:
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        length_function=token_len,  # budżetowanie świadome tokenów
        separators=["\n\n", "\n", ". ", " ", ""],  # dostosuj do Twojej zawartości (np. kod)
    )
    pieces = splitter.split_text(text)

    return [
        Chunk(text=p, meta={"strategy": "recursive_langchain", "chunk_size": chunk_size, "overlap": chunk_overlap})
        for p in pieces
    ]

Fragmentacja semantyczna z podobieństwem embeddingów (embeddingi OpenAI)

Ten podejście oblicza embeddingi dla jednostek kandydatów (zdania/paragrafy), a następnie znajduje semantyczne “punkty przerwania”.

# pip install openai numpy
import os
import numpy as np
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "YOUR_OPENAI_API_KEY"))

def embed_texts_openai(texts: list[str], *, model: str = "text-embedding-3-small") -> np.ndarray:
    # UWAGA: dla dużych partii, szanuj granice tokenów żądania i ograniczenia wielkości partii.
    resp = client.embeddings.create(model=model, input=texts)
    embs = np.array([d.embedding for d in resp.data], dtype=np.float32)
    return embs

def cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
    denom = (np.linalg.norm(a) * np.linalg.norm(b)) + 1e-8
    return float(np.dot(a, b) / denom)

def chunk_semantic(
    text: str,
    *,
    max_tokens: int = 800,
    breakpoint_threshold: float = 0.70,
) -> list[Chunk]:
    # 1) Zacznij od kandydatów w postaci zdań
    sents = [s.strip() for s in sent_tokenize(text) if s.strip()]
    if len(sents) <= 1:
        return [Chunk(text=text, meta={"strategy": "semantic", "note": "no_split"})]

    # 2) Zbuduj embeddingi dla każdego zdania
    embs = embed_texts_openai(sents)

    # 3) Oblicz spadki podobieństwa
    sims = [cosine_sim(embs[i], embs[i + 1]) for i in range(len(sents) - 1)]

    # 4) Utwórz segmenty w punktach przerwania
    out: list[Chunk] = []
    buf: list[str] = []
    buf_tokens = 0

    for i, s in enumerate(sents):
        # Dodaj zdanie
        s_tok = token_len(s)
        if buf and buf_tokens + s_tok > max_tokens:
            out.append(Chunk(text=" ".join(buf), meta={"strategy": "semantic", "reason": "max_tokens"}))
            buf, buf_tokens = [], 0
        buf.append(s)
        buf_tokens += s_tok

        # Decyzja o punkcie przerwania po zdaniu i (na podstawie podobieństwa do następnego)
        if i < len(sims) and sims[i] < breakpoint_threshold:
            out.append(
                Chunk(
                    text=" ".join(buf),
                    meta={"strategy": "semantic", "reason": "sim_drop", "sim_to_next": sims[i]},
                )
            )
            buf, buf_tokens = [], 0

    if buf:
        out.append(Chunk(text=" ".join(buf), meta={"strategy": "semantic", "reason": "final"}))

    return out

Hierarchiczna fragmentacja + scalanie wyciągania (LlamaIndex)

# pip install llama-index llama-index-llms-openai
from llama_index.core import Document, StorageContext, VectorStoreIndex
from llama_index.core.node_parser import HierarchicalNodeParser, get_leaf_nodes
from llama_index.core.retrievers import AutoMergingRetriever
from llama_index.core.storage.docstore import SimpleDocumentStore

def build_hierarchical_index(text: str):
    docs = [Document(text=text)]

    node_parser = HierarchicalNodeParser.from_defaults()  # domyślne rozmiary od grubej do drobnej
    nodes = node_parser.get_nodes_from_documents(docs)

    docstore = SimpleDocumentStore()
    docstore.add_documents(nodes)
    storage_context = StorageContext.from_defaults(docstore=docstore)

    leaf_nodes = get_leaf_nodes(nodes)
    base_index = VectorStoreIndex(leaf_nodes, storage_context=storage_context)

    base_retriever = base_index.as_retriever(similarity_top_k=6)
    retriever = AutoMergingRetriever(base_retriever, storage_context, verbose=True)
    return retriever

Fragmentacja oparta na elementach dla PDFów (Docling)

# pip install docling
# UWAGA: jakość parsowania PDFów zależy od środowiska (czytniki, OCR itd.).
from docling.document_converter import DocumentConverter
from docling.transforms.chunker.hierarchical_chunker import HierarchicalChunker

def chunk_pdf_docling(pdf_path: str) -> list[Chunk]:
    converter = DocumentConverter()
    doc = converter.convert(pdf_path).document  # DoclingDocument
    chunker = HierarchicalChunker()
    doc_chunks = list(chunker.chunk(doc))

    out: list[Chunk] = []
    for c in doc_chunks:
        # c.text zawiera seryalizowany zawartość fragmentu; c.meta zawiera informacje strukturalne
        out.append(Chunk(text=c.text, meta={"strategy": "docling_hierarchical", **dict(c.meta)}))
    return out

Fragmentacja świadoma kodu dla Pythona (AST)

import ast

def chunk_python_by_ast(code: str, *, filepath: str = "<memory>") -> list[Chunk]:
    tree = ast.parse(code)
    out: list[Chunk] = []

    for node in tree.body:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            start = getattr(node, "lineno", None)
            end = getattr(node, "end_lineno", None)
            if start is None or end is None:
                continue

            lines = code.splitlines()
            snippet = "\n".join(lines[start - 1 : end])

            kind = "klasa" if isinstance(node, ast.ClassDef) else "funkcja"
            name = getattr(node, "name", "<anon>")
            out.append(
                Chunk(
                    text=snippet,
                    meta={
                        "strategy": "python_ast",
                        "kind": kind,
                        "name": name,
                        "filepath": filepath,
                        "start_line": start,
                        "end_line": end,
                    },
                )
            )

    return out

Przykłady indeksowania

FAISS (lokalny) — minimalny, szybki punkt wyjścia

# pip install faiss-cpu numpy
import numpy as np
import faiss

def build_faiss_index(vectors: np.ndarray) -> faiss.Index:
    # wektory: kształt (N, d), typ float32
    d = vectors.shape[1]
    index = faiss.IndexFlatIP(d)  # iloczyn skalarny; podobieństwo kosinusowe, jeśli wektory są unormalizowane
    faiss.normalize_L2(vectors)
    index.add(vectors)
    return index

def faiss_search(index: faiss.Index, query_vec: np.ndarray, k: int = 5):
    q = query_vec.astype(np.float32).reshape(1, -1)
    faiss.normalize_L2(q)
    scores, ids = index.search(q, k)
    return ids[0].tolist(), scores[0].tolist()

Chroma (lokalny) — prosta trwałość dla prototypowania RAG

# pip install chromadb
import chromadb

def build_chroma_collection(chunks: list[Chunk], embeddings: np.ndarray, *, path: str = "./chroma_store"):
    client = chromadb.PersistentClient(path=path)
    col = client.get_or_create_collection(name="docs")

    ids = [sha1_id(c.meta.get("strategy", "chunk"), str(i), c.text[:50]) for i, c in enumerate(chunks)]
    col.upsert(
        ids=ids,
        documents=[c.text for c in chunks],
        metadatas=[c.meta for c in chunks],
        embeddings=embeddings.tolist(),
    )
    return col

Weaviate (samowystarczalny / chmurowy) — własne wektory

# pip install weaviate-client
import weaviate
from weaviate.classes.config import Configure

def weaviate_upsert_self_provided(
    chunks: list[Chunk],
    embeddings: np.ndarray,
):
    client = weaviate.connect_to_local()  # albo connect_to_weaviate_cloud(...)
    try:
        collection = client.collections.create(
            name="Chunk",
            vector_config=Configure.Vectors.self_provided(),  # dostarczasz własne wektory
        )

        with collection.batch.dynamic() as batch:
            for c, v in zip(chunks, embeddings):
                batch.add_object(
                    properties={"text": c.text, **{f"m_{k}": str(vv) for k, vv in c.meta.items()}},
                    vector=v.tolist(),
                )

        # Zapytanie przez wektor bliski
        q = embeddings[0]
        res = collection.query.near_vector(near_vector=q.tolist(), limit=5)
        for obj in res.objects:
            print(obj.properties.get("text")[:200])
    finally:
        client.close()

Niektóre źródła dokumentacji

  • Patrick Lewis et al., “Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks”, NeurIPS 2020; arXiv:2005.11401.- Vladimir Karpukhin et al., “Dense Passage Retrieval for Open-Domain Question Answering”, EMNLP 2020; arXiv:2004.04906.
  • Dokumentacja OpenAI API: Tworzenie embeddingów (/v1/embeddings) — ograniczenia tokenów (8192 na wejściu; 300k łącznie na żądanie) i parametr dimensions.
  • Przewodnik po OpenAI Batch API + strona z cenami OpenAI API (Batch oszczędza około 50% przy czasie przetwarzania 24h).
  • Dokumentacja LangChain: RecursiveCharacterTextSplitter i przewodnik po integracji splitters (rozmiar fragmentu/overlap, hierarchia separatorów rekursywnych).
  • Dokumentacja LlamaIndex: HierarchicalNodeParser i AutoMergingRetriever (węzły od grubej do drobnej; scalanie podczas wyciągania).
  • Blog Weaviate: “Strategie fragmentacji do poprawy wydajności RAG pipeline LLM” (opis fragmentacji opartej na LLM i kosztów).
  • Dokumentacja Docling: HierarchicalChunker tworzy fragmenty na podstawie struktury dokumentu i dołącza metadane nagłówków/legend.
  • Jimeno Yepes et al., “Fragmentacja raportów finansowych dla skutecznego Retrieval Augmented Generation” (arXiv:2402.05131v3, 2024). Marti A. Hearst, “TextTiling: Segmentowanie tekstu w podtematowe pasże wielu akapitów”, Computational Linguistics, 1997 (ACL Anthology: J97-1003).
  • Dokumentacja FAISS / repozytorium GitHub: koszty między czasem wyszukiwania, jakością, pamięcią; opcjonalna obsługa GPU.
  • Nandan Thakur et al., “BEIR: Heterogeniczny benchmark do oceny modeli wyszukiwania informacji w trybie zero-shot”, NeurIPS 2021; arXiv:2104.08663.
  • Dokumentacja RAGAS: “Faithfulness” metryka i powiązane metryki oceny RAG.
  • Dokumentacja NLTK: nltk.tokenize.sent_tokenize - zalecany tokenizator zdań oparty na Punkt.
  • Dokumentacja API spaCy: Sentencizer - wykrywanie granic zdaniowych na podstawie reguł bez analizy zależności.

Inne przydatne linki