Het is 23:00 uur. De eigenaar van het bureau heeft 312 ongelezen mails, een klant die vraagt waarom zijn campagne stilligt, een leverancier die achter een inkoopnummer aan zit, en drie koude pitches die zich voordoen als reply. Vier verschillende leveranciers hebben haar dit kwartaal verteld dat een AI-agent haar inbox wel even overneemt. Ze heeft er één geprobeerd. Die stuurde een journalist een prijsopgave die bedoeld was voor een lead. De volgende ochtend zette ze 'm uit.
Deze post is de aanpak die we elke klant meegeven voordat we een chat- of email-agent aan een echte inbox koppelen. Drie regels. Voldoet je agent niet aan alle drie, hou 'm dan in draft-mode en laat 'm niet versturen.
Regel één: een scope waarvan de agent kan aantonen dat hij erbinnen zit
De meeste inbox-agent-fouten zijn scope-fouten. De agent moet "support-mail afhandelen" en niemand heeft gedefinieerd wat dat betekent. Dus beantwoordt hij een juridische vraag van de tegenpartij. Hij citeert een refund-policy die vorig kwartaal is aangepast. Hij gaat akkoord met een meeting op een datum dat de oprichter in het vliegtuig zit.
De oplossing is saai en werkt: de agent moet het bericht classificeren in een van een korte, gesloten lijst met intents voordat hij een antwoord mag opstellen. Niet "is dit een support-mail?" — dat is een tautologie. De intents zijn de concrete dingen die je bereid bent zonder mens in de loop te laten beantwoorden.
Voor een typische MKB-inbox beginnen we met vijf: order_status, invoice_question, meeting_request, general_info, out_of_scope. Alles wat niet in de eerste vier past komt in out_of_scope en gaat door naar een mens. De agent gokt nooit. Zit de confidence onder de drempel, dan is de intent standaard out_of_scope.
INTENTS = {
"order_status",
"invoice_question",
"meeting_request",
"general_info",
"out_of_scope",
}
def classify(message: str, llm) -> tuple[str, float]:
result = llm.classify(
message,
labels=sorted(INTENTS),
system="Pick exactly one label. If unsure, pick out_of_scope.",
)
intent = result.label if result.label in INTENTS else "out_of_scope"
if result.confidence < 0.75:
intent = "out_of_scope"
return intent, result.confidence
Twee dingen zijn hier belangrijk. Ten eerste: de labelset is gesloten — de agent kan om 3 uur 's nachts geen zesde intent verzinnen. Ten tweede: een lage confidence is geen draft-probleem, het is een routing-probleem. Je wilt geen voorzichtig geformuleerd antwoord op een bericht dat je niet hebt begrepen. Je wilt dat een mens het ziet.
Een chat-agent zonder gesloten intent-lijst doet geen inbox-triage, die gokt met jouw antwoorden.
Waarom niet één grote prompt
Je wordt verleid om de classifier over te slaan en het model gewoon een lange system prompt te geven: "beantwoord alleen vragen over X, Y, Z, escaleer anders." We hebben het geprobeerd. Het drift. Het model beantwoordt behulpzaam een vraag die voor 80% binnen scope lag en voor 20% erbuiten, en die 20% is het stuk waarop je aangeklaagd wordt. Een aparte classifier-stap is een beslissingsgrens die je kunt loggen, auditen en tunen. Eén prompt is op gevoel.
Regel twee: een escalatiepad dat de default is, niet de fallback
De tweede regel draait om hoe de meeste teams over human-in-the-loop denken. De agent antwoordt niet eerst en escaleert pas als het misgaat. De agent schrijft een draft, en een mens keurt goed, totdat de agent het recht heeft verdiend om zelfstandig te versturen — per intent, niet in totaal.
We draaien elke nieuwe agent in drie fases:
- Shadow. De agent schrijft drafts in een Slack-kanaal. Er gaat geen antwoord uit. Een mens leest de draft naast het echte bericht en stuurt zelf een antwoord vanuit zijn gewone mailclient, of negeert de draft. Dit draait minstens 100 berichten per intent.
- Suggest. De agent schrijft direct in de mailclient als niet-verzonden draft. Een mens opent 'm, past aan, verstuurt. We meten edit distance. Zakt de mediaan voor een intent onder een drempel (wij gebruiken 15% van de tekens), dan promoveert die intent.
- Send. Alleen voor gepromoveerde intents verstuurt de agent zelf. Elke verzending post nog steeds in een review-kanaal met een "dit klopte niet"-knop van één klik, die de on-call mens oproept en de intent terugzet naar Suggest.
Twee intents kunnen tegelijk in verschillende fases zitten. order_status staat misschien in Send omdat het een templated lookup is tegen je orders-tabel. meeting_request zit misschien nog in Suggest, omdat agenda's lastig zijn en niemand ooit is ontslagen om een laat antwoord, maar genoeg mensen om een dubbel geboekte klantenlunch.
Promoveer een intent niet op gevoel. Promoveer 'm op basis van gemeten edit distance over echte berichten in een echt venster. Op gevoel ship je bugs naar klanten.
Regel drie: een write-lock op alles wat de buitenwereld raakt
De derde regel slaan teams over omdat 't aanvoelt als paranoia, tot de dag dat het dat niet is. Een agent die je inbox kan lezen is een research-tool. Een agent die kan schrijven — mail versturen, een API aanroepen, een CRM bijwerken, geld verschuiven — is een liability. Behandel die twee als gescheiden systemen met gescheiden credentials.
In de praktijk betekent dat: de agent heeft nooit de verzend-credential. Een smalle service wel. De agent roept die service aan met een structured request, en de service dwingt de regels af die je de agent niet kunt vertrouwen om zelf af te dwingen: rate limits per ontvanger, domain-allowlists, een harde cap op verzendingen per uur, een blokkade op alles wat lijkt op een nieuw extern domein in de eerste 24 uur van een gesprek.
def send_reply(draft: Draft, agent_token: str) -> SendResult:
# The agent has agent_token. It does NOT have SMTP creds.
if not allowlist.contains(draft.to_domain):
return SendResult.rejected("domain_not_allowlisted")
if rate_limiter.exceeded(draft.to_address):
return SendResult.rejected("rate_limited")
if draft.intent not in graduated_intents():
return SendResult.rejected("intent_not_graduated")
if contains_payment_instruction(draft.body):
return SendResult.rejected("payment_language_blocked")
return mail_gateway.send(draft) # gateway holds the real creds
Het patroon is simpel: een credential-proxy tussen de agent en het ding dat schade kan veroorzaken. De agent vraagt, de proxy beslist. De agent heeft nooit de sleutel. Of je dit schrijft als 80 regels Python of een van de opkomende agent-gateway-tools pakt, het principe verandert niet.
De payment-language-check in dat snippet is niet theoretisch. Elke agent die op facturatie-vragen antwoordt, wordt vroeg of laat gevraagd om nieuwe bankgegevens te bevestigen, en "bevestigen" is precies het woord dat je 'm niet wilt laten zeggen. Blokkeer het vocabulaire op de gateway. Laat een mens het afhandelen.
Wat deze drie regels je opleveren
Zet de drie samen — closed-set classifier, promotie per intent, write-proxy met harde regels — en je houdt een agent over die nuttig is op dag één en saai in maand drie. Saai is het doel. Een chat-agent die 60% van je inbox afhandelt op een manier die niemand opmerkt, is meer waard dan tien agents die 100% afhandelen op een manier waar iemand een screenshot van maakt.
De OWASP-werkgroep voor LLM-security komt op vergelijkbare grond uit in hun Top 10 for LLM Applications. Prompt injection, excessive agency en insecure output handling zijn allemaal varianten van dezelfde fout: de agent mocht handelen voordat hij begrensd was. NIST maakt hetzelfde argument in formelere taal in hun AI 600-1-profiel — confinement vóór capability. De drie regels hierboven zijn één praktische vorm van die begrenzingen voor een inbox.
Een audit van vijf minuten die je vandaag kunt doen
Heb je op dit moment een chat- of email-agent live, open dan drie tabs.
Tab één: de system prompt van de agent. Staat daar een gesloten lijst met intents die hij mag afhandelen, of begint het met een alinea "You are a helpful assistant that"? Als het tweede: je hebt geen scope.
Tab twee: de laatste 50 berichten die de agent heeft verstuurd. Vraag bij elk: heeft een mens de draft gelezen voordat die eruit ging? Als het antwoord is "de agent draait al weken zelfstandig", dan heb je de promotiefase overgeslagen.
Tab drie: het code-pad dat daadwerkelijk je mail-provider aanroept. Zit de verzend-credential in hetzelfde proces als de LLM-call? Ja? Dan heb je geen write-lock. Elke prompt injection die de agent bereikt, bereikt je outbox.
Toen we de triage-agent bouwden voor een Rotterdamse logistieke klant, liepen we exact tegen regel drie aan: de eerste versie had zijn eigen SMTP-credential, en één verkeerd gevormde doorgestuurde mail overtuigde 'm om een hele mailinglijst te antwoorden. We hebben 'm dezelfde week achter een gateway herbouwd, en die gateway is nu het template voor elke AI-agent die we opleveren. Drie regels. In die volgorde.




