De meeste RAG-prototypes zien er onder de motorkap hetzelfde uit. Iemand voert een PDF in een pipeline in, snijdt de tekst in nette stukjes van 512 tokens, dumpt ze in een vectordatabase en beschouwt het werk als gedaan. Voor een snelle demo kan dit indrukwekkend lijken. In productie stort het in.

Een vast chunk-formaat geeft niets om wat het doorsnijdt. Het zal een juridisch contract midden in een vrijwaringsclausule splitsen. Het zal vijf ongerelateerde API-endpoints in hetzelfde contextvenster proppen en het model verdrinken in ruis. Het dwingt je om meer fragmenten op te halen dan nodig is, wat de latentie verhoogt en tokens verspilt. Het resultaat is halve antwoorden, hallucinaties en gefrustreerde gebruikers.

We hebben onze retrieval-laag tot op het bot afgebroken en opnieuw opgebouwd. Het resultaat was een systeem dat een recall van 95 procent behaalde, terwijl de latentie met 40 procent werd verlaagd. Hier lees je precies hoe we dat hebben gedaan.

Waarom vaste chunks falen in productie

De standaardinstelling van 512 tokens is geen bewuste ontwerpkeuze. Het is een bijproduct van de contextvensters van vroege embedding-modellen en nette standaardinstellingen van bibliotheken. Het is gemakkelijk te implementeren, maar rampzalig om op te vertrouwen.

Documenten zijn niet uniform. Een juridische clausule kan zevenhonderd tokens lang zijn zonder een duidelijke breuk. Snijd je het bij vijfhonderdtwaalf, dan creëer je twee losstaande fragmenten. Wanneer een advocaat of compliance officer vraagt naar aansprakelijkheidslimieten, geeft het systeem slechts de helft van de verplichting terug. Het taalmodel hallucineert de ontbrekende helft, of erger nog, ontkent dat de limiet bestaat.

API-documentatie lijdt aan het tegenovergestelde probleem. Een chunk van vijfhonderd tokens kan een hele module opslokken: authenticatieheaders, foutcodes, rate limits en webhook-schema's. Wanneer een ontwikkelaar vraagt hoe hij AUTH_4027 moet afhandelen, presenteert de retriever een mix van ongerelateerde functies. Het model heeft geen andere keuze dan ze te middelen tot een generieke brij.

Slechte chunking verhoogt ook de latentie. Zwakke fragmenten betekenen dat je een grotere top-k nodig hebt om een onderwerp te dekken. Meer chunks betekenen langere prompts. Langere prompts betekenen tragere generatie en hogere kosten. De gebruikerservaring gaat ten onder aan duizend kleine sneetjes.

Stem de chunk af op het document

We stopten met het tellen van tokens en begonnen de inhoud te lezen. De juiste chunking-strategie hangt af van de structuur van de bron.

Juridische documenten hebben recursieve character chunking nodig met clausule-bewuste grenzen. De splitter respecteert de hiërarchie: hij zoekt eerst naar sectiekopjes, dan naar genummerde paragrafen en vervolgens naar natuurlijke zinsgrenzen. Hij breekt nooit een subclausule af of splitst een verplichtende zin over verschillende chunks. Wanneer je een passage over vrijwaring ophaalt, krijg je de volledige clausule, de limiet en de uitzonderingen.

API-documentatie vereist structuur-bewuste chunking. We parsen op basis van functiedefinities, niet op basis van een tokenbudget. Elke chunk bevat een volledige functiesignatuur, de beschrijvingen van de parameters en de direct aangrenzende foutafhandeling-notities. Als een ontwikkelaar zoekt naar een specifieke methode, ontvangt hij het volledige contract en niet een fragment dat vastzit in een willekeurige splitsing.

Supporttickets zijn luidruchtig en niet-lineair. Een thread kan beginnen met een bugrapport, een workaround introduceren en eindigen met een interne escalatienote. Semantic chunking detecteert onderwerpswisselingen door de embedding-gelijkenis tussen zinnen te meten. We staan alleen breuken toe bij natuurlijke thematische grenzen, zodat een gesprek over inlogproblemen gescheiden blijft van een vervolg over facturatiecycli.

Wikis waren het moeilijkst. Ze zijn omvangrijk, onderling verbonden en losjes georganiseerd. We gebruikten agentic chunking, waarbij een lichtgewicht LLM een pagina leest en de breuken bepaalt op basis van thematische samenhang. Dit kost iets meer bij het inladen (ingestion), maar de resulterende chunks zijn op zichzelf staand en klaar voor retrieval. Een pagina over best practices voor deployment wordt opgesplitst in logische eenheden: pre-flight checks, rollback-procedures en monitoring-setup, in plaats van willekeurige tekstblokken.

Hybride retrieval: trefwoorden en vectoren samen

Dense vector search begrijpt betekenis. Het is echter slecht in het vinden van exacte strings. Als een gebruiker zoekt naar een specifieke foutcode zoals AUTH_4027 of een klantnaam zoals "Stark Industries", kunnen vector embeddings het doel missen omdat ze optimaliseren voor conceptuele nabijheid en niet voor nauwkeurigheid op karakterniveau.

Pure trefwoordzoekopdrachten via BM25 hebben het omgekeerde gebrek. Het zal AUTH_4027 perfect vinden, maar het mist de conceptuele brug tussen "authorization failure" en "login denied".

We run both in parallel. BM25 and vector search operate independently over the same corpus. Their result lists are merged using Reciprocal Rank Fusion, which reorders candidates by balancing their positional ranks. You do not need calibrated weights. You simply get the precision of exact match and the intuition of semantic search in a single ranked list.

Then we add a cross-encoder reranker. This is a separate model that scores each passage against the original query, producing a relevance signal far finer than either retriever alone. It adds about 50 milliseconds of latency. It increases recall by 15 percent. If you care about answer quality, that trade is non-negotiable.

Query Expansion: Fix the Search Before It Starts

Bad queries are the dirty secret of every retrieval system. Users do not write like your embedding space. They type "it broke." They paste truncated stack traces. They use internal jargon your index has never seen.

We transform the query before it ever touches the index. First, we expand a single query into three to five diverse search terms. If the original is "payment failed," we also search for "transaction error," "billing declined," and "charge unsuccessful