← Blog

RAG

RAG in een gereguleerd tandtechnisch lab: het playbook

Een tandtechnisch lab van 27 mensen krijgt 1.320 vragen van behandelaars per week. Zo bouwden we een RAG-agent die elke passage citeert vóór die de orderqueue raakt.

Jacob Molkenboer· Oprichter · A Brand New Company· 28 jan 2026· 9 min
Open eiken kaartenbak op licht bureau, één crème kaart op messing tab met groen lint, stapel grootboekpapier ernaast.

Dinsdag 07:40, Enschede. De QA-lead van een tandtechnisch laboratorium met 27 mensen opent haar inbox: 38 berichten van behandelaars uit de nacht. Kan ik deze zirkonia kroon cementeren met Variolink Esthetic? Is de IFU voor dit implantaat-abutment nog actueel? Welk reinigingsprotocol geldt voor deze gietlegering? Ze heeft 22.000 procedure-documenten in het QMS om antwoorden tegen te checken. Om 09:00 heeft ze er zes afgehandeld. De andere 32 wachten. Tegen vrijdag zijn er 1.320 vragen binnengekomen.

Het lab maakt kroon-en-brugwerk en implantaatwerk, jaarlijks geaudit tegen NEN-EN-ISO 13485, geregistreerd onder EU MDR. De procedurebibliotheek is grotendeels PDF, deels gescand, deels Word, en veel staat geplakt in een 13 jaar oude custom Lab-Manager in PHP 7.4, gebouwd door een developer die inmiddels met pensioen is in Twente. Twee senior tandtechnici namen het grootste deel van de vragenstroom op zich, bovenop hun werk aan de werkbank. Beiden wilden ermee stoppen en weer fulltime kronen maken.

De opdracht: bouw een RAG-agent die vragen van behandelaars accuraat beantwoordt, de exacte passage in de exacte IFU citeert, en nooit een antwoord zonder bron doorlaat naar de productie-orderqueue voor kroon-en-brugwerk. Het playbook hieronder is wat we hebben gedraaid. Het loopt nu ongewijzigd op elk gereguleerd kennisproject dat we aannemen.

De randvoorwaarde die alles bepaalt

In MDR- en ISO 13485-omgevingen ligt de waarde niet in het antwoord. Die zit in het audit trail. Een antwoord dat niet te herleiden is naar een gecontroleerd, versie-beheerd document is erger dan geen antwoord, want het maakt de auditor niet uit of het model het bij het juiste eind had. Wat telt is dat er een papieren spoor ligt.

De architectuurregel die we op dag één vastlegden: er gaat geen antwoord de deur uit zonder citatie, en elke citatie verwijst naar een document in het QMS met de huidige status effective. Vindt retrieval geen match, dan zegt de agent “geen bron gevonden”. Hij parafraseert niet uit trainingsdata. Hij improviseert niet. Het model mag alleen behulpzaam zijn voor zover het QMS daar toestemming voor geeft.

Het corpus, gesorteerd

22.000 documenten is absoluut gezien klein. Het past in elke vector database. De rommel zit, daarentegen, in de inhoud. We hebben hem in twee rondes geïnventariseerd.

De eerste ronde was structureel: PDF-met-OCR-nodig, PDF-tekst, DOCX, gescande afbeelding, BLOB-in-Lab-Manager, alleen-hyperlink. Elk type krijgt een ander extractiepad. Tesseract voor de scans, pdfminer voor de tekst-PDF's, een kleine Go-service die de BLOBs uit Lab-Manager naar een staging bucket trekt.

De tweede ronde was redactioneel. Elk document kreeg een tag voor status (effective, superseded, draft, archived), documenttype (IFU, risico-analyse, work instruction, CAPA, complaint) en productscope (kroon, brug, implantaat, ortho, uitneembaar). De retrieval-index bevat alleen documenten met de tag effective. Superseded en draft documenten worden bewaard maar gemarkeerd: vraagt een behandelaar naar een historische procedure, dan kan de agent de superseded versie vinden en expliciet waarschuwen dat die niet meer van kracht is.

document_id: IFU-CR-2024-017
title: "Cementatie zirkonia kroon - Variolink Esthetic"
status: effective              # effective | superseded | draft | archived
effective_from: 2024-11-03
superseded_by: null
doc_type: IFU                  # IFU | risk_analysis | work_instruction | CAPA
product_scope: [crown]
material_scope: [zirconia]
qms_owner: K. Veldhuis
review_due: 2026-05-01
source_path: /qms/cr/2024/IFU-CR-2024-017.pdf
sha256: 4a8c8d1f9b2e…

De retrieval-filter is dan een harde predicate: status == "effective" AND review_due > today(). Wat over de reviewdatum heen is, valt uit de antwoordpool en triggert dezelfde ochtend een Slack-melding naar de QA-owner. Die ene regel ving in de eerste week op staging negentien IFU's op die over hun reviewdatum heen waren — geen van alle urgent, maar geen van alle hadden ook beantwoord mogen worden.

Chunking die overeind blijft als een tandtechnicus 'm leest

