Chunkingstrategier i RAG-jämförelse: Alternativ, kompromisser och exempel

Jämförelse av chunkningsstrategier i RAG

Sidinnehåll

Chunking är den * mest undervärderade * hyperparametern i Retrieval ‑ Augmenterad Generering (RAG): den bestämmer tyst och osynligt vad din LLM “ser”, hur dyrt ingångsarbete blir, och hur mycket av LLM:s kontextfönster du förbrukar per svar.

Detta artikel behandlar chunking som ett ingenjörsproblem: definiera mål, välj en strategi, mät, sedan iterera.

Om du är ny i RAG-arkitektur, börja med huvudartikeln Retrieval-Augmented Generation (RAG) Tutorial: Architecture, Implementation, and Production Guide.

färg tetris på bordet

TL;DR (Exekutiv sammanfattning)

RAG-system hämtar chunkar, inte dokument. Chunking definierar därmed enheten för hämtning, enheten för inbäddningskostnad, och enheten för bevis som du kan visa eller citera. I den ursprungliga RAG-formuleringen levererar hämtningen passager till genereringen; passagergränserna är effektivt dina chunkgränser.

En bra chunkingstrategi söker en Paretofront över: hämtningens kvalitet (återhämtning/precision av bevis), koherens (chunkar måste vara tolkbara), och kostnad (inbäddning, lagring och frågefördröjning). Det finns ingen globalt optimal chunkstorlek eller metod, och produktionsystem rutinmässigt blandar strategier (t.ex. strukturmedveten chunking för PDF:er + semantiskt medvetna delar för prosa + AST-chunking för kod).

För de flesta “dokumentation QA” och interna kunskapsbankar, är en säker standard en strukturrespekterande rekursiv splitter med modig överlapp (för att minska gränsförluster), stödd av en vektorlager med metadatafiltrering och valfri omrankning. LangChain:s RecursiveCharacterTextSplitter är en vanlig implementering av detta hierarkiska-separator-idé; överlapp existerar specifikt för att minska informationsförlust när relevant kontext skäras vid gränser.

När dokumenten har stark struktur (PDF:er med rubriker, tabeller, listor, rubriker), kan elementbaserad / strukturmedveten chunking överträffa tokenräkningens skärning medan den producerar färre chunkar. En 2024-studie på SEC-filningar fann att elementtypbaserad chunking förbättrade RAG-resultat och också minskade antalet chunkar (och därmed vektorer) ungefär med hälften jämfört med strukturagokra metoder – minskade indexkostnad och potentiellt förbättrade frågeförsening.

Om du kan erbjuda mer uppföljningsberäkning, semantisk chunking (skär vid ämnesbyten med hjälp av inbäddningssimilaritet) kan betydligt förbättra hämtningens exakthet för narrativ text och blandade ämnesidor. Äldre ämnessegmenteringsalgoritmer som TextTiling visar den allmänna principen: starka ord/vectorspråksbyten är goda gränskandidater.

För mycket långa, interna tvärvetenskapliga material (policyer, RFC:er, standarder, stora handböcker), kan hierarkisk chunking + hierarkisk hämtning/merging (förälder/barnnoder) återställa större kontinuerliga kontext vid efterfrågan. LlamaIndex:s hierarkiska nodparser producerar grov-till-fin chunkhierarkier, och AutoMergingRetriever kan slå samman bladnoder till föräldernoder vid hämtningstid när tillräckligt många relaterade barn hämtas.

Chunkingmål och avvägningar

Chunking är inte bara “del text så att den passar in i en inbäddningsmodell”. Det styr flera nedströms och driftbeteenden.

Hämtningens detaljgrad vs hämtningsskumma. Mindre chunkar ökar chansen att den exakta meningen som innehåller ett svar är hämtbar (högre återhämtning vid fast top-k). Men de producerar också fler vektorer, vilket ökar indexstorleken och ibland visar “nära matchar” som är semantiskt lika men inte faktiskt bevisande (lägre precision). Täta hämtare som DPR byggdes runt att hämta passager effektivt för QA, vilket visar att passagergränser spelar roll för slutpunkt QA-prestanda.

