RAG
RAG voor een notariskantoor: elke passage terug naar Wna
Een kandidaat-notaris stelt één vraag over een vruchtgebruikclausule. De agent levert bij elke zin een Wna-artikel of BW-bepaling, of weigert het antwoord.

Het is 22:47 op een dinsdag. Een kandidaat-notaris in Haarlem zit twee alinea's diep in een conceptakte voor een splitsing in appartementsrechten en weet niet meer of de modelakte die ze uit de KNB-bibliotheek trok nog de wijziging uit 2024 op artikel 5:113 BW draagt of de oudere versie. Veertien tabbladen open. De akte moet om 10:30 passeerklaar zijn. Ze opent de RAG-agent op het tweede scherm in plaats van de senior notaris thuis te bellen.
Dat is het moment waarop de RAG-agent die we voor een notariskantoor van 23 mensen aan het Spaarne bouwden zijn plek moet verdienen. Hij krijgt één vraag, geeft één antwoord, en elke zin in dat antwoord draagt een bronvermelding naar een Wna-artikel of een BW-bepaling. Kan hij niet citeren, dan weigert hij. De kandidaat-notaris krijgt geen beleefd 'helaas kan ik je niet helpen'. Ze krijgt de kandidaat-passages die de agent overwoog en één regel uitleg waarom hij ze niet zou gebruiken.
Op het kantoor gebeurt dat patroon 1.080 keer per week. Ruwweg 60% van het volume raakt BW Boek 4 (erfrecht), 25% raakt Boek 3 en Boek 5, de rest is een lange staart. Dit is het draaiboek voor hoe we daar kwamen zonder de tuchtcode te breken.
Eén corpus, eigenlijk twee
De maatschap leverde ons op dag één twee bronnen van waarheid aan. De KNB-bibliotheek met 26.400 modelakten — schoon, versie-beheerd, elke clausule te herleiden tot de KNB-werkgroep die ervoor tekende. En een archief van veertien jaar met ongeveer 38.000 gepasseerde aktes in hun eigen Dias Notariaat dossier-systeem, een PHP/MySQL-build uit 2012 met een zelfgebouwde WYSIWYG en drie rondes 'laten we er gewoon een kolom bij zetten'-schemaverval.
De twee corpora beantwoorden verschillende vragen. De modelakten vertellen je hoe een clausule er in 2026 onder de huidige wet uit hoort te zien. Het dossier-archief vertelt je wat dit kantoor de afgelopen veertien jaar daadwerkelijk heeft gedaan — inclusief de lastige gevallen, de familiebedrijfsoverdracht waar twee broers en zussen het oneens waren, het levenstestament met de ongebruikelijke volmacht. Beide in één retrieval pool gooien sloopt allebei de signalen.
Dus splitsten we. Twee indexen, twee embedding-passages, twee retrieval-calls per vraag, aparte score-drempels.
-- Pass 1: vetted Wna/BW passages for the claim-citation gate
SELECT id, text, artikel, lid, geldig_tot
FROM vetted_passages
WHERE corpus IN ('wna', 'bw')
AND (geldig_tot IS NULL OR geldig_tot > CURRENT_DATE)
ORDER BY embedding <#> $query_embedding
LIMIT 12;
-- Pass 2: precedent retrieval for style, phrasing, prior-akte context
SELECT id, text, modelakte_ref, dossier_ref, passeerdatum
FROM kantoor_chunks
WHERE intrekkingsdatum IS NULL
ORDER BY embedding <#> $query_embedding
LIMIT 8;
Pass 1 is de enige pool waaruit de citation gate mag lezen. Pass 2 informeert het concept, maar wordt nooit een bronvermelding. Dat is de belangrijkste architecturale keuze die we in het project gemaakt hebben. De kandidaat-notaris ziet beide pools in de UI, duidelijk gelabeld, maar alleen Pass 1 verschijnt ooit in het bronvermelding-blok van een antwoord.
Chunken op artikel, niet op alinea
Het standaard RAG-advies — een sliding window van 800 tokens met 100 tokens overlap — was verkeerd voor de Wna en rampzalig voor Boek 4 BW. Erfrecht-artikelen hebben leden die elkaar tegenspreken als je ze los uit de volgorde leest. Artikel 4:13 BW lid 1 regelt de wettelijke verdeling; lid 2 wijst de kinderen aan als schuldeisers van een geldvordering; lid 4 laat de langstlevende echtgenoot die vordering opschorten. Haal lid 4 in isolatie op, en de agent vertelt een kandidaat-notaris vol vertrouwen dat de kinderen geen vordering hebben. Die hebben ze wel, in lid 2.
De retrieval-eenheid is dus het artikel. De citatie-eenheid is het lid. Elke chunk draagt allebei mee.
def chunk_wetboek(text: str, corpus: str) -> list[Chunk]:
# Retrieval unit: the whole artikel (so leden read in context).
# Citation unit: the lid (so the bronvermelding is precise).
for artikel in split_on_artikel(text):
full = artikel.text
for lid_no, lid_text in artikel.leden:
yield Chunk(
corpus=corpus, # "wna" or "bw"
artikel=artikel.number, # e.g. "4:13"
lid=lid_no, # e.g. 2
text=lid_text, # the cited span
parent_text=full, # what the embedder sees
geldig_van=artikel.geldig_van,
geldig_tot=artikel.geldig_tot,
)
De embedder ziet het volledige artikel. De bronvermelding verwijst naar het lid. De reranker — een bge-reranker-v2 multilingual checkpoint boven op open-source BGE-M3-embeddings — ziet hetzelfde volledige artikel. De LLM die het antwoord opstelt ziet alleen het lid dat de gate heeft geaccepteerd.
De KNB-modelakten chunken we anders. Elke modelakte wordt gesplitst op de <clausule>-tags die de KNB in zijn XML-export meelevert, met de titel van de moederakte als metadata. We embedden de toelichting-paragrafen niet apart. Die trekken de retrieval richting de uitleg en weg van de operatieve clausule. De toelichting woont in een zusterveld dat de UI bij hover toont, nooit in de retrieval pool.
De citation gate
De citation gate is het onderdeel waarop de maatschap stond voordat de eerste kandidaat-notaris het systeem aanraakte. De regel is simpel: geen claim in het antwoord verlaat de server zonder een Wna- of BW-bronvermelding, en die bronvermelding passeert drie controles.
def vet_citation(claim: str, cite: Citation, corpus: VettedCorpus) -> Verdict:
if cite.source not in ("Wna", "BW"):
return Verdict.reject("source not in vetted set")
passage = corpus.fetch(cite.source, cite.artikel, cite.lid)
if passage is None:
return Verdict.reject(
f"{cite.source} art. {cite.artikel} lid {cite.lid} not in corpus"
)
if passage.geldig_tot is not None and passage.geldig_tot <= today():
return Verdict.reject(
f"passage withdrawn on {passage.geldig_tot.isoformat()}"
)
if entailment_score(claim, passage.text) < 0.72:
return Verdict.reject("claim not entailed by cited passage")
return Verdict.accept(passage)
Drie controles: het artikel-lid bestaat, het is op de datum van vandaag nog van kracht (de Wna en het BW worden vaak genoeg gewijzigd om dit niet optioneel te maken), en de claim wordt daadwerkelijk gedragen door de geciteerde passage. De entailment-controle gebruikt een klein, Nederlandstalig NLI-model dat draait op de eigen hardware van het kantoor — bewust niet hetzelfde model dat het antwoord opstelt.
De eerste maand in productie produceerde de draftende LLM bronvermeldingen als 'art. 4:13 lid 2 BW' die syntactisch perfect waren en naar een bestaand lid wezen, maar het verkeerde lid citeerden. Het patroon van artikelnummers is zo regelmatig dat het model plausibele verwijzingen kon verzinnen zonder de passage ooit te openen. De entailment-controle op lid-niveau — niet de bestaanscheck op artikel-niveau — is wat dat tegenhoudt. Sla 'm niet over.
Faalt één claim aan de gate, dan houden we het hele antwoord tegen. De agent geeft de oorspronkelijke vraag terug, de opgehaalde kandidaten die hij probeerde te gebruiken, en de afwijzingsreden per claim. De kandidaat-notaris leest vier regels en besluit of ze opschaalt naar de notaris of de vraag herschrijft. We loggen elke afwijzing. Zes maanden later is het afwijzingslog het bruikbaarste trainingssignaal dat we hebben, beter dan upvotes, beter dan een feedbackformulier.
Dias inpakken zonder te migreren
Het Dias dossier-systeem is veertien jaar oud, draait op PHP 7.4, en het schema heeft 211 tabellen. Drie verschillende developers bouwden drie verschillende manieren om partijen op te slaan: een JSON-blob in de ene tabel, genormaliseerde rijen in een andere, een derde die wijst naar een users-tabel die in 2016 is verwijderd. Migreren had het project gekost. Dus migreerden we niet.
In plaats daarvan bouwden we een ETL die elke nacht draait, een Dias MySQL-replica leest, en een schone Parquet-snapshot wegschrijft van (dossier_id, akte_type, passeerdatum, partij_rollen, clause_texts, akte_status). De PHP-laag raken we nooit aan. De Dias-UI blijft werken. Het secretariaat blijft doen wat het sinds 2012 doet. Onze snapshot zit downstream; breekt hij om 03:17, dan fixen we hem voor 09:00 zonder dat iemand het merkt.
def snapshot_dias(replica: MySQL, out: Path) -> None:
# The schema is messy, so we coerce as we read.
rows = replica.query("""
SELECT d.id, d.akte_type, d.passeerdatum, d.status,
d.partijen_json, k.tekst, k.clausule_type
FROM dossiers d
LEFT JOIN klauzules k ON k.dossier_id = d.id
WHERE d.status IN ('gepasseerd', 'concept', 'ingetrokken')
AND d.bijgewerkt_op > %(since)s
""", since=last_snapshot())
for batch in chunked(rows, 500):
write_parquet(out / f"dias-{batch_id()}.parquet", coerce(batch))
De clause_texts worden gechunkt, geëmbed en naar Pass 2 van de retrieval pool gepusht. Ingetrokken aktes blijven bewaard, maar krijgen een vlag. Kandidaat-notarissen moeten ze in retrieval kunnen zien ('dit hebben we geprobeerd, het ging niet door') ook al zijn ze geen precedent. Concept-aktes van de laatste 30 dagen sluiten we uit; het kantoor wil niet dat een concept van gisterochtend het antwoord van vandaag stuurt.
Loggen voor de tuchtcode
Een notaris in Nederland valt onder KNB-tuchtrecht, met de Kamer voor het notariaat als toezichthouder. De tuchtcode noemt AI niet specifiek, maar de onderliggende plicht is glashelder: een notaris moet jaren later de redenering achter elke clausule in een gepasseerde akte kunnen reconstrueren, desnoods voor een tuchtrechter. Een RAG-agent die een conceptakte informeert, valt vierkant binnen die plicht.
Het systeem schrijft daarom een content-addressed event voor elke vraag. Elk event bevat de vraagtekst, de retrieval-resultaten (met embedding-modelversie en index-timestamp), de uitspraken van de gate, het uiteindelijke antwoord, de gebruiker, en een SHA-256 van de prompt die naar de draftende LLM ging. Events gaan naar append-only object storage met een aparte schrijfsleutel die de applicatieserver niet kan intrekken.
Het stuk over bewaartermijn is belangrijker dan mensen verwachten. De IT van het kantoor hield serverlogs 90 dagen aan, omdat dat de AVG-comfortabele default is. Voor een systeem dat conceptakten informeert, is 90 dagen verkeerd. We hebben de retentie verankerd aan het langste van de tuchtrechtelijke verjaringstermijn en de archiefverordening van het kantoor: in de praktijk de looptijd van het dossier plus drie jaar.
Bijeffect: vraagt een kandidaat-notaris in juni 'waarom zei de agent dit in maart?', dan spelen we in seconden de exacte retrieval en de exacte gate-uitspraken opnieuw af. Die replay-capaciteit heeft meer voor de adoptie gedaan dan welke feature dan ook die we uitbrachten. De notarissen vertrouwen het systeem omdat ze het kunnen auditen.
Wat we volgende keer anders doen
Drie dingen, op volgorde van prioriteit.
We chunkten het BW op dag één boek voor boek omdat de structuur van de wet dat uitnodigde. We hadden er vanaf het begin rechtsgebied-metadata bovenop moeten leggen, zodat erfrecht-vragen nooit een personenrecht-artikel ophalen dat toevallig embedding-dichtbij ligt. Die metadata hebben we later toegevoegd, maar de retrieval-bias zat toen al in de index gebakken en we hebben een re-embedding-passage betaald.
De UI voor het afwijzingslog hebben we te klein gemaakt. Kandidaat-notarissen wilden op één scherm elke vraag van die week zien die de gate had geweigerd, gesorteerd op reden. We brachten het uit als een verstopte /audit-pagina. Het had het dashboard moeten zijn.
En we lieten de draftende LLM de Pass 2 precedent pool te vroeg zien. De eerste versie gaf hem drie opgehaalde precedent-clausules naast de geverifieerde passages, en het model begon clausuletaal letterlijk over te nemen uit een modelakte uit 2019 die op het punt stond te worden ingetrokken. Pass 2 wordt nu samengevat in een gestructureerd precedent-context-object (akte_type, datum, status, één regel kern), en de letterlijke clausuletekst wordt pas opgehaald nadat de gate het antwoord heeft geaccepteerd.
Het kleinste wat je vandaag kunt doen
Neem de volgende vijf antwoorden die je team schrijft op een terugkerende vraag — een juridische, een proces-, een prijsvraag. Schrijf voor elk op welke enkele bron je zou moeten citeren om het antwoord te verdedigen tegenover een toezichthouder of een klant. Kun je niet één bron aanwijzen, dan is het antwoord nog niet bronvermelding-klaar, en elke RAG die je erbovenop bouwt erft die kloof. Het corpus-werk gaat voor het model-werk.
Toen we deze RAG-agent voor het Haarlemse kantoor bouwden, was het deel dat het langst duurde niet de embeddings of de reranker; het waren de citation gate en de Dias-snapshot. We hebben hetzelfde gate-patroon sindsdien hergebruikt voor twee andere projecten met gereguleerde kennis (een zorginstelling en een fiscalist), elke keer met opnieuw getrokken corpus-grenzen. Past dezelfde vorm bij jouw werk, dan loopt onze pagina over AI-agents de rest door.
Kern
Een RAG-agent voor gereguleerd werk verdient zijn plek bij de citation gate, niet bij de embedder. Kun je niet citeren, dan stel je niets op.
FAQ
Waarom splits je het corpus in geverifieerde passages en precedent-clausules?
De Wna en het BW vertellen je wat de wet nu toestaat. De gepasseerde aktes van het kantoor vertellen je hoe het lastige gevallen heeft afgehandeld. Allebei mengen laat precedent-taal in bronvermeldingen lekken, en juist dat verbiedt de tuchtcode.
Wat gebeurt er als de citation gate een antwoord afwijst?
De kandidaat-notaris ziet de oorspronkelijke vraag, de kandidaat-passages die de agent ophaalde, en één regel uitleg per claim. Het draft-pad wordt nooit bereikt. De afwijzing wordt gelogd en wordt trainingssignaal.
Hebben jullie het Dias Notariaat dossier-systeem van veertien jaar gemigreerd?
Nee. We lieten Dias draaien en bouwden een nachtelijke ETL die een MySQL-replica leest en een schone Parquet-snapshot wegschrijft. De PHP-laag raken we nooit aan. 211 tabellen schemaverval migreren had het project gekost.
Hoe lang bewaren jullie de logs van de agent?
De looptijd van het dossier plus drie jaar, verankerd aan de archiefverordening van het kantoor en de tuchtrechtelijke verjaringstermijn, niet de 90-dagen-default die IT gebruikte. Herhaalbaarheid is het hele punt.