Chat agents
Chat agent playbook: 1.560 storingen per week triëren
Een Zwols installatiebedrijf met 26 man, 1.560 storingsmeldingen per week, een 14 jaar oude Syntess ERP, een SQL Server 2014-bak, een SLA van 4 uur. Zo triëerden we het in 75 seconden.

Het is een zondag in februari, min vier in Zwolle, en de kantoorlijn van een installatiebedrijf met 26 man rinkelt voordat het espressoapparaat is opgewarmd. Een vrouw in Stadshagen belt omdat haar CV-ketel niet wil branden en haar douche koud is. Zij is de zevende beller van de ochtend. De dispatcher die weekenddienst heeft, is op de ene monitor ingelogd in Syntess Atrium en op de andere in een zelfgebouwde SQL Server 2014-database met onderhoudscontracten. Hij heeft ongeveer vier uur, de Techniek Nederland-norm voor een warmwater-weg-melding, om een monteur bij haar voordeur te krijgen. Hij neemt haar telefoontje niet aan. Een chat agent neemt eerst op, en heeft 75 seconden voordat de SLA-klok gaat tikken.
Die dispatcher is de bottleneck die wij moesten weghalen. Niet vervangen. Weghalen uit het kritieke pad. Dit is de playbook van wat we hebben gebouwd, ongeveer in de volgorde waarin we het bouwden.
De intake in kaart brengen voor er één regel code stond
Voordat we de agent aanraakten, zaten we twee volle maandagen op de dispatch. Maandagen zijn bij deze klant de piek: 312 storingsmeldingen in een gemiddelde week, bijna een vijfde van de 1.560 per week. De helft gaat over warmwater. De andere helft valt uiteen in CV-druk, thermostaat-koppeling, vloerverwarming, en het deed het gisteren nog.
We legden elk telefoontje vast in een spreadsheet met acht kolommen: beller, postcode, klacht in hun eigen woorden, klacht zoals de dispatcher die codeerde, contracttype, ingezette monteur, responstijd, uitkomst. Na twee weken hadden we 624 rijen. Daaruit ontwierp de routinglogica zichzelf bijna.
Kun je de triageboom niet op één A4 tekenen voor sprintplanning, dan hallucineert je chat agent die boom tijdens runtime.
De twee systemen waarin de waarheid leeft
Syntess Atrium is een veertien jaar oude ERP, gebouwd voor de Nederlandse installatiebranche. Hij houdt de klantgegevens bij, het adres, de geïnstalleerde ketels (merk, model, serienummer), en de historische werkbonnen. Hij biedt een SOAP-webservice aan die bij de klant sinds 2018 door niemand was aangeraakt. De inloggegevens stonden op een Post-it die inmiddels was weggegooid.
De onderhoudscontract-database is een SQL Server 2014-instantie die draait op een Dell-tower onder een bureau in het achterkantoor. Hij bevat 4.200 actieve servicecontracten, de SLA-tier van elk, en de laatste servicedatum. SQL Server 2014 bereikte het einde van Microsoft's extended support in juli 2024, wat betekent dat de bak op het moment dat wij begonnen op geleende tijd draaide en zonder patches.
We hebben geen van beide systemen gemigreerd. Dat was belangrijk. De klant had van een ander bureau een offerte van €180k gehad om beide te herbouwen. Onze opdracht was het tegenovergestelde: laat de systemen met rust, ga ervoor zitten, en laat de agent de menselijke equivalent zijn die met beide praat.
Het triagebudget van 75 seconden
De agent heeft 75 seconden tussen het eerste bericht en de 'monteur is onderweg'-SMS. Dat getal is niet willekeurig. We rekenden terug vanaf de SLA van 4 uur, trokken de mediane rijtijd in het servicegebied Zwolle eraf (38 minuten), de mediane reparatieduur (110 minuten), en een veiligheidsmarge van 27 minuten voor vertraging aan monteur-kant. Wat overbleef was 65 seconden. We rondden af op 75 om het model ruimte te geven om dubbelzinnige klachten uit te vragen.
Binnen die 75 seconden:
- 0–8s — groeten, postcode en huisnummer vragen, een lookup richting Syntess afvuren.
- 8–25s — terwijl Syntess antwoordt (langzaam, meestal 6 tot 9 seconden), de klacht in de woorden van de beller bevestigen.
- 25–45s — de contract-tier opzoeken in de SQL Server-database. Spoed-contracten gaan voor in de wachtrij.
- 45–60s — de storing classificeren. Het model kiest één van zeventien tags, met één specifieke exit:
cv_zonder_warmwater. - 60–75s — overdragen. Bij een spoed schrijven naar een Redis-queue die het scherm van de dispatcher elke twee seconden polt, en de bevestigings-SMS versturen.
Waarom de agent niet direct met Syntess praat
Een veelgemaakte fout bij het aansluiten van een LLM op een legacy ERP is om het model de SOAP-client en een prompt te geven. Wij hebben dat één keer gedaan. Het werkte zeventig procent van de tijd, wat in operations betekent dat het niet werkte.
In plaats daarvan schreven we een dunne Python-proxy, acht endpoints, 340 regels FastAPI, die tussen de agent en Syntess zit. De agent roept GET /customer?postcode=8043AB&huisnummer=12 aan. De proxy vertaalt dat in de SOAP-envelope die Syntess wil, regelt de WS-Security-headers, parset het antwoord, normaliseert de datums, en geeft schone JSON terug. Als Syntess een timeout geeft, doet de proxy retries met backoff en geeft hij een nette 503 terug waar de agent op kan herstellen.
from fastapi import FastAPI, HTTPException
import httpx
app = FastAPI()
SYNTESS_URL = "https://syntess.internal/atrium/ws"
@app.get("/customer")
async def get_customer(postcode: str, huisnummer: str):
envelope = f"""<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body><GetKlantByAdres>
<postcode>{postcode.upper()}</postcode>
<huisnummer>{huisnummer}</huisnummer>
</GetKlantByAdres></soap:Body></soap:Envelope>"""
headers = {"Content-Type": "text/xml", "SOAPAction": "GetKlantByAdres"}
try:
async with httpx.AsyncClient(timeout=9.0) as client:
r = await client.post(SYNTESS_URL, content=envelope, headers=headers)
r.raise_for_status()
except (httpx.TimeoutException, httpx.HTTPStatusError):
raise HTTPException(503, "syntess_timeout")
return {"raw_xml": r.text} # parsing/normalisation handled downstream
Die proxy is het saaie deel van het systeem. Het is ook het deel dat zeven maanden niet is uitgevallen.
De prioriteitsmatrix
In de agent zit een kleine prioriteitsmatrix. Het is geen modelbeslissing. Hij is hard-coded, omdat de responsnormen van Techniek Nederland niet onderhandelbaar zijn en omdat een CV-storing zonder warmwater op een zondag met een baby in huis niet het soort oordeel is dat wij willen uitbesteden aan een kansverdeling.
De matrix kent vier tiers:
- Spoed: geen warmwater, of geen warmte met een buitentemperatuur van 5°C of lager, of een Spoed-contract. Komt in de spoed-monteur-wachtrij. SLA-klok start direct.
- Urgent: geen warmte terwijl de binnentemperatuur nog boven de 15°C ligt, of een gedeeltelijke storing. Wordt ingeboekt op het eerste slot van de volgende ochtend.
- Regulier: storingen die af en toe optreden, lauw water, thermostaat-koppeling. Volgend regulier slot, meestal twee tot vier werkdagen.
- Geen storing: beller wil een offerte, een inspectie-afspraak, of heeft het verkeerde nummer. Wordt op maandagochtend doorgezet naar de kantoorlijn.
Het LLM stelt een tier voor. Een rules engine valideert die tegen de contractdata en de buitentemperatuur opgehaald van het KNMI-meetstation Zwolle. Als die twee het oneens zijn, wint de rules engine en wordt de case gevlagd voor menselijke review aan het eind van de dienst.
Een LLM dat noodgevallen classificeert mag over-escaleren, maar nooit onder-escaleren. Asymmetrische loss-functions horen in code, niet in de system prompt.
Wat we met SQL Server 2014 hebben gedaan
We hebben hem niet geüpgraded. We deden drie andere dingen.
Eerst zetten we er een read-only replica achter, draaiend op een kleine Ubuntu-VM, gevoed door Change Data Capture. De agent leest alleen van de replica. Als het verkeer van de agent piekt — er trekt een storm over, een merk ketel wordt teruggeroepen — blijft de primaire bak onaangetast.
Twee: we pakten het schema in een dunne query-laag in. Drie views, twee stored procedures. De agent ziet de ruwe tabellen nooit, wat betekent dat op de dag dat de klant besluit te migreren naar PostgreSQL of naar een hosted Atrium-opvolger, wij de viewdefinities aanpassen en verder niets verandert.
Drie: we sloten het netwerk dicht. De Dell-tower kan nu alleen nog met het replicatie-doel en de kantoorprinter praten. Niets anders. Dat is strikt genomen geen agent-issue, maar als je vanuit welk nieuw systeem dan ook een oude Microsoft SQL-instantie aanspreekt, behandel die bak dan als in quarantaine.
Observability zonder Datadog te kopen
Een bedrijf van 26 man heeft geen observability-stack van €1.200 per maand nodig. We loggen één gestructureerde JSON-regel per gesprek naar een bestand dat de proxy dagelijks roteert: timestamp, gehashte postcode, klachttag, voorgestelde tier, definitieve tier, latency in milliseconden, overdrachtspad. Dat bestand rsynct elke vijf minuten naar een tweede bak. Niets anders.
Drie counters leven in Redis: tier-override-count, Syntess-timeout-count, SMS-failure-count. Als een counter binnen een venster van vijftien minuten boven een drempel komt, kleurt de bijbehorende tegel op het scherm van de dispatcher oranje. De drempels zijn niet slim. Drie overrides in vijftien minuten. Twee Syntess-timeouts in vijftien minuten. Eén SMS-failure, ooit. Slimmigheid voeg je pas toe als saaie counters niet meer genoeg zijn.
Elke werkdag om 09:00 krijgt de operations manager een mail met de afgelopen 24 uur: totaal aantal gesprekken, verdeling over de tiers, elke override met het oorspronkelijke transcript-fragment, mediane latency, en eventuele staleness van de KNMI-feed. Zes regels tekst en één tabel. Ze leest het voor haar koffie. In zeven maanden tijd heeft ze tweemaal een interne SOP aangepast op basis van wat ze in dat rapport zag.
De eerste maand, in cijfers die we vertrouwen
We gingen live op een dinsdag in november, bewust niet op een maandag. Week één telde 1.488 storingsmeldingen. De agent handelde er 1.213 end-to-end af zonder dat een dispatcher de case aanraakte. De mediane time-to-spoed-dispatch zakte van 6 minuten 40 seconden, de baseline gemeten over de zes maanden vóór livegang, naar 58 seconden.
Twee cases werden door het model onder-geëscaleerd in de eerste drie weken. Beide werden door de rules-engine-override gevangen voordat ze bij een klant terechtkwamen. Geen van beide zou de 4-uursgrens hoe dan ook hebben overschreden, maar we behandelen ze allebei als P1-incident en hebben voor elk een post-mortem gedraaid. De fix was in beide gevallen dezelfde: de KNMI-temperatuuruitlezing was langer gecached dan we dachten, waardoor een buitentemperatuur die de Spoed-regel voor koud weer had moeten triggeren nog steeds een verouderde 7°C van een uur eerder toonde. We verlaagden de cache TTL van zestig naar tien seconden en voegden een freshness-check toe waarzonder de rules engine weigert te evalueren.
De rol van de dispatcher is verschoven, niet verdwenen. Hij reviewt nu overdag de classificaties van de agent, handelt de achttien procent van de gesprekken af die om een mens vragen (vooral oudere klanten die niet met een bot willen praten), en draait het spoed-monteur-bord. Zijn zondagen zijn rustiger.
De architectuur op een bierviltje
Als het op een bierviltje moest: één webhook van de telefoonprovider, één agent-proces, één Python-proxy die SOAP praat met Syntess, één read replica van SQL Server, één Redis-queue, één SMS-gateway, één KNMI-feed, en één dashboard voor de dispatcher. Acht bewegende delen. We weigerden er een negende bij te zetten.
Toen we de chat agent voor dit installatiebedrijf bouwden, liepen we steeds tegen hetzelfde aan: de asymmetrische kosten van misclassificatie. Spoed fout inschatten is onvergeeflijk, Regulier fout inschatten is één telefoontje. We losten het op door het LLM aan de ene kant van een rules engine te zetten en de contractdata aan de andere kant, met de engine als laatste woord. Dat is het patroon waar wij nu naar grijpen bij elke AI-agent die we leveren in een operatie met een SLA.
Het kleinste wat je vandaag kunt doen: open een spreadsheet, ga één maandagochtend naast je dispatch zitten, codeer elk gesprek in acht kolommen. De triageboom ligt voor lunch op tafel.
Kern
Hard-code de SLA-matrix, laat het LLM voorstellen, laat de rules engine beslissen. Onder-escalatie is de failure-mode die je vóór livegang wegontwerpt.
FAQ
Waarom Syntess Atrium en SQL Server 2014 niet gewoon vervangen?
Omdat de klant daarvoor een offerte van €180k op tafel had liggen en dat geld er niet was. De agent zit voor beide systemen, leest wat hij nodig heeft, en laat de systemen onaangetast. Migratie is een aparte, tragere beslissing.
Wat gebeurt er als Syntess plat ligt op het moment dat een klant belt?
De proxy geeft een 503 terug, de agent valt terug op een degraded flow die het gesprek handmatig vastlegt, het standaard als Urgent in de wachtrij zet, en de telefoon van de dispatcher pingt. Geen enkel gesprek gaat verloren door een backend-storing.
Neemt het LLM de prioriteitsbeslissing?
Het stelt een tier voor. Een rules engine valideert dat voorstel tegen de contractdata en de KNMI-temperatuurfeed. Bij onenigheid wint de rules engine. Het model heeft nooit het laatste woord over Spoed.
Hoe lang duurde de bouw end-to-end?
Zeven weken van kickoff tot livegang. Twee weken observeren op de dispatch, drie weken integratiewerk op Syntess en SQL Server, twee weken begeleide uitrol met een dispatcher die elk gesprek meekeek.