← Blog

Voice agents

Voice agents en taalwissels: case study Utrechtse dierenarts

Een Utrechtse dierenartsketen verving zijn nachtservice door een voice agent in drie talen. Hoe we voorkwamen dat hij midden in het gesprek omschakelde, en wat één gesprek kost.

Jacob Molkenboer· Oprichter · A Brand New Company· 5 jun 2026· 9 min
Zwarte bakelieten telefoonhoorn op crème vilt, groen zijden lint eroverheen, gevouwen papieren kaarten met rood lakzegel.

Het is zaterdag, 23:14. Een man in Overvecht houdt een kat vast die een stuk naaigaren heeft ingeslikt. Hij belt zijn vaste dierenarts. De lijn neemt op na de tweede beltoon, in het Nederlands, kalm, en vraagt hem te beschrijven wat hij ziet. Twee vragen later wordt hij doorverwezen naar de spoedkliniek in Nieuwegein, krijgt het adres op zijn telefoon, en wordt de dienstdoende dierenarts gepiept zodat ze weet wat eraan komt. Het hele gesprek duurt tweeënnegentig seconden.

De stem aan de andere kant is geen mens. Het is een voice agent die we vier maanden geleden hebben opgeleverd voor een keten van zeven dierenartspraktijken rond Utrecht. Hun avond- en nachtlijn ging vroeger naar een betaalde antwoorddienst die, op een goede nacht, het juiste bericht binnen twintig minuten bij de juiste dierenarts kreeg. Op een slechte nacht kwam het bericht om 06:00 binnen met de tekst "klein hondje, ademt raar" en geen terugbelnummer.

Deze post gaat over wat we hebben gebouwd, wat als eerste brak, en waarom "wissel nooit van taal tijdens een gesprek" de lastigste eis bleek te zijn.

Voice, geen chat

We hebben in het eerste gesprek tegen voice geduwd. Voice agents zijn per minuut duurder dan een chatwidget, de latency is moeilijker te verbergen, en de fouten zijn luider. Een chatbot die "kat" verkeerd leest als "kit" produceert een rare zin. Een voice agent die hetzelfde doet leest die rare zin hardop voor, met een kalme robotstem, terwijl iemands hond een aanval heeft.

De eigenaar van de keten liet ons de bel-log zien. Eenenveertig procent van de avondbellers kon niet comfortabel typen: oudere eigenaars, mensen met twee handen aan het dier, ouders met een kind op de arm. Nog eens twintig procent belde vanuit de auto. De rest probeerde de website-chat en belde daarna alsnog, want niemand vertrouwt om 02:00 een chatbot met een ziek huisdier.

Dus werd het voice.

Drie talen, één centrale

Utrecht heeft een Nederlandstalige meerderheid, een flinke Engelstalige populatie (veel universiteitspersoneel, veel kennismigranten), en de praktijk in Kanaleneiland heeft een vaste Arabischsprekende klantenkring. Grofweg: 78% Nederlands, 14% Engels, 8% Arabisch, met een lange staart van al het andere.

In onze eerste versie moest de beller een taal kiezen. "Voor Nederlands, druk 1. Voor Engels, druk 2. Voor Arabisch, druk 3." In tests werkte het prima. We hebben het uitgerold. We hebben het binnen tien dagen weer eruit getrokken.

Het probleem was niet het menu. Het probleem was dat mensen in nood niet naar menu's luisteren. Ze beginnen te praten voordat de prompt klaar is, en ze praten in de taal die ze denken. Een Nederlandstalige drukt 1 en zegt dan "ja hallo, mijn hond". Een Engelstalige drukt niks en begint gewoon te praten. Een Arabische beller drukt 3 en schakelt direct over naar Nederlands, want dat is de taal die ze normaal met de receptionist spreekt.

We hebben de boel opnieuw opgebouwd rond taaldetectie vanaf de eerste uiting.

De detector

Taalidentificatie op één korte uiting is een bekend probleem. Whisper doet het als bijproduct van de transcriptie, het Deepgram language detection endpoint geeft een code en een confidence-score per request terug, en er zijn kleinere dedicated modellen als je er zelf één wilt draaien.