We begonnen met chunking op character-windows. Voor de blog-scrape projecten die we eerder deden, werkte dat. Hier faalde het. Een IFU heeft een Contra-indicaties-sectie van drie regels; die over twee chunks splitsen vernietigt de betekenis. Een risico-analyse heeft een tabel met hazards; een rij doormidden knippen is erger dan hem niet ophalen.

Dus schreven we een Nederlandstalige section-parser. Die zoekt op bekende koppen (Indicaties, Contra-indicaties, Bewaarcondities, Reinigingsprotocol, Risico, Validatie), tabellen en genummerde lijsten. Elke sectie wordt één chunk, met de documenttitel, het kop-pad en het versienummer in de chunk-header.

def chunk_qms_document(doc: Document) -> list[Chunk]:
    sections = split_on_headings(doc.text, KNOWN_HEADINGS_NL)
    chunks = []
    for sec in sections:
        if sec.is_table():
            chunks.append(Chunk(
                text=sec.text,
                heading_path=sec.heading_path,
                doc_id=doc.id,
                version=doc.version,
                kind="table",
            ))
            continue
        for para_group in greedy_pack(sec.paragraphs, max_tokens=400):
            chunks.append(Chunk(
                text=f"{sec.heading_path}\n\n{para_group.text}",
                heading_path=sec.heading_path,
                doc_id=doc.id,
                version=doc.version,
                kind="prose",
            ))
    return chunks

De gemiddelde chunk landde rond de 280 tokens. De kop-context in de chunk-tekst is niet redundant. Die zorgt dat de retrieval de intentie van de vraag matcht, want behandelaars schrijven zelden zoals het QMS dat doet.

Retrieval, in twee stappen

Single-vector retrieval miste ongeveer 18% van de vragen in onze eval-set, vooral omdat behandelaar-Nederlands (“kan ik die kroon ook cementeren met…”) niet matcht met QMS-Nederlands (“Cementatieprotocol voor lithiumdisilicaat-restauraties”). We stapelden twee stappen.

Stap één is hybride: BM25 plus dense vector (we gebruiken bge-m3, die Nederlands goed aankan en klein genoeg is om zelf te hosten). Pak de top 40. Stap twee is een cross-encoder rerank, gescoped op de productklasse waar de behandelaar naar vraagt. Pak de top zes.

De cross-encoder kost ruwweg 8ms per chunk op een kleine GPU en leverde 14 punten accuratesse op de eval-set op. Hem skippen om €40 per maand op inference te besparen, had twee weken corpus-tagging weer ongedaan gemaakt.

De eval-set ís het werk

Die hadden wij niet. We hebben hem gebouwd door 600 historische vragen uit de helpdesk-module van Lab-Manager te exporteren en de twee senior tandtechnici met de hand te laten opschrijven wat het juiste antwoord was en uit welk document het kwam. Dat kostte twee weken. Het is veruit het waardevolste artefact in het project. Elke architectuurwijziging sindsdien is daar tegen gemeten. Sla je deze stap over, dan doe je geen RAG. Dan gok je in productie.

De 13 jaar oude Lab-Manager aansluiten

De Lab-Manager draait op PHP 7.4 en een MySQL 5.7-instance waar niemand aan wil zitten. Hij houdt de patiënt-order-mapping, het materiaalregister en de MDR-registratie bij — elk device dat het lab verlaat, wordt hier gelogd. Schema-wijzigingen lagen vanaf de kickoff van tafel.

Dus zaten we er niet aan. We bouwden een dunne, read-only sidecar in Go die via Debezium de MySQL-binlog volgt, voor elke order, elk materiaal en elke IFU-link Kafka-events uitstuurt, en een Postgres read model materialiseert dat de agent kan querien. De agent schrijft nooit direct naar Lab-Manager. Hij leest de Postgres-mirror, formuleert het antwoord en plaatst zijn aanbeveling als comment op de order via de bestaande REST endpoint van Lab-Manager — die de vorige developer, gelukkig, nog had gebouwd voordat hij met pensioen ging. De order zelf blijft onder menselijke controle.

Onthouden

Refactor nooit een 13 jaar oude bedrijfsapplicatie om plek te maken voor een AI-agent. Wrap 'm, mirror 'm, lees 'm. Het oude systeem blijft draaien. Het nieuwe systeem blokkeert het nooit.

De gate vóór de productiequeue

Dit is het deel van het playbook waar de auditors om geven, en het deel dat de meeste teams te dun bouwen.

Elk antwoord dat de agent produceert is een JSON-object met drie velden: answer, citations, confidence. Citations zijn pointers: document_id, version, chunk_id, heading_path. Voordat het antwoord aan een productie-order voor kroon-en-brugwerk kan worden gekoppeld, draait er een gate-functie.

def gate(answer: AgentAnswer, order: Order) -> Decision:
    if not answer.citations:
        return Decision.HUMAN_REVIEW  # no source = never auto-approve
    for c in answer.citations:
        doc = qms.get(c.document_id)
        if doc.status != "effective":
            return Decision.HUMAN_REVIEW
        if doc.product_scope and order.product not in doc.product_scope:
            return Decision.HUMAN_REVIEW
        if doc.version != c.version:
            return Decision.HUMAN_REVIEW  # cited a stale revision
    if answer.confidence < 0.75:
        return Decision.HUMAN_REVIEW
    return Decision.AUTO_APPROVE

