RAG
Citation-first RAG: 22.000 NHG-aktes, één veilige schrijfroute
Een hypotheekadviseursnetwerk in Breda met 24 mensen stuurt wekelijks 1.560 vragen door een RAG-agent die weigert te antwoorden tot elke passage naar een geverifieerde bron verwijst.

Op een dinsdag in maart had een adviseur bij een Bredaas hypotheekkantoor een klant aan de lijn met de vraag of een zzp-bouwvakker met twee jaar jaarcijfers nog door de NHG-toetsing kwam onder de voorwaarden van 2026. Ze had dertig seconden voor de stilte ongemakkelijk werd. De relevante passage zat ergens in een NHG-leidraad PDF, ergens in een Stater LMS-export, en ergens in een memo die de compliance lead in december had geschreven. Haar echte workflow was drie Chrome-tabs en een onderbuikgevoel.
Dat kantoor is één van de 24 in het netwerk. Samen beheren ze zo'n 22.000 actieve aktes en verwerken ze het soort vragen dat, fout beantwoord, bij de AFM eindigt. Ze hadden ons gevraagd iets als ChatGPT te bouwen voor hun aktes, maar met één harde garantie: geen enkel antwoord komt in het schrijfpad van het klantadvies-portaal terecht zonder citaat dat terugverwijst naar een geverifieerde bron. Die laatste zin maakte het project interessant.
De vorm van het probleem
De briefing was concreet. Ongeveer 1.560 hypotheekvragen per week (we hebben een maand gemeten voor we de scope vastlegden). Vijf categorieën: NHG-grenzen, energielabel-eisen, regels rond een tweede woning, BKR-quirks en oversluiten. Twee onderliggende bronnen van regelgevingswaarheid: de NHG-voorwaarden 2026 (PDF, 240 pagina's) en de AFM-leidraad Hypothecaire kredietverlening. Twee onderliggende databronnen voor klant-specifieke context: een Stater LMS die sinds 2012 draait, en een SharePoint vol notitiebestanden van zes jaar terug.
De Stater LMS is de adder. Als je met Nederlandse hypotheek-backends hebt gewerkt, ken je het verhaal: een stabiele, trage, SOAP-vormige backend waar niemand aan wil zitten. We hadden read-only toegang tot een replica, wat het juiste antwoord is. De 22.000 aktes werden in nachtelijke batches geëxporteerd en opnieuw geïndexeerd in onze eigen store. We laten de agent nooit live met Stater praten.
Waarom naïeve RAG het verkeerde startpunt was
Het eerste instinct, en degene die we expliciet vermeden, was alle PDF's in stukken hakken, embedden, in een vector database stoppen en een LLM aanroepen met top-k context. Precies dat hebben we als baseline gebouwd. Op 200 evaluatievragen produceerde het zelfverzekerd klinkende antwoorden die in 17% van de gevallen fout waren. Fout in de zin van: het antwoord paste bij voorwaarden uit 2023, niet 2026. Fout in de zin van: de AFM-leidraad werd geparafraseerd, niet geciteerd. Fout in de zin van: een passage uit de SharePoint-notities was ronduit gehallucineerd.
Voor een klantadvies-workflow die richting toezichthouder gaat, is 17% een projectkiller. De AFM-pagina's over hypotheektoezicht zijn duidelijk over de herleidbaarheid van advies: een adviseur moet kunnen aanwijzen waar elke aanbeveling op gebaseerd is. Een black-box LLM die parafraseert is in deze sector erger dan helemaal geen tool.
Dus gooiden we de baseline weg en begonnen we vanuit een andere premisse. De agent mag geen enkele token in het portaal-veld schrijven tot een citaatpoort is gepasseerd.
De citation-first retrieval-pipeline
De pipeline ziet er ruwweg zo uit:
user vraag
-> intent classifier (5 categories + "other")
-> source router (NHG | AFM | Stater | Sharepoint | klant-akte)
-> retriever (hybrid: BM25 + dense, separate index per source)
-> reranker (cross-encoder, top 8 -> top 3)
-> citation validator (must match source hash + page + paragraph)
-> answer composer (LLM, with strict template)
-> write gate (rejects any answer without >=1 valid citation)
-> klantadvies-portaal
Twee ontwerpkeuzes deden het meeste werk.
Een eigen index per bron
NHG-voorwaarden, AFM-leidraden, Stater akte-data en SharePoint-notities krijgen elk hun eigen index. Een vraag over de NHG-grens 2026 mag nooit een passage uit een interne notitie van zes jaar oud trekken. De intent classifier routeert de query naar één of twee indexen, meer niet. Dit is geen performance-trucje. Het is een regelgevingsgrens. Een auditor kan op de routing-logica wijzen en zien dat interne notities nooit de bron kunnen worden van advies over harde NHG-regels.
Citaatvalidatie vóór compositie
De validator is het dragende deel. Nadat de reranker zijn top drie passages heeft gekozen, hashen we elke passage tegen het oorspronkelijke brondocument. Als de hash afwijkt (omdat iemand een PDF heeft vervangen, omdat een export opnieuw is gecodeerd), wordt de passage afgewezen. De agent probeert dan ofwel de volgende passage, ofwel geeft 'geen geverifieerde bron' terug in plaats van een antwoord.
De schrijfpoort, in code
De simpelste versie van de schrijfpoort ziet er zo uit. Dit is het laatste dat draait voordat er ook maar één token in het klantadvies-portaal-veld belandt:
def gate(answer: ComposedAnswer, citations: list[Citation]) -> WriteDecision:
if not citations:
return WriteDecision.refuse("no citations")
valid = [c for c in citations if c.source_hash_matches()
and c.is_from_allowed_source(answer.intent)
and c.version_year >= 2026]
if not valid:
return WriteDecision.refuse("no valid citations after validation")
# every claim sentence in the answer must map to >=1 cited passage
unmapped = [s for s in answer.claim_sentences
if not any(c.supports(s) for c in valid)]
if unmapped:
return WriteDecision.refuse(f"{len(unmapped)} unmapped claims")
return WriteDecision.allow(citations=valid)
Niets bijzonders. Het slimme zit in wat we kozen te weigeren. Als het antwoord niet volledig onderbouwd kan worden, ziet de adviseur een 'geen advies, alleen bronnen'-weergave: de opgehaalde passages, met paginanummers, en een notitie dat de adviseur zelf het antwoord moet geven. Dat gebeurt in ongeveer 6% van de gevallen. Adviseurs haten dit iets minder dan een verkeerd antwoord aan een klant geven.
Wat we met de Stater LMS deden
De 14 jaar oude Stater-installatie was in het begin het engste deel van het project. In de praktijk was het het makkelijkste, omdat we 'm niet aanraakten.
Het patroon dat we gebruikten, hebben we op andere legacy backends al vaker herhaald: read-only nachtelijke export, gehashed op rij-niveau, geïndexeerd in onze eigen store. De agent leest tijdens een request nooit uit Stater. Daar zijn twee redenen voor. De eerste is operationeel: een trage upstream-call in een klantgerichte flow is een klantenservice-probleem dat erop wacht te gebeuren. De tweede is regelgevend: als de export-pipeline om 03:00 draait en de indexer voor elke rij de source hash logt, heb je een audit trail die het live-aanroepen-patroon je niet gratis geeft.
Voor de SharePoint-notities deden we iets bewust minder agressiefs. Die notities zijn geen toezichthouderbronnen. Het is interne context. De agent mag ze ophalen, maar de validator markeert elk antwoord dat alleen op SharePoint is gebaseerd als 'intern advies, niet voor klantcommunicatie.' Het is gewoon een metadata-vlag, maar het is het ene stukje plumbing dat de compliance lead tevreden genoeg maakte om akkoord te gaan.
Hoe 1.560 wekelijkse vragen er echt uitzien
Het getal klinkt netjes. De verdeling is dat niet. Na drie maanden productieverkeer zagen we deze vorm:
- Zo'n 38% van de vragen gaat over NHG-grenzen en energielabel-eisen. De agent beantwoordt deze schoon omdat de brondocumenten schoon zijn.
- Zo'n 22% gaat over een klant-specifieke akte ('wat is de huidige rentevastperiode voor akte X'). Deze worden vanuit de Stater-export opgelost.
- Zo'n 18% zijn oversluit-vragen met zowel regelgevende als akte-specifieke context. Deze zijn het duurst in de pipeline omdat ze uit twee indexen halen en de validator twee keer draaien.
- Zo'n 16% zijn edge cases rond BKR en AFM-grenzen.
- De resterende 6% zijn de 'geen advies'-weigeringen.
De accuratesse-cijfers, gemeten tegen een panel van senior adviseurs als evaluators: 94% van de niet-geweigerde antwoorden werd correct en goed onderbouwd geacht. 5% was correct maar citeerde de verkeerde leidraad-paragraaf (de meeste daarvan hebben we opgelost door de reranker-prompt aan te scherpen). 1% was fout. Die 1% kwam allemaal uit de SharePoint-index, en daarom markeren we die antwoorden nu standaard als intern.
Een paar dingen die niet werkten
Wat eerlijke verslaglegging vanuit de bouw.
We hebben geprobeerd één gecombineerde index met metadata-filters te gebruiken in plaats van aparte indexen per bron. Op 50 vragen werkte dat prima en bij 500 viel het uit elkaar, omdat de dense retriever passages met vergelijkbare formuleringen uit de NHG- en AFM-corpora ging mixen. Aparte indexen waren niet alleen schoner, ze waren meetbaar accurater.
We hebben geprobeerd antwoorden op vraag-niveau te cachen. Hypotheekadviseurs formuleren dezelfde vraag op twaalf manieren, en de naïeve cache hit-rate was zo'n 3%. We hebben dat vervangen door caching op het niveau van opgehaalde passages, dat zit nu op 41% en bespaart echt geld.
We hebben geprobeerd de cross-encoder reranker over te slaan omdat hij 400ms kost. De accuratesse-daling zonder hem was 6 procentpunten. De adviseurs merkten de 400ms niet. De foute antwoorden wel.
Als je RAG bouwt over documenten richting toezichthouder en je versiebeheert je bronbestanden niet op hash, ga je uiteindelijk een antwoord serveren uit een achterhaalde leidraad. De agent klinkt overtuigd. De auditor niet.
Wat de adviseur ziet
De interface is met opzet saai. Een vraag-veld bovenaan. Een antwoord eronder. Onder het antwoord drie citaatkaarten: bronnaam, documentversie, paginanummer, de passage in zijn geheel. Een knop om de bron-PDF op de geciteerde pagina te openen. Een knop om het antwoord-plus-citaten-blok in het klantadvies-portaal te plakken.
Er is geen chat-interface. We hebben er één geprobeerd. Adviseurs wilden 'm niet. Wat ze wilden was een snelle lookup die tekst opleverde die ze konden plakken, met voetnoten die ze konden verdedigen als hun compliance lead erom vroeg.
Het kleinste wat je vandaag kunt doen
Als je naar een RAG-project in een gereguleerde sector kijkt, schrijf dan eerst de citaatvalidator voor je iets anders schrijft. Definieer wat een geldig citaat in jouw domein betekent (source whitelist, versie-eis, hash-check, paragraaf-mapping) en weiger zonder live te gaan. Alles eromheen (vector store, embeddings, modelkeuze) is een tuning-parameter. De validator is de architectuur.
Toen we de RAG-agent voor het Bredase netwerk bouwden, was het ding waar we tegenaan liepen dat het model nooit de bottleneck was. De bottleneck was beslissen wat we bereid waren in een klantadvies-portaal te schrijven zonder bron. We hebben dat opgelost door die beslissing expliciet in code te zetten, voor de LLM ook maar één token mocht componeren. De NHG-voorwaarden zijn een openbaar document; het antwoord dat jouw adviseur in een portaal plakt, wordt een registratie. Behandel het tweede met dezelfde ernst als het eerste.
Kern
In gereguleerde financiën is RAG geen retrieval-probleem. Het is een citaat-integriteitsprobleem. Bouw de schrijfpoort voor je de modelaanroep bouwt.
FAQ
Waarom een eigen index per bron in plaats van één gecombineerde vector store?
Cross-mixing tussen NHG-voorwaarden en AFM-leidraden gebeurt op schaal omdat de formulering vergelijkbaar is. Aparte indexen per bron geven een auditor ook een duidelijke regelgevingsgrens.
Wat gebeurt er als er geen geldig citaat bestaat voor een vraag?
De agent weigert een antwoord te schrijven. Hij toont de adviseur de opgehaalde passages met paginanummers en labelt het resultaat 'geen advies, alleen bronnen.' Dat gebeurt in zo'n 6% van de gevallen in productie.
Waarom de agent niet direct tijdens een request aan de Stater LMS koppelen?
Snelheid en audit trail. Een nachtelijke read-only export, gehashed op rij-niveau en geïndexeerd in onze eigen store, is sneller voor de adviseur en geeft een verifieerbare registratie die een auditor kan teruglezen.
Hoe accuraat is de agent in productie?
94% van de niet-geweigerde antwoorden werd door een panel senior adviseurs correct en goed onderbouwd bevonden. 5% citeerde de verkeerde leidraad-paragraaf. 1% was fout, vrijwel allemaal uit de SharePoint-index, die nu standaard als intern wordt gemarkeerd.