← Blog

RAG

Cited RAG voor pomp-datasheets: een case uit Dordrecht

Een Dordtse pompendistributeur van 27 mensen beantwoordt nu 1.180 technische vragen per week uit 38.000 datasheets. Elke passage verwijst eerst terug naar de bron-PDF.

Jacob Molkenboer· Oprichter · A Brand New Company· 25 dec 2025· 9 min
Half geopende eiken kaartenbak op ivoorkleurig linnen, kaart met groen tabje, messing scheider, stapel grootboekpapier.

Dinsdag, 14:07. Een applications engineer in Dordrecht staart naar een Grundfos CRN 32-3 datasheet op het ene scherm en een chatvenster van een maritieme aannemer in Rotterdam op het andere. De aannemer heeft een NPSH-curve nodig bij 1450 rpm met een andere waaiertrim dan wat op de pagina staat. De engineer heeft dit type vraag deze maand al 41 keer beantwoord. Het antwoord zit op drie plekken: pagina 8 van de PDF, een voetnoot in een KSB-engineering bulletin uit 2019, en de 15 jaar oude onderdelen-catalogus die in 2010 begon als Microsoft Access-bestand en nu in MySQL leeft achter een wankele ODBC-bridge.

Die bridge, en de 38.000 datasheets die zich erachter opstapelen, is de reden dat dit bedrijf ons belde om een cited RAG-systeem te bouwen.

Het probleem van 1.180 tickets per week

Eerst de cijfers. De distributeur, 27 mensen, vooral buitendienst en applications engineers, kreeg zo'n 1.180 technische vragen per week binnen via e-mail, WhatsApp Business en de chat-widget op hun website. Ongeveer 40% was herhaalwerk. Zo'n 25% had een parts cross-reference nodig die alleen in de verouderde catalogus bestond. De rest had een echte engineer nodig die een echte datasheet las.

Het applications-team had twee faalmodes. Of ze waren te traag (een reactietijd van vier uur op een maandagochtend betekende dat de aannemer al een concurrent had gebeld) of te snel en fout (een geoffreerde waaierdiameter die niet matchte met de curve die de klant eigenlijk nodig had).

Hun eerste reflex, zoals bij iedereen, was om er GPT tegenaan te gooien. Ze hebben het geprobeerd. De resultaten waren voorspelbaar. Het model citeerde met overtuiging een NPSH-waarde die in geen enkele datasheet stond die ze verkochten. Het verzon een Wilo-onderdeelnummer met de juiste prefix en de verkeerde suffix. Het vertelde een klant dat een pomp ATEX-gecertificeerd was terwijl dat niet zo was.

Hier stoppen de meeste bedrijven en concluderen dat AI niet voor hen werkt. Dat doet het wel. Alleen niet zo.

38.000 PDF's die niemand van kaft tot kaft had gelezen

Het eerste wat we deden, was de datasheets tellen. Het aantal, 38.142 toen we begonnen, lag een orde van grootte hoger dan de eigen schatting van het team. Hun netwerkschijf liep vol sinds eind jaren negentig. Grundfos, KSB, Wilo, plus elf kleinere merken. Versies, revisies, uitgefaseerde modellen die nog steeds in bedrijf zijn bij drie Rotterdamse rioolwaterzuiveringen.

We hebben niet alle 38.000 op dag één in een vector store gegooid. Dat is de meest voorkomende RAG-fout die we zien. Een vector store vol ongeverifieerde, ongedateerde, mogelijk verouderde documenten is een zelfverzekerde leugenaar met oneindig geheugen.

In plaats daarvan bouwden we een kleine pre-processing-pipeline:

def ingest(pdf_path: Path) -> Doc | None:
    meta = extract_meta(pdf_path)   # brand, model, revision, date
    if not meta.brand or meta.brand not in VETTED_BRANDS:
        return quarantine(pdf_path, reason="unvetted_brand")
    if meta.revision_date < CUTOFF or meta.superseded_by:
        return quarantine(pdf_path, reason="stale_revision")
    pages = render_pages(pdf_path, dpi=300)
    text = vlm_extract(pages)       # tables matter, pump curves are tables
    chunks = chunk_by_section(text, meta)
    return Doc(meta=meta, chunks=chunks, source_uri=pdf_path)

Ongeveer 9.400 PDF's belandden bij de eerste pass in quarantaine. De meeste waren vervangen revisies. Zo'n 600 waren echt verweesd, merken die de distributeur jaren geleden uit het assortiment had gehaald. Die hebben we niet verwijderd. Ze gingen in een aparte index die de agent wel kan lezen maar niet kan citeren.

Dat onderscheid, wel-lezen-niet-citeren, bleek belangrijker dan al het andere in de architectuur.

Een 15 jaar oude Access-catalogus overbruggen

De onderdelen-catalogus heeft een verhaal.

Hij is in 2010 gebouwd door de zwager van de oprichter, overdag Delphi-ontwikkelaar en in het weekend Access-hobbyist. Hij heeft 47 tabellen, waarvan drie beginnen met tbl_OUD_ en die het bedrijf sinds 2014 niet durft te droppen. In 2012 is hij gemigreerd naar MySQL 5.1 via een ODBC-bridge die niemand helemaal begrijpt, inclusief de oorspronkelijke auteur. Hij is al 14 jaar de source of truth voor parts cross-references.