We zijn uitgekomen op een pipeline in twee stappen. De eerste 1,2 seconden audio gaan naar een snelle classifier die een taalcode en een confidence-score teruggeeft. Als de confidence boven de 0,85 zit, vergrendelen we die taal voor de rest van het gesprek. Zo niet, dan spelen we een korte uitleg af in Nederlands en Engels ("hallo, you can answer in Dutch, English, or Arabic"), en classificeren we opnieuw op het antwoord.

Het vergrendelen is het cruciale deel, en dat is de volgende sectie.

# Simplified language-lock state
class CallState:
    def __init__(self):
        self.language = None        # 'nl' | 'en' | 'ar'
        self.locked = False
        self.lock_confidence = 0.0

    def consider(self, detected_lang: str, confidence: float):
        if self.locked:
            return self.language
        if confidence >= 0.85:
            self.language = detected_lang
            self.lock_confidence = confidence
            self.locked = True
        return self.language

Dat is de hele language-lock. Negen regels. We hebben twee weken iets slimmers geprobeerd, en elke slimmere versie maakte het erger.

Waarom vergrendelen

Meertalige voice-modellen willen graag aansluiten. Als een beller in het Nederlands begint en één Engels woord in een zin laat glijden ("mijn dog is ziek"), wisselt de onderliggende STT soms zijn taalgok voor de volgende beurt, en antwoordt de TTS in het Engels. De beller, die Nederlandstalig is, hoort een Engelse zin, en nu hebben we een probleem. Ze schakelt of over naar Engels om de agent te volgen (wat tweetalige mensen automatisch doen), of ze raakt in de war en hangt op. Hoe dan ook, we hebben gefaald.

Let op

Het standaardgedrag van elke kant-en-klare voice-stack die we hebben getest was: bij elke beurt opnieuw de taal detecteren. Dat klinkt als een feature. In een triagegesprek is het een bug.

Zodra we vergrendelen, kan er alleen gewisseld worden als de beller daar expliciet om vraagt, in welke taal dan ook. We hebben een kleine intent-classifier op elke beurt toegevoegd die luistert naar verzoeken om van taal te wisselen ("kunt u Engels spreken", "can you speak English", en het Arabische equivalent). Als die met hoge confidence afgaat, vragen we opnieuw, detecteren opnieuw, en vergrendelen opnieuw.

We gebruiken één TTS-stem per taal, allemaal van ElevenLabs, gekozen om over drie accenten heen als dezelfde rustige persoon te klinken. Dezelfde naam in de introductie. Dezelfde afsluitende zin. Die cosmetische consistentie voorkomt dat bellers het gevoel krijgen dat het systeem ze tussen drie verschillende bots heen en weer gooit.

De system prompt, grotendeels

De agent draait op een system prompt die grotendeels de triagebeslisboom is, plus een korte set gedragsregels. We houden de prompt onder de 1.500 tokens. We zetten niet de volledige rode-vlag-taxonomie in de prompt; we zetten de gestructureerde beslisboom erin, en bovenaan één korte voorbeelddialoog per taal.

De nuttigste enkele regel in de prompt is: "Als je het antwoord niet weet, draag over aan een persoon. Niet gokken."

Drie dingen zijn bewust uit de prompt gehouden. De telefoonnummers van de dierenartsen: de agent weet niet wie dienst heeft, hij belt een webhook die dat bepaalt. De adressen van de praktijken: zelfde reden, want adressen veranderen en system prompts worden niet bijgewerkt om 03:00 op een zondagochtend. En de klantendatabase: de agent vraagt om de naam en het telefoonnummer van de eigenaar en zoekt ze daarna op via een tool call naar het praktijkmanagementsysteem.

Het scheiden van wat de agent weet en wat de agent opzoekt is het stuk waar de meeste interne voice-agent-projecten de fout in gaan. Als de prompt het antwoord bevat, moet de prompt opnieuw worden uitgerold elke keer dat het antwoord verandert. Als de prompt de vraag bevat en een tool het antwoord teruggeeft, blijft de agent klein.

Telefonie, kort

