← Blog

Voice agents

Voice agent boekte 47 patiënten in gesloten kliniek

De telefoon van een ops-lead gaat om 09:14 op een dinsdag. Een patiënt staat voor een gesloten tandartspraktijk in Eindhoven. Haar afspraak was 09:00. Daarna nog 46 telefoontjes.

Jacob Molkenboer· Oprichter · A Brand New Company· 11 jun 2026· 8 min
Crème bakelieten telefoon van haak op donkergroen leren vloeiblad, gekrulde snoer, notitieblok met groen lint, koperen bel, rood label.

Een operations lead bij een tandartsketen met 26 medewerkers in Eindhoven pakte haar telefoon op om 09:14 op een dinsdag in mei. Een patiënt stond voor een van hun praktijken in het zuiden van de stad. De deur zat op slot. De lichten waren uit. Haar afspraak, drie dagen eerder geboekt via de AI-voice-agent van de keten, was voor 09:00.

De praktijk was met Pasen gesloten. Tien weken eerder. De locatie werd klaargemaakt voor een nieuwe huurder.

Tegen de tijd dat haar koffie koud was, hadden drie andere patiënten gebeld vanaf hetzelfde dichte pand. Aan het eind van de dag stond de teller op zevenenveertig. Zevenenveertig bevestigde boekingen, allemaal door de voice agent verwerkt, allemaal gestuurd naar een praktijk die sinds Tweede Paasdag geen tandarts meer had gezien. Niemand bij de keten had enig idee dat dit gebeurde tot die dinsdag.

Wij hebben deze voice agent niet gebouwd. Het vorige bureau van de keten had hem het jaar daarvoor opgezet, en hij draaide goed genoeg dat niemand eraan zat. De post-mortem die ze ons lieten schrijven is de reden dat dit verhaal bestaat. Namen en herleidbare details zijn op verzoek van de klant aangepast.

Wat de agent dacht te weten

De agent draaide op een redelijk schone stack. Hij beantwoordde de hoofdlijn van de keten, handelde de Nederlandse en Engelse flows af, legde verzekeringsgegevens vast, en schreef boekingen rechtstreeks in het centrale afspraaksysteem. De stem was goed. De intent classifier was goed. De wachtmuziek was goed. Patiënten vonden hem prettig.

De enige bron van waarheid voor welke locaties actief waren, was een JSON-bestand dat 's nachts synct vanuit het afspraaksysteem naar de RAG store van de agent. Dat bestand was de autoritatieve lijst van praktijken, openingstijden, services en beschikbaarheidsvensters. Alles wat de agent over een locatie zei, kwam uit dat bestand.

{
  "clinics": [
    {
      "id": "clinic-zuid",
      "name": "Tandheelkunde Eindhoven Zuid",
      "address": "Voorbeeldstraat 12, Eindhoven",
      "state": "active",
      "hours": {"mon-fri": "08:00-18:00"}
    }
  ]
}

De sync job die dat bestand schreef, was een kleine cron taak. Hij draaide om 03:00, haalde de lijst met actieve locaties op via de API van het afspraaksysteem, normaliseerde 'm, en pushte 'm naar de RAG bucket. De agent pikte het nieuwe bestand op bij zijn volgende cold start. Niets hiervan was exotisch. Alles was aan elkaar geknoopt zoals een competente leverancier het zou doen.

De Pasen-deploy die de keten brak

De interne IT-lead van de keten had een kleine wijziging gepusht naar de API van het afspraaksysteem op de vrijdag voor Pasen (begin april dit jaar). Een veldnaam in het locations endpoint veranderde van status naar state. Drie karakters. Hij meldde de wijziging de dinsdag erna in het interne standup-kanaal. De voice-agent-leverancier zat in een aparte Slack en zag het nooit.

De sync job probeerde zaterdagochtend de actieve locaties op te halen, kreeg een KeyError op status, logde de trace naar een bestand in het ephemeral filesystem van de container, en stopte.