Kontextens koherens vs gränsförlust. Koherenta chunkar hjälper LLM att resonera korrekt och minskar hallucinationer genom att ge komplett lokal kontext (definitioner, begränsningar, förutsättningar). Överlapp minskar gränsförlust men skapar duplicerad text, vilket kan leda till redundanta hämtresultat och inflationerad promptlängd om du inte deduplicerar/merger.

Inbäddnings- och indexkostnad. Inbäddningskostnad är vanligtvis proportionell till inbäddade token, och ingångstid skalar med antalet chunkar (plus vektor DB skrivöverskott). För OpenAI-inbäddningar har begäran en per-ingång maxtokengräns (8192 token för alla inbäddningsmodeller) och en max total token som summeras över ingångar per begäran (300 000 token). För stora korpusar kan Batch API minska kostnaden med cirka 50% med en asynkron, 24-timmars omvändning – användbar för backfills och periodiska omindexeringar.

Vektorindexstorlek, RAM och försening. Fler chunkar innebär fler vektorer och potentiellt fler minne och långsammare frågor (beroende på indextyp). FAISS framställer indexdesign explicit som en uppsättning avvägningar mellan sökningstid, sökningens kvalitet och minne per indexvektor; det erbjuder också GPU-implementationer för snabb exakt och ungefärlig sökning.

Nedströms LLM-promptlängd / kontextfönsteranvändning. Hämtarens utdata blir promptbudget. En chunkingstrategi som konsistently hämtar “just rätt” kontext kan förbättra svarskvalitet och minska kostnad. Tvärtom kan överlapp och för stora chunkar inflationera promptlängd. I praktiken justerar du ofta: (chunkstorlek, överlapp, top-k, omrankning/merger) tillsammans.

Uppdaterings/ingångskostnad och deduplivering. Chunking påverkar hur dyrt det är att uppdatera data. Mindre chunkar gör delvisa uppdateringar billigare (du kan om-embed bara den ändrade sektionen) men gör också deduplivering svårare om överlappande eller nära-duplicerade chunkar sprids.

Var chunking sitter i RAG-arbetsflödet

chunking i rag flöde

Chunkingstrategier och alternativ

Här nedan är de huvudsakliga chunkfamiljer du kommer att stöta på i modern RAG. I praktiken blandar du ofta två: strukturförst chunking (respektera dokumentgränser) plus tokenbudgetenforcement (se till att chunkar passar dina inbäddnings- och promptbudgeter).

Fixstorlek chunking

Vad det är. Dela texten i lika stora block genom tecken eller token.

Varför det finns. Det är enkelt, snabbt, förutsägbart och lätt att parallellisera. Det är också den enklaste strategin för streaming-ingång där du inte har full dokumentkontext.

Var det misslyckas. Det ignorerar gränser (sätten, avsnitt, kodblock) så det kan bryta definitioner eller dela upp “fråga/svarpar” över chunkar, vilket ökar hämtfel.

Operativ profil. Lägsta ingångskomplexitet; förutsägbart antal chunkar; lättaste cachelagring. Men du behöver ofta överlapp (nedan) för att undvika gränsförlust.

Överlapp chunking

Vad det är. Alla strategier där konsekutiva chunkar delar en fix överlappningsregion (t.ex. 10–20% av chunkstorlek). Överlapp är standard i många ramverk eftersom det minskar informationstap när kontext delas.

Varför det spelar roll. Överlapp är effektivt en “mässig gräns” – det låter hämtning fånga en fakta som sträcker sig över en gräns.

Kostnader och fällor. Fler token inbäddade; fler duplicerad text i indexet; högre risk för att hämta flera nästan identiska chunkar om du inte deduplicerar vid hämtningstid (t.ex. slå samman efter källförskjutningar eller använd MMR).

Sats- och styrsbaserad chunking

Vad det är. Dela texten vid sats- eller styrsgränser, sedan packa samman sats/styrs i chunkar upp till en tokenbudget.

Varför ingenjörer gillar det. Det förbättrar koherens för naturligt språk och är robust för dokument med konventionella punkt och avstånd.