De telefoonkant draait op Twilio. De bestaande nummers van de kliniek zijn geport, de avond- en nachtregels staan in een Twilio Studio flow, en de websocket-brug tussen Twilio Media Streams en onze agent-loop is een kleine Node-service. Niets exotisch. Media Streams geeft je ruwe audio in beide richtingen, en dat is alles wat je echt nodig hebt.

De end-to-end latency van end-of-speech naar start-of-response zit meestal tussen 850 en 1100 ms. Onder 1,2 s ligt de grens waaronder een beller niet bewust voelt dat hij met een machine praat. Boven 1,5 s begint het te voelen als een slechte VoIP-lijn.

Triagelogica

De dierenartsen gaven ons een uitgeprinte rode-vlag-lijst die ze al jaren gebruikten: signalen die betekenen "nu naar spoed" versus "morgenochtend als eerste" versus "kan wachten tot maandag". We hebben het uitgetypt, gestructureerd, en omgezet in de beslisboom van de agent.

De agent kan per gesprek drie dingen doen. Hij kan de beller doorsturen naar de 24-uurs spoedkliniek in Nieuwegein, waarbij het adres tijdens het gesprek via sms wordt verstuurd. Hij kan de dienstdoende dierenarts van de betreffende praktijk piepen, met een terugbelnummer en een samenvatting van één zin, zodat de telefoon van de dierenarts overgaat met die samenvatting al op het scherm. Of hij kan een normale afspraak boeken voor de volgende ochtend, direct in het bestaande praktijkmanagementsysteem.

{
  "call_id": "01J7K2...",
  "language": "nl",
  "language_confidence": 0.97,
  "duration_s": 92,
  "outcome": "send_to_er",
  "summary_for_vet": "Kat, ~4 jaar, draad ingeslikt ca. 30 cm. Geen braken. Eigenaar op weg.",
  "summary_for_owner_sms": "Spoedkliniek Nieuwegein, Pijlsweerd 12. Bel vooruit: 030-...",
  "red_flags": ["foreign_body_string", "cat"],
  "handoff_at_ms": 88300
}

Die JSON is wat we naar de telefoon van de dierenarts sturen. Geen audio-replay, geen vertraging van twintig minuten, alleen de gestructureerde beurt-voor-beurt-registratie en de samenvatting van één zin. De dierenartsen vroegen na ongeveer week drie niet meer om nieuwe features in dit deel van het systeem.

Wat we gemeten hebben

In de eerste dertig dagen na go-live waren dit de cijfers die ertoe deden:

  • 412 buiten-uren-gesprekken afgehandeld.
  • Mediaan gespreksduur: 1 min 38 sec.
  • Correct gerouteerde gesprekken (volgens de review door de dierenarts de volgende dag): 96,1%.
  • Gesprekken waarin de beller om een persoon vroeg en we hebben overgedragen: 4,4%.
  • Gesprekken waarin de agent tegen de bedoeling van de beller in van taal wisselde: 0. Dit was de metric waar de eigenaar van de keten het meest om gaf.
  • Kosten per afgehandeld gesprek: 0,27 EUR inclusief telefonie, STT, LLM en TTS. De vorige antwoorddienst rekende 1,85 EUR per gesprek met een maandelijks minimum van 12 EUR per praktijk.

De belangrijkste feedback van de eigenaar na de eerste maand ging over een heel ander probleem: de dienstdoende dierenartsen slapen nu door minder vals alarm heen, omdat de agent de "is dit normaal"-vragen wegtrieert voordat ze worden gebeld. Eén dierenarts vertelde ons dat het julirooster de eerste maand in drie jaar was waarin ze niet door een niet-spoedgeval wakker werd gebeld.

Drie dingen die kapot gingen

De Arabische TTS sprak de praktijknamen verkeerd uit. Nederlandse plaatsnamen zijn lastig voor elke TTS die er niet op getraind is. We hebben dit opgelost met een SSML-phoneme-override voor elke praktijknaam en straat. ElevenLabs accepteert IPA binnen <phoneme>-tags. Dat kostte een middag.

