RAG
Citation-first RAG: zo bouwden we een agent voor een kantoor
Een kantoor van 27 mensen in Den Haag beantwoordt wekelijks 1.560 bezwaarvragen. De associates wilden snelheid, geen hallucinaties. Zo bouwden we de agent die weigert te schrijven zonder ECLI.

Het is dinsdagmiddag in maart en een senior advocaat-medewerker bij een 27-koppig publiekrecht-kantoor in Den Haag staart naar een stapel van 14 bezwaarschriften die allemaal vóór vrijdag een conceptbrief nodig hebben. Elk vraagt drie dingen: een schone weergave van de feiten, de juiste Awb-artikelen, en één of twee ECLI-geciteerde uitspraken die de invalshoek onderbouwen die zij wil kiezen. Vorig jaar had die middag zes declarabele uren gekost: jurisprudentie uitpluizen op Rechtspraak.nl, drie verschillende ordners, en een dertien jaar oud Cleverdesk-dossierarchief waar niemand bij IT aan wilde komen.
Dit jaar kost het haar veertig minuten. De agent doet het uitzoekwerk. Hij weigert iets op te stellen totdat elke passage in zijn context is vastgemaakt aan een geverifieerde ECLI of een specifiek artikel uit de Algemene wet bestuursrecht. Hieronder staat het citation-first RAG-draaiboek dat we daarvoor schreven, in de volgorde waarin we het achteraf hadden willen doen.
De vorm van een bezwaarschrift-middag
Het kantoor behandelt ongeveer 1.560 bezwaarschrift-vragen per week: een mix van intake-triage, “is dit überhaupt bezwaar-vatbaar”, termijn-checks, motiveringsbeoordelingen, en de conceptbrieven zelf. Ongeveer de helft wordt binnen tien minuten beantwoord door een paralegal; de rest vraagt om echt onderzoek door een junior of senior. Het corpus waar ze naar grijpen bestaat uit twee stapels:
- ~41.000 Awb-jurisprudentie-uitspraken, in veertien jaar samengesteld door de eigen kennispartner van het kantoor. Niet heel Rechtspraak.nl, maar een geselecteerde subset met interne aantekeningen over relevantie.
- Een 13 jaar oud custom Cleverdesk-dossierarchief met ~28.000 gesloten dossiers, inclusief pleitnota's, memoranda, en de eigen “wat zouden we de volgende keer aanvoeren”-aantekeningen van het kantoor.
Twee dingen waren belangrijk voordat er één regel code was geschreven. Eén: elke uitvoer moest citeerbaar zijn. De beroepsaansprakelijkheidsverzekeraar van het kantoor ging het risico van een fantoom-ECLI niet dragen. Twee: het Cleverdesk-archief kon niet in hetzelfde kwartaal worden gemigreerd. Het draaide, was in gebruik, en zat vol toegangscontrole-eigenaardigheden die sinds 2014 niemand had gedocumenteerd.
De citation-first-invariant
De enige regel die we op dag twee met stift op het whiteboard schreven:
Geen enkele token bereikt het concept-pad van de brief, tenzij elke passage in zijn context is gekoppeld aan een geverifieerde ECLI of een specifiek Awb-artikel. De verificatie gebeurt voordat het LLM de chunk ziet, niet erna.
De meeste RAG-systemen zijn andersom geschreven: ophalen, genereren, en dan het model vragen om “je bronnen te citeren”. Dat werkt voor blogsamenvattingen. Het werkt niet voor een kantoor met echt tuchtrecht-risico. Tegen de tijd dat het LLM een zin heeft opgebouwd rond een niet-onderbouwde claim, is de schade al berokkend. Zelfs een “afgewezen” uitvoer kost beoordelingstijd.
Dus we draaiden het om. De taak van de retrieval-laag is niet “vind relevante chunks”. Het is “vind chunks die al een geverifieerd citaat-handvat dragen, en geef niets anders door”. Een passage zonder ECLI of verwijzing naar een Awb-artikel komt simpelweg de prompt niet in.
Dertien jaar uit Cleverdesk halen zonder het te migreren
Het Cleverdesk-archief is een custom PHP/MySQL-systeem uit 2013 met twee webfrontends erop geplakt, een SOAP-endpoint waarvan niemand zich herinnert hem geschreven te hebben, en een documents-tabel die alles als base64 opslaat in een longblob-kolom. Het soort systeem dat je niet uitzet, en ook niet onder een deadline overzet.
Wat we wel deden: een eenrichtings change-data-capture-stream van Cleverdesks MySQL naar een parallelle index. Een klein Go-proces leest de binlog, haalt de longblob-payloads eruit, draait ze door Apache Tika voor tekst, en schrijft ze in een aparte tabel in onze eigen Postgres met pgvector. De Cleverdesk-UI blijft werken. Niets binnen Cleverdesk verandert. De toegangscontrole-regels van het kantoor worden gespiegeld als row-level security policies, zodat de agent nooit een dossier kan tonen dat de vragende gebruiker niet al in de legacy-UI kan zien.
create table dossier_chunks (
id bigserial primary key,
dossier_id text not null,
source text not null check (source in ('cleverdesk','rechtspraak','firm_memo')),
ecli text,
awb_artikel text,
passage text not null,
embedding vector(1024),
acl_group text[] not null,
created_at timestamptz default now()
);
create index on dossier_chunks using hnsw (embedding vector_cosine_ops);
create index on dossier_chunks (ecli) where ecli is not null;
create index on dossier_chunks (awb_artikel) where awb_artikel is not null;Twee indexen die je misschien niet verwacht: één op ecli en één op awb_artikel. Niet voor vector search. Voor de verificatie-gate. We raken ze bij elke query aan.
Jurisprudentie chunken zonder de ratio decidendi te verliezen
De 41.000 Awb-uitspraken kwamen binnen als pdf's, HTML-scrapes van rechtspraak.nl, en een paar duizend uit een betaald corpus waar het kantoor op geabonneerd is. Generieke chunking, zeg 800 tokens met 100 overlap, sloopte deze. Een bezwaar-relevante overweging zit vaak acht alinea's diep in een uitspraak, en de juridische conclusie tien alinea's verder. Knip daartussen en je hebt aan beide kanten wezen.
We chunkten in plaats daarvan langs de structuur van het document zelf: feitelijke gronden, beoordeling, overwegingen, dictum. Elke chunk draagt de ECLI van de bovenliggende uitspraak, plus een getypeerd label voor de sectie waaruit hij komt. Een retrieval die een dictum-chunk oplevert, haalt automatisch de bijbehorende overweging mee, omdat het LLM niet over de operatieve uitspraak kan redeneren zonder de redenering te zien die ertoe leidde.
Voor het corpus aan firm-memo's en pleitnota's gebruikten we een andere strategie: de eigen alinea-nummering van het kantoor was al betekenisvol, dus die respecteerden we.
Retrieval die niet doet alsof het Nederlands recht snapt
We hebben bewust geen model fijngetuned op Awb-jurisprudentie. Veertien maanden van nu kijken we er opnieuw naar, maar de kosten-zekerheidsverhouding in 2026 klopt niet: elke domein-finetune is één uitspraak van de Hoge Raad verwijderd van subtiel verouderd zijn, en verouderde juridische redenering is slechter dan geen redenering.
In plaats daarvan loopt retrieval in drie banen die onafhankelijk scoren en samenvloeien:
- Dense vector over een meertalig embedding-model. Cosine similarity. Goed in parafrase, blind voor zeldzame ECLI's.
- BM25 over dezelfde chunks. Vangt de exacte ECLI of het Awb-artikel dat de gebruiker mogelijk al heeft ingetypt.
- Citation-graph-uitbreiding. Als een chunk een andere uitspraak via ECLI aanhaalt, wordt dat doel ook opgehaald, met een verlaagde score. Zo herstellen we de ladder van uitspraken die op elkaar voortbouwen.
De drie banen voeden een reranker die het brontype kent. Een senior-memo-chunk mag alleen winnen van een uitspraak als de vraag procedureel is in plaats van inhoudelijk. Die regel werd door een van de partners van het kantoor op het whiteboard geschreven, niet door ons.
De verificatie-gate
Dit is het onderdeel dat het meest uitmaakt, en het onderdeel dat de meeste RAG-draaiboeken overslaan.
Tussen de retriever en het LLM zit een verificatiestap. Elke kandidaat-chunk wordt gecontroleerd tegen twee grond-waarheid-indexen:
- Een ECLI-register, 's nachts gespiegeld vanaf rechtspraak.nl. De ECLI van een chunk moet oplossen naar een echte uitspraak, en de geclaimde publicatiedatum van de chunk moet overeenkomen.
- Een Awb-artikel-register, afgeleid van wetten.overheid.nl, inclusief de geldigheidsperiode van het artikel. Een chunk die artikel 6:7 aanhaalt moet worden gecontroleerd tegen de versie van de wet die van kracht was op het moment van het onderliggende besluit, niet die van vandaag.
Als het citaat van een chunk niet verifieert, wordt hij stil weggegooid, met een logregel. We patchen niet en gokken niet. Of het citaat is echt en actueel, of de chunk bestaat voor deze query niet.
def gate(chunk: Chunk, besluit_date: date) -> Chunk | None:
if chunk.ecli:
u = ecli_index.get(chunk.ecli)
if not u or u.published_at != chunk.cited_date:
return None
if chunk.awb_artikel:
v = awb_index.in_force(chunk.awb_artikel, on=besluit_date)
if not v:
return None
return chunkDe gate is saaie code. Het is ook waar 80% van de projectkwaliteit zit. We hebben hier twee van de zes projectweken in gestoken en zouden het zo opnieuw doen.
ECLI-normalisatie, het saaie deel dat ertoe doet
ECLI-strings lijken gestandaardiseerd. In het wild zijn ze dat niet. We zagen ECLI:NL:RBDHA:2019:1234, ECLI:NL:RbDHA:2019:1234, ECLI NL RBDHA 2019 1234, en in een memo uit 2014 ooit een handgetypte ECLI:NL:RBSGR:2019:1234, waar de rechtbankcode al drie jaar eerder was opgeheven.
Eén valkuil die het noemen waard is: als je iets bouwt dat Nederlandse jurisprudentie aanhaalt, normaliseer ECLI-strings bij ingest, sla de canonieke vorm op, en sla ook elke geobserveerde variant op. Je hebt beide nodig. Eén voor het matchen van wat de gebruiker intypt, één voor het matchen van wat je eigen corpus in 2014 schreef. We houden een alias-tabel bij die geobserveerde vormen op canonieke vormen mapt, en een tabel met opgeheven rechtbanken voor codes als RBSGR die zijn hernoemd. De verificatie-gate raadpleegt beide.
Evalueren op bezwaarschriften die het kantoor al had verloren
De nuttigste evaluatieset is geen synthetische. Het kantoor gaf ons 220 historische bezwaarschriften waarvan de uitkomst al bekend was (gewonnen, verloren, of geschikt), en de partners hadden achteraf gemarkeerd welke uitspraken hadden geholpen. We maten de agent op de vraag of hij die uitspraken naar boven haalde in zijn top tien.
Baseline (alleen dense vector): 41% van de gemarkeerde uitspraken in de top tien. Met de driebanen-retrieval en citation-graph-uitbreiding: 78%. Met de verificatie-gate die niet-onderbouwde chunks dropt: nog steeds 78%, omdat de gate ruis dropt, geen signaal. Dat laatste cijfer was het cijfer waar de partners om gaven.
We rapporteren geen hallucinatiecijfer, omdat de architectuur die vraag verkeerd stelt: een niet-onderbouwde claim kan het concept-pad niet bereiken. Wat we wél rapporteren is het percentage waarop de agent weigert te antwoorden, momenteel 11% van de queries, meestal omdat de relevante jurisprudentie te recent is om al te zijn ingelezen. De associates verkiezen een weigering boven een bluf.
Wat er voor de associates veranderde
De agent stelt geen conceptbrieven van begin tot eind op. Dat was nooit het doel en het kantoor wilde dat ook niet. Wat hij wel doet:
- Beantwoordt “is dit bezwaar-vatbaar”-intakevragen in seconden, met het relevante Awb-artikel erbij geciteerd.
- Haalt voor een gegeven casus de top vijf kandidaat-uitspraken op met een korte samenvatting waarom elk relevant is, en een directe ECLI-link naar de volledige tekst.
- Genereert een gestructureerde onderzoeksbrief (feiten, juridische gronden, ondersteunende jurisprudentie, tegenargumenten) die de associate verwerkt tot een conceptbrief.
Het kantoor hield de tijd van associates aan bezwaaronderzoek bij, voor en na. De mediane tijd op een conceptbrief daalde van 3u10 naar 1u05. De partners hebben de declarabele uren niet geschrapt; ze hebben ze verlegd naar intake van nieuwe dossiers. Het kantoor nam in Q1 2026 met dezelfde bezetting 30 dossiers extra aan.
Het kleinste dat je deze week kunt doen
Voordat je iets architectureert, schrijf je eigen versie van de citation-first-invariant op. Eén zin. Maak hem falsifieerbaar. Als je niet in één regel kunt zeggen wat je agent verboden is te doen, heb je nog geen spec. Je hebt een gevoel. Plak hem aan de muur.
Toen we deze RAG-agent voor het Haagse kantoor bouwden, liepen we er steeds tegenaan dat elke “slimme” toevoeging aan de prompt de invariant beetje bij beetje uitholde. We hebben het opgelost door elke verificatiestap uit de prompt te halen en in deterministische code te zetten die het LLM nooit ziet.
Kern
Draai de citaat-gate in deterministische code voordat het LLM een chunk ziet. Verifiëren na generatie is theater: dan bestaat de niet-onderbouwde zin al.
FAQ
Waarom geen model finetunen op Nederlandse jurisprudentie?
Het corpus verschuift bij elke uitspraak van de Hoge Raad. Een gefinetuned model verslijt stilletjes. We hielden het LLM generiek en duwden alle domeinkennis in retrieval en verificatie, die we 's nachts kunnen verversen.
Kan de agent een volledige conceptbrief van begin tot eind opstellen?
Hij kan het, maar het kantoor wil dat niet. Hij produceert een gestructureerde onderzoeksbrief die de associate redigeert. De mens blijft op het concept-pad. Dat is een bewuste keuze, geen technische limiet.
Hoe regel je de toegangscontrole over het verouderde Cleverdesk-archief?
We spiegelen Cleverdesks per-gebruiker dossier-ACL's naar row-level security policies in Postgres. De agent erft precies wat de vragende gebruiker al in de legacy-UI kon zien, nooit meer.
Wat gebeurt er als de ECLI van een chunk niet verifieert?
Hij wordt stil gedropt en gelogd. We patchen niet, gokken niet, en waarschuwen de gebruiker niet. Of een citaat is echt en actueel, of die chunk bestaat voor deze query niet.
Hebben jullie Cleverdesk in hetzelfde project gemigreerd?
Nee. We hebben de MySQL-binlog naar een parallelle Postgres-index laten lopen en het oude systeem ongemoeid laten draaien. Migratie is een apart gesprek, op een ander tijdspad, met een ander risicoprofiel.