
Dit artikel beschrijft hoe we nul-hallucinatie Q&A in onze AI-reader hebben geïmplementeerd: antwoorden zijn strikt gebaseerd op de brontekst van het geopende boek, en belangrijke uitspraken zijn met één klik te traceren naar de exacte passage. Als je AI-lezen, document-Q&A of RAG-achtige apps bouwt, hopen we dat drie iteraties aan lessen en de uiteindelijke architectuur nuttig zijn.
I. Evolutie in drie fasen
Nul-hallucinatie Q&A was niet vanaf dag één perfect ontworpen. Het evolueerde onder spanning tussen kosten, latentie en nauwkeurigheid. Hieronder een chronologisch overzicht van drie fasen—nuttige context om te begrijpen waarom de huidige architectuur er zo uitziet.
Fase 1: de volledige boektekst in de context dumpen (eenvoudigst—en als eerste problematisch)
Aanpak: Wanneer een gebruiker een boek opent en een vraag stelt, plaats je alle geëxtraheerde hoofdtekst in de system prompt of het user-bericht en laat je het chatmodel antwoorden. Overschrijdt het boek ongeveer 400.000 tekens, dan volgt harde truncatie—alleen het begin blijft behouden; latere hoofdstukken zijn onzichtbaar voor het model.
Voordelen:
- Zeer lage implementatiekosten; vrijwel geen preprocessing;
- Werkt redelijk op korte boeken en eenvoudige documenten—het model heeft het boek echt «gezien»;
- Eenvoudige UX: vragen en antwoord krijgen, geen «even wachten terwijl we analyseren»-status.
Nadelen (snel onacceptabel):
- Trage reacties: Bij elke vraag gaat een enorme payload mee; time-to-first-token en totale latentie groeien met de boeklengte;
- Hoge tokenkosten: Je betaalt bij elke vraag opnieuw voor de volledige boekinput;
- Lange boeken vervormen sterk: Na 400.000 tekens bestaan het tweede deel, bijlagen en conclusies feitelijk niet meer—en de UI geeft vaak niet duidelijk aan dat er is afgekapt;
- Nul retrieval-granulariteit: Het model moet «naar een speld in een hooiberg zoeken» over honderdduizenden tekens—gemakkelijk details missen en makkelijker plausibel klinkende samenvattingen zonder basis produceren—precies wat leesapps moeten vermijden.
Fase 1 is prima voor een MVP, niet voor een productwaardige oplossing.
Fase 2: een lichter LLM gebruiken om sleutelzinnen te extraheren (context comprimeren—maar te agressief)
Aanpak: Vóór Q&A (of bij eerste openen) draait een goedkoper model over de hoofdtekst: splitsen per spine-hoofdstuk (of het hele boek in stukken), sleutelzinnen extraheren, positietags zoals [fBestand-start-einde] behouden, en de fragmenten samenvoegen tot kortere context voor latere Q&A.
Typische pipeline: Extract → Cache → Chat. Eén keer extraheren (offline of on demand), een «sleutelzinnenbundel» opslaan, hergebruiken bij elke vraag—dezelfde gedachte als veel document-Q&A-prototypes die eerst comprimeren en dan antwoorden.
Voordelen:
- Elke vraag stuurt veel minder tekst; tokenverbruik per request daalt t.o.v. fase 1;
- Preprocessing kan worden gecached; geen her-extractie per vraag voor hetzelfde boek;
- Positietags leggen de basis voor bronverwijzingen.
Nadelen (blijft falen bij lange boeken):
- Zwaar detailverlies: «Sleutelzinnen» worden door het model gekozen; beperkingen, tegenargumenten en argumentatieketens vallen vaak weg—antwoorden worden «correct maar eenzijdig»;
- Context nog groot bij lange boeken: Zelfs sleutelzinnenbundels voor grote werken zijn aanzienlijk—latentie en kosten zijn verlicht, niet opgelost;
- Dubbele LLM-fout: Extractie kan missen; Q&A kan fragmenten verkeerd lezen—fouten stapelen;
- Statische context: Of de gebruiker naar één hoofdstuk of de hele boekstructuur vraagt, het model krijgt altijd dezelfde vooraf geëxtraheerde blob—geen dynamische vernauwing per vraag.
Les: het probleem is niet «of we comprimeren», maar of compressie on demand is en of we terug kunnen naar brontekst.
Fase 3: segmentindex + Tool-retrieval on demand + brontekst terug (huidig)
Aanpak: Geïnspireerd door PageIndex. T.o.v. fase 2 drie kernverschuivingen:
- Preprocessing levert een gestructureerde index (inhoudsopgave-samenvattingen + exacte character spans), geen fragmenten die direct als Q&A-context dienen;
- Bij elke vraag gebruikt het model Tool Calling om on demand te retrieven, en haalt brontekst met positietags op om te antwoorden;
- System prompt + frontend dwingen het citatieformaat af en ondersteunen klik-naar-springen met markering in de reader.
Vergelijking drie fasen:
| Dimensie | Fase 1 (volledige dump) | Fase 2 (sleutelzinnen) | Fase 3 (huidig) |
|---|---|---|---|
| Context per vraag | Heel boek (of afgekapt eerste deel) | Vooraf geëxtraheerde sleutelzinnen | Alleen bronfragmenten relevant voor de vraag |
| Nauwkeurigheid lange boeken | Ineenstorting na ~400k tekens | Hangt af van extractie; verliest detail | Opzoeken via TOC/span; geen harde truncatie van heel boek |
| Reactiesnelheid | Traag | Iets beter; lange boeken nog traag | Retrieval + korte context—merkbaar sneller |
| Tokenkosten | Zeer hoog | Middelhoog | Gespreid preprocess + betalen naar behoefte |
| Traceerbaarheid | Zwak (moeilijk te citeren) | Tags bestaan maar inhoud is tweemaal gefilterd | Voetnoten verwijzen naar echte bronspans |
| Engineeringcomplexiteit | Laag | Middel | Hoog |
Waarom we stopten bij fase 3: Voor lezen is nul hallucinatie niet «laat het model zoveel mogelijk tekst zien», maar «haal vóór het antwoorden bronbewijs op voor de vraag». Fase 1–2 vochten contextgrootte; fase 3 splitst de pipeline in index (preprocess) → retrieve (Tool) → bewijs (bron) → antwoord (beperkte generatie)—nauwkeurigheid, kosten en traceerbaarheid in balans.
Hieronder details van fase 3.
II. Probleemstelling: bij boek-Q&A is hallucinatie erger dan in generieke chat
Gebruikers vergeven af en toe fouten in een algemene chatbot. Bij boek-Q&A is de prijs hoger:
- Gebruikers vragen wat dit boek zegt—niet wat in het parametrische geheugen van het model zit;
- Een plausibel klinkende «mening uit het boek» kan notities, citaten en herdelingen misleiden;
- Zonder bronnen kan de gebruiker niet verifiëren—vertrouwen is moeilijk op te bouwen.
«Nul hallucinatie» wordt daarom drie afdwingbare regels:
- Boekvragen moeten eerst het boek raadplegen: Alles wat plausibel over het geopende boek gaat, moet via retrieval (Tool) vóór het antwoord;
- Antwoorden moeten traceerbaar zijn: Belangrijke conclusies dragen positietags die de UI kan parsen en naar kan springen;
- Zeg het wanneer je het niet vindt: Staat het niet in het boek, zeg dat—verkleed geen algemene kennis als «wat het boek zegt».
Het vervolg volgt de fase 3-datastroom en hoe deze regels worden geïmplementeerd.
III. Architectuur: Preprocess → Tool-retrieval → Beperkte generatie → Klikbare bronverwijzingen
Kernidee: laat het model niet «uit het geheugen antwoorden»—laat het «bewijs verzamelen, dan antwoorden, en bronnen markeren».
IV. Preprocessing: het hele boek omzetten in een doorzoekbare segmentindex
Als elke vraag nog fase 1 volledige boekcontext gebruikte, exploderen tokenbudgetten bij lange boeken en is retrieval te grof. Fase 3: bij de eerste AI-chat voor een boek draait op de achtergrond een segment-samenvattingstaak—splitsen op TOC of tekstlengte in Segments, elk samenvatten, persistent in lokale IndexedDB.
Elk Segment bevat samenvatting plus fysieke positie in de hoofdtekst:
| Veld | Betekenis |
|---|---|
startFileIndex / endFileIndex | Spine-bestandsindex (PDF: één bestand per pagina) |
startOffset / endOffset | Teken begin/einde |
sequence | Lineaire leesvolgorde |
title | TOC-titel |
Splitsen balanceert precisie en kosten: is de hoofdtekst van een TOC-node onder ~20KB, alleen die node samenvatten; sibling-nodes kunnen in batches (15–20KB) vóór LLM-aanroepen; ongestructureerde lange blokken splitsen in ~30–40k tekens.
De samenvattingssystem prompt vereist behoud van inline positietags ([fNummer-Nummer-Nummer]) zodat door Tool opgehaalde bron overeenkomt met spine-offsets. Kernbeperking:
Als samenvattingsinhoud betrekking heeft op een passage, behoud de positietag aan het einde [fNummer-Nummer-Nummer] (bijv. [f1-90-109]).
Tags zijn atomair—wijzig, voeg samen of laat geen enkel teken of cijfer weg.
Na preprocessing hangt Q&A af van een gestructureerde segmentindex, niet van hele-boek-context—de engineeringvoorwaarde voor nul hallucinatie bij lange boeken.
V. Positietagsysteem: «waar» in tekst coderen
Nul hallucinatie vereist inhoud uit de bron én machine-parseerbare, in de UI springbare herkomst. We gebruiken inline tags:
[f{fileIndex}-{startChar}-{endChar}]
Voorbeeld: [f5-123-165] = spine-bestand 5 (0-based), tekens 123–165.
5.1 Hoe tags in de hoofdtekst worden geschreven
De extractielaag voegt aan segmenteinden [f{fileIndex}-{start}-{end}] toe:
const position = `[f${fileIndex}-${absOffset}-${absOffset + segment.length}]`;
fileLines.push(segment.text.trim() + position);
Of het nu preprocess-samenvattingen of Tool-fragmenten zijn: posities sluiten aan op spine-tekenoffsets—geen door het model geschatte paginanummers.
5.2 Beperkingen op modeloutput
De system prompt bevat Position Citation Rules—vijf kernpunten:
- Standaardformaat: Moet
[f_fileIndex-startChar-endChar]gebruiken; alle drie numerieke delen verplicht; - Alleen kopiëren uit huidige bronnen: Voetnoten moeten letterlijk uit system/user-berichten of Tool-returns van deze beurt komen;
- Geen verzinsels: Bereken, bewerk of verzin geen posities;
- Liever weglaten: Geen geldige tag in context—antwoord normaal—geen positietags outputten;
- Inline bij uitspraken: Tags volgen de relevante zin; geen citatielijsten aan het einde.
De UI filtert ook incidentele tweedelige ongeldige tags (bijv. [f1-293]) vóór render.