Verktyg. NLTK:s sent_tokenize() använder Punkt-satsgränsdetektering som standard, och spaCy erbjuder regelbaserade satsgränsverktyg som Sentencizer (nyttigt när du vill ha satsdelar utan ett fullständigt beroendemodell).

Misslyckningslägen. Ovanlig punkt (loggar, chatttranskript), tabeller, kod och punktlistor kan bryta satssegmenteringssuppositioner.

Glidande fönster chunking

Vad det är. Skapa chunkar med en fix fönsterstorlek och ett steg (strid). Detta är den “systematiska överlappning” versionen av chunking.

När det är bra. Tidsserietext, transkript, chattloggar, mötesanteckningar – något där relevanta fakta kan visas över lokala nabolägen och du vill ha robust återhämtning.

När det är dåligt. Det förstärker redundans och kan vara dyrt på stora skala. Det tenderar också att hämta redundanta fönster om inte deduplicerat.

Rekursiv / separatorhierarki chunking

Vad det är. Starta med stora “naturliga” separerare (t.ex. \n\n för styrs) och rekursivt dela upp i mindre enheter (sats, utrymme) endast när det behövs för att hålla under en storleksbudget. LangChain dokumenterar detta beteende explicit: en rekursiv splitter försöker behålla större enheter intakta och endast går tillbaka till mindre separerare när en enhet fortfarande överskrider chunkstorlek. Varför det är en stark standard. Det respekterar struktur utan att kräva komplex dokumentparsing. Det är en praktiskt smart lösning för Markdown, HTML-som-text och dokumentation.

Viktiga justeringsknappar. chunk_size, chunk_overlap, och length_function (tecken vs token), samt anpassade separerare för flerspråkiga kodbaser.

Semantisk (inbäddningsmedveten) chunking

Vad det är. Detektera ämnesbyten med hjälp av semantiska representationer (inbäddningar) och dela upp där likhet sjunker. Det speglar klassiska segmenteringsidéer som TextTiling, som använder förändringar i lexikala koherens för att hitta underämnesgränser.

Varför det kan överträffa storleksbaserat chunking. Du stannar upp vid godtyckliga tokenräkningar och istället justera chunkar med ämnesgränser – ofta förbättra hämtningens precision för flerämnesdokument (bloggar, designdokument, tickets, incidentrapporter).

Kostnader. Du kan behöva ytterligare inbäddningar under chunking (satsnivå eller styrsnivå inbäddningar) innan slutgiltiga chunkinbäddningar. Det kan fördubbla eller trippela inbäddningsanrop om du inte återanvänder mellanliggande inbäddningar.

Praktiskt trick. “Semantiskt medveten packning”: beräkna satsinbäddningar en gång, gruppera satsen i ämneskoherenta segment, sedan inbäddning varje slutgiltigt segment.

Hierarkisk chunking (förälder/barn)

Vad det är. Bygg en flergransskap representation: grova förälderchunkar (t.ex. avsnittsstorlek) med finare barnchunkar (t.ex. styrsstorlek). LlamaIndex:s hierarkiska nodparsing producerar “grovtillfint” hierarkier som standard (t.ex. 2048 → 512 → 128 tokenstegrader), och AutoMergingRetriever kan slå samman barnnodar till föräldrar vid hämtningstid när tillräckligt många relaterade barn hämtas.

Varför det hjälper. Det undviker att välja mellan “små chunkar för återhämtning” och “stora chunkar för koherens” genom att lagra båda, och välja vid frågetid.

Kostnader. Mer komplex ingångs- och hämtninglogik, samt potentiellt mer lagring (eftersom du lagrar flera granulariteter).

Anpassningsbar / LLM-baserad chunking

Vad det är. Använd en LLM för att bestämma chunkgränser (och eventuellt generera sammanfattningar eller kontextuella rubriker). Weaviate beskriver LLM-baserad chunking explicit som att låta LLM skapa semantiskt koherenta chunkar, istället för att förlita sig på fasta regler eller inbäddningssimilaritet.