Je kunt dit niet eruit rukken. We hebben het niet geprobeerd.

In plaats daarvan zetten we er een dunne read-only adapter voor die gestructureerde rijen teruggeeft die de agent als tool kan aanroepen:

@tool
def lookup_part(query: str) -> list[Part]:
    """
    Cross-reference a part number, OEM number, or free-text
    description against the onderdelen-catalogus. Returns up
    to 8 matches with manufacturer, model, supersession chain,
    and stock status. Never invents a row.
    """
    rows = catalogus.query(query, limit=8)
    return [Part.from_row(r) for r in rows]

Twee regels in die adapter waar het systeem op leunt.

De agent kan niet schrijven naar de catalogus. Nooit. Niet direct, niet via een gewrapte functie, niet via de offerte-engine.

De adapter geeft rijen terug of helemaal niets. Hij geeft nooit een best guess. Als de query nul rijen oplevert, krijgt de agent nul rijen te zien en moet hij de klant om verduidelijking vragen in plaats van een match te hallucineren.

De MySQL-bridge is, eerlijk gezegd, lelijk. De adapter niet. Dat is het contract dat we wilden.

De citation gate

Dit is het deel van de architectuur waar we het meest trots op zijn, en het is het kleinste deel van de code.

Elk antwoord dat de agent genereert, gaat door een citation gate voordat het bij de offerte-engine, de e-mailcomposer of de chat-widget aankomt. De regel is: elke numerieke claim, elk onderdeelnummer, elke certificering, elk debiet, elke druk, elk toerental moet gekoppeld zijn aan een specifieke passage in een specifiek document op een specifieke pagina. Als de gate de bronvermelding niet kan vinden, geeft de agent geen antwoord. Hij escaleert naar een mens.

Onthouden

De goedkoopste manier om te voorkomen dat een RAG-agent liegt, is hem niet laten spreken zonder voetnoot. Het lastige is dit afdwingen in code, niet in de system prompt.

De gate is zo'n 180 regels Python. Hij parseert het concept-antwoord van het model, haalt elke claim eruit die op een getal of onderdeel-id lijkt, en checkt elke claim tegen de opgehaalde context. De context draagt cruciaal genoeg provenance mee: paginanummer, document-URI, retrieved-at timestamp, en een hash van de bronbytes.

De system prompt vraagt het model om te citeren. De gate zorgt dat het ook echt gebeurt.

def gate(draft: Answer, context: list[Chunk]) -> Verdict:
    claims = extract_claims(draft.text)
    for c in claims:
        evidence = find_evidence(c, context)
        if not evidence:
            return Verdict.escalate(reason=f"uncited:{c.kind}:{c.value}")
        if evidence.confidence < 0.82:
            return Verdict.escalate(reason=f"weak:{c.kind}")
        draft.attach_citation(c, evidence)
    return Verdict.allow(draft)

De drempel van 0,82 komt niet uit de lucht vallen. We hebben hem getuned tegen een eval-set die we bouwden uit 600 echte historische tickets die het applications-team al had beantwoord. Voor elk ticket wisten we het juiste antwoord. We tuneden tot de false-positive rate (agent geeft met overtuiging een verkeerd getal) onder 0,3% lag, en accepteerden vervolgens de escalatie-ratio die daaruit kwam. Op dag één was dat zo'n 18% escalaties. Nu zit het rond de 11%.

Chunking die de engineer respecteert

Pomp-datasheets zijn geen proza. Het zijn tabellen, curves en voetnoten. Naïeve chunking, opdelen per 500 tokens, breekt ze op manieren die subtiel catastrofaal zijn. Een curve verliest zijn aslabels. Een tabel verliest zijn header-rij. Een voetnoot verliest de asterisk waar hij bij hoorde.

Wij chunken per documentsectie en gebruiken daarvoor de structuur die de fabrikanten zelf al leveren. Grundfos, KSB en Wilo publiceren hun datasheets met redelijk consistente sectie-ankers: Technical data, Performance curves, Dimensions and weights, Materials of construction. Je kunt de conventie zelf controleren door een willekeurige actuele curve op te halen uit het Grundfos Product Center — de ankers zijn al jaren stabiel. We pakken die secties eruit en behandelen elk als één chunk, met een aparte kleine chunk per individuele curve-afbeelding (gerenderd als beeld plus een via VLM geëxtraheerde tekstbeschrijving die de eenheden op de assen behoudt).

Tabellen worden behandeld als tabellen, niet als platgeslagen strings. De agent ziet ze als rijen-en-kolommen en kan ze ook zo redeneren. Klinkt vanzelfsprekend. De meeste RAG-pipelines doen het niet.

De eval-set die zichzelf terugverdiende

Zonder eval-set hadden we de citation gate niet kunnen bouwen, en zonder drie middagen van een applications engineer hadden we de eval-set niet kunnen bouwen.

