← Blog

RAG

RAG voor juridische uitgevers: antwoorden met bronvermelding

Een redacteur opent dinsdagochtend om 9:14 een vraag: welke arresten van de Hoge Raad sinds 2019 perken artikel 6:248 BW in? De agent antwoordt in elf seconden, met zeven citaties.

Jacob Molkenboer· Oprichter · A Brand New Company· 9 dec 2025· 9 min
Open eiken kaartenbak met manilla kaarten, messing tussenschot, lichtgroen tabblad, papiertje op ivoor linnen.

Een redacteur bij de Leuvense juridische uitgever opent dinsdagochtend om 9:14 een vraag: welke arresten van de Hoge Raad sinds 2019 hebben de reikwijdte van artikel 6:248 BW in commerciële contracten ingeperkt? De agent antwoordt in elf seconden, met zeven citaties, elk netjes terug te leiden naar een geverifieerde uitspraak op Rechtspraak.nl. Ze sleept twee alinea's de nieuwsbrief van volgende week in. Om 9:18 is ze al bij de volgende vraag.

Dat moment kostte achttien maanden werk. De uitgever bedient 4.800 abonnees in Vlaanderen en Nederland. Hun redactie beantwoordt zo'n 2.260 jurisprudentievragen per week, over 84.000 ECLI-uitspraken plus een twaalf jaar oud XML-archief op maat van lagere rechtspraak dat de uitgever zelf digitaliseerde voordat ECLI-standaardisatie bestond. De opdracht was makkelijk te formuleren en lastig uit te voeren: bouw een RAG-agent die antwoorden voor de redacteur opstelt, maar laat nooit een passage zonder bronvermelding doorlopen naar het abonneeportaal of de nieuwsbrief-queue.

Twee write paths, één citation gate

De meeste RAG-demo's stoppen bij "antwoord in chat". Een uitgever kan dat niet. Twee write paths in hun stack waren bepalend.

Het abonneeportaal, waar redacteuren antwoorden opslaan als onderzoeksmemo onder de naam van de uitgever. Eenmaal opgeslagen is een memo factureerbaar advies. De nieuwsbrief-queue, waar een alinea die wordt gepromoveerd naar de weekdigest in 4.800 inboxen belandt voordat een mens 'm nog eens heeft gelezen.

We hebben beide behandeld als productie-write paths en de agent gebouwd als een pipeline die fysiek geen token kan afleveren zonder geverifieerde bron erbij. Het output-type van de agent is geen string. Het is (string, [Citation]), en de writer middleware weigert een memo op te slaan waarvan de citaties niet binnen 48 uur zijn geverifieerd tegen Rechtspraak.nl. Geeft één citatie een 404 tijdens de verificatie, dan zakt het hele antwoord naar "concept, redactie controleren" en komt het de queue nooit in.

Het corpus dat we kregen

84.000 ECLI-uitspraken klinkt als één ding. Het zijn er drie.

Moderne uitspraken (vanaf 2013): keurige ECLI-XML met gestructureerde rechtsoverwegingen, partijen, rechtbank, datum en een stabiele URL op Rechtspraak.nl. Ongeveer 51.000 stuks.

2007 tot 2013: ECLI-getagd, maar inconsistent. Zo'n 9% heeft kapotte XML. De helft mist duidelijke grenzen tussen rechtsoverwegingen. We konden de body indexeren, maar niet altijd een citatie aan een specifieke alinea koppelen.

Pre-2007 maatwerkarchief: 23.000 uitspraken van lagere rechters die de uitgever in 2014 scande en taggde in een eigen XML-schema. Het schema staat in een Word-document. Niemand die het destijds schreef werkt er nog.

De verleiding bij zo'n corpus is om alles in één vorm te gieten en één index te bouwen. Niet doen. Wij hielden de drie corpora als drie indexes met drie retrievers, omdat de failure modes verschillen en je wilt weten uit welk corpus een fout antwoord komt.

