← Blog

RAG

RAG-freshness gates: ingetrokken arrest in een conclusie

Donderdag 16:47: een paralegal in Maastricht spot een ingetrokken Hoge Raad-arrest in een concept-conclusie van 14 pagina's. De bulk feed had één veld laten vallen.

Jacob Molkenboer· Oprichter · A Brand New Company· 29 jun 2026· 8 min
Half geopende eiken kaartenbak, opgetilde kaart met groen tabje, koperen tussenschot, papierstapel, rood lakzegel.

Het was 16:47 op een donderdag in mei toen een paralegal bij een Maastrichts advocatenkantoor met 28 medewerkers iets opmerkte in een concept-conclusie. Een Hoge Raad-arrest dat op pagina negen werd aangehaald — ECLI:NL:HR:2019:1278 — was maanden eerder ingetrokken. De conclusie moest maandag de deur uit. De advocaat-medewerker die ermee bezig was, had zijn bureau al opgeruimd.

Het citaat kwam niet van een junior. Het kwam van de RAG-agent van het kantoor, die al zes maanden zonder incidenten jurisprudentie ophaalde. Het Slack-bericht aan ons kwam om 16:53 binnen. Om 18:20 wisten we de oorzaak. De dinsdag daarop moest elke juridische concepttekst op dat systeem een nieuwe gate passeren voordat de agent het werk als af mocht aanmerken.

Wat de bulk feed stilletjes deed

Rechtspraak.nl publiceert een bulk feed met Nederlandse jurisprudentie onder hun Open Data-programma. Elke uitspraak komt als XML-record met een stabiele ECLI-identifier, de volledige tekst en een set metadata-velden — waaronder de intrekkings-marker die aangeeft of een arrest is ingetrokken, in revisie vernietigd of anderszins van gezag ontdaan.

Die marker vertelt de buitenwereld: citeer dit niet. Zes maanden lang lazen we de feed in, embedden we de body en sloegen we de marker op als sidecar-flag. Elke chunk waarbij de marker stond, werd bij retrieval uitgesloten. Schone scheiding.

Op 2026-05-04 leverde de feed een record voor ECLI:NL:HR:2019:1278 waarin het intrekkings-markerveld aanwezig maar leeg was. Niet afwezig — leeg. Ons ingestion-script behandelde de lege string als "niet ingetrokken" en overschreef de eerder gezette flag. De chunk werd weer opvraagbaar. Zes weken later dook hij op bij een semantisch verwante query over cassatiegrenzen, scoorde 0.84 cosine-similariteit, en belandde in alinea 27 van een conclusie die op het punt stond de rechtszaal in te lopen.

Wat het lastig te spotten maakte: de tekst van het arrest was ongewijzigd. De inhoud van de uitspraak klopte nog steeds. De intrekkings-marker is één Boolean-achtig veld aan de rand van het record, en het embedding-model heeft geen reden zich erom te bekommeren. Het signaal zit volledig in de metadata, en juist daar kijkt nooit een mens naar tenzij er iets misgaat.

Waarschuwing

Stille schema-drift upstream is de meest voorkomende oorzaak van verouderde RAG-output die we tegenkomen. Een lege string is niet hetzelfde als "geen wijziging". Idempotente overschrijvingen zijn precies hoe ingetrokken data weer tot leven komt.

Waarom onze bestaande RAG-hygiëne dit niet zag

De RAG-stack van het kantoor was, naar elke redelijke maatstaf, goed opgezet. Wekelijks opnieuw embedden van het hele corpus. Bronvermelding op elke passage. Een guardrail die weigerde iets te citeren zonder ECLI. Een tweede pass die elk citaat ouder dan vijf jaar markeerde voor menselijke review. De medewerker had die ochtend zeven citaten beoordeeld. Deze had hij niet gemarkeerd, want het arrest was uit 2019 — ruim binnen het venster — en de confidence-score van de agent was hoog.