600 historische tickets. Voor elk ticket markeerde de engineer het juiste antwoord, het document waar het in stond, de pagina, en één regel over het waarom. Die set werd de test harness. Elke wijziging aan de retriever, de chunker, de gate of de prompt wordt eerst hiertegen gecheckt voordat hij in de buurt van productie komt.

Dit is het onsexy deel van een RAG-systeem live krijgen. Er is geen model-upgrade en geen slimme prompt die dit vervangt. Hamel Husain hamert hier al ruim een jaar op in zijn veelgelezen stuk over why your AI product needs evals, en we zijn het met hem eens: de moat is de eval-set, niet het model. Drie middagen engineer-tijd is de goedkoopste investering die je in een RAG-systeem kunt doen dat je wilt behouden.

Resultaten, met de saaie kanttekeningen

Na vier maanden in productie worden de 1.180 vragen per week ruwweg zo verdeeld afgehandeld: 71% volledig door de agent beantwoord met bronvermelding, 18% geëscaleerd naar een engineer met de concept-tekst van de agent erbij, 11% direct naar een mens omdat de gate weigerde überhaupt een antwoord te genereren.

Mediane reactietijd in de 71%-bucket is 38 seconden. Mediane reactietijd in de 18%-bucket, waar de engineer een concept-antwoord reviewt, is 9 minuten. De overige 11% duren nog steeds wat ze altijd al duurden: een engineer met een PDF.

Het applications-team is niet gekrompen. Twee van hen besteden nu een derde van hun week aan de eval-set en aan het reviewen van de edge cases van de agent. De andere engineers hebben weer tijd voor bezoeken op locatie, wat de oprichter eigenlijk al die tijd wilde.

De offerte-engine laat de agent nog steeds niet schrijven. De agent maakt een concept-offerte met onderdelen en prijzen. Een mens klikt op goedkeuren. Dat is niet veranderd en we zijn ook niet van plan dat te veranderen.

Waarschuwing

Als je RAG-agent direct kan schrijven naar een system of record, heb je geen RAG-agent. Dan heb je een zelfverzekerde stagiair met database-credentials. Houd het schrijfpad mens-gegated tot je eval-set iets anders zegt.

Wat we anders zouden doen

Twee dingen.

We hebben in de eerste maand te lang aan de chunker gewerkt. De eerste versie was goed genoeg. De derde versie was marginaal beter en kostte ons drie weken. De eval-set had ons dit verteld als we die eerst hadden gebouwd. Dat hebben we niet gedaan. Tegenwoordig bouwen we de eval-set vóór de retriever.

We hebben onderschat hoeveel de Access-naar-MySQL-bridge voor het moreel betekende. De eerdere ervaring van het team met softwareleveranciers was dat iedereen hem wilde vervangen. Wij hebben hem niet vervangen. We hebben hem gewrapt. Het eerste gesprek waarin we zeiden "we komen niet aan de catalogus" was het moment waarop het project echt werd voor de oprichter.

Het kleinste wat je vandaag kunt doen

Pak 50 tickets van de afgelopen maand die je team al heeft beantwoord. Markeer het juiste antwoord, het bron-document en de pagina. Dat is je eval-set. Al het andere, de vector store, de chunker, het model, hangt daaronder.

Toen we de RAG-agent bouwden voor deze Dordtse distributeur, was het deel dat het langst duurde niet de retrieval-pipeline. Het was het systeem leren te weigeren een antwoord te geven als dat niet hoort. Wij leveren dit end-to-end onder AI-agents; de citation gate is een standaardmodule geworden in elke knowledge-base-build die we opleveren.

Kern

Een RAG-agent is zo eerlijk als zijn citation gate. Bouw eerst de eval-set, weiger te antwoorden zonder provenance, en houd het schrijfpad mens-gegated.

FAQ

Hoe lang duurde de build end-to-end?

Vier maanden van scoping tot productie. Ruwweg twee maanden voor ingest, chunking en de citation gate, en twee voor de eval-set, de catalogus-adapter en live tuning tegen echte tickets.

Hebben jullie de Access-naar-MySQL-catalogus vervangen?

Nee. We hebben hem in een read-only adapter gewrapt die de agent als tool aanroept. Het verouderde schema en de bestaande interne applicaties blijven hem ongewijzigd gebruiken.

Wat voorkomt dat de agent een onderdeelnummer verzint?

De citation gate. Elke numerieke claim of identifier-claim moet gekoppeld zijn aan een opgehaalde passage met provenance. Niet-geciteerde of zwak geciteerde claims triggeren een escalatie in plaats van een antwoord.

Welk model draait onder de agent?

Dat pinnen we niet vast. De retriever en de gate zijn model-agnostisch. We wisselen het generatie-model op basis van kosten en latency, en de eval-set vertelt ons binnen een uur of de wissel veilig is.

Schrijft de agent naar het offerte-systeem?

Nee. Hij maakt een concept-offerte met onderdelen en prijzen. Een mens reviewt en keurt goed. Het schrijfpad blijft mens-gegated by design, niet omdat het model niet meer zou kunnen.

ragai agentscase studyknowledge basearchitectureautomation

Iets bouwen?

Start een project