AI agents
AI-agent voor het lab: een LIMS-playbook met citaties
Een chemiebedrijf van 38 mensen in Eindhoven verwerkt 620 syntheseplannen per week. Zo bouwden we de agent die ze allemaal samenvat en elke CAS-lookup citeert.

Dinsdagochtend in Eindhoven. De QA-chemicus opent haar review-queue en ziet 124 syntheseplan-samenvattingen klaarstaan. Elke samenvatting moet getoetst worden aan gevaarsclassificaties, reactiecondities en de interne SOP's van het lab. Vrijdag heeft ze meer chemie gelezen dan een postdoc in een hele maand. Haar oudere collega zat afgelopen weekend vijf uur aan zijn keukentafel om de vorige batch met de hand op te stellen. Dit was de bottleneck die we moesten wegnemen.
De klant is een MKB-bedrijf in speciale chemicaliën met 38 mensen, dat R&D en kleinschalige productie doet voor Europese farma. Hun LabWare LIMS draait sinds 2003. Tweeëntwintig jaar schema-drift, tweeëntwintig jaar in-house plugins, en een berg RTF-blobs in wat technisch gezien een relationele database is. Niets van betekenis verlaat dat systeem. Wat we ook bouwden, het moest eruit lezen, nooit eromheen.
De bottleneck van 620 plannen
Het lab draait ongeveer 620 syntheseplannen per week, verdeeld over twee locaties. Zo'n 18% zijn herhalingen met kleine variaties; de rest is maatwerk. Elk plan raakt tussen de vier en tweeëntwintig stoffen. Elke stof heeft een CAS-nummer, een GHS-classificatie en een stapel reactieconditie-flags die een QA-chemicus moet verifiëren tegen actuele brondata voordat het plan naar de werkbank gaat.
De senior chemicus die de wekelijkse samenvattingen opstelt doet dit zaterdagochtend aan zijn keukentafel. Hij is uitstekend. Hij is ook één griep verwijderd van zes weken vertraging. De plantmanager belde ons in maart 2026 en vroeg, ongeveer letterlijk, of een AI LabWare kon lezen en de samenvattingen kon schrijven. We zeiden ja, onder voorwaarden. Die voorwaarden zijn waar deze post over gaat.
De architectuur in één alinea
Elke nacht trekt een read-only ODBC-view de syntheseplannen van die week binnen als parquet-snapshot aan onze kant. Een parser pakt de RTF- en JSON-blobs uit naar gestructureerde planrecords. Elke genoemde stof krijgt zijn CAS-nummer gevalideerd via de check digit en wordt opgezocht tegen vooraf opgehaalde GESTIS- en ECHA-entries die als versioned cache in Postgres staan. De drafter (een LLM met structured output) mag alleen een gevaars- of conditie-uitspraak schrijven als hij een citation-ID koppelt naar één van die opgehaalde entries. Drafts die de citatiepoort niet halen, komen nooit in de QA-queue. Drafts die slagen gaan in een eenvoudige HTML-goedkeuringsapp waar de QA-chemicus ze accepteert, afwijst met een reden, of inline aanpast. De geaccepteerde draft wordt teruggeschreven naar LabWare via dezelfde ODBC-brug als gestructureerd plan-samenvattingrecord, met de citation-ID's bewaard in een audittabel.
Dat is het hele systeem. Elke interessante beslissing zit in één van die zinnen.
Citaten vóór tokens
De hardste regel die we hebben afgedwongen: het model mag geen stoffeit verzinnen. Niet 'wordt afgeraden', niet 'wordt bestraft'. Structureel niet toegestaan.
Het mechanisme is onspectaculair. Voordat de drafter een syntheseplan ziet, halen wij elk CAS-nummer uit het plan en pre-fetchen we de bijbehorende GESTIS- en ECHA-records. Die records krijgen een korte, stabiele citation-ID, bijvoorbeeld gestis:50-00-0:2026-05-01. Het model krijgt een structured-output schema waarin elke gevaarszin een cited_from-array van die ID's vereist. Komt een zin zonder citation-ID terug, dan wijst de parser de hele draft af en triggert een hergeneratie met een strakker prompt.
Dit is de saaie truc die het systeem betrouwbaar maakt. De chemicus hoeft niet te twijfelen of het model de LD50-waarde voor formaldehyde heeft verzonnen. Het getal staat er alleen omdat de GESTIS-entry waar het uit komt eraan vastzit, en de hover in haar queue toont dat GESTIS-fragment direct ernaast.
Laat je een LLM gevaarsteksten schrijven zonder elke claim aan een vooraf opgehaalde bron te koppelen, dan heb je een hallucinatiemotor met een scheikundewoordenschat gebouwd. Citatie-vóór-token is geen extraatje voor regulated werk. Het is de enige modus die productie mag halen.
CAS-checksums vangen veel nepnummers
Voor we überhaupt iets opzoeken, valideren we de check digit van het CAS-nummer. Verrassend veel slechte LLM-outputs die we in vroege tests vingen waren CAS-strings die simpelweg niet valideerden. De cycles die het kost zijn triviaal:
def validate_cas_checksum(cas: str) -> bool:
"""CAS Registry Numbers carry a check digit. Reject anything that fails."""
parts = cas.split("-")
if len(parts) != 3 or not all(p.isdigit() for p in parts):
return False
digits = parts[0] + parts[1]
check = int(parts[2])
total = sum(int(d) * (i + 1) for i, d in enumerate(reversed(digits)))
return total % 10 == check
assert validate_cas_checksum("50-00-0") # formaldehyde
assert not validate_cas_checksum("50-00-9") # corrupted
Elk CAS dat de checksum niet haalt wordt gemarkeerd, de draft pauzeert en het plan gaat terug naar LIMS met een CAS-suspect tag. Dat gebeurde 14 keer in de eerste elf weken. Twee keer ving het een typo die een mens in 2019 had ingevoerd. Dat het systeem oude datafouten boven water haalt stond niet in de briefing, maar het is nu de feature waar de QA-lead het meest over praat.
Een 22 jaar oude LIMS lezen zonder hem stuk te maken
LabWare is een serieus stuk software. Het is hier ook twee decennia lang op locatie aangepast. We hadden drie regels bij de start:
- We schrijven nooit direct naar de live LIMS-database. Schrijven gaat via de ondersteunde ODBC-brug met een serviceaccount dat geaudite insert-rechten heeft op twee tabellen en verder niets.
- We gaan nooit uit van het schema. Elke nachtelijke snapshot begint met een schema-fingerprint check; is een kolom door een interne admin hernoemd (dat is twee keer gebeurd), dan stopt de pipeline en pingt Slack de IT-contactpersoon van het lab.
- Alles wat in de brondata op een plan-body lijkt wordt op drie manieren geparset (RTF, plaintext, embedded XML) en de parsers stemmen. Verschillen worden plannen met een vlag die QA zonder hulp beoordeelt. Ongeveer 1,8% van de plannen belandt daar.
De verleiding bij een legacy LIMS is om hem te moderniseren. Doe het niet. De chemici vertrouwen LabWare omdat het hun data al tweeëntwintig jaar accuraat bewaart. De agent is een laag die eruit leest en een smal, goed gedefinieerd record erin terugschrijft. Dat is de hele relatie.
De queue die chemici echt gebruiken
De QA-goedkeuringsapp is gewone server-rendered HTML. Geen SPA. Geen client routing. Hij laadt in ongeveer 180ms op de Dell-desktops van het lab, die nog steeds op Windows 10 draaien en dat blijven doen tot de refresh van volgend jaar. Eén toets accepteert. Eén toets wijst af, met een verplichte vrije-tekst reden die teruggevoed wordt in de retrieval-set. De citation-ID's renderen als kleine inline chips; over een chip hoveren opent het echte GESTIS- of ECHA-fragment dat de zin onderbouwt.
We bouwden drie prototypes voor deze. Twee waren React. De chemici haatten ze allebei, om dezelfde reden: alles wat flitste, animeerde of een tweede klik vereiste brak hun flow. De HTML-versie, geserveerd vanuit een kleine Flask-app achter hun VPN, won in één middag usability-testen.
Bronverval en de citatiecache
ECHA-classificaties veranderen. GESTIS update. Een gevaarsuitspraak die in maart klopte, klopt in augustus soms niet meer. Het naïeve ontwerp cachet een stof-entry één keer en vergeet hem. Wij deden dat niet.
De citatiecache in Postgres is gepartitioneerd op bron en snapshot-datum. Elke partitie bevat de GESTIS- of ECHA-entries die op een gegeven dag zijn opgehaald. Komt er een nieuwe snapshot van een stof, dan gaat hij in de huidige partitie; verloopt een partitie buiten de bewaartermijn van 18 maanden, dan droppen we hem in zijn geheel. Een thread die vorige week op Hacker News rondging zei het bot: de enige schaalbare delete in Postgres is DROP TABLE. Voor een cache met miljoenen kleine rijen is partition-and-drop wat de storage vlak houdt en index-bloat ervan weerhoudt query-tijden om zeep te helpen. Hetzelfde principe geldt voor embedding-tabellen in elk RAG-systeem dat zijn bandbreedte waard is.
Elk citation-ID bevat de snapshot-datum, dus opent een QA-chemicus zes maanden later een goedgekeurde draft, dan toont de hover het GESTIS-fragment zoals het was op de dag dat de draft geschreven werd, niet de versie van vandaag. Dat is wat auditors willen zien. Het is ook de goedkoopste verzekering die we ooit verkocht hebben.
Wat er als eerste brak, en wat we maten
Drie dingen braken in de eerste zes weken productie.
Stale snapshots die ongemerkt de drafter voedden. De ODBC-brug crashte op een vrijdagmiddag. De agent bleef tot maandag drafts genereren tegen de data van donderdag. Fix: een heartbeat die generatie blokkeert als de snapshot ouder is dan 12 uur, plus een Slack-alert die na de tweede gemiste cyclus escaleert.
Drafts die technisch klopten maar volgens de huisstijl verkeerd waren. Het lab heeft interne benamingsconventies; sommige stoffen hebben bijnamen die ouder zijn dan de chemici die ze gebruiken. We voegden een woordenlijst toe aan de retrieval-set en de drafter matcht nu in ongeveer 96% van de drafts de huistaal.
QA-vermoeidheid op bijna identieke herhaalplannen. 18% van de plannen zijn kleine varianten op dezelfde familie. We voegden een diff-tegen-laatst-geaccepteerde-view toe, zodat de chemicus alleen ziet wat sinds vorige week veranderd is. Review-tijd op herhalingen daalde met ongeveer 70%.
Elf weken later ging de schrijftijd van de senior chemicus van vijf uur per week naar zo'n 35 minuten review. De QA-afwijzingsratio op agent-drafts staat op 4,1%, tegen een baseline van 6,2% die we maten op de eerdere mens-only workflow. We sampleden 1.100 gevaarszinnen uit productie-drafts op citatieaccuratesse. Drie waren fout: één stale snapshot, één CAS-botsing tussen twee stoffen met vergelijkbare handelsnamen, één parser-bug die we in week acht hebben gefixt. Nul gehallucineerde gevaarsclaims hebben productie bereikt, omdat de citatiepoort die faalmodus rekenkundig onmogelijk maakt.
Wat je vandaag kunt doen
Kijk je naar een vergelijkbare workload (regulated tekst die je opstelt tegen een verouderd system of record), dan is het kleinste nuttige dat je vanmiddag kunt doen: zet elk extern feit op een rij waar de draft over moet kloppen, en vraag je per feit af waar de bron leeft en hoe je daar een stabiele citation-ID aan vast zou knopen voordat het model één woord schrijft. Kun je dat voor de helft van de feiten niet beantwoorden, dan is de agent niet de volgende stap. De retrieval-set wel.
Toen we deze lab-notebook agent bouwden voor de site in Eindhoven, was de moeilijkste ontwerpkeuze niet het model of het prompt. Het was de beslissing om luid te falen als een citatie ontbrak, in plaats van een lekker leesbare draft te leveren. Wil je hulp bij een vergelijkbare build, dan begint onze aanpak van AI-agents voor regulated workloads vanuit precies die beperking.
Kern
Kan een regulated draft niet voor elk extern feit de bron citeren voordat het model één woord schrijft, dan heb je geen agent gebouwd maar een hallucinatiemotor.
FAQ
Kan dit soort agent ook werken zonder een LIMS?
Ja, maar je hebt wel een gestructureerd system of record voor de plannen nodig. Een gedeelde map met Word-documenten werkt; één Excel-bestand niet. De agent heeft iets nodig dat queryable is om citaties aan op te hangen.
Wat gebeurt er als een CAS-nummer niet in GESTIS of ECHA staat?
De draft pauzeert op die stof en wordt zonder gegenereerde samenvatting naar QA gerouteerd. De chemicus kan een interne SDS koppelen, die vervolgens een nieuwe citeerbare bron in de retrieval-set wordt voor toekomstige runs.
Hoe lang duurde de bouw van kickoff tot productie?
Negen weken. Drie daarvan gingen op aan het parsen van verouderde RTF-blobs uit LabWare. Het werk aan model en prompt was minder dan twee weken echte inspanning.
Waarom server-rendered HTML in plaats van een moderne frontend?
Lab-desktops zijn traag, chemici werken in korte gefocuste bursts, en elke animatie of routing-vertraging brak hun flow. Platte HTML laadde in 180ms; de React-prototypes niet.