← Blog

Voice agents

Voice agent voor binnenvaart: 35 seconden tot escalatie

Om 03:12 belde een schipper met alarm op een propaanontluchting. De voice agent had 35 seconden om een kapitein wakker te bellen voordat de IVS90-klok van 30 minuten begon te lopen.

Jacob Molkenboer· Oprichter · A Brand New Company· 8 mei 2026· 9 min
Zwarte bakelieten telefoonhoorn op crème leren onderlegger, koperen stopwatch, groen zijden lint, gevouwen vlag in schaduw.

De call van 03:12

Het nummer staat geschilderd op de zijkant van de stuurhut, verweerd door het weer maar nog leesbaar. Om 03:12 op een dinsdag in februari toetst de schipper van een 86 meter lange type-C-tanker het in. De stuurhut ruikt naar propaan. Hij zegt niet “ik heb een ADN klasse 2-incident”. Hij zegt, in langzaam Dordts: “Er zit gas in de stuurhut, ik weet niet of het van mij of van de buurman komt.”

De voice agent heeft 35 seconden voordat dit een probleem voor de kapitein wordt, en 30 minuten voordat het een probleem voor Rijkswaterstaat wordt. Dit stuk is de playbook waarmee we die twee klokken in elkaar hebben laten passen.

Wat we aantroffen

De rederij draait met 28 mensen aan wal en een vloot van 19 schepen tussen Antwerpen en het Ruhrgebied. Hun dispatch-stack, in 2026:

  • Autena Maritime vlootbeheer, on-premise, versie 4.x, voor het laatst noemenswaardig geüpgraded in 2012. Stelt een SOAP 1.1-API beschikbaar achter een 100Mbit-glasvezel en Basic Auth.
  • Een zelfgebouwde sluis-planning-tool op basis van Exchange 2019-agenda-items. Elke sluisboeking is een agenda-item in een gedeelde mailbox; de onderwerpregel draagt de sluiscode, de body de cargo-string.
  • Een dispatchbord op een 32-inch touchscreen, aangedreven door een Excel-werkmap en een WinSCP cron job.
  • WhatsApp-groepen per route. Marifoon voor alles wat veiligheidskritisch is.
  • Een noodlijn die overging op een Polycom-bureautoestel in de planningskamer. Na sluitingstijd doorgeschakeld naar de planner van dienst, die opnam met een Nokia 6310 die hij weigerde te vervangen.

1.540 schipper-meldingen per week liepen via dat bureautoestel en die WhatsApp-groepen. Ruwweg 4% was tijdkritisch. De planners hadden geen snellere manier om te triëren dan “opnemen, luisteren, opschrijven, naar het bord lopen”. Wij werden binnengehaald om de niet-kritische 96% van hun handen te nemen zonder de 4% die er wél toe deed te breken.

Waarom voice, niet chat

Je kunt voor bijna elke operations-agent het argument voor chat maken. Wij hebben het geprobeerd. Schippers typen niet terwijl ze op de Oude Maas varen. Ze praten. Ze praten met de diesel op de achtergrond, met wind in de mic, met één hand aan het roer. Dus voice. We gebruiken een Nederlands-Vlaamse ASR met een maritieme keyword-biaslijst (sluiscodes, ADN-klassen, de namen van de 19 schepen, de elf sluizen tussen Lobith en Dordrecht). Die keyword-bias doet meer werk dan de modelwissel deed.

Het budget van 35 seconden

Vijfendertig seconden is de interne SLA van “binnenkomende oproep” tot “telefoon kapitein trilt”. Het is niet de wettelijke klok — dat zijn die 30 minuten — maar het is de enige klok die de kapitein tijd koopt om het wettelijke papierwerk netjes te doen. Anatomie:

  • 0–2s: begroeting in het Nederlands, barge-in open zodat de schipper kan onderbreken.
  • 2–12s: intent-capture. Vrije tekst. Geen menu.
  • 12–18s: ADN-klasse-detectie. Eerst een goedkope regex (keywords als gas, lek, damp, klasse 2), daarna een kleine classifier als second opinion.
  • 18–25s: load-lookup in Autena om te bevestigen wat het schip daadwerkelijk vervoert.
  • 25–30s: sluisvenster checken in Exchange, zodat de agent weet waar op de route het schip zit.
  • 30–35s: routeren. Klasse 2 naar de kapiteinsqueue, overige ADN-klassen naar de plannersqueue, de rest blijft bij de bot.

De lookup-stappen lopen parallel. Ligt Autena plat, dan gaat de agent ervan uit dat het schip beladen is met de laatst bekende manifest, escaleert sowieso, en geeft de staleness door aan de kapitein. We hebben false positives boven false negatives gekozen met een ruime marge.

Waarschuwing

Je interne SLA is niet de wettelijke klok. De IVS90-meldplicht van Rijkswaterstaat is de wettelijke klok, en die start zodra de schipper het incident opmerkt, niet wanneer jouw agent opneemt. Schep je op over 35 seconden, schep dan op over hoeveel van de resterende 29 minuten 25 seconden je voor de kapitein hebt gekocht.

