
Ik bouwde een demo om een beroemde AI-fout te reproduceren. Mijn baseline weigerde hem te maken.
De fout die ik niet voor elkaar kreeg
Ik begon aan deze build met de wens een specifieke, goed gedocumenteerde fout na te bootsen. AI voor patiëntmatching leest een klinische notitie als tekst, en verwart zo woorden die op elkaar lijken maar in de geneeskunde iets anders betekenen. Het canonieke voorbeeld is helder: een fase III-studie met een antistollingsmiddel sluit patiënten uit met een eerdere hartkatheterisatie, in de notitie van een patiënt staat plaatsing van een centraalveneuze katheter, een similariteitsmatcher ziet twee cardiovasculaire katheterprocedures, scoort ze dicht bij elkaar, en sluit een patiënt uit die eigenlijk geschikt was. Gepubliceerde evaluaties bevestigen dat echte modellen precies deze fout maken (Fierce Biotech, 2025). Ik wilde dat mijn demo liet zien hoe het gebeurt, en daarna hoe mijn engine het opvangt.
Dus schreef ik een eerlijke baseline die de schurk moest spelen. Entity-level TF-IDF cosine similariteit, woord- plus karakter-3-tot-5-grams, een echte vector-similariteitsmethode. Ik gaf hem zelfs een genereuze setup en cross-valideerde zijn beslissingsdrempel in zijn eigen voordeel (gestratificeerde 3-voudige ROC, Youden's J, seed 13, uitkomend op t = 0.6932), want een stroman bewijst niets. Daarna draaide ik de hartkatheterisatie-casus en wachtte op de onterechte uitsluiting.
Die kwam niet. De baseline scoorde de twee katheterfrases ruim onder zijn eigen cross-gevalideerde drempel en gaf terug: geschikt. De fout waarvoor ik de hele demo had gebouwd, liet zich simpelweg niet reproduceren.
Ik was van plan een beroemde fout in scène te zetten en ontdekte dat mijn eerlijke schurk te zwak was om hem te begaan.
De reden bleek leerzaam, en ik wil er precies over zijn omdat het gemakkelijk is om te overdrijven. Een schrale lexicale baseline produceert die specifieke valse uitsluiting niet. Daarvoor zijn dichte semantische embeddings nodig om die twee frases dicht genoeg bij elkaar te trekken om de drempel te raken. Een zwaar embeddingmodel toevoegen zou de demo hebben opgeblazen tot iets dat je niet offline met één commando kunt draaien, dus ik maakte een keuze: houd de baseline eerlijk en schraal, en stop met doen alsof hij een misdaad begaat die hij niet kan begaan. Die beslissing herordende het hele stuk dat ik bouw. Als je het zelf wilt draaien, staat het op veriprajna.com/nl/demos/redeneerengine-voor-geschiktheid-van-klinische-trials.
Wat is een centrale lijn eigenlijk?
Ik hield de hartkatheterisatie-casus toch aan, omdat die iets beters bleek te bewijzen dan een betrapte fout. Hij bewijst waarom het antwoord van mijn engine überhaupt betrouwbaar is. Beide concepten hier hebben echte, controleerbare SNOMED-CT-identifiers. Centraalveneuze katheterisatie is 392230005. Hartkatheterisatie is 41976001. Je kunt beide in elke openbare SNOMED-browser plakken en bevestigen dat ze op verschillende takken van de hiërarchie zitten. Er is geen is-a pad van de ene naar de andere. Een centrale lijn is geen hartkatheterisatie, en alleen een hiërarchie weet dat.
Dat is de hele stelling in één rand van een graaf. Een similariteitsscore kan "is-a" niet representeren. Hij kan alleen "deze strings lijken op elkaar" representeren, en op elkaar lijken is niet hetzelfde als hetzelfde betekenen. Wanneer mijn engine de uitsluiting "geen eerdere hartkatheterisatie" evalueert, scoort hij niets. Hij stelt een structurele vraag: valt het geverifieerde feit van de patiënt onder het verboden concept? Hij wandelt door de ontologie, vindt geen subsumptiepad, en geeft terug: geschikt, met een driestappenspoor dat beide concept-ID's en de graafrand die hij heeft gecontroleerd noemt.

Toen ik dat spoor voor het eerst zag renderen, was het niet het oordeel dat me trof. Het was de kwitantie eronder. Het vak van de baseline op hetzelfde scherm toont een similariteitsgetal zonder herkomst en zonder iets reproduceerbaars. Het vak van mijn engine noemt de twee SCTID's en de exacte is-a vraag die hij stelde. Eén hiervan kan een toezichthouder archiveren. Het andere is een getal met een schouderophalen eraan. Dat contrast, niet een betrapte fout, is wat de hartkatheterisatie-casus werkelijk oplevert.
De patiënt die de matcher werkelijk weggooide
Ik had nog steeds een echt verloren patiënt nodig, dus ging ik op zoek naar waar mijn eerlijke baseline echt faalt, en ik vond het in één woord: niet. Het synthetische heldendossier P-074 bevat de notitieregel "No evidence of diabetes." Een van de uitsluitingen van het oncologieprotocol is "geen diagnose van diabetes mellitus." De vectorbaseline ziet het token "diabetes" vlak naast het "diabetes" van het criterium en matcht ze op similariteit 1.0. Een perfecte score. Hij heeft geen model van negatie, dus leest hij een zin die diabetes uitsluit alsof die diabetes insluit, en hij sluit een patiënt uit die geschikt was.
Dit is de patiënt die de matcher weggooit, en dit is de slag die ik oorspronkelijk van de hartkatheterisatie-casus had verwacht. Negatie is waar een schrale baseline eerlijk breekt, op zijn eigen beste drempel, zonder gemanipuleer.

Ik blijf nadenken over hoe stil deze mislukking is. Er is geen foutmelding, geen low-confidence-vlag, geen signaal dat er iets misging. De score is 1.0, het hoogst mogelijke, het meest zelfverzekerde dat het systeem ooit kan zijn. De baseline is nooit zekerder dan precies op het moment dat hij het meest fout zit. Een coördinator die een wachtrij hiervan beoordeelt, heeft geen manier om te weten dat deze specifieke perfecte match een patiënt is die had moeten worden ingeschreven. Vermenigvuldig dat over een protocol en je begrijpt waarom 80% van de studies hun inschrijvingstermijnen mist (sectorconsensus, 2025), en waarom elke screeningmislukking gemiddeld ongeveer $1,200 kost (Antidote.me, 2025).
De baseline was nooit zelfverzekerder dan precies op het moment dat hij het meest fout zat. Dat is geen bug die je weg kunt tunen. Het is een categoriefout.
Waarom leeft het oordeel buiten het model?
Ik nam vroeg één architecturale beslissing die ik inmiddels als de enige zie die ertoe deed, en dat was om het taalmodel volledig weg te houden van het oordeel. Er is precies één probabilistische stap in de hele pijplijn. Een provider-verwisselbaar model, alleen adviserend, leest de rommelig proza en stelt kandidaatfeiten voor, elk met de letterlijke span waaruit het het feit las en een kandidaat-concept-ID uit een kleine gesloten vocabulaire. Dat is het enige waar een model echt goed in is: lezen. Het krijgt geen stem over wie geschikt is.
Alles daarna is deterministische code die ik kan auditen. Voordat enig voorgesteld feit een beslissing bereikt, daagt een adversariële verifier het uit tegen de letterlijke notitie met drie checks: is de span daadwerkelijk aanwezig, is hij ontkend, en is het subject de patiënt en niet een familielid. Het feit "No evidence of diabetes" faalt de negatiecheck en bereikt de engine nooit. Op hetzelfde dossier faalt "Family history of breast cancer" de subjectcheck, omdat die voorgeschiedenis bij een familielid hoort en niet bij de patiënt, en het wordt gemarkeerd als REJECTED met de mislukte check bij naam.

Over de volledige gold set wees deze verifier 7 feitinstanties, 3 afzonderlijke slechte feiten af (een ontkende diabetesvermelding, een toeschrijving van familiegeschiedenis van borstkanker, en een geplant gehallucineerd medicijn zonder ondersteunende span), verspreid over 4 van de 13 gescoorde casusruns, allemaal voordat ze een oordeel konden raken. Wanneer mensen me vragen "hoe vertrouw ik wat de agent uit mijn notities haalde," is dit paneel het hele antwoord. Ik vraag je niet om het te vertrouwen. Ik laat zien wat het voorstelde en wat werd weggegooid en waarom.
Dan is het oordeel zelf gewoon Python buiten het agentframework: een deontische-logica-engine die verboden, temporele uitzonderingen en vereisten evalueert over de ontologie en wat datumrekenwerk. Een model kan deze poort niet overrulen, omdat het model niet in de kamer is wanneer de poort draait. Dat is ook wat de engine reproduceerbaar maakt. Wanneer de logica deterministische code over een vaste ontologie is, levert het opnieuw draaien van hetzelfde dossier byte voor byte hetzelfde antwoord op, elke keer opnieuw.
Het model leest. Het stemt niet. Die ene grens is wat een herhaling byte-identiek maakt.
Het enige cijfer waar mijn lezer in clinical operations om gaf
Ik besteedde weken aan het optimaliseren van metrics waarvan ik mezelf uiteindelijk toegaf dat de koper er niet wakker van ligt. Beslissingsnauwkeurigheid is een leaderboardcijfer. De persoon die haalbaarheid beheert bij een sponsor of CRO vergelijkt geen leaderboardscores. Die ziet een inschrijvingstijdlijn wegglijden, en elke dag uitloop is duur. Het Tufts CSDD Impact Report (2024) zet de kosten van een inschrijvingsvertraging op grofweg $800K per dag aan misgelopen receptenverkoop, en hoger in de therapeutische gebieden die deze demo raakt: ongeveer $840K per dag in oncologie en $1.4M per dag in cardiovasculair. Protocolcomplexiteit is sinds 2005 met 139% gestegen in studieprocedures (IQVIA, 2026), wat meer criteria betekent, meer clausules, en meer plekken waar een tekstmatcher er één mis kan hebben.
Dus stopte ik met vooroplopen met nauwkeurigheid en begon ik met het cijfer dat echt op die pijn aansluit: geschikte patiënten die je niet verloor. Op een vaste gelabelde gold set van 13 cases getrokken uit 7 synthetische patiënten over 2 synthetische protocollen verliest mijn engine 0 geschikte patiënten. De eerlijke baseline verliest 3. Dezelfde set, dezelfde drempel cross-gevalideerd in het eigen voordeel van de baseline.

Ik wil precies zijn over wat die cijfers wel en niet zijn. Het is de eigen output van de harness op die ene vaste set van 13 cases, geen open-world-belofte. De 100% is "100% op deze gold set," nooit "altijd goed." Ik ga je niet vertellen dat TrialProof nooit fout zit, omdat ik de data niet heb om dat te zeggen en ik niemand zou geloven die dat wel deed. Wat ik kan zeggen is smaller en, denk ik, nuttiger: op deze set verliest de engine nul geschikte patiënten, elke beslissing draagt een reproduceerbaar spoor, twee beslissingen onthielden zich veilig met NEEDS-REVIEW wanneer een vereiste labwaarde of vitaal gegeven ontbrak in plaats van te gokken, en opnieuw draaien van de hele set was byte-identiek, 13 van 13. Alle patiënten, notities en protocollen zijn synthetische fixtures, nergens echte dossiers. Je kunt elk van die runs bekijken op veriprajna.com/nl/demos/redeneerengine-voor-geschiktheid-van-klinische-trials.
Het cijfer waar ik om geef is niet nauwkeurigheid. Het is de geschikte patiënten die ik niet weggooide. Op deze set is dat nul verloren tegen de drie van de baseline.
Er zit ook een regelgevende vorm aan, en die noem ik voorzichtig. De Clinical Decision Support-richtlijn van de FDA van januari 2026 is het relevante kader voor een human-in-the-loop matchinghulp zoals deze. Elke beslissing die de engine uitzendt, kan worden geëxporteerd als een CDISC SDTM IE-record, één rij per patiënt en criterium, met het oordeel, het redeneerspoor, de concept-ID's en de deontische operatie. Dat is geen clearance en ik claim er geen. Het is afstemming en richting. Maar het betekent dat het spoor geen debugginggemak is. Het is een archiveerbaar artefact, en het bestaat per constructie bij elke beslissing in plaats van als een bijgedachte.
Waar ik steeds op terugkom
Ik keer steeds terug naar het moment waarop mijn schurk weigerde zijn rol te spelen, omdat het de vraag veranderde die ik stelde. Drie jaar lang vraagt het vakgebied hoe we het model beter kunnen laten beslissen wie geschikt is. Betere prompts, grotere context, meer retrieval, allemaal gericht op het zo betrouwbaar maken van een probabilistisch systeem dat het mag oordelen over de inschrijving van een patiënt. Ik zat het eerste stuk van deze build ook in die framing, en probeerde een model op een fout te betrappen zodat ik het model kon repareren.
Wat uiteindelijk klikte, is dat het de verkeerde laag was. Een similariteitsscore kan "is-a" niet representeren, kan "niet" niet representeren, en kan "tenzij de therapie meer dan twaalf maanden vóór randomisatie was voltooid" niet representeren. Geen enkele hoeveelheid prompting voegt dat toe, omdat het geen taalproblemen zijn. Het zijn logicaproblemen. Dus de seniorzet is niet om het model betrouwbaar te maken. Het is om vertrouwen overbodig te maken. Laat het model het ene doen waar het goed in is: proza lezen en feiten voorstellen met de span waaruit het ze las. Laat daarna een verifier weggooien wat de notitie niet ondersteunt, en laat gewone, auditbare code over een medische ontologie het oordeel berekenen.
Geschiktheid moet worden berekend, niet voorspeld. Wat ik van tevoren niet had verwacht, was dat de beloning helemaal niet als een benchmark zou aanvoelen. Het voelt als een kwitantie. Hetzelfde dossier geeft elke keer hetzelfde antwoord, het antwoord noemt de concept-ID en de graafrand die het besliste, en het cijfer waar een haalbaarheidslead echt wakker van ligt daalt naar nul weggegooide geschikte patiënten.
En als je het liever bekijkt dan mij het te horen beschrijven, hier is het hele ding end-to-end draaiend.
Dus hier is de vraag die ik niet stop met omdraaien, en ik zou echt graag willen weten hoe jij hem beantwoordt. Wanneer de inzet de kans van een echt persoon op een studie is, waar wil je dat je vertrouwen leeft: in een model dat je moet geloven, of in code die je kunt lezen?