När det är värt. Högverdiga korpusar där rättighet dominerar kostnad (juridisk, komplian, support runbooks), och där dokumenten är otydliga, heterogena och dåligt segmenterade.

Risker. Kostnad, fördröjning och oavgjärlighet. Du vill ha cachelagring, avgjärlig dekodning och regressionstester (se bedömningssnitt).

Struktur- och elementbaserad chunking (dokumenten är inte ren text)

Vad det är. Parsa dokumentet till element (rubriker, styrs, listor, tabeller, rubriker) med en dokumentförståelselager, sedan chunka med dessa element. Unstructured:s chunkningsfunktioner använder explicit metadata och dokumentelement (producerade av partitionering) för att skapa chunkar för RAG. Docling:s HierarchicalChunker skapar chunkar per detekterat dokumentelement och kopplar strukturell metadata som rubriker/rubriker.

Bevis från nylig forskning. En 2024-studie på SEC-filningar hävdar att endast styrschunking försummar dokumentstruktur och föreslår chunking efter strukturella element; det rapporterar förbättrade RAG-resultat och färre chunkar/vektorer än strukturagokra metoder.

Varför det spelar roll för multimodal. Tabeller, figurer och rubriker innehåller ofta den sanna verkligheten. “Flätning” dem till ren text kan förstöra signaler som hämtning annars skulle utnyttja.

Kodmedveten chunking (AST/struktur)

Vad det är. Chunka kod efter syntaktiska enheter (funktioner, klasser, moduler), eventuellt inklusive dokumentsträngar och kommentarer.

Varför det spelar roll. Faststorleks token-splits tenderar att skära funktioner i halva och separera dokumentsträngar från implementeringar – dåligt för kod sökning och “förklara denna funktion” RAG-användningsscenarier.

Implementeringsalternativ. För Python är den inbyggda ast-modulen ofta tillräcklig. För flerspråkiga repos är tree-sitterbaserade chunkers vanliga.

Bedömningsdimensioner och hur man jämför chunkstrategier

Chunking bör bedömas som en systemkomponent.

Hämtningens kvalitetsmått

Använd standard IR-mått för hämtningslager:

  • Recall@k / Precision@k: Innehöll de top-k de gyllene bevisen?
  • MRR / nDCG: Rankade de gyllene bevisen högt?

BEIR är ett vidare använda heterogent benchmark för IR-bedömning över uppgifter/områden, och markerar avvägningar mellan sparsamma, tät, sen-interaktion och omrankningsmetoder.

Chunking påverkar dessa mått eftersom det definierar vad som räknas som “en relevant hämtad artikel”.

Slutpunkt RAG-svars kvalitetsmått

Om du bygger QA eller assistenter, är hämtningens mått nödvändiga men inte tillräckliga. Du behöver också:

  • Kontext återhämtning / precision: om hämtade kontexter innehåller relevant bevis och undviker brus.
  • Tillitvärdhet: om det genererade svaret stöds av hämtad kontext.

RAGAS ger konkreta definitioner och implementeringar för “tillitvärdhet” och andra RAG-orienterade mått.

Systemkostnad och prestandadimensioner

Chunking ändrar dessa levers:

Fördröjning (p50/p95). Frågeförsening ökar vanligtvis med fler vektorer och fler efterbearbetningar. Din vektorindex spelar också roll: FAISS-indextyper avväger sökningstid, kvalitet, minne och träning/addingstid.[^faiss]

Inbäddningskostnad och throughputs. OpenAI-inbäddningar faktureras per token; embeddings-API har explicita per-ingångs och per-begärandegräns.[^openai_embed_create] För offline-ingång, minskar Batch-API kostnaden och erbjuder högre kvot i utbyte för icke-realtid omvändning.[^openai_batch]

Indexstorlek och minne. Runt, att lagra N float32-vektorer av dimension d kostar ~4 * N * d bytes endast för de råa vektorerna (plus metadata + indexöverhäng). Chunking påverkar N. Inbäddningsdimensionalitet påverkar d, och OpenAI:s embeddings-API tillåter kontroll av utdata-dimensionen via dimensions-parametern.[^openai_embed_create]