Chunken op uitspraakstructuur, niet op token count

Elke RAG-tutorial raadt sliding windows van 512 tokens aan. Juridische tekst straft die aanpak af. Een rechtsoverweging is een op zichzelf staand juridisch argument, soms 80 tokens, soms 1.400. Knip 'm doormidden en je hebt een chunk die de helft van een oordeel citeert.

Wij chunkten langs de ECLI-XML-structuur.

def chunk_ruling(ruling: ECLIRuling) -> list[Chunk]:
    chunks = []
    # Each rechtsoverweging is one chunk, regardless of length.
    for ro in ruling.rechtsoverwegingen:
        chunks.append(Chunk(
            ecli=ruling.ecli,
            ro_number=ro.number,
            court=ruling.court,
            date=ruling.date,
            text=ro.text,
            url=f"https://uitspraken.rechtspraak.nl/details?id={ruling.ecli}",
        ))
    # Plus one chunk for the dictum (the operative ruling).
    chunks.append(Chunk(
        ecli=ruling.ecli,
        ro_number="dictum",
        court=ruling.court,
        date=ruling.date,
        text=ruling.dictum,
        url=f"https://uitspraken.rechtspraak.nl/details?id={ruling.ecli}",
    ))
    return chunks

Voor het pre-2007-archief schreven we een parser die het eigen schema uit 2014 op dezelfde Chunk-vorm liet aansluiten. Dat kostte drie weken. Het was de meest waardevolle engineeringbeslissing in het project: één uniforme Chunk-vorm over drie corpora betekende dat de retriever, de reranker en de citation gate niet hoefden te weten uit welk archief een antwoord kwam.

Hybride retrieval met een domein-reranker

We draaiden BM25 en dense vectors naast elkaar. BM25 vangt exacte verwijzingen naar wetsartikelen, ECLI-nummers en Nederlandse juridische formuleringen die embeddings uitvegen. Vectoren vangen parafrases en synoniemen ("opzegging" versus "beëindiging"). Gebruik de BM25 van Elasticsearch of wat er in jouw stack zit. De implementatie is niet het verhaal.

Het verhaal is de reranker. Standaard rerankers zijn getraind op webteksten en promoten vrolijk een uitspraak van een lagere rechter uit 2003 boven een Hoge Raad-arrest uit 2024 omdat de keyword match daar dichter is. We hebben dus een kleine reranker fine-tuned op 18.000 paren met redactiebeoordelingen die de uitgever sinds 2021 verzamelde in hun interne onderzoekstool. Ze wisten het zelf niet, maar die beoordelingen waren het waardevolste bezit in het pand.

Door de reranker te fine-tunen ging nDCG@10 van 0,71 naar 0,86 op onze held-out eval set. Dezelfde reranker zonder fine-tuning bleef op 0,63 steken.

De citation gate

De taak van de agent is niet om antwoord te geven. De taak is een antwoord opstellen waarin elke feitelijke claim verankerd is in een specifieke chunk die de agent net heeft opgehaald. Dat dwingen we af met een two-pass-structuur.