VI. Tool Calling: eerst retrieven, dan antwoorden
Wanneer chat aan een boek is gekoppeld (resourceId aanwezig, chatType === 'chat'), registreren we vóór elke generatie twee Tools met executors—standaard OpenAI-achtige function calling loop.
6.1 get_related_segment_summaries — gerichte segmentlookup
Voor: concepten, personages, plot, hoofdstukdetails—duidelijke retrieval-intentie.
Flow:
- Model herschrijft gebruikersformulering naar termen die waarschijnlijk in het boek voorkomen («Optimize Search Queries» in system prompt);
- Tool aanroepen met
question; - Alle segment-samenvattingen batchen op tokenbudget (~30k tokens per batch, max 5 batches);
- Per batch: apart LLM-verzoek kiest relevante segment-ID's (max 5) uit
{ id, title, summary }, JSON zoals{"Thinking":"...","answer":["1","3"]}; - Voor geselecteerde segmenten getagde brontekst uit spine ophalen—geen samenvattingen—als Tool-resultaat.
Kernontwerp: Tool retourneert bron, geen samenvattingen. Het model antwoordt van echte alinea's met inline [f…], vermijdt «samenvatting → hersamenvatting»-drift.
6.2 get_full_book_segment_summaries — heel-boek-overzicht
Voor: «vat het boek samen», «recenseer dit boek», «algemene structuur/thema's»—globaal beeld.
Alle segment-summary-velden in leesvolgorde samenvoegen—voorkomt missen van sleutelhoofdstukken via alleen per-chunk relevantie.
6.3 System prompt: Book first, tools first
Met gekoppeld boek geldt Core Principles for Reading Assistant:
1. Book First, Tool First
- Elke vraag die mogelijk over het boek gaat, moet eerst tools aanroepen;
- Antwoorden moeten vooral op retrieval steunen—nooit «boekinhoud» verzinnen zonder retrieval.
2. General Knowledge as Fallback Only
- Alleen voor: informele chat / gebruiker slaat boek expliciet over / tools geven niets;
- Staat het niet in het boek, zeg «niet vermeld in dit boek» vóór algemene kennis.
3. Direct Style
- Ga direct ter zake—vermijd «op basis van de aangeleverde materialen…» en vergelijkbare opvulling.
Generatie draait de tool-loop: tool_calls → uitvoeren → role: tool toevoegen → doorgaan tot definitieve tekst. Met tools aan staat het thinking-kanaal uit om protocolconflicten te vermijden.
VII. Frontend-traceerbaarheid: van voetnoot naar markering
Modeloutput [f5-123-165] wordt niet rauw getoond; de renderlaag maakt er klikbare citaten van.
7.1 Voetnoten renderen
Tags normaliseren naar Markdown-links zoals [1]([f5-123-165]), renderen als genummerde voetnoten; deduplicatie van dezelfde positie tegen UI-rommel.
7.2 Klikinteractie
- Eerste klik: Parse
[f…]→ fileIndex + offsets → spine-tekst extraheren → preview (optioneel TOC-titel); - Zelfde voetnoot opnieuw: Preview sluiten;
- Spring bevestigen: Leesweergave openen, tekenbereik markeren.
Van gekopieerde modeltag tot zichtbare bron voor de gebruiker: de keten gaat nooit door een tweede LLM-aanroep—deterministisch en reproduceerbaar.
VIII. Randgevallen en eerlijke degradatie
Nul hallucinatie ≠ «altijd een antwoord»—het betekent geen bewijs, geen verzinsels:
| Scenario | Gedrag |
|---|---|
| Segment-samenvattingen nog niet klaar | Eerst volledige tekst extraheren en samenvatten |
| Tool vindt niets | Retourneer (No relevant segment excerpts found…); model moet zeggen dat het niet in het boek staat |
| Ongeldige tweedelige tags van model | Frontend filtert; geen kapotte voetnoten |
| Informele chat | System prompt staat algemene kennis buiten het boek toe |
| Chat exporteren | Voetnoten kunnen reader-deeplinks worden voor delen/archiveren |