LLM-promptbudget. Större chunkar och överlapp inflationerar prompttoken. Detta kan öka försening och kostnad, och öka “förlorad i mitten” typ av misslyckanden där modeller betonar mindre på vissa kontexter. I praktiken justerar du ofta:

  1. hämta små chunkar,
  2. slå samman/deduplicera,
  3. eventuellt sammanfatta,
  4. skicka en kompakt bevisuppsättning till LLM.

Uppdaterings/ingångskostnad. Mindre chunkar gör delvis om-embed men ökar bokföring. För streaming-ingång, föredra deterministisk, inkrementell chunking (fast eller glidande fönster) och koppla stabila ID:er (dokument_id, offsetintervall, hash).

Experimentell design: en pragmatisk benchmarkloop

En återproducerbar chunkingbenchmark har typiskt:

  • En fast korpusnapshot + fast uppsättning frågor med gyllene bevis (eller minst förväntade svarsintervall).
  • En fast inbäddningsmodell och vektorindexkonfiguration.
  • En “endast hämtning” bedömning (recall@k, nDCG) plus “RAG”-bedömning (tillitvärdhet, svarsrelevans).
  • Kostnadstelemetri: #chunkar, inbäddade token, $/månad lagring, p95 frågeförsening, prompttoken.

Unstructured SEC-filningarpaper är ett bra exempel på att bedöma chunkstrategier med både hämtningssorterade mått och QA-precisionmått.

Praktiska riktlinjer, beslutsdiagram och rekommenderade standarder

Rekommenderade standarder som fungerar förvånansvärt bra

Om du behöver en robust “dag 1”-strategi för allmän dokumentation QA:

  1. Parse lätt: bevara rubriker och grundläggande metadata (källa, avsnittstitel, URL/sökväg, tidsstämpel).
  2. Chunka med en rekursiv separator splitter (avsnitt → sats → ord), med modig överlapp.
  3. Inbäddning med en stark generell inbäddningsmodell.
  4. Index med metadata (dokument_id, avsnitt, ACLs) och deduplicera under hämtning.
  5. Lägg till omrankning eller hierarkisk sammanslagning endast om din bedömning visar ett gap.

Detta överensstämmer med hur vanliga RAG-ramverk beskriver chunköverlapp och strukturrespekterande delning.

Vilken chunkingmetod att använda - Beslutsdiagram

Vilken chunkingmetod att använda - Diagram

Användningssnitt Rekommenderad chunkingstandard Viktiga parametrar att justera Vanlig misslyckningsmod Uppgraderingsväg
Kortform QA över dokument (FAQ, intern wiki) Rekursiv/separator chunking + överlapp chunk_size, överlapp, top-k Saknar tvärsatsbevis vid gräns Lägg till semantisk chunking eller omrankare
Långform QA (policyer, standarder, handböcker) Hierarkisk chunking + sammanfogningshämtare förälder/barnstorlekar, sammanfogningsgräns Hämtar små fragment; LLM saknar full kontext Auto-sammanfogning/hierarkisk hämtning
Sammanfattning (per dokument / per avsnitt) Strukturmedvetna chunkar (avsnitt) avsnittsidentifiering, max token Sammanfattningar missar tvärsnittsankar Hierarkisk sammanfattning + avsnittsgraf
Kod sökning & “förklara denna funktion” AST/funktionsnivå chunkar inkludera dokumentsträngar/kommentarer, max token Funktionssplit; förlorar signatur/bruk Repo-medveten hierarki (modul→klass→funktion)
Multimodal PDF:er (tabeller/figurer) Elementbaserad chunking (rubrik/tabell/rubrikmedveten) tabellserialisering, rubriksammanfogning Tabellinnehåll förlorat eller förstörd Använd Docling/Unstructured + strukturerade serialisatorer
Streaming-ingång (loggar, chattar, tickets) Glidande fönster eller faststorleks token fönster, steg, dedup Överhämtning av redundant fönster Lägg till semantisk gränsdetektering på batchar

chunking - Kvalitativ prestandajämförelse

Behandla detta som “förväntad riktning för förändring” (validera med dina egna data).