Dat alleen was nog te herstellen geweest. Maar het script had een fallback. Als het nieuwe bestand leeg was, bleef het vorige staan. "Defensieve programmering," had het vorige bureau in een code-comment gezet. De bedoeling was om een tijdelijke API-storing te overleven. Het effect was dat het wereldbeeld van de agent bevroor op wat hij wist op Goede Vrijdag.

Datzelfde paasweekend sloot de zuid-Eindhovense praktijk voor het laatst zijn deuren. De afbouw was maanden van tevoren gepland. Het locatie-record werd op zaterdagochtend van Pasen omgezet van active naar inactive in het afspraaksysteem, precies volgens planning. Het RAG-bestand van de agent, bevroren op vrijdagavond, zei nog steeds active.

Het zei nog steeds active op die dinsdag in mei, tien weken later, toen de eerste patiënt belde vanaf de dichte deur.

Waarschuwing

Een voice agent met een 'houd het laatste goede bestand' fallback liegt met perfect vertrouwen tegen je klanten op het moment dat de databron eronder omvalt. De fallback is niet de bug. Het ontbrekende alarm op de fallback is de bug.

Waarom niemand het twee maanden doorhad

Dit is het deel dat pijn doet. Elk onderdeel in de keten had een gezonde heartbeat. De voice agent beantwoordde telefoontjes, en het dashboard zei het. Het afspraaksysteem accepteerde writes, en het dashboard zei het. De cron job draaide elke nacht, en het dashboard zei het. De logs van de cron waren zelfs groen, want het wrapper-script slikte de inner Python-error in en gaf op het fallback-pad een exit code van nul.

Drie lagen "alles is in orde, baas" bovenop een sync job die sinds 3 april geen byte meer had overgezet.

Wat het uiteindelijk opmerkte, was een patiënt op een stoep, niet een monitor. Dat is geen systeem. Dat is geluk.

Het hoofdstuk over monitoring van distributed systems in het Google SRE-boek beschrijft de failure mode helder: een dashboard gebouwd op het verkeerde signaal is erger dan geen dashboard, omdat het de neiging om te checken actief onderdrukt. Precies dat gebeurde hier. De agent had drie groene lampjes en één stale bestand, en de groene lampjes wonnen.

De regelgevingsrichting in de EU maakt de rekening alleen maar scherper. Als jouw agent met overtuiging iets onwaars vertelt aan een klant, draagt het bedrijf dat de agent runt de gevolgen. "De sync job is gecrasht" is geen verdediging die iemand hoeft te accepteren. Reden genoeg om de data-plumbing achter een agent net zo serieus te nemen als de agent zelf.

De fix in achtenveertig uur

We werden woensdagochtend gebeld. Donderdagavond was de keten weer veilig. De fix was onsexy.

Eerst vervingen we de silent-fallback logica door een hard failure. Als de locations sync de live lijst niet kan ophalen, wordt het bestand geschreven met een top-level "stale": true vlag en een timestamp. De agent leest die vlag bij opstart. Is het bestand stale, dan weigert de agent om welke boeking dan ook te bevestigen en routeert de beller naar een mens. Een geweigerde boeking is irritant. Zevenenveertig boekingen op het verkeerde adres zijn een persbericht.

def write_locations(payload, dest):
    if not payload.get("clinics"):
        # never silently keep the previous file
        raise SyncFailed("empty payload, refusing to overwrite")
    payload["synced_at"] = utcnow().isoformat()
    payload["stale"] = False
    dest.write_text(json.dumps(payload, indent=2))

Ten tweede voegden we een dead-man switch toe. De sync job pingt een Healthchecks.io endpoint bij een succesvolle write. Komt de ping niet binnen 26 uur binnen, dan krijgen zowel de ops-lead van de keten als de on-call engineer een push-notificatie. Healthchecks.io kost twintig euro per jaar. Het had dit al gevangen op zaterdagochtend van het paasweekend.

Ten derde schreven we een canary booking test van vijf regels die elke vijftien minuten draait. Hij belt de productie voice agent, vraagt naar de zuid-Eindhovense locatie, en controleert dat de agent ofwel een echte boekbare slot biedt, ofwel zegt dat de locatie dicht is. Biedt de agent met overtuiging een slot aan bij een praktijk die in de bron-database als inactief gemarkeerd staat, dan piept hij ons op.