Wat de stack niet had, was een per-citaat liveness-check op het moment dat het concept werd opgeleverd. De check liep bij ingestion. Zodra een passage met sidecar-metadata in de vector store stond, vertrouwden we die metadata. Het single point of failure was het ingestion-script, en dat faalde stilletjes op één record, zes weken voordat iemand het in de gaten had.

De forensische analyse duurde zo'n negentig minuten. We pulden de ingestion-logs, vonden de run van 4 mei, deden een diff van het record tegen de vorige ingestion van dezelfde ECLI, en zagen de intrekkings-marker van true naar lege string omklappen. Daarna gingen we direct naar Rechtspraak.nl en bevestigden dat het live record de zaak nog steeds als ingetrokken toonde. De bug zat volledig aan onze kant, in de vier regels ingestion-code die het XML-veld mapten op onze sidecar-metadata. Het concept van de medewerker was gecompromitteerd door een ontbrekende null-check.

Dit is het deel dat ons stoort. De fout was geen hallucinatie. Het model verzon het citaat niet. Het haalde correct een echte ECLI op, met een echte passage, die daadwerkelijk paste bij de juridische vraag. De pipeline had het stilletjes verteld dat de passage geldig was, terwijl dat niet zo was.

De citation-freshness gate

De fix is een gate tussen het concept van de agent en de menselijke reviewer. Voordat een alinea met een jurisprudentieverwijzing uit de agent-loop mag vertrekken, wordt elke ECLI erin gecheckt tegen het live record op Rechtspraak.nl. Niet de gecachte versie. De live versie.

De gate doet drie dingen:

  1. Haalt elke ECLI:NL:*-verwijzing uit het concept met een strikte regex.
  2. Voor elke verwijzing wordt het canonieke XML-record opgehaald van data.rechtspraak.nl met een harde timeout van 800ms.
  3. Checkt de intrekkings-marker, de procesgang-status en eventuele "vernietigd"-annotatie. Resolveert een daarvan naar ingetrokken of vernietigd, dan wordt het concept geblokkeerd en gaat er een gestructureerde foutmelding naar de agent én de reviewer.

De gate draait nadat het concept is opgesteld en voordat de agent een afgewerkt artefact retourneert. Het is geen zachte waarschuwing. De agent kan er niet zelf overheen lopen.

Hoe de gate er in code uitziet

Hieronder een uitgeklede versie van de validator. We draaien hem als aparte worker, zodat een trage Rechtspraak-response de agent-thread niet vasthoudt.

import re
import asyncio
import httpx
from dataclasses import dataclass

ECLI_PATTERN = re.compile(r"ECLI:NL:[A-Z]{2,4}:\d{4}:\d+")
RECHTSPRAAK = "https://data.rechtspraak.nl/uitspraken/content"

@dataclass
class CitationStatus:
    ecli: str
    live: bool
    reason: str | None = None

async def check_one(client: httpx.AsyncClient, ecli: str) -> CitationStatus:
    try:
        r = await client.get(RECHTSPRAAK, params={"id": ecli}, timeout=0.8)
        r.raise_for_status()
    except (httpx.TimeoutException, httpx.HTTPError) as e:
        # Fail closed: if we can't verify, we don't ship.
        return CitationStatus(ecli, live=False, reason=f"unverifiable: {e}")

    body = r.text
    if "<intrekking>" in body or "ingetrokken" in body.lower():
        return CitationStatus(ecli, live=False, reason="ingetrokken")
    if "vernietigd" in body.lower():
        return CitationStatus(ecli, live=False, reason="vernietigd in revision")
    return CitationStatus(ecli, live=True)

async def gate(draft_text: str) -> list[CitationStatus]:
    eclis = sorted(set(ECLI_PATTERN.findall(draft_text)))
    async with httpx.AsyncClient() as client:
        results = await asyncio.gather(*(check_one(client, e) for e in eclis))
    return results