Strategi Hämtningens exakthetspotential Koherens i hämtad kontext Ingångskomplexitet Vektortal / indexstorlek Inbäddningskostnad Frågeförseningseffekt Bäst för
Fixstorlek (ingen överlapp) Medel Låg Låg Medel Låg Medel snabba prototyper, homogena text
Fixstorlek + överlapp Medel–Hög Medel Låg Hög Medel–Hög Medel–Hög QA där gränsförlust skadar
Sats/paragrafpackning Hög (prosa) Hög Medel Medel Medel Medel dokument, artiklar, ren prosa
Glidande fönster Hög återhämtning Medel Medel Mycket hög Mycket hög Hög transkript, loggar, chatt
Rekursiv/separator Hög Hög Medel Medel Medel Medel “standard” dokument RAG
Semantisk chunking Hög–Mycket hög Hög Hög Medel Hög Medel flerämnesidor, narrativ text
Hierarkisk (förälder/barn) Mycket hög Mycket hög Hög Hög Hög Medel långa handböcker / standarder
LLM-baserad/anpassningsbar Mycket hög Mycket hög Mycket hög Medel Mycket hög Medel–Hög högintensiva korpusar
Element-/struktur-baserad Hög–Mycket hög Hög Hög Låg–Medel Medel Medel PDF:er, rapporter, tabeller, blandade layouter
Kodmedveten (AST) Hög (kod) Hög Medel Medel Medel Medel kod sökning, repoassistent

DevOps och hårdvaranoteringar (ofta övergående)

Chunkval har påverkan på hur mycket infrastruktur du behöver:

  • Mindre chunkar → fler vektorer → större index och mer RAM/disk. För självvärdbad FAISS kan det tvinga shardning eller diskbaserade index.
  • Om du inbäddar lokalt, blir inbäddningsdurchput ett GPU-schemaläggningsproblem; om du inbäddar via API, blir tokenvolym ett FinOps-problem (Batch API är din vän för backfills).
  • Vissa motorer (FAISS) ger GPU-accelerad sökning; detta kan flytta kostnad från RAM-begränsade CPU till GPU-minne och PCIe-throughput.
  • Strukturmedveten parsing (PDF-layout, OCR, tabelluttag) är ofta CPU-begränsad och kan överväldiga inbäddningskostnaden för skannade dokument; budgetera separat.

Chunkning - Python-implementationsexempel

Alla exempel är utformade för att vara läsbara och körbara. Om du behöver en API-nyckel eller en körande databas, är det tydligt ur koden.

Delade verktyg: tokenräkning och stabila chunk-ID:or

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()

# Använd någon tokenizer som ungefär matchar din LLM/embedding tokenisering.
TOKENIZER = AutoTokenizer.from_pretrained("bert-base-uncased")

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

Fixstorlek tokenchunkning

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

Fixstorlek med överskridande + rullande fönster

def chunk_sliding_window(
    text: str,
    *,
    window_tokens: int = 512,
    stride_tokens: int = 384,  # mindre steg = mer överskridande
) -> list[Chunk]:
    assert 1 <= stride_tokens <= window_tokens, "steg måste vara inom (0, fönster]"
    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

Satsbaserad chunkning (NLTK) med tokenbudgetpackning

# 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)},
            )
        )
        # Överskrid med att behålla de sista N-satserna
        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)
        # Om en enskild sats överskrider budgeten, återgå till fixstorlek chunkning på det spannet
        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

Satsbaserad chunkning (spaCy) när du vill ha regelbaserad eller modellbaserad SBD

# 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]:
    # För lättviktig regelbaserad delning (ingen beroende analys), använd sentencizer.
    nlp = spacy.blank("en")
    nlp.add_pipe("sentencizer")  # regelbaserad satsgränsdetektering
    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)

Rekursiv separatorchunkning (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,  # tokenmedveten budgetplanering
        separators=["\n\n", "\n", ". ", " ", ""],  # anpassa efter din innehållstyp (t.ex. 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
    ]

Semantisk chunkning med embeddingslikhet (OpenAI embeddings)