De agent zei ongeveer één keer per week midden in een beurt gedag. Het bleek dat we het LLM-antwoord afkapten op een leesteken dat binnen een afkorting voorkwam ("dr."). We hebben de streaming-stop-heuristiek aangepast: er moet nu een leesteken zijn gevolgd door 600 ms modelstilte.

Een handvol Nederlandse bellers werd op de eerste uiting als Duits geclassificeerd, omdat ze alleen "ja" antwoordden. We hebben "ja", "nee", "hallo", "alstublieft" en een tiental andere één-woord-antwoorden hard-coded als Nederlandse override, voordat de classifier überhaupt draait.

De overdracht

De beste beslissing die we hebben genomen was om het pad naar een mens duidelijk en kort te houden. Elke beller die, in welke taal dan ook, iets zegt dat neerkomt op "ik wil een persoon spreken" wordt overgedragen. We proberen geen klachten af te handelen. We proberen geen facturatiekwesties af te handelen. We laten de agent met niemand discussiëren.

Kern

Het werk van een voice agent is de saaie 90% goed doen en uit de weg gaan voor de andere 10%. Bouw de overdracht voordat je de slimme dingen bouwt.

Wat we anders zouden doen

We zouden de language-lock bouwen vóór de eerste deploy, niet erna. We zouden ook starten met één taal en de andere twee in week drie toevoegen, in plaats van proberen alle drie tegelijk te lanceren. De dekking van Arabische accenten duurde langer dan we verwachtten, en we hadden onze STT moeten benchmarken op Levantijnse, Egyptische en Maghrebijnse samples voordat we ons committeerden.

Toen we deze voice agent voor de Utrechtse keten bouwden, was het stuk dat we onderschat hadden hoe sterk bellers in nood willen voelen dat ze met één persoon praten die hen begrijpt, en niet met een systeem dat tussen modi schakelt. Elke ontwerpkeuze na de eerste maand kwam daarop terug. Denk je over een vergelijkbaar project na, dan hebben we onze algemene aanpak voor voice- en chat-agents op de dienstpagina beschreven. De korte versie: kies één stem, kies één taal tegelijk, en vergrendel allebei.

Eén ding om vandaag te proberen

Heb je een buiten-uren-telefoonlijn en wil je weten of een voice agent kan helpen? Doe dit een week lang. Log van elk gesprek de duur, het tijdstip, en een uitkomst van één regel ("geboekt", "verkeerd nummer", "naar spoed", "factuurvraag"). Tel het op. De vorm van die spreadsheet vertelt je of je een voice-agent-probleem hebt of een personeelsprobleem. Het zijn verschillende problemen, en ze hebben verschillende oplossingen.

Kern

Vergrendel de taal van de beller na één detectie. De meeste voice-stacks detecteren bij elke beurt opnieuw, en in een triagegesprek is dat een bug, geen feature.

FAQ

Waarom de beller niet zelf een taal laten kiezen met een druk-1, druk-2-menu?

Bellers in nood praten dwars door het menu heen. We hebben het geprobeerd en na tien dagen weer eruit gehaald. Taal detecteren op de eerste uiting en vervolgens vergrendelen is wat in productie wel werkte.

Wat kostte de voice agent per gesprek vergeleken met de vorige antwoorddienst?

0,27 EUR per afgehandeld gesprek inclusief telefonie, STT, LLM en TTS, tegenover 1,85 EUR per gesprek bij de vorige antwoorddienst, met een maandelijks minimum van 12 EUR per praktijk.

Hoe gaat de agent om met een beller die echt van taal wil wisselen?

Op elke beurt draait een kleine intent-classifier die luistert naar expliciete wisselverzoeken in een van de drie talen. Als die met hoge confidence afgaat, vraagt de agent opnieuw, detecteert opnieuw, en vergrendelt opnieuw.

Wat gebeurt er als de agent niet weet hoe hij een gesprek moet afhandelen?

Hij draagt over. De regel in de system prompt is één zin: als je het antwoord niet weet, draag over aan een persoon, niet gokken. Het overdrachtspad is gebouwd vóór de slimme delen.

voice agentsai agentscase studyintegrationsworkflowautomation

Iets bouwen?

Start een project