Ten vierde, en deze had er vanaf dag één moeten zijn: we voegden een write-then-read verificatiestap toe aan de sync. Nadat het locations bestand geschreven is, leest het script het opnieuw in, parseert het, en bevestigt dat de set actieve locatie-IDs overeenkomt met de actieve set van de live API. Mismatch faalt luid.

Het patroon onder het verhaal

Voice agents (en chat agents, en email agents) hebben een failure mode die klassieke CRUD-software niet heeft. Wordt de databron van een CRUD-app stale, dan toont de app meestal een leeg scherm of een 500-error. Wordt de databron van een voice agent stale, dan verzint de agent met overtuiging vloeiende zinnen op basis van het laatste wat hij wist. De gebruiker kan het verschil niet zien.

Dit is het allerbelangrijkste om je eigen te maken voordat je een agent in productie zet. De agent is niet het risico. De data-plumbing van de agent is het risico. En die data-plumbing moet luid falen, elke laag, elke keer. Geen defensieve fallbacks. Geen ingeslikte exceptions. Geen groene dashboards bovenop dode jobs.

Kernpunt

Een AI-agent is altijd maar zo waarheidsgetrouw als de sync job erachter. Elke productie-agent verdient een dead-man switch op zijn databronnen en een canary die de agent elke vijftien minuten, voor altijd, een vraag stelt waarvan het juiste antwoord bekend is.

De audit van vijf minuten die je vanmiddag doet

Draai je een voice of chat agent in productie? Dan is dit het huiswerk. Open de cron-logs voor de job die de kennis van de agent voedt. Kijk naar de laatste succesvolle write timestamp. Is die ouder dan 36 uur, dan heb je dezelfde bug. Kijk of de writer een "houd vorig bestand bij lege payload" tak heeft. Zo ja, dan zit op die tak geen alarm, en heb je dezelfde bug in de wacht. Kijk of er, ergens, iemand daadwerkelijk een notificatie zou krijgen als de sync vanavond stopt. Is het antwoord "het dashboard wordt geel," dan is dat geen antwoord.

Toen we de voice agent voor de tandartsketen herbouwden, was het stuk dat het langst duurde niet de stem, niet de prompts, niet de booking flow. Het was de saaie observability-stellage eronder en de gating-logica die ervoor zorgt dat de agent weigert antwoorden te verzinnen wanneer de ground truth onzeker is. Dat is het werk achter elke AI-agent die we opleveren.

Kies vandaag één productie-agent. Vind zijn sync job. Voeg een dead-man switch en een canary toe. Dat is het hele huiswerk.

Kern

Een AI-agent is altijd maar zo waarheidsgetrouw als de sync job erachter. Bouw een dead-man switch en een canary op zijn databronnen voordat je de stem in productie zet.

FAQ

Hoe kon een voice agent 47 boekingen bevestigen naar een gesloten kliniek zonder dat iemand het merkte?

De sync job die het kennis-bestand van de agent voedde, faalde stilletjes na een API-veldhernoeming. Een defensieve fallback hield het laatste goede bestand op zijn plek, dus de agent las tien weken lang een bevroren snapshot.

Wat is een dead-man switch voor een voice agent?

Een externe check die elke N uur een heartbeat-ping verwacht. Stopt de ping, dan stuurt hij een melding naar een mens. Tools als Healthchecks.io doen dit voor ongeveer twintig euro per jaar.

Waarom geldt een silent fallback als de bug, en niet de sync-fout zelf?

Sync jobs falen af en toe. Dat is normaal. De bug is dat je stale data stilletjes op zijn plek houdt zonder iemand te alarmeren, want niets downstream kan het verschil zien tussen verse waarheid en bevroren waarheid.

Hoe vaak moet een voice agent getest worden tegen zijn ground truth?

Elke vijftien minuten in productie. Een simpele canary die de agent een vraag stelt waarvan het juiste antwoord bekend is en die alarmeert bij mismatch, vangt de meeste data-source drift voordat klanten dat doen.

voice agentsai agentsoperationsarchitecturecase studyautomation

Iets bouwen?

Start een project