Den här metoden beräknar embeddings för kandidatenheter (satser/paragrafer), och hittar sedan semantiska “brytpunkter”.

# 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:
    # OBS: för stora batchar, respektera begärans tokengränser och batchstorleksgränser.
    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) Börja från satskandidater
    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) Embed varje sats
    embs = embed_texts_openai(sents)

    # 3) Beräkna likhetsfall
    sims = [cosine_sim(embs[i], embs[i + 1]) for i in range(len(sents) - 1)]

    # 4) Skapa segment vid brytpunkter
    out: list[Chunk] = []
    buf: list[str] = []
    buf_tokens = 0

    for i, s in enumerate(sents):
        # Lägg till sats
        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

        # Bestäm brytpunkt efter sats i (baserat på likhet med nästa)
        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

Hierarkisk chunkning + sammanfogning av hämtning (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()  # standardvärden är grov-till-fin storlekar
    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

Elementbaserad chunkning för PDF:er (Docling)

# pip install docling
# OBS: kvaliteten på PDF-parsning beror på din miljö (tecken, OCR, osv.).
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 innehåller serialiserad chunkinnehåll; c.meta bär strukturinformation
        out.append(Chunk(text=c.text, meta={"strategy": "docling_hierarchical", **dict(c.meta)}))
    return out

Kodmedveten chunkning för Python (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 = "class" if isinstance(node, ast.ClassDef) else "function"
            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

Indexeringsexempel

FAISS (lokalt) — minimal, snabb baslinje

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

def build_faiss_index(vectors: np.ndarray) -> faiss.Index:
    # vectors: shape (N, d), dtype float32
    d = vectors.shape[1]
    index = faiss.IndexFlatIP(d)  # inre produkt; cos sim om vektorerna är normaliserade
    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 (lokalt) — enkel persistens för RAG-prototyper

# 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 (självvärd / moln) — ta med egna vektorer

# 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()  # eller connect_to_weaviate_cloud(...)
    try:
        collection = client.collections.create(
            name="Chunk",
            vector_config=Configure.Vectors.self_provided(),  # du tillhandahåller vektorer
        )

        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(),
                )

        # Fråga med närvector
        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()

Vissa Källa Dokument

  • 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.
  • OpenAI API Referens: Skapa embeddings (/v1/embeddings) — tokengränser (8192 per indata; 300k totalt per förfrågan) och dimensions-parameter.
  • OpenAI Batch API-guide + OpenAI API-prisinformationssida (Batch sparar ~50% med 24h omkörning).
  • LangChain dokumentation: RecursiveCharacterTextSplitter och splitters integrationsguide (chunkstorlek/överskridande, rekursiv separatorhierarki).
  • LlamaIndex dokumentation: HierarchicalNodeParser och AutoMergingRetriever (grov-till-fin noder; hämtningstidssammanfogning).
  • Weaviate blogg: “Chunking Strategies to Improve LLM RAG Pipeline Performance” (LLM-baserad chunkningbeskrivning och avvägningar).
  • Docling dokumentation: HierarchicalChunker skapar chunkar från dokumentelementstruktur och bifogar rubriker/bildtextmetadata.
  • Jimeno Yepes et al., “Financial Report Chunking for Effective Retrieval Augmented Generation” (arXiv:2402.05131v3, 2024). Marti A. Hearst, “TextTiling: Segmenting Text into Multi-Paragraph Subtopic Passages”, Computational Linguistics, 1997 (ACL Anthology: J97-1003).
  • FAISS dokumentation / GitHub-lagringsplats: avvägningar mellan sökningstid, kvalitet, minne; valfri GPU-stöd.
  • Nandan Thakur et al., “BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models”, NeurIPS 2021; arXiv:2104.08663.
  • RAGAS dokumentation: “Faithfulness”-mått och relaterade RAG-utvärderingsmått.
  • NLTK dokumentation: nltk.tokenize.sent_tokenize - rekommenderad satsdelare baserad på Punkt.
  • spaCy API-dokumentation: Sentencizer - regelbaserad satsgränsdetektering utan beroendeanalys.

Andra Nytta Länkar