Twee design-keuzes die het noemen waard zijn. Eén: de gate faalt closed. Is Rechtspraak.nl niet bereikbaar, dan vertrekt het concept niet. We houden liever een conclusie tien minuten vast dan dat we een verouderd citaat versturen. Twee: de output van de gate is een gestructureerde lijst, geen ja/nee. Wordt een citaat geblokkeerd, dan ziet de reviewer de ECLI plus de reden en kan hij óf het concept van de agent accepteren zonder die alinea, óf de passage met de hand herschrijven.

De latency is in productie prima. Een gemiddeld concept bevat 6–14 ECLI-verwijzingen. De gate draait ze parallel en voegt 220ms toe aan de mediane concepttijd. De reviewer merkt er niets van.

De andere drie dingen die we veranderden

Een gate aan de rand is de eerste verdedigingslinie, niet de enige. We hebben die week nog drie dingen aangepast.

Eén: ingestion behandelt ontbrekend-of-leeg nu als ontbrekend, niet als leeg. De intrekkings-marker heeft drie geldige states: true, false, of echt afwezig. Een lege string wordt niet langer geaccepteerd als "false"; het wordt gelogd als parse-fout en de vorige waarde blijft behouden. Het ingestion-script kreeg een expliciete state machine voor marker-velden, die we achttien maanden geleden hadden moeten schrijven.

Twee: elke ingestion-run schrijft nu een delta-rapport: hoeveel records sinds de vorige run van marker-state zijn veranderd, en elk record waarvan de marker zonder bijbehorende Rechtspraak-aankondiging van ingetrokken terugkeert naar actief, triggert een hold. In de afgelopen zes weken zijn er twee extra gevallen gevlagd. Geen van beide bleek een echte omkering. Allebei waren het parse-artefacten upstream.

Drie: we hebben een wekelijkse kruiscontrole toegevoegd tegen een tweede, onafhankelijke bron. Voor Nederlandse jurisprudentie is dat kennisbank.overheid.nl. Twee bronnen die het oneens zijn over de status van een ECLI lossen niet automatisch op; het opent een ticket. Voor RAG-systemen die gereguleerde output produceren is two-source verification op corpus-niveau goedkope verzekering.

Conclusie

Een vector store is een cache. De source of truth zit ergens anders. Elke agent die dat onderscheid vergeet, levert vroeg of laat met volle overtuiging iets verouderds op.

De prijs van agent-zelfvertrouwen

Er zit een breder punt achter, en Anthropic's stuk over het bouwen van effectieve agents komt op dezelfde observatie uit: hoe meer autonomie je een agent geeft, hoe meer individuele fouten lijken op het werk van een zorgvuldig mens. De juridische RAG faalde niet op een manier die om aandacht schreeuwde. Hij produceerde een rustig, netjes opgemaakt, plausibel concept met een echte verwijzing naar een echte zaak. Het enige signaal dat er iets mis was, kwam van buiten — het geheugen van een paralegal die zich een intrekkingsaankondiging herinnerde.

Betrouwbare agentic systemen zijn niet de slimste. Het zijn de systemen met harde, deterministische checks op elk punt waarop hun output een grens oversteekt naar de echte wereld. Voor een juridische agent is die grens het moment dat een concept de loop verlaat. Voor een inbox-triage-agent het moment dat een reply de deur uit gaat. Voor een facturatie-automation het moment dat er geld beweegt. Op al die grenzen wil je een gate die niet afhankelijk is van het oordeel van het model. Dat is ook de rode draad bij Anthropic: hou de patronen eenvoudig, en verifieer bij elke externe actie in plaats van het model zichzelf te laten verifiëren.

Dit is, naar onze ervaring, de failure mode die ons het meeste zorgen baart. Agents falen niet luid. Ze falen op manieren die op competent werk lijken. Een conclusie van 14 pagina's met één fout citaat leest voor een gehaaste lezer identiek aan een conclusie van 14 pagina's met allemaal goede citaten. Het enige eerlijke antwoord is aannemen dat de agent ergens, op enig moment, fout zit, en de check te plaatsen op een punt waar die fout er niet doorheen kan.