Pass één: het model schrijft een concept met inline citatiemarkers ([ECLI:NL:HR:2021:1313#3.4]) gekoppeld aan de chunks die het ophaalde.

Pass twee: een aparte verifier-prompt krijgt het concept en de geciteerde chunks (en verder niks), en moet beantwoorden: wordt elke claim ondersteund door de chunk die geciteerd wordt, ja of nee. Elke "nee" stuurt die claim naar de redactioneel-controleren-stapel.

Voordat het concept het write path naar het abonneeportaal bereikt, lost een derde proces elke ECLI op naar de canonical URL op Rechtspraak.nl en bevestigt met een HEAD request dat de uitspraak nog bestaat. Het HEAD-resultaat cachen we 48 uur. Uitspraken worden zelden ingetrokken, maar het gebeurt, en de uitgever houdt liever een memo vast dan dat ze er een publiceren die naar een vernietigd arrest verwijst.

Let op

Laat je verifier-prompt nooit de oorspronkelijke vraag zien. Doet hij dat wel, dan praat hij zwakke citaties goed om de vrager tevreden te stellen. Geef 'm alleen het concept en de chunks, meer niet.

Het twaalf jaar oude XML-archief

Het migreren van het pre-2007-archief was de langste staart van het project. Het schema had drie ongedocumenteerde quirks: uitspraken konden binnen andere uitspraken genest zitten (bij gevoegde zaken), het datumveld gebruikte vier verschillende formaten door het corpus heen, en 312 uitspraken waren met de hand bewerkt met HTML midden in de XML, wat de oorspronkelijke parser geruisloos liet vallen.

We schreven een strikte parser, draaiden 'm op het hele archief, logden elke parse-exception naar een CSV, en gingen twee middagen lang met de hoofdredacteur van de uitgever de long tail bij langs. Ongeveer 1,5% van het archief had handwerk nodig. Dat percentage is klein, maar de twee middagen input van die redacteur waren het verschil tussen een corpus dat we vertrouwden en een corpus waarvan we hoopten dat 'ie schoon was.

Migreer je een verouderd XML-archief naar een RAG-pipeline, reken dan op die middagen. Ze zijn niet optioneel.

Redacteur in de loop, geen human in the loop

"Human in the loop" is de term die iedereen gebruikt. Te lage lat. De redacteur valideert niet, de redacteur redigeert. We hebben de UI zo ontworpen dat het concept van de agent in de normale workflow van de redacteur belandt, precies zoals het concept van een junior onderzoeker dat zou doen: als een memo met track changes aan, in het CMS van de uitgever, met de citaties al gelinkt. De redacteur leest, accepteert of weigert elke geciteerde passage, en bewaart. Het accept-of-weiger-signaal gaat rechtstreeks de gradecollector in en traint de volgende reranker.

Dat verandert de economics. De redactie doet geen extra werk om de agent te beoordelen. Ze doen hun normale werk, en het systeem registreert het. Na vier maanden in productie hadden we 4.800 verse beoordelingen uit echt gebruik, bovenop de 18.000 historische. De reranker die we op de gecombineerde set trainden tilde held-out citation precision van 0,91 naar 0,94.

De eval suite

We bouwden een eval set van 480 vragen, beoordeeld door drie hoofdredacteuren, met elke vraag gescoord op drie assen: feitelijke juistheid, citation precision (ondersteunen de geciteerde uitspraken de claim echt) en citation recall (heeft de agent een leidend arrest gemist dat de redactie zelf had aangehaald). De suite draait bij elke betekenisvolle wijziging in de pipeline. Een volledige run kost 22 minuten en zo'n €1,40 aan inference. Geen promptaanpassing, geen retriever-aanpassing, geen chunker-aanpassing gaat naar productie tenzij de suite groen terugkomt.

Zonder die suite is elke wijziging een gevoelscheck. Mét de suite werd de prompttweak die vlotheid omhoog deed maar citation precision van 0,94 naar 0,88 trok dezelfde dag gevangen, in plaats van drie weken later, wanneer een redacteur zou hebben gezien dat een memo naar een vernietigd arrest verwees. We houden ook een uitsplitsing per corpus bij. Het pre-2007-archief blijft consequent zo'n zes punten achter op de moderne uitspraken qua citation recall, en dat gat vertelt ons waar de volgende ronde parserwerk heen moet.

Wat dit kostte qua vorm, niet qua euro's

Achttien maanden. Drie engineers aan onze kant, twee redacteuren aan hun kant voor ongeveer 40% van hun tijd, één hoofdredacteur voor 20%. De totale rekening voor compute over training plus achttien maanden productie blijft onder €18.000. Het dure deel was de tijd van de redactie, en dat was het waard.

Een bruikbaar frame uit een recente Hacker News-discussie over AI-native startups: de moat is niet het model. De moat is de achttien maanden dataset met redactiebeoordelingen, de parser voor het eigen schema uit 2014, en de citation gate die de agent verandert van een zelfverzekerde chatbot in een systeem waar redacteuren hun naam onder durven te zetten.

Wat we anders zouden doen

Bouw de citation gate vóór de retriever. Wij bouwden 'm als laatste en gingen bijna live zonder. De gate is het product. Bouw 'm op dag één met een stub-retriever erachter en je ontwerpt elk ander component zo dat het er netjes in voert. First-pass retrieval mag de domste BM25 zijn die je overeind kunt zetten. Bewaar het reranker-werk tot de gate er staat.

Versioneer het corpus vanaf dag één. Rechtspraak.nl herpubliceert uitspraken stilletjes met gecorrigeerde metadata. We vingen een geval waarin een antwoord verwees naar een uitspraak waarvan de ECLI was vervangen. We snapshotten nu wekelijks de canonical URL-response en draaien er een diff overheen.

Behandel de gradecollector voor de redactie als het eerste product, niet als het laatste. De 18.000 historische beoordelingen trainden de reranker. Hadden we in week één een grade-collecting UI live gezet, dan hadden we in maand vier al 5.000 verse beoordelingen op de output van de agent zelf gehad. Dat is de dataset die rente op rente oplevert, en er is geen vervanging voor vroeg beginnen.

Toen we de agent voor de Leuvense uitgever bouwden, was wat we steeds onderschatten de verifier-prompt: hoe vaak hij stilletjes een citatie goedkeurde die de claim niet ondersteunde. We hebben het uiteindelijk opgelost door de verifier blind te maken voor de oorspronkelijke vraag en hem te dwingen eerst de ondersteunende zin uit de chunk te citeren voordat hij een oordeel mocht geven. Dat soort loodgieterswerk is in de praktijk het meeste van wat ons werk aan AI-agents behelst.

Heb je een RAG-systeem in productie staan, open dan vandaag je verifier-prompt en check of hij de vraag van de gebruiker ziet. Zo ja, dan is dat een wijziging van één regel die je citaties tegen maandag al strakker zet.

Kern

Kan de output van je RAG-systeem zonder citation gate productie bereiken, dan heb je geen RAG-agent. Dan heb je een zelfverzekerde chatbot voor een database.

FAQ

Waarom geen chunks van 512 tokens zoals elke tutorial aanraadt?

Juridische argumenten eindigen niet bij token 512. Een rechtsoverweging die je doormidden knipt levert een chunk die de helft van een oordeel citeert. Chunk langs de documentstructuur, niet op tokenaantal.

Hoe voorkom je dat het model citaties verzint?

Een two-pass-structuur. De drafter schrijft met citatiemarkers; een aparte verifier ziet alleen het concept plus de geciteerde chunks (niet de vraag) en beoordeelt elke claim. Gefaalde claims gaan naar de redactiestapel.

Is de dataset van achttien maanden redactiebeoordelingen herbruikbaar, of specifiek voor deze uitgever?

Specifiek. De beoordelingen leggen de redactionele standaarden van deze uitgever vast. Daarom is het een moat. Een concurrent zou zelf een eigen set moeten verzamelen.

Wat had het twaalf jaar oude XML-maatwerkarchief nodig dat het moderne ECLI-corpus niet nodig had?

Een eigen parser die geneste uitspraken, vier datumformaten en inline HTML in 312 met de hand bewerkte bestanden aankon. Zo'n 1,5% van het archief had handmatige triage door de redactie nodig.

Waarom drie indexes in plaats van één genormaliseerd corpus?

De failure modes verschillen. Als een antwoord verkeerd is, wil je weten uit welk corpus 'ie kwam. Met drie indexes kun je ook citation recall per corpus scoren en parserwerk richten waar het het meeste oplevert.

ragai agentsknowledge basecase studyarchitecturemigration

Iets bouwen?

Start een project