Voice agents
Voice agent voor een fysioketen in Maastricht: het draaiboek
24 fysio's, vier vestigingen, 1.820 verzetbelletjes per week in het Nederlands én Limburgs. Dit is het draaiboek van de voice agent die de balie weer boven water kreeg.

De eerste rinkel om 08:47
De receptioniste bij de vestiging in Wyck nam op om 08:47 op een dinsdagochtend. Om 09:03 had ze zeven gesprekken gehad. Vijf waren hetzelfde gesprek. "Ik moet woensdag verzetten, kan vrijdag?" Twee waren een vergoedingsvraag die ze niet kon beantwoorden zonder Intramed te openen, de polisbladen te zoeken en drie regels kleine lettertjes voor te lezen aan iemand die al te laat was voor het werk.
De keten heeft vier vestigingen in Maastricht en Heerlen. Vierentwintig fysiotherapeuten. Eén balie per vestiging, soms twee tijdens de ochtendspits. De telefoonlogs uit de SIP-trunk lieten zien dat ze 1.820 inkomende gesprekken per week aannamen. Ongeveer 63% was puur verzetten. 22% waren facturatie- of vergoedingsvragen. 11% nieuwe boekingen. De rest viel in een kleine maar operationeel dodelijke categorie: mensen wiens pijn 's nachts was opgelopen en die zich naar binnen wilden praten voor een plek diezelfde dag.
Waarom voice en niet een chatwidget
De praktijk had al een Calendly-achtige webboeker. Die handelde nieuwe boekingen prima af. Aan het belvolume deed hij vrijwel niets, want de bellers waren niet dezelfde populatie. De verzetters waren ouder. Ze hadden een band met een specifieke fysio. Ze wilden door een Nederlandse stem horen worden zeggen "ik moet verzetten", niet door een chatveld op een website waar ze nog nooit waren geweest.
Verzetten past ook slechter bij een chat agent dan mensen denken. De beller weet meestal niet wat zijn voorkeur is totdat hij de opties hoort. Een voice loop ("woensdag om half drie, of donderdag om elf uur?") sluit in minder dan twintig seconden. Dezelfde uitwisseling in een chatdraad duurt vier minuten.
Het EPD lezen zonder een API die je kunt vertrouwen
De keten draait op Intramed. Dat is al meer dan tien jaar de standaard Nederlandse fysio-EPD, en de installatie bij deze praktijk is twaalf jaar oud. Er is een API. Niet de API die je wilt.
Intramed biedt een SOAP-service die genoeg is om agenda's te lezen en losse afspraakwijzigingen weg te schrijven, maar die geeft je niet de patiënt-context die een voice agent nodig heeft: bij welke fysio iemand normaal komt, bij welke verzekeraar hij zit, of zijn vergoedingenpot al op is, of er een no-show flag staat. Dat zit allemaal in schermen die de balie leest, niet in de SOAP-envelope.
We hadden twee opties. Een screen-scraping laag bovenop de Windows-client zetten. Of een read-through cache bouwen die 's nachts trekt en overdag bijna-vers blijft. We kozen voor de cache.
// Read-through cache, refreshed every 4 hours during opening.
// Writes go straight to Intramed SOAP; cache is invalidated on write.
type PatientCtx = {
patient_id: string;
primary_therapist_id: string;
insurer: 'CZ' | 'VGZ' | 'Zilveren Kruis' | 'Menzis' | 'other';
vergoeding_remaining_sessions: number | null;
no_show_flag: boolean;
last_seen_at: string; // ISO
};
async function getPatientCtx(phone: string): Promise<PatientCtx | null> {
const cached = await cache.get(`ctx:${phone}`);
if (cached && !staleBy(cached, '4h')) return cached;
const fresh = await intramed.readPatientByPhone(phone);
if (!fresh) return null;
await cache.set(`ctx:${phone}`, fresh, '24h');
return fresh;
}
Het cache-patroon houdt het voice-pad weg van het trage SOAP-endpoint. Een cached read zit onder de 40ms. Een live SOAP read zit tussen 1,4 en 3,1 seconden, en dat is genoeg om het ritme van het gesprek te breken. Writes (verzettingen) gaan nog steeds direct door. Writes cachen we niet.
Nederlands en Limburgs in dezelfde beurt
Het eerste wat we in tests ontdekten was dat de standaard Nederlandse STT-modellen het niet hielden zodra een beller halverwege een zin in het Limburgs overschakelde. "Ich höb mörge gein tied" is niet "ik heb morgen geen tijd", en een model dat op Standaardnederlands is getraind hoort het als wartaal en gaat dan gokken.
We hebben dit op twee manieren opgelost. Eerst draaiden we een kleine tweetalige fine-tune op een Whisper-large variant, met ongeveer 18 uur Limburgse audio van regionale omroep L1, plus een interne dataset van gespreksopnames waar toestemming op zat (met het juridische papierwerk dat de FG van de keten had ondertekend voordat er één seconde audio werd bewaard). Daarnaast maakten we de agent toleranter voor ambigue transcripties: in plaats van te handelen op de eerste STT-uitkomst bevestigt de agent de intentie terug in gewoon Nederlands ("u wilt uw afspraak van woensdag verzetten, klopt dat?") en gaat alleen verder bij een ja.
Als je fine-tunet op echte gespreksaudio is het AVG-papierspoor het project. We zijn niet begonnen met trainen totdat de praktijk een ondertekende verwerkersovereenkomst had, een bewaartermijnschema, en een verwijderingsprocedure die de praktijkmanager zelf kon uitvoeren. Sla dat over en de toezichthouder beëindigt je pilot.
Het 90-seconden code-rood pad
De riskantste categorie gesprekken was de kleinste. Ongeveer 2% van het wekelijkse volume was een patiënt die acute pijn beschreef ("ik kan mijn arm niet optillen sinds gisteren") en die anders in de verzet-rij zou zitten terwijl de agent vrolijk een plek over elf dagen aanbood.
We bouwden een aparte classifier bovenop het live transcript die elke beurt scoort op wat we intern code-rood signalen noemden: woorden die op acuut ontstaan duiden, ernstomschrijvingen, red-flag anatomie (borst, kaak, plotselinge eenzijdige zwakte), en een kleine set zinnen die wijzen op mogelijk ernstige pathologie. Zodra de score een drempel passeert doet de agent drie dingen achter elkaar:
- Hij stopt de boekingsflow midden in de beurt en zegt, in rustig Nederlands, dat hij een senior fysio aan de lijn gaat zetten.
- Hij pingt de dienstdoende senior via de bestaande Signal-groep van de praktijk, met het transcript tot dat moment erbij.
- Hij doet een warm transfer van de SIP-leg naar de senior die als eerste accepteert, met een hard plafond van 90 seconden. Accepteert er geen senior, dan gaat het door naar de praktijkmanager en wordt er een incident gelogd.
Het plafond van 90 seconden is niet willekeurig. De seniors waren het vooraf eens: alles wat langer duurt mist het doel. De beller zit al in nood, en stilte van die lengte klinkt alsof de lijn is uitgevallen. In de eerste zes weken live triggerde de agent het code-rood pad 11 keer. Negen keer haalde een senior het binnen het plafond. Twee keer ging het naar de praktijkmanager, die binnen een minuut een fysio aan de lijn had. Nul misclassificaties in de door senioren beoordeelde steekproef, al houden we het risico op false-negatives op het dashboard en leest de praktijkmanager dat wekelijks.
De vergoedingenrij
Vergoedingsvragen in de Nederlandse fysiotherapie zijn niet door een agent alleen op te lossen. Het antwoord hangt af van de specifieke polis van de patiënt, onder welk hoofdstuk fysiotherapie hij valt, hoeveel sessies de huisartsverwijzing dekt, en of hij dit jaar zijn eigen risico al heeft volgemaakt. De agent kan het meeste hiervan uit de cache lezen. Het antwoord geven kan hij nog steeds niet, omdat een fout antwoord een gereguleerde schade is.
Dus bouwden we een rij. Elke vergoedingsvraag landt in een kleine web-app die de praktijkmanager 's ochtends één keer opent en na de lunch nog een keer. De agent heeft het werk al gedaan: polis opgezocht, hoofdstuk gelezen, resterende sessies opgehaald, een antwoord van één alinea opgesteld. De praktijkmanager keurt het goed (één klik, de agent belt terug), past het aan en keurt het goed, of escaleert. De gemiddelde afhandeltijd per item is 41 seconden. Voordat de agent live ging duurden dezelfde gesprekken voor de balie tussen 4 en 7 minuten.
De saaie infrastructuur
De stack is met opzet onromantisch. Een SIP-trunk in een Twilio-nummer dat de hoofdlijnen van Maastricht afdekt. Een kleine Node-service ontvangt de mediastream en draait de STT- en TTS-legs. Het orkestratiemodel draait in een Nederlandse hostingregio. Function calls gaan naar de Intramed-cache, de Signal-pager, de queue API en een Postgres write-log die elke statusovergang vastlegt.
De write-log is waar we op gelet hebben. Een voice agent zonder state log is een black box tijdens een incident review, en incidents zijn juist wanneer je leert wat de agent echt doet. Op Hacker News kwam de observatie terug dat de enige schaalbare delete in Postgres DROP TABLE is, wat half een grap en half een operationele waarheid is. Wij hebben de call-log tabel vanaf dag één per week gepartitioneerd (zie de PostgreSQL partitioning docs voor het patroon), zodat de driemaandelijkse opschoning door de praktijkmanager (we bewaren call-metadata 90 dagen, audio 30) een DROP is, geen bulk DELETE. Het kost niets en het voorkomt dat AVG-bewaartermijnen een zondagavond-cron-ramp worden.
Wat we onderweg fout deden
De "always be booking" mislukking
De eerste versie van de agent was getuned om te sluiten. Was er een plek, dan bood hij die aan. Patiënten begonnen te klagen dat ze zich opgejaagd voelden. We tuneden opnieuw zodat hij twee opties aanbiedt, altijd in hetzelfde formaat ("woensdag om half drie, of donderdag om elf uur"), en zodat hij een hele tel wacht voordat hij opnieuw vraagt. De boekingen daalden niet. De tevredenheid steeg.
Het accent-op-de-naam probleem
Fysio-namen als "Roel" en "Geert" kwamen schoon terug, maar de langere Limburgse achternamen waren een rommeltje. De agent verzon een fonetisering waar de beller om moest lachen, en dat is een keer leuk en twee keer niet. We hebben de 24 fysio-namen in de stem van de agent voorgenomen en spliceten ze erin, in plaats van de TTS ze te laten verzingen.
De dinsdagochtend stormloop
De eerste dinsdag na een lang weekend nam de agent 312 gesprekken aan in 90 minuten. De cache hield het. Het Intramed SOAP write endpoint niet. We voegden een write-queue toe met een plafond van 2 per seconde richting Intramed en een terugval-antwoord ("we hebben uw verzoek genoteerd, u ontvangt een SMS-bevestiging zodra die in het systeem staat") voor het overschot. Beide paden slagen nu.
Waar de cijfers landen
Zes weken erin: 84% van de verzet-gesprekken wordt volledig afgesloten door de agent zonder dat een mens eraan hoeft te komen. De resterende 16% gaat door naar de balie met de context al klaar, waardoor het gesprek gemiddeld 47 seconden duurt in plaats van de eerdere 3 minuten 20. De rij van de praktijkmanager sluit 96% van de vergoedingsvragen voor het einde van dezelfde dag. De balie won ongeveer 31 uur telefoontijd per week terug over de vier vestigingen, die ze besteedden aan de persoonlijke onthaalervaring waar de keten ook echt in wil investeren.
De voice agent is niet het product. Het product is de grens die je trekt tussen wat de agent afhandelt, wat in een rij voor een mens komt, en wat binnen 90 seconden escaleert. Trek die grens voordat je een regel code schrijft.
Toen we de voice agent voor de Maastrichtse keten bouwden, was de moeilijkste helft van het project niet het model. Het was een twaalf jaar oude Intramed-installatie, een regionaal dialect, een AVG-gevoelige audio-pipeline en de dagelijkse workflow van een praktijkmanager op één lijn krijgen. De technische lift was Twilio plus een cache plus een rij. De operationele lift was drie workshops met de balie voordat er ook maar één regel code draaide.
Wil je zelf klein beginnen in je eigen praktijk, dan is dit de audit van vijf minuten: trek één week aan inkomende gespreksmetadata, bucket de gesprekken op reden (verzetten, vergoeding, nieuwe boeking, pijn-nu), en vraag de receptioniste welke emmer ze met plezier nooit meer zou willen beantwoorden. Dat ene antwoord vertelt je welke agent je als eerste moet bouwen.
Kern
De voice agent is niet het product. Het product is de grens tussen wat hij afhandelt, wat in een rij voor een mens komt, en wat binnen 90 seconden escaleert.
FAQ
Hoe accuraat moet Nederlandse STT zijn voor een voice agent in de zorg?
Mik op meer dan 95% woordaccuratie op Standaardnederlands en meer dan 88% op regionale dialecten zoals het Limburgs. Daaronder: bevestig de intentie eerst terug in gewoon Nederlands voordat je een boekingswijziging doorvoert.
Mag een voice agent juridisch gezien afspraakwijzigingen doorvoeren in een Nederlands fysio-EPD?
Ja, mits er een ondertekende verwerkersovereenkomst ligt, een gedocumenteerd bewaartermijnschema, en een terugval naar een mens bij elke escalatie. De praktijkmanager blijft verwerkingsverantwoordelijke.
Waarom niet voor elke read direct integreren met de SOAP API van Intramed?
Live SOAP reads duren 1,4 tot 3,1 seconden, en dat breekt het ritme van het gesprek. Een read-through cache die elke vier uur vernieuwt houdt context-lookups onder de 40ms, terwijl writes nog steeds direct doorgaan.
Hoe ga je om met een beller die acute pijn beschrijft?
Een aparte classifier scoort elke transcript-beurt op red-flag signalen. Bij een drempeloverschrijding voert de agent binnen 90 seconden een warm transfer uit naar een senior fysiotherapeut, met het transcript tot dat moment erbij.