Het is ook de reden waarom de bredere discussie over identity-controls voor agent-gedreven acties het volgen waard is. De industrie beweegt langzaam richting sterkere verificatie op high-stakes agentic capabilities. "Een agent deed het" is nog geen acceptabel antwoord wanneer de actie echte gevolgen heeft, en de systemen die wij opleveren mogen niet doen alsof dat wel zo is.

De audit van vijf minuten

Draai je een RAG-agent die output produceert waar een mens zijn naam onder zet — een juridisch stuk, een medische samenvatting, een financieel advies, een klantantwoord — stel dan één vraag. Tussen het moment dat het model klaar is met schrijven en het moment dat het concept een artefact wordt: is er een deterministische check tegen de source of truth? Geen zachte waarschuwing. Geen confidence-score. Een check die kan weigeren te versturen.

De fout die wij zes maanden lang maakten, was aannemen dat een check bij ingestion gelijkstond aan een check bij output. Dat is niet zo. Ingestion-checks verdedigen tegen slechte data die binnenkomt. Output-checks verdedigen tegen slechte data die naar buiten gaat. Het zijn andere problemen en ze hebben andere gates nodig. Heeft je RAG alleen het eerste, dan is je agent één upstream schema-wijziging verwijderd van een conclusie zoals deze.

Is het antwoord op de audit nee, schrijf de check dan vandaag. Begin met een regex over het concept voor het citatiepatroon waar het bij jou om draait — ECLI, CVE, SKU, ISO-nummer, intern ticket-ID — en één HTTP-call per verwijzing. Faal closed. Verfijnen kan later. De versie die we de dinsdag na het incident aan het Maastrichtse kantoor leverden, was minder dan tachtig regels.

Toen we de Rechtspraak-bewuste RAG-agent voor dat advocatenkantoor bouwden, liepen we ertegenaan dat hygiëne bij ingestion niet hetzelfde is als hygiëne bij emit. We losten het op door elke externe citatie als untrusted te behandelen totdat hij tegen zijn live bron was gevalideerd, elke keer opnieuw. De vector store is een cache, geen rechtsregister.

Kern

Een vector store is een cache, geen rechtsregister. Elke citatie die een agent uitspuugt heeft een live check tegen zijn source of truth nodig voordat het concept de loop verlaat.

FAQ

Waarom ving de confidence-score van de RAG-agent het verouderde citaat niet?

Confidence-scores beoordelen semantische relevantie, geen juridische geldigheid. De agent haalde een echte zaak op die daadwerkelijk paste bij de vraag. Het was alleen toevallig een ingetrokken zaak. Liveness moet apart worden gecheckt, tegen de source of truth.

Kun je de citation-freshness checks niet gewoon cachen?

We cachen resultaten vijftien minuten, nooit langer. Het hele punt van de gate is upstream-wijzigingen oppikken die het corpus heeft gemist. Een lange cache zou precies de failure mode terugbrengen die we net hebben gefikst.

Geldt dit ook voor niet-juridische RAG, zoals medisch of financieel?

Ja. Elk domein waarin de source of truth wijzigt na ingestion heeft een emit-time check nodig. Medicijn-recalls, CVE-updates, ingetrokken ISO-normen, deprecated API's. Hetzelfde patroon, andere regex, dezelfde fail-closed-regel.

Wat als Rechtspraak.nl down is wanneer de agent wil valideren?

De gate faalt closed. Het concept wordt vastgehouden en de reviewer ziet een expliciete foutmelding. We blokkeren liever een maandagochtend-conclusie een uur dan dat we een citaat versturen dat we niet tegen de bron konden controleren.

ragai agentsknowledge basecase studyarchitectureoperations

Iets bouwen?

Start een project