De Autena-adapter

Autena Maritime stelt een SOAP 1.1-service beschikbaar. Geen WS-Security, Basic Auth over plaintext op het LAN, MTOM voor cargo-attachments. De WSDL liegt over array-cardinaliteit — bij één cargo krijg je een string, bij meerdere een array. Veertien jaar oude SOAP-services doen dit veel. We wikkelen het in een dunne Node-service en behandelen de wrapper als source of truth.

import { createClientAsync, BasicAuthSecurity } from 'soap'

const WSDL = process.env.AUTENA_WSDL!

export async function getLoadManifest(barge: string, signal: AbortSignal) {
  const client = await createClientAsync(WSDL, {
    wsdl_options: { timeout: 3000 },
    forceSoap12Headers: false,
  })
  client.setSecurity(new BasicAuthSecurity(
    process.env.AUTENA_USER!, process.env.AUTENA_PASS!
  ))

  const [res] = await client.GetLoadManifestAsync(
    { bargeCode: barge },
    { timeout: 3000, signal }
  )

  // WSDL says Cargo: Cargo[]; reality returns a single object when
  // the boat carries one cargo type. Normalise.
  const raw = res?.GetLoadManifestResult?.Cargo
  const cargoes = Array.isArray(raw) ? raw : raw ? [raw] : []

  return cargoes.map(c => ({
    adnClass: String(c.ADNClass ?? '').trim(),
    unNumber: String(c.UNNumber ?? '').trim(),
    tonnage: Number(c.Tonnage ?? 0),
  }))
}

De timeout van 3 seconden doet ertoe. Binnen het budget van 35 seconden hebben we een venster van 7 seconden voor de lookup. We geven Autena drie seconden, proberen één keer opnieuw, en als beide falen vallen we terug op de laatste gecachete manifest in Redis. De cache wordt geïnvalideerd zodra de Autena-laadbon-webhook afgaat; heb je geen webhook, poll het laadjournaal dan elke 60 seconden en accepteer de staleness.

De Exchange 2019 sluis-planning-sync

De planning leeft in agenda-items op een gedeelde Exchange 2019-mailbox. We benaderen die via EWS (Exchange Web Services), wat Microsoft al jaren probeert uit te faseren maar wat in 2026 nog steeds werkt op Exchange Server on-prem. Microsofts documentatie van de EWS managed API is nog steeds de canonieke referentie voor het protocol; behandel die als het contract.

De onderwerpregel is een strikt format: SLZ-<lockcode>-<ETA HH:mm>-<bargeCode>. De body is een vrije-tekstbeschrijving van de cargo, en daar worden de menselijke planners creatief — afkortingen, typo’s, af en toe een “check met Marco”-notitie. We parsen de onderwerpregel met een strikte regex (hard falen op malformed) en de body met een kleine LLM-call die null teruggeeft als hij het niet zeker weet. Null is prima. De agent kan ook zonder geparsede cargo-string escaleren; de kapitein heeft de manifest.

Read-only is niet onderhandelbaar. De agent schrijft nooit naar de planningsmailbox. Als de planners één spook-agenda-item van een bot betrappen, verliest het hele project vertrouwen. Lezen, loggen, escaleren — dat is het contract.

De escalatieboom

Drie bakken. De taxonomie ís de agent.

  • Kapiteinsqueue. ADN klasse 2 (gassen) bevestigd, of klasse 2 vermoed en de load-lookup mislukt. De telefoon van de kapitein van dienst gaat. Geen antwoord binnen 90 seconden? De tweede kapitein. Geen antwoord binnen 180 seconden? Dan escaleert de agent naar het mobielnummer van de technisch directeur en sms’t beide kapiteins parallel.
  • Plannersqueue. Elke andere ADN-klasse, planningsverschuivingen, sluisconflicten, mechanische meldingen die niet veiligheidskritisch zijn. Gaat naar de planner van dienst. SLA: 4 minuten.
  • Bot. ETA-updates, hernieuwde sluisboekingen, cargo-bevestigingen, simpele beschikbaarheidsvragen. De agent handelt deze end-to-end af en schrijft één regel terug naar het dispatchbord.

De ADN-klassedefinities komen uit de regelgeving van UNECE ADN; we coderen ze in een JSON-bestand en versiebeheren het. Toen de editie van 2025 één UN-nummer van klasse 3 naar klasse 6.1 verschoof, hebben we de JSON aangepast en een backtest over de afgelopen zes maanden aan calls gedraaid. Twee van de 1.200 calls zouden anders gerouteerd zijn. We hebben het de kapiteins verteld. Ze wisten het al.

Wat in week één brak

Drie dingen, achteraf alle drie overduidelijk.