IX. Ontwerpafweging: waarom geen «vector RAG»?
Peers die document-Q&A bouwen vragen vaak: als je retrieval-augmented generation doet, waarom geen Embedding + vector DB Top-K?
We doen RAG—retrieven vóór genereren. Het verschil: «RAG» in communitytaal impliceert vaak vector-gelijkenis; onze fase 3 is segmentindex + Tool on-demand bronpull—bewust geen vectorlaag. Hieronder architecturale redenen, geen ontkenning van de waarde van vector RAG.
Scope: niet «geen retrieval», maar «geen vector-retrieval»
- Brede RAG: ophalen → genereren → dat doen we;
- Vector RAG: recall via embedding-gelijkenis → niet in deze versie.
Preprocessing bouwt een segment-samenvattingsindex; het model kiest segmenten via Tools en krijgt brontekst. Retrieval bestaat zonder apart embedding-model en vectorindex-onderhoud.
Reden 1: aangepaste LLM-providers—houd het integratieoppervlak klein
Gebruikers kunnen eigen API-keys, custom base URL's of lokale Ollama koppelen—chatmodel is hun keuze; kosten en datapad blijven onder controle.
Typische vector RAG verbreedt integratie:
- Naast chatmodel meestal een embeddingmodel (andere naam, soms ander endpoint);
- Lokale Ollama heeft apart embeddingmodel plus dimensie/API-compatibiliteit;
- Meer faalmodi: chat werkt maar lege retrieval—embedding, index of dimensiemismatch; moeilijker te debuggen dan één provider end-to-end.
Hier delen segmentkeuze en antwoorden één providerconfig—geen «chat op A, index op B». Voor plug-in LLM-apps weegt dat vaak zwaarder dan enkele recallpunten.