Ongeveer 71% van de antwoorden gaat automatisch door. De rest wacht op de ochtend-review van de QA-lead. Voorheen verwerkte zij 1.320 vragen per week. Nu zijn dat er rond de 380. Die 71% zijn niet de simpele vragen — daar zitten cementatieprotocollen, legeringscompatibiliteit en reinigingsvolgordes tussen. Het zijn simpelweg de vragen waar de bron eenduidig is en de gate dat ook kan bewijzen.

Twee dingen die we in de eerste ronde verkeerd hadden

Eén: we lieten de agent in het Engels antwoorden als behandelaars in het Engels vroegen. De helft van de IFU's is alleen Nederlands, en de agent vertaalde ze braaf. Het QA-team flagde het binnen een dag: vertaalde regelgevingstekst ís niet de regelgevingstekst. We hebben antwoorden geforceerd in de taal van het brondocument en bij een mismatch een expliciete noot toegevoegd: “Source document is in Dutch; the verbatim passage is below.”

Twee: we cachten chunk-embeddings, maar niet de chunk-tekst. Als een IFU een nieuwe revisie kreeg, werd de embeddings-index bijgewerkt, maar bleef er ongeveer tien minuten lang een stale chunk-tekst-pad in de answer cache hangen. De agent citeerde dan het juiste document, met de verkeerde zin. We invalideren nu de answer cache bij elke QMS write-event, en we hashen de chunk-tekst in de citatie-pointer zodat een mismatch bij de gate wordt gevangen.

De cijfers na zes maanden

  • 1.320 vragen per week → 380 naar menselijke review, 940 automatisch beantwoord met citatie.
  • Mediane antwoord-latency: 2,1 seconden end-to-end.
  • Citatie-accuratesse op de eval-set (beantwoordt de geciteerde passage daadwerkelijk de vraag): 94,6%.
  • Twee senior tandtechnici weer fulltime aan de werkbank.
  • Nul MDR-audit findings gerelateerd aan de agent bij de laatste surveillance-audit.

De laatste regel is waar het lab om geeft. De eerste regel is wat de bouw heeft betaald.

Het playbook, samengevat

Bouw je RAG in een gereguleerde omgeving, dan zijn dit de regels die achttien maanden van dit project hebben overleefd:

  1. Citatie is geen feature. Het is het contract. Geen citatie, geen antwoord.
  2. Status-filters in retrieval verslaan slim reranken. Een effective-filter is meer waard dan welke embedding-upgrade dan ook.
  3. Section-aware chunking, geen character-windows, voor elk document dat een mens echt leest.
  4. Bouw eerst de eval-set, met de domeinexperts, op echte historische vragen. Het gaat traag. Doe het toch.
  5. Wrap het legacy-systeem. Refactor het niet.
  6. Gate de queue, niet het model. De agent zit er soms naast. De queue mag dat niet.

Toen we deze agent voor het lab in Enschede bouwden, zat het lastigste niet in de retrieval — het zat in het aansluiten van de answer gate op een 13 jaar oude Lab-Manager zonder het schema aan te raken, en in het aantonen aan de QA-lead dat elk automatisch goedgekeurd antwoord een audit zou overleven. Dit werk doen we als onderdeel van onze AI-agents-praktijk, en het playbook hierboven draaien we nu op elk gereguleerd kennisproject.

Wil je morgen beginnen: trek een sample van 50 vragen die je team vorige maand heeft beantwoord en schrijf naast elke vraag op uit welk document het antwoord kwam. Die lijst is je startende eval-set. Al het andere komt daaruit voort.

Kern

In gereguleerde RAG is de citatie het contract: gate de productiequeue op document-status en -versie, niet op model-confidence.

FAQ

Waarom geen algemene LLM met een lang context window in plaats van RAG?

Omdat de auditor een citatie nodig heeft, geen overtuigende alinea. Long-context antwoorden kunnen niet bewijzen uit welk document een claim komt, en in gereguleerde omgevingen gelden antwoorden zonder bron als afwijking.

Hebben jullie een model fine-tuned op de documenten van het lab?

Nee. Fine-tuning bakt content in de gewichten en breekt het audit trail zodra documenten veranderen. Retrieval over een versie-beheerd QMS, met een citatie-gate, is goedkoper en beter te verantwoorden.

Hoe ga je om met een document dat midden in een vraag wordt superseded?

De QMS-write triggert een cache-invalidatie en een re-index. De gate vergelijkt bovendien de geciteerde versie met de huidige effective versie, dus elke drift stuurt het antwoord naar menselijke review.

Kan een kleiner lab deze aanpak ook gebruiken?

Ja. De corpus-tagging en de eval-set doen het meeste werk. De infra (Postgres-mirror, hybride retrieval, cross-encoder rerank) draait prima op één middelgrote server voor elk lab onder de 100 medewerkers.

ragai agentsknowledge baseintegrationsarchitecturecase study

Iets bouwen?

Start een project