Chat agents
Chat agent triage: 1.420 offertes per week bij Bredase hovenier
Het is dinsdag 06:48 in maart. De seizoens-planning in de loods in Etten-Leur sluit om 07:30, en er staan nog 87 offerteaanvragen ongetrieerd in de inbox.

Het is dinsdag 06:48 in maart. De seizoens-planning in de loods in Etten-Leur sluit om 07:30, en er staan nog 87 offerteaanvragen ongetrieerd in de inbox van het bedrijf. De meewerkend voorman — 51 jaar, legt sinds 1994 tuinen aan — is aan z'n derde koffie en scrolt door mails op een Samsung-tablet met opgedroogde potgrond erop. Voor elke aanvraag is een ja-of-nee nodig: kan zijn ploeg dit offreren, en hoeveel van een bestaand beplantingsplan kunnen we hergebruiken.
Dat was de situatie bij het hoveniersbedrijf in Breda voordat we eind 2024 hun chat agent live zetten. Ze waren in zes jaar gegroeid van 8 naar 22 medewerkers, voornamelijk via mond-tot-mondreclame in de regio Brabant, en de inbox was sneller gegroeid dan de mensen die hem moesten lezen.
Het volumeprobleem dat niemand in het discovery-gesprek noemde
De briefing was: "we willen een chatbot op de site." Wat de data liet zien, nadat we een week lang de inbox en het WhatsApp Business-nummer hadden meegelezen, was iets anders: 1.420 offerteaanvragen in zeven dagen tijdens de piek van maart tot mei. Ruwweg 60% kwam binnen via het formulier op de site, 25% via WhatsApp, en de rest via mail, doorgestuurd vanuit verschillende intake-adressen (info@, offerte@, planning@, plus drie persoonlijke adressen van mensen die in 2017 het aanspreekpunt waren en daarna nooit van de site zijn gehaald).
Van die 1.420 was ongeveer 78% onder de 100m² — terras opnieuw beplanten, haag knippen, een rij buxus vervangen na de rupsplaag in de buurt. Dat soort werk loopt langs een vast spoor: de catalogus in Exact Online heeft het geprijsd als standaardpakket, de voorman hoeft er niet naar te kijken, en de leerling kan binnen een uur na binnenkomst een offerte sturen.
De 22% die menselijk oordeel vraagt is het hele probleem. Alles boven de 400m² raakt aan drainage, grondsoort, soms vergunningen, en bijna altijd hergebruik of uitbreiding van een bestaand beplantingsplan uit het PostgreSQL-archief — twaalf jaar aan ontwerpen, getekend in een zelfgebouwde tool die de vorige IT-vrijwilliger in 2013 in elkaar had gezet voordat hij naar Eindhoven verhuisde.
De twee systemen waar de agent mee moest praten
Twee databronnen, geen van beide ooit ontworpen om door een chat agent bevraagd te worden.
De eerste was hun Exact Online-tenant, waar de tuinplan-catalogus stond als Items met een wirwar aan custom division fields: oppervlaktebandbreedte, grondsoortband, zonband, gemiddelde install-uren, en een vrij tekstveld "notitie voor planner." De velden zijn tussen 2012 en 2024 een voor een toegevoegd door drie verschillende boekhouders, dus de naamgeving is inconsistent — m2_band op sommige records, OppervlakteBand op andere, OPP op een handvol uit 2015.
De tweede was het beplantingsplan-archief: een PostgreSQL 14-database op een Hetzner-bak, met circa 4.200 plannen, elk met een polygoon (PostGIS), een plantenlijst en een scan van de oorspronkelijke handgetekende schets. De plantenlijst gebruikt ongeveer de helft van de tijd Latijnse namen en de andere helft Nederlands — Buxus sempervirens op de ene regel, "lavendel" op de volgende.
Als je kennisbank door drie mensen in twaalf jaar is aangeraakt, kaatst de agent hun onderlinge meningsverschillen terug naar de klant, tenzij je upstream normaliseert. Dat leerden we in week één, toen een testantwoord een Latijnse en een Nederlandse naam voor dezelfde struik in opeenvolgende zinnen citeerde.
Triage in 90 seconden, geen offerte in realtime
De agent probeert niets te offreren. Hij trieert. Elke binnenkomende aanvraag valt binnen 90 seconden in een van vier bakken:
- Standaard onder 100m² — automatisch opgestelde offerte uit de Exact-catalogus, naar de wachtrij van de leerling.
- Standaard 100–400m² — idem, maar gemarkeerd voor een sanity check voordat hij eruit gaat.
- Boven 400m² — geparkeerd in de wachtrij van de meewerkend voorman, met daaraan vastgekoppeld de "closest match" uit het beplantingsplan-archief.
- Onduidelijk — de agent vraagt om één specifieke verduidelijking (bijna altijd: oppervlakte of postcode), en draait daarna opnieuw.
Die 90 seconden zijn geen marketinggetal. De seizoens-planning in de loods sluit om 07:30. Alles wat na 07:30 in de wachtrij van de voorman valt, schuift een dag op. In de piek moet hij de nachtwachtrij tussen 06:30 en 07:30 leeglopen. Hij heeft grofweg 60 minuten voor wat vroeger 90 minuten lezen was. De agent koopt dat halfuur voor hem terug door vóór-sorteren en vóór-matchen.
Hoe de 400m²-regel echt wordt afgedwongen
Oppervlakte is veruit het belangrijkste veld, en de klant geeft het bijna nooit netjes op. Ze zeggen "ongeveer een halve voetbalveld" of "achtertuin van een rijtjeshuis in Princenhage." De agent doet drie dingen in volgorde:
- Vraagt één keer om een getal, accepteert m², een lengte × breedte, of een postcode + huisnummer.
- Als er een postcode + huisnummer binnenkomt, bevraagt hij de BAG voor het perceel en berekent hij een grove perceel-oppervlakte minus de footprint van het pand.
- Als het resultaat binnen 50m² van de 400m²-grens ligt, escaleert hij sowieso naar de voorman — liever twee minuten van zijn tijd verspild dan een klus gemist die zijn oog nodig had.
Die "vraag het één keer"-regel telt. De agent stelt de klant nooit twee keer dezelfde vraag. Heeft de tweede beurt nog geen getal opgeleverd, dan vraagt hij in plaats daarvan om de postcode. Drie beurten en nog geen getal, dan parkeert hij het gesprek in de onduidelijk-bak en schrijft hij één regel notitie voor de mens. Klanten haken af bij een chat die als een formulier voelt.
def classify_size(payload: dict) -> SizeBucket:
m2 = payload.get("m2_explicit")
if m2 is None and (lw := payload.get("length_x_width")):
m2 = lw[0] * lw[1]
if m2 is None and (pc := payload.get("postcode_huisnummer")):
m2 = bag_perceel_m2(pc) - bag_pand_m2(pc)
if m2 is None:
return SizeBucket.UNKNOWN
if 350 <= m2 <= 450:
return SizeBucket.FOREMAN_BORDERLINE
if m2 > 400:
return SizeBucket.FOREMAN
if m2 >= 100:
return SizeBucket.APPRENTICE_CHECK
return SizeBucket.APPRENTICE_AUTO
Matchen tegen twaalf jaar beplantingsplannen
Wanneer een klus in de wachtrij van de voorman belandt, hangt de agent er de drie dichtstbijzijnde matches uit het PostgreSQL-archief aan vast. "Dichtstbij" is een gewogen score over oppervlakte, postcodeafstand (rijtijd telt voor een hoveniersbedrijf zwaarder dan hemelsbreed), grondsoortband en stijl-tags. We hebben de grondsoortband retroactief ingevuld vanuit een publieke bodemkaart en het zwaartepunt van elke gearchiveerde polygoon vooraf berekend, zodat de score onder de 200ms blijft.
SELECT
plan_id,
ST_Distance(centroid, ST_MakePoint($1, $2)::geography) AS meters,
abs(area_m2 - $3) AS m2_delta,
soil_band,
similarity(style_tags, $4) AS style_sim
FROM beplantingsplannen
WHERE area_m2 BETWEEN $3 * 0.5 AND $3 * 2.0
ORDER BY
(0.4 * (m2_delta / 1000.0))
+ (0.3 * (meters / 20000.0))
+ (0.3 * (1 - style_sim))
LIMIT 3;
De voorman opent de wachtrij, ziet de aanvraag, en heeft meteen drie eerdere plannen om naar te kijken. In ongeveer 70% van de gevallen pakt hij er een van die drie als basis voor de nieuwe offerte. In de overige 30% tekent hij vers, of vraagt hij de agent om een bredere zoekopdracht — allebei ondersteund.
Wat de eerste drie weken stuk ging
Twee dingen liepen mis die we niet hadden zien aankomen.
Eerst de rate limit van de Exact Online API. De tenant had een dagplafond en we brandden dat halverwege de middag op tijdens de piek. De oplossing was een Redis-backed read-through cache voor de catalogus, 's nachts om 03:00 ververst. Items veranderen niet vaak genoeg om realtime fris te hoeven zijn — de boekhouder past de prijzen één keer per kwartaal aan. Het loont om de rate-limit-documentatie van Exact te lezen voordat je hun API in productie gebruikt; de limieten zijn per endpoint, niet plat per tenant — dat soort detail kom je liever niet op de harde manier tegen.
Ten tweede vertrouwde de voorman de "closest match"-suggesties van de agent pas toen we lieten zien waarom. We hebben onder elke suggestie één regel uitleg toegevoegd: "zelfde grondsoortband, 1,2 km van de klus, vergelijkbare oppervlakte." Daarna verdubbelde zijn pick-rate uit de suggesties ruwweg in een week. De uitleg wordt gegenereerd uit dezelfde velden waar de score op rust, dus hij kan nooit fout zijn — hij vertelt alleen na wat de score doet.
Een agent wint geen vertrouwen door gelijk te hebben. Hij wint vertrouwen door z'n werk te laten zien in een zin die de operator in twee seconden kan verifiëren.
De cijfers na zes maanden
In april 2026 handelde de agent alle 1.420 wekelijkse aanvragen tijdens de piek af. De ochtendwachtrij van de voorman was gemiddeld om 07:18 leeg, twaalf minuten voor sluiting. De leerling stuurde rond de 740 standaardoffertes per week zonder dat de voorman ze ook maar zag — werk dat voorheen op zijn bureau belandde en daar tot donderdag bleef liggen. De onduidelijk-bak draait op circa 4% van het volume, en dat komt overeen met het percentage echt rare aanvragen (een bestaande klant die om een totaal ander klusje vraagt, iemand die de bot test, een sales pitch van een leverancier).
De conversie op de automatisch opgestelde offertes zit binnen twee procentpunt van die op de mens-opgestelde offertes uit dezelfde catalogus een jaar eerder. De wachtrij van de voorman heeft een hogere conversie dan vóór de agent, en dat schrijven we toe aan snellere reactietijd — aanvragen onder de 400m² krijgen tijdens kantooruren nu binnen een uur een prijs, waar dat eerder twee tot drie dagen was en de klant in de tussentijd al drie andere hoveniers had gebeld.
Wat dit niet heeft opgelost
De agent heeft de catalogus niet opgelost. Er staan nog steeds m2_band, OppervlakteBand en OPP naast elkaar voor dezelfde data. We hebben een normalisatielaag geschreven die alle drie leest en wegschrijft naar een vierde veld dat de agent zelf beheert, maar de onderliggende rommel zit er nog. De boekhouder ruimt het op "als er tijd voor is," wat er niet is. Dit is de eerlijke versie van elk AI-bovenop-legacy-verhaal: de agent plakt over de rommel heen; hij ruimt hem niet op.
Het PostgreSQL-archief mengt ook nog steeds Nederlandse en Latijnse plantnamen. We hebben een synoniemen-tabel gebouwd met zo'n 180 ingangen, die de long tail behoorlijk dekt, maar elke zes maanden voegt iemand een nieuwe vaste plant toe in slechts één van de twee talen, en het duurt een week voordat dat opvalt.
De kleinste audit die je vandaag kunt doen
Open je inbox-map van de afgelopen zeven dagen. Tel de aanvragen. Verdeel ze in bakken op oppervlakte of klusomvang — of, als je geen hoveniersbedrijf bent, op wat dan ook standaardwerk onderscheidt van maatwerk. Als 70% of meer mechanisch op elkaar lijkt, verdient een agent zichzelf terug voordat het volgende seizoen begint. Is alles maatwerk, dan heb je geen agent-probleem — dan heb je een prijsprobleem, en een bot lost dat niet op.
Toen we deze chat agent voor het Bredase hoveniersbedrijf bouwden, was het langste stuk werk niet het taalmodel. Het waren de BAG-lookup, de Exact-cache, en twaalf jaar inconsistente custom fields zo ver krijgen dat ze zich gedragen als één schema. Dat is het werk in de meeste AI-agent-projecten: niet het model, maar het leidingwerk eromheen.
Kern
Een agent wint vertrouwen door z'n score na te vertellen in één zin die de operator in twee seconden kan verifiëren — die ene regel verdubbelde de pick-rate van de voorman ruwweg.
FAQ
Waarom een drempel van 400m² en geen prijs per vierkante meter over de hele linie?
Omdat onder de 400m² het werk grotendeels catalogus-gedreven is en de leerling het kan offreren. Boven de 400m² spelen drainage, grondsoort en hergebruik van een beplantingsplan, en alleen de voorman heeft het oordeel om die af te wegen.
Wat gebeurt er als de klant nooit een oppervlakte opgeeft?
De agent vraagt één keer om een getal, één keer om een postcode + huisnummer, en parkeert de chat dan in de onduidelijk-bak met een notitie. Hij blijft niet doorvragen. Drie beurten is het maximum.
Zou je Exact Online niet kunnen overslaan en de catalogus rechtstreeks opslaan?
Dat zou kunnen, maar de boekhouder leeft in Exact. Twee bronnen van waarheid voor prijzen is hoe je eindigt met offertes op de prijs van gisteren. De Redis-cache geeft je snelheid zonder de data te forken.
Hoe lang heeft het project end-to-end geduurd?
Ongeveer tien weken van kickoff tot het moment dat de agent 100% van het piekvolume afhandelde. Twee weken inbox meelezen, vier weken bouwen, vier weken begeleide uitrol waarin elk antwoord van de agent dubbel werd nagekeken.
Maakt de agent ooit zelf een offerte, zonder mens ertussen?
Ja, voor de bak onder de 100m² waar de catalogus een standaardpakket heeft. Ongeveer 740 van de 1.420 wekelijkse aanvragen gaan nu automatisch opgesteld de deur uit. Alles boven de 100m² krijgt nog steeds een menselijke blik voor verzending.