Voice agents
Voice agents bij een tandartsketen: LiveKit, Vapi, DIY
Vrijdag 18:47, de NZa heeft net een tarief aangepast, veertig patiënten in de wacht. Het goedkoopste antwoord en het audit-antwoord waren niet hetzelfde. Audit won.

Het is vrijdag 18:47. De NZa heeft zojuist een tussentijdse herziening van het C22-consulttarief gepubliceerd. Veertig mensen bellen de balie van een tandartsketen met 20 medewerkers in Haarlem met de vraag of hun controle van maandagochtend duurder wordt. Er is geen balie. De balie is om 17:30 naar huis gegaan. De voice agent neemt gesprek 1 op, gesprek 2, gesprek 3. Over zes minuten komt er een vraag waar hij geen vertrouwen in zijn eigen antwoord op heeft, en dan moet er iemand achter een toetsenbord zitten.
Afgelopen winter hebben wij die voice agent gebouwd. We hadden drie serieuze kandidaten voor de voicelaag: LiveKit Agents, Vapi, en een zelfgebouwde stack op Twilio Media Streams met een Claude tool-use loop. In dit stuk staat wat we leerden toen we ze scoorden op de drie dingen waar de praktijkeigenaresse écht om gaf.
De opdracht, in één alinea
5.800 gesprekken per week, vrijwel allemaal terugkerende afspraken van een half uur: controles, mondhygiëne, kinderbeugels, af en toe een verzetting. Openingstijden doordeweeks 08:00 tot 20:00, zaterdag tot 13:00. De agent moest uit de Exquise-agenda kunnen lezen via een stabiele API, voorlopige afspraken terugschrijven, de sms-bevestiging versturen, en doorverbinden naar een echte assistente zodra de beller het woord pijn of iets dat erop leek liet vallen. Elk gesprek moest een audit onder Wkkgz en de KNMT praktijkrichtlijn overleven. En de praktijkeigenaresse had één persoonlijk getal dat verzet moest worden: de gemiddelde wachttijd op maandag aan de balie was 4 minuten 11 seconden. Zij wilde dat onder de 90 seconden.
Kosten per gesprek bij 5.800 gesprekken per week
We draaiden voor elke stack een representatieve pilot van twee weken. Dezelfde Deepgram nova-3 streaming STT in eu-west, dezelfde Cartesia Sonic-2 Nederlandse stem, hetzelfde Claude Haiku 4.5-model achter de tool loop. Een gesprek duurde gemiddeld 1 minuut 28 seconden. Dit kwam er in de spreadsheet uit:
Vapi (managed)
Vapi platform $0.05 / min
Twilio SIP $0.014 / min
Deepgram STT $0.0077/ min
Cartesia TTS $0.020 / min (egress)
Claude Haiku 4.5 ~$0.018 / call
-----------------------------------
Per call (~1m28s): ~$0.155
Per week: ~$899
Per year: ~$46,700
LiveKit Cloud + own LLM
LiveKit Cloud $0.005 / min
Twilio SIP $0.014 / min
Deepgram STT $0.0077/ min
Cartesia TTS $0.020 / min
Claude Haiku 4.5 ~$0.018 / call
-----------------------------------
Per call: ~$0.087
Per week: ~$505
Per year: ~$26,260
Twilio Media Streams + Claude tool loop
Twilio Media $0.0125/ min in + $0.0085 stream
Deepgram STT $0.0077/ min
Cartesia TTS $0.020 / min
Claude Haiku 4.5 ~$0.018 / call
Hetzner CCX23 EUR 30 / month
-----------------------------------
Per call: ~$0.077
Per week: ~$447
Per year: ~$23,250
Korte conclusie: managed Vapi is per gesprek ongeveer twee keer zo duur als de zelfgebouwde loop. Bij dit volume betaalt dat verschil in jaar twee een extra mondhygiënist. Geen afrondingsfout. Maar ook niet de doorslaggevende factor, want die staat in het volgende hoofdstuk.
Wkkgz-verdedigbaarheid onder de KNMT praktijkrichtlijn
Wkkgz wil drie dingen van een voice agent: een helder toestemmingsmoment aan het begin van het gesprek, een getrouw verslag van wat aan beide kanten is gezegd, en een verdedigbare escalatieroute zodra de beller iets zegt dat klinisch kan zijn. De KNMT-richtlijn legt daar dataminimalisatie bovenop. Je bewaart de ruwe audio van mijn dochter heeft een gat in haar kies niet langer dan nodig om de afspraak in te plannen.
Vapi slaat transcripten standaard op in de VS, en je moet schriftelijk vragen om een EU-only retentielaag. Het auditlog is van hen. Krijg je over zes maanden een Wkkgz-klacht en moet je precies reconstrueren welke versie van het prompt op 14 maart om 11:08 live stond, dan begin je met een supportticket. We hebben dat op een ander project gedaan. Het werkt. Het is niet snel, en het ligt niet bij jou.
LiveKit Cloud heeft een EU-regio in Frankfurt en een self-host-optie, en die deur wilden wij openhouden. Het transcript en de audio zijn vanaf minuut één van jou. Het auditlog is wat je er zelf omheen bouwt, en dat is de prijs.
De Twilio Media Streams-stack die wij bouwden draaide de orchestrator op een Hetzner-bak in Falkenstein, schreef elke beurt van het gesprek weg naar een Postgres in dezelfde VPC, en liet de ruwe audio nooit Europa verlaten. Elke promptwijziging was een git commit op een branch met twee verplichte approvals. Toen de praktijkmanager zes weken na livegang vroeg welk prompt live stond op de dag dat mevrouw Jansen over het implantaat belde, was het antwoord één git log verderop.
Bij Wkkgz is de vraag niet 'is deze vendor compliant?'. De vraag is 'kun je met versiebeheer reconstrueren wat jouw agent op een specifieke dinsdag heeft gezegd?'. Twee van deze drie stacks maken dat makkelijk. Eén niet.
De vrijdagavond-patch van de NZa
Terug naar de openingsscène. Het tarief voor code C22 is net gewijzigd. Veertig mensen in de wacht. Wie past het prompt aan, en hoe lang duurt het tot het in productie staat?
Op Vapi is dat antwoord goed. Wie toegang tot het dashboard heeft, past het systeemprompt aan in een textarea, drukt op opslaan, en het volgende gesprek krijgt de nieuwe versie. De praktijkmanager kan het vanuit de trein op haar telefoon doen. Dat is op vrijdag om 19:00 oprecht waardevol. De keerzijde: ze kan het ook breken vanaf haar telefoon, er zit geen review-stap voor, en de versiegeschiedenis is dun. Op een ander project zagen we een echte productieprompt geklobberd worden door een per ongeluk geplakt stuk WhatsApp.
Op LiveKit en op onze zelfgebouwde stack is het prompt code. Op vrijdagavond een update doen betekent dat iemand met deploy-rechten een PR opent, een approval krijgt, CI bekijkt en uitrolt. We hebben dat end-to-end gemeten op 11 minuten voor een wijziging van één regel. Prima voor een tariefupdate. Prima voor een typo. Niet prima voor 'de agent hallucineert een verrichtingscode, haal hem nu offline', en het antwoord daar is niet om deploys sneller te maken, maar om een feature flag te hebben.
Die hebben we dus gebouwd. Eén environment variable, om te flippen via een Slack slash command door de dienstdoende engineer of door de praktijkmanager zelf, die de agent omschakelt van 'afspraak inboeken' naar 'bedankt voor uw telefoontje, ons team belt u morgenochtend terug'. Gemiddelde tijd van 'dit gaat fout' naar 'de agent is stil en belooft netjes een terugbelafspraak': 14 seconden. Gemiddelde tijd om een echte promptfix uit te rollen: nog steeds 11 minuten.
De overdracht is waar de veiligheid zit
Wkkgz interesseert het niet wat de LLM heeft gezegd. Wat de praktijk heeft gedaan met wat de beller zei, dáár gaat het om. De zelfgebouwde loop gooide elke uiting van de beller door twee parallelle checks voordat er werd geantwoord: een intentclassifier en een single-token klinisch-risicoflag (pijn, zwelling, bloed, gebroken, kind, medicatie). Sloeg één van de twee aan, dan stopte de agent met inboeken. Hij zei één zin, verbond door naar het waarneemnummer, en schreef een rij in de handoff-tabel die het ochtendteam de volgende dag om 07:45 triageerde.
Dit kan een kant-en-klaar voice-platform ook, maar alleen als je stopt met het systeemprompt te zien als één instructie en het gaat zien als een state machine. Het geraamte:
type State = 'greet' | 'intent' | 'slot' | 'confirm' | 'handoff'
async function turn(state: State, transcript: string, ctx: CallCtx) {
const risk = await classifyRisk(transcript) // 1 token, ~40ms
if (risk.flagged) return { next: 'handoff', say: HANDOFF_LINE }
switch (state) {
case 'greet': return askIntent(ctx)
case 'intent': return routeIntent(transcript, ctx)
case 'slot': return fillSlots(transcript, ctx) // calls Exquise
case 'confirm': return readBack(ctx)
case 'handoff': return transfer(ctx)
}
}
Elke state krijgt zijn eigen strakke prompt en een strikte toollijst. De greet-state kan book_appointment niet aanroepen. De slot-state kan het gesprek niet beëindigen. Die scheiding maakte de agent verdedigbaar op papier en betrouwbaar aan de telefoon.
Wat wij kozen
We hebben de zelfgebouwde Twilio Media Streams + Claude tool-use loop in productie gezet. Drie redenen, op volgorde van gewicht:
- Wkkgz-reconstructie. Prompts onder git, auditlog in onze eigen Postgres, audio die de Atlantische Oceaan nooit overstak.
- Kosten op schaal. De helft van de prijs per gesprek van Vapi betaalt bij 5.800 gesprekken per week de engineer die het onderhoudt, met geld over.
- Tooloppervlak. De agent praat met Exquise, het sms-bevestigingssysteem, een soft-booking-holdingtabel en een Slack-escalatiekanaal. Vier tools in een Claude loop hangen kost ongeveer honderd regels TypeScript. Vier tools aan een managed platform hangen kan, maar dan ligt de orchestratielogica deels in het vendordashboard en deels in jouw code, en dat is het slechtste van twee werelden.
Op het getal van de praktijkeigenaresse: de maandagochtend-wachttijd zakte in week drie onder de drempel van negentig seconden en bleef daar het hele voorjaar. Dát, meer dan het auditverhaal, was het cijfer dat de partners in het kwartaaloverleg aan haar teruggaven. Het auditverhaal maakte het project verdedigbaar. De wachttijd maakte er een ja van.
Dit is geen argument tegen Vapi. Voor een loodgietersbedrijf met vijf man dat volgende week een intake-agent voor buiten kantoortijden nodig heeft, is Vapi het juiste antwoord en raden wij hem ook maandelijks aan. Het omslagpunt ligt ergens rond 1.000 gesprekken per week en in een sector waar het auditverhaal er echt toe doet. Daaronder betaal je een engineer om vijftig euro per maand te besparen.
De twee dingen die wij verkeerd deden
Eerst: barge-in. Nederlandse bellers vallen je in de rede. Een nette Engelstalige agent praat dan nog een hele zin door, en een Nederlandse beller hoort dat als negeren en hangt op. We moesten de VAD binnen 180 ms de TTS-weergave laten onderbreken. Onze eerste versie zat op 600 ms. Zeshonderd milliseconden is het verschil tussen 'dit werkt' en 'dit is stuk'. De fix was een server-side endpointing-model op de inkomende stream, niet het model dat de TTS-vendor standaard meelevert.
Twee: het model is niet de agent. We begonnen met een systeemprompt van 1.200 woorden en één Claude-aanroep per beurt. De betrouwbaarheid was slecht en we konden niet zien waarom. We hebben het opnieuw opgebouwd als de state machine hierboven, met een aparte Claude-aanroep per state, elk met een strak prompt en een strikte toollijst. Hallucinaties zakten naar bijna nul. Dat past bij het patroon in het stuk van Anthropic over het bouwen van effectieve agents: de betrouwbaarheidswinst zit in de orchestratie, niet in het model.
De kleinste vervolgstap
Draai je vandaag een voice agent en kun je de vraag 'welk prompt stond donderdagmiddag om 14:33 live?' niet met een commando beantwoorden, dan is dat de audit die je in vijf minuten zelf kunt doen. Open je repo, je dashboard, je wiki, waar het prompt ook echt staat, en probeer de versie van gisteren te reconstrueren. Lukt dat niet, dan is dat het eerste om op te lossen, ongeacht op welke stack je zit.
Toen wij de voice agent voor die Haarlemse tandartsketen bouwden, liepen we ertegenaan dat het kostenantwoord en het auditantwoord op verschillende stacks wezen. We hebben het uiteindelijk opgelost met een prompt onder git, een Postgres-auditlog in Frankfurt en een Slack-killswitch die de praktijkmanager vanaf haar eigen telefoon bedient.
Kern
Voor voice agents in gereguleerd werk wijzen het kostenantwoord en het auditantwoord meestal op verschillende stacks. Kies het auditantwoord.
FAQ
Is Vapi compliant met Wkkgz en de KNMT-praktijkrichtlijn?
Vapi is bruikbaar voor de Nederlandse zorg, maar het audit- en dataresidentie-verhaal vraagt om een EU-retentielaag en een supportticket-workflow om historische prompts te reconstrueren. Dat werkt; snel is het niet en het ligt niet bij jou.
Wanneer wint een zelfgebouwde Twilio + Claude voice agent het van een managed platform?
Grofweg boven 1.000 gesprekken per week, of in een gereguleerde context waar je prompts onder git en EU-residentie van audio nodig hebt. Daaronder weegt de engineeringkost niet op tegen de besparing per gesprek en is Vapi het betere antwoord.
Hoe snel patch je het prompt van een voice agent als op vrijdagavond een tarief verandert?
Op Vapi seconden, via het dashboard, zonder review-stap. Op LiveKit of een zelfgebouwde stack met een normale deploypipeline rond de 11 minuten per PR, plus een Slack-bediende killswitch voor de echte noodgevallen.
Welke architectuurwijziging maakte de agent betrouwbaar?
Het systeemprompt van 1.200 woorden opsplitsen in een state machine met een aparte Claude-aanroep per state, elk met een strak prompt en een strikte toollijst. Hallucinaties zakten naar bijna nul zodra het model niet meer het verkeerde tool kon aanroepen.