Eén. Een schipper uit Sint-Niklaas met een stevig West-Vlaams accent verwarde de intent-classifier. De biaslijst hielp op zelfstandige naamwoorden maar niet op de omringende grammatica. We hebben een second-pass-classifier toegevoegd die over de ruwe transcript loopt zodra de confidence van de eerste onder 0.6 zakt. Trager, maar het stopte het misrouten.

Twee. Een kapitein had zijn werktelefoon op stil gezet voor een doktersafspraak en vergat hem weer aan te zetten. De agent belde hem netjes, kreeg de voicemail, escaleerde na 90 seconden naar de tweede kapitein — en dat werkte. Maar we hadden “telefoon kapitein staat stil” niet als terugkerende klasse ingepland. Bij een klasse 2 pingen we nu vanaf de eerste seconde beide kapiteins parallel; de tweede hang-up kost ons niets.

Drie. De Autena-machine was gedimensioneerd op tien dispatchers, niet op tien gelijktijdige agent-threads. Synchrone load-lookups tijdens de ochtendrush brachten de SQL Server-CPU op 95%. We hebben een cache met een TTL van 60 seconden voor Autena geplaatst en een single-flight-lock zodat twee bellers over hetzelfde schip één lookup delen. CPU zakte naar 30%.

Replay or it didn’t happen

Elke call wordt opgenomen met toestemming van de schipper (een eenmalige enrollment, niet per call). Elke beslissing van de agent wordt gelogd met de inputs die hem voedden: ASR-transcript, ADN-klassekandidaat, Autena-respons, Exchange-respons, route. Zegt een kapitein “de agent heeft me voor niks gebeld”, dan trekken we de replay binnen 90 seconden en lopen we de redenering door. Twee keer in vijf maanden vonden we een echte bug. Zes keer vonden we een kapitein die was vergeten wat het beleid zei. De replay-tool is niet onderhandelbaar voor vertrouwen.

Wat we anders zouden doen

De wachtdienst voor de kapiteins binnen het datamodel van de voice agent zelf bouwen. We hebben eerst PagerDuty gebruikt omdat het er toch al stond, en uiteindelijk meer plakwerk geschreven dan het rooster zelf had geweest. Vijf tabellen in Postgres waren goedkoper geweest.

Intent-classificatie en ADN-classificatie als twee aparte calls draaien. We hebben geprobeerd ze in één prompt te vouwen om 400ms te besparen. We hebben de latency bespaard en de accuracy opgegeten. De kosten van één verkeerde routing om 03:12 zijn veel hoger dan 400ms op een dinsdagmiddag.

Testen met echte opnames. We hebben de pilot gedraaid met geënsceneerde voice-clips waarbij de planners scripts voorlazen. Echte schippers hebben motorlawaai en marifoongetater achter zich; geënsceneerde voice niet. Drie weken pilotdata zijn meer waard dan drie maanden geënsceneerde tests.

Het kleinste wat je vandaag kunt doen

Pak je eigen noodlijn, in welke vorm dan ook, en schrijf het budget van 35 seconden op een whiteboard. Waar gaat de tijd vandaag heen? Opnemen, in de wacht, opzoeken, overdragen. Weet je het niet, ga dan een dienst lang naast degene zitten die opneemt en klok het. De agent komt later. Het budget ís de playbook.

Toen we deze voice agent voor de Dordtse rederij bouwden, was het lastigste niet het model of de SOAP-service — het was de kapiteins en de planners op één lijn krijgen over wat in het grijze gebied als klasse 2 telde. We hebben de taxonomie op een vrijdagmiddag met hen geschreven en die op maandag gecodeerd.

Kern

Je interne SLA is niet de wettelijke klok. Bouw eerst het budget van 35 seconden, dan de agent die erin past.

FAQ

Hoe verschilt dit van een standaard IVR?

Een IVR menu&rsquo;t de beller. Deze agent niet. De schipper praat vrij; de agent classificeert, kijkt de manifest op, checkt het sluisvenster en routeert. Geen &lsquo;1 voor planning, 2 voor noodgevallen&rsquo;-boom.

Waarom doet de agent de IVS90-melding niet zelf?

Omdat de wettelijke melding de verantwoordelijkheid van de kapitein is, niet van de agent. De agent koopt tijd, verzamelt context en geeft de kapitein een voorgevuld concept. Indienen blijft mensenwerk.

Hoe ga je om met Vlaams en regionale accenten?

Een maritieme keyword-biaslijst doet het meeste werk. Zakt de confidence van de eerste classifier onder 0.6, dan draaien we een tweede ronde over de ruwe transcript. Het kost latency, maar het stopt misrouten.

Wat kost klasse 2-detectie aan latency?

Zes seconden binnen het budget van 35 seconden: eerst een goedkope regex, daarna een kleine classifier als second opinion. De lookups in Autena en Exchange lopen daarna parallel, niet ervoor.

voice agentsai agentsintegrationslegacy sitescase studyoperations

Iets bouwen?

Start een project