Reden 2: embeddings binden aan de index—providerwissels zijn duur
In vector RAG zijn vectoren geen universeel tussenformaat—het zijn coördinaten onder één embeddingmodel. Index met A, query met B: gelijkenis is meestal niet vergelijkbaar—vaak volledige her-embedding, en dimensies (768 / 1024 / 1536 …) vergrendelen het opslagschema.
Fase 3 persist gestructureerde samenvattingen + character spans, geen vectoren; chatmodel wisselen bouwt de index niet opnieuw; bewijsketen (bronposities) blijft gelijk—in lijn met «probeer anytime verschillende LLM's».
Reden 3: gestructureerde routing is vaak genoeg voor TOC-zware lange docs
E-boeken en PDF's hebben meestal hoofdstukstructuur; preprocessing levert segmenttitels + samenvattingen. Voor «wat zegt hoofdstuk X» of «hoe definieert het boek Y» werkt segmentkeuze uit de catalogus en dan bron ophalen stabiel in de praktijk; Tool retourneert bron met [f…], dus nul hallucinatie blijft verankerd op character spans.
Vectoren helpen bij vage semantiek, meertaligheid, lange letterlijke mismatch; voor readers met TOC + preprocess + sterke traceerbaarheid is investeren in Tool + bronreturn + citatieregels vaak betere ROI.
Toekomst: hybride recall, geen herschrijving
We kunnen vector grove recall toevoegen (embedding alleen voor Top-N hoofdstukkandidaten), nog steeds eindigend in segment kiezen → bron → klikbare trace—nul-hallucinatieregels ongewijzigd. Indien toegevoegd: embedding optioneel, expliciete her-index-meldingen bij modelwissel—vermijd stille verkeerde retrieval.
Tot dan: elke OpenAI-compatibele chat-API werkt; chatmodel wisselen herbouwt de lokale index niet.
X. Samenvatting
| Stap | Methode | Rol |
|---|---|---|
| Preprocess | Splitsen op TOC/lengte + segment-samenvattingcache | Lange boeken doorzoekbaar & lokaliseerbaar |
| Positietags | [fBestand-start-einde] in bron | Machine-parseerbare herkomst |
| Tool-retrieval | Per vraag segmenten / heel-boek-samenvattingen, bron terug | Bewijs afdwingen vóór antwoord |
| System prompt | Book first, geen valse tags, zeg wanneer ontbreekt | Generatie beperken |
| Frontend | Voetnoot → preview → spring & markeer | Gebruiker verifieert bewijs |
| Geen vector-retrieval | Eén provider; chatmodel wisselen zonder re-index | Lagere integratie- & migratiekosten |
«Nul hallucinatie» betekent niet dat het model nooit fout gaat—het betekent engineering die output aan een bewijsketen koppelt: geen retrieval → niet doen alsof het boekinhoud is; met retrieval → verifieerbare bronposities geven.
Als je AI-lezen of document-Q&A bouwt, hopen we dat het pad volledige dump → sleutelzinnen → Tool-first on demand retrieval, plus inline positietags + bronreturn, een nuttige referentie-implementatie is.
Dit zijn lessen uit het bouwen van de Foxycape AI-reader—alleen ter referentie. Probeer de reader op de downloadpagina.
