Voice agents
Voice agent voor de huisarts: 380 calls per dag binnen AVG
Het is dinsdag 08:07, de receptenlijn van een Nederlandse huisartsenpraktijk heeft 41 calls in de wacht, en de voice agent neemt call 42 op in 800 ms.

Het probleem van 08:07
Het is dinsdag 08:07 en de receptenlijn van een Nederlandse huisartsenpraktijk heeft 41 calls in de wacht. De doktersassistente heeft al elf inloopconsulten getrieerd, een gestreste moeder teruggebeld over het piepende ademen van haar zoon, en het espressoapparaat staat nog niet aan. Om 09:30 piekt de wachtrij op 67. Tegen het middaguur heeft ze 38 herhaalrecepten met de hand in HiX getypt. Ergens binnen die 380-calls-dag zit het volume dat een voice agent zou moeten dragen.
Trek de ochtend eens door. Drie op de vier calls zijn herhaalreceptaanvragen waarbij de patiënt al weet welk medicijn, de praktijk de patiënt al kent, en HiX de laatste uitgiftedatum al weet. Het is het saaiste, meest noodzakelijke werk in het pand. Het is ook precies waar een voice agent zijn plek verdient.
Hieronder de playbook die we gebruikten om er één live te zetten. Geen plaatjes van "de toekomst van de zorg". Alleen de stukken die pijn deden tijdens de bouw.
Wat de agent precies doet
Hij neemt op binnen één keer overgaan. Hij begroet de patiënt in het Nederlands. Hij vraagt om het BSN, accepteert het via DTMF of uitgesproken cijfers, valideert het met de elfproef, en leest het terug in vier-drie-twee. Hij bevestigt de geboortedatum als tweede factor. Vervolgens handelt hij één van vier intents af:
- Herhaalrecept. Patiënt noemt het medicijn. De agent zoekt de bijbehorende actieve MedicationRequest in HiX via FHIR, bevestigt de dosis, en zet een herhaalreceptaanvraag in de wachtrij zodat de huisarts die voor de lunch tekent.
- Triageslot. De agent doorloopt een kort klachtgesprek tegen de Nederlandse Triage Standaard, boekt een slot in HiX bij groen of geel, en escaleert rood naar de doktersassistente.
- Uitslag van een onderzoek. De agent zegt nee, die geeft de huisarts persoonlijk, en biedt in plaats daarvan een consultatieslot van 9 minuten aan.
- Iets anders. De agent zegt dat hij hier niet bij kan helpen en verbindt binnen vier seconden door naar een mens.
Die laatste intent is de belangrijkste. Het werk van de agent is niet indrukwekkend zijn. Zijn werk is de 73% van de calls afhandelen die mechanisch zijn, en de rest snel overdragen.
De telefonielaag
Elke Nederlandse huisartsenpraktijk waar wij mee hebben gewerkt draait of KPN ÉÉN, of Voys, of een lokale SIP-setup met een gehoste PBX. Alle drie eindigen ze als SIP. We wijzen één DID toe aan een LiveKit-room op een Hetzner-bak in Falkenstein. LiveKit verzorgt de real-time audiobrug tussen de beller en de agent-loop. De hele stack blijft binnen één EU-regio.
De room is kortlevend. Eén call, één room, geen opname, geen transcript langer bewaard dan de dialoogbeurt.
# livekit-sip-trunk.yaml
sip_trunk:
name: gp-prescription-line
numbers: ["+31201234567"]
inbound_addresses: ["sip.voys.nl"]
inbound_auth_username: ${VOYS_SIP_USER}
inbound_auth_password: ${VOYS_SIP_PASS}
dispatch:
rule:
name: route-to-agent
trunk_ids: [gp-prescription-line]
room_config:
agents:
- agent_name: huisarts-agent
metadata: '{"practice_id":"prk_0421","language":"nl-NL"}'
Falkenstein is geen willekeurige keuze. Verwerkersovereenkomsten met Nederlandse huisartsenpraktijken benoemen steeds vaker een specifieke EU-regio in plaats van "de EU" in het algemeen, en het datacenter van Hetzner in Falkenstein geeft ons een concrete contractuele locatie op 410 km van Amsterdam, met een round trip onder de 15 ms naar de PBX van de praktijk. Die latency telt aan de telefonierand op een manier die hij aan de applicatierand niet doet.
ASR, TTS en het BSN-probleem
Nederlandse ASR op rumoerige 8kHz telefoonaudio is het deel dat de meeste voice agents stilletjes om zeep helpt. Wij kwamen uit bij Azure Speech in West Europe voor ASR, met aangepaste phrases voor medicijnnamen (het stockmodel schrijft "Metoprolol" anders ongeveer één op de twaalf keer over als "Meta-Prolog"). Voor TTS gebruiken we Azure Neural Voices, ook West Europe, stem nl-NL-FennaNeural.
BSN is het lastige geval. Patiënten lezen negen cijfers achter elkaar voor in spreektempo, de ASR laat er één vallen of plakt er twee samen, en de agent kijkt nu naar de verkeerde patiënt. Drie dingen maakten het betrouwbaar:
- DTMF eerst. De agent biedt invoer via het toetsenbord standaard aan en valt alleen terug op uitgesproken cijfers als de beller een handset zonder DTMF heeft.
- Elfproef-validatie. Elk BSN wordt eerst tegen de officiële 11-proef getoetst voordat er ook maar een HiX-lookup loopt. Een mislukte checksum betekent opnieuw vragen, geen retry.
- Gegroepeerd teruglezen. De agent leest het BSN terug in stukken van vier-drie-twee, langzaam, met SSML-pacing.
def valid_bsn(bsn: str) -> bool:
"""11-proef. See logius.nl/diensten/burgerservicenummer-bsn"""
if not bsn.isdigit() or len(bsn) != 9:
return False
weights = [9, 8, 7, 6, 5, 4, 3, 2, -1]
return sum(int(d) * w for d, w in zip(bsn, weights)) % 11 == 0
def speak_bsn(bsn: str) -> str:
grouped = f"{bsn[:4]}, {bsn[4:7]}, {bsn[7:]}"
return (
f'<speak><prosody rate="80%">'
f'Uw burgerservicenummer is {grouped}.'
f'</prosody></speak>'
)
De grootste winst in accuratesse kwam uit de LLM duidelijk maken dat elk BSN dat hij van de ASR krijgt voorlopig is, en dat de enige geldige bevestiging een DTMF-invoer is of een gesproken "ja" na het gegroepeerde teruglezen. Zonder die regel ging het model vrolijk door met een uitgeschreven negen-cijferige string die op de volgende beurt de elfproef niet haalde.
De akoestische omstandigheden van patiënten variëren meer dan welke ASR-demo dan ook suggereert. Patiënten op een vaste lijn thuis lezen cijfers ongeveer twee keer zo snel voor als patiënten met speakerphone op een parkeerterrein, en het gegroepeerd teruglezen op standaardtempo wordt onbruikbaar voor de vaste-lijn-groep. We hebben de SSML-prosody afgesteld op 80% tempo met een pauze van 220 ms tussen de groepen, wat ongeveer 1,4 seconden looptijd per call kost en een hele categorie "sorry, kun je dat herhalen"-reprompts wegneemt. De parkeerterrein-groep heeft een andere fix nodig: de agent luistert in de eerste beurt naar voertuiggeluid en, als dat herkend wordt, opent met één regel waarschuwing dat hij voor het BSN alleen DTMF accepteert.
HiX-integratie via FHIR
De HiX van ChipSoft biedt een HL7 FHIR R4 endpoint aan binnen het praktijknetwerk. De agent roept dat nooit direct aan. Een kleine mediation service in het VLAN van de praktijk vertaalt tool calls van de agent naar FHIR-searches en -writes, beperkt tot één OAuth-client met leesrechten op Patient en MedicationRequest, en schrijfrechten alleen op een herhaalreceptaanvragen-worklist.
Twee FHIR-tools plus één scheduling-tool is alles wat de agent nodig heeft:
async def find_active_prescription(bsn: str, drug_query: str) -> dict | None:
patient = await fhir.search(
"Patient",
identifier=f"https://fhir.nl/fhir/NamingSystem/bsn|{bsn}",
)
if not patient.entry:
return None
pid = patient.entry[0].resource.id
meds = await fhir.search(
"MedicationRequest", subject=f"Patient/{pid}", status="active"
)
q = drug_query.lower()
for m in meds.entry:
text = m.resource.medicationCodeableConcept.text.lower()
if q in text:
return {
"id": m.resource.id,
"drug": text,
"dose": m.resource.dosageInstruction[0].text,
"last_issued": m.resource.authoredOn,
}
return None
async def queue_repeat_request(mr_id: str, patient_ref: str, note: str) -> str:
task = await fhir.create("Task", {
"status": "requested",
"intent": "order",
"focus": {"reference": f"MedicationRequest/{mr_id}"},
"for": {"reference": patient_ref},
"description": "Herhaalrecept via spraakagent",
"note": [{"text": note}],
})
return task.id
De huisarts ziet de openstaande Tasks in HiX precies zoals ze elke andere herhaalreceptaanvraag ziet, en tekent ze in batch af. Geen nieuw scherm, geen nieuwe login, geen schaduwworklist.
Triage die weet wanneer hij moet stoppen
Voor de triage-slot-intent gebruiken we de Nederlandse Triage Standaard als rubriek. De agent loopt met de beller door een kort klachtgesprek en kent U1 tot en met U5 toe. U1 en U2 worden direct doorverbonden. U3 boekt een slot voor dezelfde dag. U4 en U5 boeken het eerstvolgende vrije slot of bieden een teleconsult aan.
De system prompt bevat geen medisch redeneren. Hij bevat een beslisboom, de tekst per tak, en één harde regel: als iets in de woorden van de patiënt een rode-vlag-lijst triggert (pijn op de borst, plotselinge gevoelloosheid, suïcidale gedachten, hoge koorts bij een kind), verbindt de agent in zijn volgende uiting door. Geen "laat me eerst even iets checken". Geen geruststelling. Het volgende wat de beller hoort is de doktersassistente.
De agent diagnosticeert nooit, stelt nooit gerust, zegt nooit "dat klinkt prima". Geruststelling van een LLM in een triagecontext is een aansprakelijkheid waar de praktijk niet op zit te wachten. Een Canadees tribunaal hield Air Canada aansprakelijk voor wat zijn chatbot tegen een klant zei, en wuifde het argument dat de bot een aparte rechtspersoon zou zijn van tafel. De zorg is waar die doctrine als volgende landt, en de bewijslast ligt bij wie het model heeft uitgerold.
Audio binnen de AVG-grens houden
Zowel de AVG als de Wet aanvullende bepalingen verwerking persoonsgegevens in de zorg behandelen de stem van een patiënt in een klinische context als bijzondere persoonsgegevens. NEN 7510 legt daar operationele eisen bovenop. Daaruit volgden drie regels die het ontwerp hebben bepaald:
- Geen audio verlaat de EU. SIP, ASR, TTS, LLM en FHIR-mediation draaien allemaal in West Europe of Falkenstein. Azure-resources worden uitgerold met customer-managed key en de regio West Europe vastgepind in het deployment manifest, niet alleen in de portal.
- Audio wordt niet opgeslagen. LiveKit streamt audioframes naar ASR en gooit ze weg. We bewaren geen opname. Het enige artefact is een gestructureerde turn log (intent, slotwaardes, gehashed BSN, timestamps) die 30 dagen bewaard wordt voor incident review en daarna verwijderd.
- BSN staat nooit in een log. De mediation service ziet het BSN één keer, zoekt de patiënt op, en vervangt het downstream overal door het HiX patient-id. De turn log slaat een SHA-256 op van het BSN plus een per-praktijk pepper, puur om een klacht aan een call te kunnen koppelen.
De DPIA van de praktijk documenteert de dataflow op dit detailniveau. We starten bij het DPIA-sjabloon van de Autoriteit Persoonsgegevens en zetten daar de NEN 7510 control-mapping bovenop. De verwerkersovereenkomst tussen de praktijk, de implementatiepartner en Microsoft benoemt West Europe als contractuele verplichting, niet als deployment-detail. Voor de FHIR-kant dekt de HL7 FHIR security guidance de OAuth-scoping en audit-event-logging die we tegen HiX gebruiken.
Wat we meten
De agent wordt afgerekend op vier getallen. Geen daarvan is "afgevangen calls".
- Time to first word. Mediaan 740 ms van "hallo" tot de eerste lettergreep van de agent. Boven de 1200 ms gaan patiënten opnieuw "hallo?" zeggen en raakt het gesprek uit de pas.
- Doorverbindpercentage. 22% van de calls gaat naar de doktersassistente. We willen dit getal tussen 18% en 28% houden. Lager betekent dat de agent te ambitieus is. Hoger betekent dat de BSN-flow of de intent classifier kapot is.
- Verkeerde-patiënt-percentage. Nul over 41.000 calls. Elfproef plus geboortedatum als tweede factor plus gegroepeerd teruglezen is wat dat overeind houdt. We auditen wekelijks tegen de herhaalreceptqueue in HiX.
- Uren teruggewonnen voor de assistente. 5,8 per dag, gemeten tegen de baseline van de zes weken ervoor. Dat is anderhalve FTE die de praktijk niet meer hoeft te werven.
Hoe we het uitrolden
Je zet geen voice agent live bij een huisartsenpraktijk op een maandagochtend. We hebben eerst twee weken shadow mode gedraaid. De agent nam niets op. Hij luisterde, transcribeerde en produceerde een parallelle beslisboom voor elke call die de doktersassistente live afhandelde. Aan het einde van haar dienst zag ze een klein dashboard met de zou-zijn-uitkomst van de agent per call. Verschillen gingen in een reviewqueue.
Na twee weken shadow mode en vier iteraties op de system prompt gingen we naar canary. De agent handelde 10% van de inkomende calls af, beperkt tot het rustigste uur van de dag (11:00 tot 12:00). Elke doorgezette call genereerde automatisch een post-mortem-entry. In die weken sneuvelden drie klassen bugs die we niet hadden voorspeld: het medicijn-naam-homofoon (Lyrica vs Lyric), de piepende kleuter waarbij de agent vals-groen een U4 boekte in plaats van te escaleren, en het Limburgse accent dat de cijferherkenner versloeg op ongeveer één BSN op de drie.
Volledige uitrol kwam pas toen het doorverbindpercentage in canary tien werkdagen op rij binnen 18-28% bleef. De eerste ochtend op 100% hadden we een senior engineer aan de uitbellijn en had de doktersassistente één toetsaanslag om de agent terug te zetten op 0% als iets er verkeerd uitzag. Ze gebruikte hem twee keer in de eerste week. Beide keren om de juiste reden: een onbekend belpatroon dat een apotheek bleek te zijn die via de patiëntenlijn binnenkwam.
Het kleinste wat je vandaag kunt doen
Toen we de voice agent voor deze praktijk bouwden, was het ding dat het project bijna om zeep hielp niet de LLM, de ASR of de FHIR-integratie. Het was het terugleggen van het BSN. Patiënten vertrouwen negen cijfers aan een stem aan de lijn ongeveer net zo graag toe als jij je bankpas aan een vreemde geeft. Zolang de agent die cijfers niet gegroepeerd, langzaam en elke keer goed terug kon lezen, deed de rest er niet toe. We losten het op door een door de ASR opgevangen BSN als voorlopig te behandelen en alleen DTMF of een verbaal "ja" na het teruglezen als ground truth te accepteren.
Pak één telefoongesprek dat jouw team veertig keer per dag afhandelt. Luister vijf ervan terug. Schrijf de vier intents op. Dat is je spec.
Kern
Een voice agent in de zorg verdient zijn plek door het terugleggen van het BSN en het doorverbinden bij twijfel goed te krijgen, voordat er iets ingewikkelders draait.
FAQ
Stelt de voice agent diagnoses?
Nee. Hij loopt de rubriek van de Nederlandse Triage Standaard af, boekt slots, en zet alles wat rood of dubbelzinnig is binnen vier seconden door naar de doktersassistente.
Waar gaat de audio heen?
Nergens permanent. LiveKit streamt audioframes naar Azure Speech in West Europe en gooit ze weg. Geen opname, geen transcript dat langer bewaard wordt dan de gestructureerde turn log.
Hoe verifieert de agent de patiënt?
BSN ingevoerd via DTMF of uitgesproken cijfers, gevalideerd met de elfproef, gegroepeerd teruggelezen ter bevestiging, en daarna geboortedatum als tweede factor voordat er ook maar een HiX-lookup loopt.
Kan de agent direct in HiX schrijven?
Alleen via een mediation service in het VLAN van de praktijk met OAuth-scopes die beperkt zijn tot Patient en MedicationRequest lezen, plus de schrijflijst voor de herhaalreceptaanvragen-Task.
Wat gebeurt er als de agent in de war raakt?
Hij zegt dat hij niet kan helpen en verbindt in zijn volgende uiting door naar een mens. Ongeveer 22% van de calls wordt doorgezet. Het systeem is daarop ontworpen, niet daartegen.