RAG
RAG-citaties: het vangnet dat we na de Duitse zaak shippen
Een Duitse rechter heeft gehallucineerde antwoorden het probleem van de uitgever gemaakt, niet van het model. Dit is de check die we nu draaien voor elk RAG-antwoord.

Het is een dinsdag in mei. Je pusht de nieuwe RAG-helpbot om 14:00 naar productie, kijkt hoe de eerste honderd gesprekken binnenstromen en voelt de stille trots van een ding dat eindelijk de vraag beantwoordt in plaats van naar de FAQ te wijzen. Op vrijdag gaat er een screenshot rond in de Slack van je klant. De bot heeft met overtuiging een retourbeleid verzonnen dat niet bestaat. Je support lead vraagt, met wat doorgaat voor geduld, van wie dit probleem eigenlijk is.
Tot vorige week was het eerlijke antwoord in de meeste Nederlandse SaaS-shops een schouderophalen en "het model". Dat antwoord werkt niet meer.
Wat de Duitse uitspraak feitelijk zegt
Een Duitse rechter heeft Google aansprakelijk gehouden voor onjuiste beweringen in zijn AI Overviews. De juridische details worden de komende jaren uit elkaar geplukt, maar de vorm van de uitspraak is wat telt voor iedereen die een chat-agent shipt: de partij die de gegenereerde zin publiceert, draagt de gevolgen van die zin. Niet de modelaanbieder. Niet de retrieval index. Het product dat de woorden op het scherm zette.
Als je een klantgerichte RAG-agent draait op een .nl-domein, behandel dat dan als je nieuwe uitgangspunt. Duitsland is Nederland niet, maar het Nederlandse en Duitse consumentenrecht rijmen meer dan ze verschillen, en de transparantieverplichtingen van de EU AI Act liggen bovenop beide. Engineeringkringen merkten het binnen een week op; de rest van de kamer is inmiddels bijgepraat.
Waarom "het model zei het" geen verdediging meer is
Een RAG-stack voelt veiliger dan een rauwe LLM-call omdat er een retrieval-stap is, en die retrieval-stap voelt als grounding. Het is geen grounding. Het is suggestie. Het model leest de opgehaalde chunks zoals een vermoeide junior om 23:30 zijn bronmateriaal leest: meestal accuraat, met af en toe een zelfverzekerde aanvulling in de gaten.
In onze eigen productielogs over veertien live agents is de failure mode bijna nooit "model verzint uit het niets". Het is "model parafraseert drie echte chunks en plakt er een vierde zin aan die ze aan elkaar knoopt met een feit dat in geen van de drie staat". Die vierde zin is degene die in het screenshot belandt.
Het verificatiegat dat niemand wil dichten
De meeste teams hebben één van drie patronen gelanceerd. De eerste shipt de rauwe modeloutput en bidt. De tweede plakt een regel "verzin niets buiten de volgende context" in de system prompt en noemt dat grounding. De derde vraagt het model om te citeren en zet de citaten netjes naast het antwoord.
Geen van deze controleert iets. De eerste twee zijn open-loop. De derde is de wrede: toen we eerder dit jaar de bot van een klant auditten, wees 18% van de inline [1][2]-markers naar chunks die de geciteerde bewering helemaal niet bevatten. De gebruiker ziet een citatie en vertrouwt het. Maar die citatie is gegenereerd door precies hetzelfde proces dat ook de hallucinatie produceerde.
De uitspraak heeft geen boodschap aan welk van de drie patronen je hebt gelanceerd. Het gaat om de zin die gepubliceerd is.
Het drieregelige vangnet dat we nu standaard shippen
Elke nieuwe RAG-agent die we shippen draait een post-generatie check voordat de response de server verlaat. Het idee is ouder dan de uitspraak; de urgentie niet. De check is niet glamoureus en niet nieuw. Het is het ding dat elk team van plan was te bouwen en nooit echt prioriteit gaf.
import numpy as np
from nltk.tokenize import sent_tokenize as split_sentences
# embed(text) -> np.ndarray; use your provider's embeddings endpoint.
def cosine(a, b):
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
def verify_citations(answer: str, chunks: list[str], embed, threshold: float = 0.78) -> list[str]:
unverified = []
for sent in split_sentences(answer):
if max(cosine(embed(sent), embed(c)) for c in chunks) < threshold:
unverified.append(sent)
return unverified
Drie regels echte logica, ingepakt in een functie zodat we het kunnen loggen. De volledige integratie is dikker (sentence splitting respecteert de citatie-markers, de embedding call wordt gebatcht, de threshold zit per tenant in de config), maar de kern is de loop hierboven. Elke zin in het antwoord moet minstens één opgehaalde chunk hebben die er sterk genoeg op lijkt om als bron te tellen. Als een zin dat niet heeft, hebben we een keuze te maken, en die maken we expliciet.
In de praktijk routeren we het gezakte antwoord door één van drie handlers, op volgorde. Eerst: schrap de gezakte zin en render opnieuw; staat het antwoord ook zonder die zin overeind, ship dan de ingekorte versie. Tweede: draagt de zin het hele antwoord, regenereer dan één keer met de gezakte zin teruggequote in de prompt als "beweer dit niet zonder bron". Derde: zakt de hergeneratie ook, geef dan een nette "ik heb geen betrouwbare bron voor dit, hier is de dichtstbijzijnde passage die we hebben"-response terug en haal er een mens bij.
De threshold van 0.78 is geen magie. Het is wat de long tail van "model heeft twee chunks aan elkaar gelijmd met een derde feit" eruit filtert, zonder antwoorden te wurgen die één chunk netjes parafraseren. Tune het op je eigen corpus. Wij hertunen het maandelijks per klant.
Cosine similarity vangt parafrase, geen rekenwerk. Als je agent zegt "de korting is 15% dus het totaal is €127,50", laat de verifier vrolijk een fout getal door dat een goed getal parafraseert. Getallen vragen een aparte symbolische check.
Wat het eerlijk gezegd niet vangt
Het vangnet pakt de failure mode die het screenshot produceert. Het pakt geen model dat de verkeerde chunk ophaalt en die trouw parafraseert. Het pakt geen model dat drie correcte chunks samenvat tot een misleidend geheel. Het pakt geen toon, bias, of het zelfverzekerd-behulpzame antwoord dat technisch klopt en operationeel rampzalig uitpakt.
Daar zijn andere lagen voor: een retrieval evaluator, een answer-level judge, een kleine set red-team probes die bij elke prompt-wijziging in CI draait. Wij draaien ze allemaal op de agents die geld raken. Sommige modelaanbieders shippen inmiddels native citatie-primitieven die de verifier-loop kunnen verkorten (de citations-feature van Anthropic is de strakste in de bus), maar de server-side check blijft van jou. De citatieverifier verdedigt tegen precies de faal die de Duitse rechter zojuist duur heeft gemaakt, en is de goedkoopste van de vier om toe te voegen.
Als je dit kwartaal maar één nieuwe check shipt, ship deze.
De audit van vijf minuten die je vandaag kunt draaien
Trek je laatste 200 productiegesprekken op. Voor elk assistant-bericht met een inline citatie, grep de geciteerde chunk op de zelfstandig naamwoorden uit de geciteerde zin. Bevat minder dan 90% van de citaties de bewering daadwerkelijk, dan produceert je bot nu al de zin die de volgende eiser zal screenshotten. Toen we eerder dit jaar de support-agent bouwden voor een Nederlandse logistieke SaaS, was dit gat precies wat ons beet; we hebben de verifier hierboven uiteindelijk geshipt als laatste server-side hop voordat de response stream opengaat naar de client. Het is één van de saaie lagen van het goed bouwen van AI-agents, en degene die de oprichter laat slapen.
Kern
Ship je een klantgerichte RAG-agent, verifieer dan elke zin tegen de opgehaalde chunks voordat hij de server verlaat. De uitgever is nu eigenaar van die zin.
FAQ
Wat heeft de Duitse rechter precies beslist?
Een Duitse rechter hield Google aansprakelijk voor onjuiste beweringen in zijn AI Overviews. De uitgever van de gegenereerde zin draagt het risico, niet de onderliggende modelaanbieder.
Geldt dit ook voor een Nederlandse SaaS?
Het Nederlandse en Duitse consumentenrecht overlappen sterk, en de EU AI Act dekt beide. Ga uit van dezelfde aansprakelijkheidsvorm tot een Nederlandse zaak het tegendeel zegt.
Lost het tonen van citaties naast het antwoord het probleem op?
Nee. In één klantaudit wees 18% van de inline citatie-markers naar chunks die de geciteerde bewering niet bevatten. De citatie moet geverifieerd worden, niet alleen getoond.
Welke cosine similarity threshold moet ik gebruiken?
Wij beginnen op 0.78 en tunen maandelijks per corpus. Te hoog wurgt echte parafrase; te laag laat zelfverzekerde aanvulling door. Er bestaat geen universele waarde.