← Blog

Chat agents

Juridische intake-agent: state machine verdubbelt boekingen

De intake-chatagent van een Haarlems online juridisch advieskantoor bleef hangen op 11 gekwalificeerde consulten per week. Een state machine in vijf fases tilde dat in een maand naar 23.

Jacob Molkenboer· Oprichter · A Brand New Company· 26 nov 2024· 9 min
Leren afsprakenboek, koperen bel, groen lint, vulpen op indexkaart, rode lakzegel op ivoren bureau.

Een intake-wachtrij om 19:42

Het is een dinsdagavond in februari. De intake-lead van een online juridisch advieskantoor in Haarlem (24 mensen) heeft veertien chatsessies open in drie browservensters. Elf daarvan eindigen zonder geboekt consult. Twee boeken wel, maar komen niet opdagen omdat de chat nooit om een telefoonnummer heeft gevraagd. Eén boekt en wordt een betalend dossier.

Het kantoor draait op elke landingspagina (arbeidsrecht, huurrecht, incasso, familierecht) een publieke chat-widget. De widget stond negen weken live. Hij was gebouwd op één systeemprompt van 1.800 tokens die alles in één keer probeerde te doen: begroeten, kwalificeren, feiten ophalen, jurisdictie checken, een slot voorstellen en de behandelend jurist mailen. Hij produceerde gemiddeld elf gekwalificeerde consulten per week. De conversie van chatstart naar boeking lag rond de 9%.

Ons werd gevraagd naar de prompt te kijken. We vervingen de prompt door een state machine in vijf fases. Vier weken later leverde dezelfde widget 23 gekwalificeerde consulten per week op. Hetzelfde verkeer, dezelfde juristen, dezelfde agenda. Het verhaal van hoe dat gebeurde, gaat vooral over het model minder te laten denken, niet meer.

Waarom één grote prompt vastliep

De originele opzet zag er op papier verstandig uit. Een lange systeemprompt, tool calls naar de agenda-API, een paar guardrails ("als de gebruiker juridisch advies vraagt, geef het niet, boek een betaald consult"). In de praktijk faalde hij op vier voorspelbare manieren.

Eén: het model sloeg vragen over. Een gebruiker typt "ik ben gisteren ontslagen en mijn laatste maand wordt niet uitbetaald." Een monolithische agent springt vaak meteen naar "ik boek je in bij ons arbeidsrechtteam" zonder te vragen of het dienstverband onder Nederlands recht viel, of de gebruiker al een advocaat had, of wat de bedrijfsnaam van de werkgever was. De jurist opent de boeking dan zonder bruikbare feiten.

Twee: het model herhaalde vragen. Als de gebruiker in beurt twee uit zichzelf een telefoonnummer noemde en de agent dat niet schoon extraheerde, kwam in beurt zeven alsnog "op welk nummer kunnen we je bereiken?". Daar haakten gebruikers af.

Drie: het model gaf, ondanks de guardrail, juridische meningen. Een prompt van 1.800 tokens is lang genoeg dat "geef geen juridisch advies" wordt verdund door de acht andere dingen die het model moet doen. We zagen transcripts waarin de agent een huurder vertelde dat zijn huurverhoging waarschijnlijk onrechtmatig was. Dat is een compliance-probleem voordat het een UX-probleem is.

Vier: de overdracht naar de jurist was ongestructureerd. De jurist kreeg een agenda-uitnodiging en een link naar het transcript. Veertig beurten heen-en-weer doorlezen voor een consult van 30 minuten is geen workflow die een drukke week overleeft.

De state machine in vijf fases

Wat de prompt verving was, structureel, wat je zou bouwen voor een meerstapsformulier. Het gesprek doorloopt vijf benoemde fases. Elke fase heeft zijn eigen korte systeemprompt (200 tot 400 tokens), zijn eigen schema met verplichte velden en zijn eigen exit-conditie. Het model mag pas door naar de volgende fase als het schema van de huidige fase gevuld is. Het model kan in geen enkele fase juridisch advies geven omdat geen enkele prompt daarom vraagt.

De vijf fases:

  1. Triage. Classificeer de zaak in één van zeven rechtsgebieden. Detecteer urgentie (deadline binnen 14 dagen, zittingsdatum, ontruimingsbevel). Exit zodra practice_area en urgency_band gezet zijn.
  2. Feiten. Verzamel de minimale feiten die de jurist nodig heeft om het dossier te openen: wie zijn betrokken, wat is er gebeurd, wanneer, en waar. Exit zodra parties, event_date en een summary van één alinea gezet zijn.
  3. Jurisdictie en conflict. Bevestig Nederlandse jurisdictie. Vraag of de gebruiker al een advocaat heeft in deze zaak, en of de wederpartij een naam is waartegen het kantoor niet mag optreden. Exit zodra beide checks slagen.
  4. Contact en toestemming. Leg naam, e-mail, telefoon en expliciete toestemming voor de privacyverklaring vast. Exit zodra alle vier gevalideerd zijn (e-mail-regex, NL-telefoonformaat, naam niet leeg, toestemmingstijdstempel gezet).
  5. Boeking. Lees de agenda van de jurist via de praktijkmanagement-API, stel drie slots voor, bevestig er één. Exit zodra een booking-ID teruggegeven wordt.

Elke fase draait als zijn eigen LLM-call. De structured output van de vorige fase wordt context voor de prompt van de volgende. De gespreksgeschiedenis wordt doorgegeven (de gebruiker ziet één doorlopende chat), maar het model achter de schermen krijgt op elk moment maar één ding te doen.

Kernpunt

Heeft je chatagent meer dan één taak? Geef elke taak zijn eigen prompt, zijn eigen schema en zijn eigen exit-conditie. Het model wordt scherper. De overdracht wordt leesbaar.

Wat "formulier-extractie" hier feitelijk betekent

Elke fase is een formulier. De taak van het model in die fase is het formulier invullen. Het gesprek is alleen de invoermethode.

In de praktijk betekent dat: elke fase-call gebruikt structured output. We gebruiken het tool-use-mechanisme van de modelprovider, dezelfde vorm als bij OpenAI's structured outputs of zoals beschreven in de tool-use docs van Claude. Het model krijgt één tool, submit_stage_data, met een JSON-schema dat overeenkomt met de verplichte velden van de fase. De systeemprompt zegt: stel de vragen die je nodig hebt, en als je genoeg informatie hebt om het schema in te vullen, roep dan de tool aan. Roep de tool niet aan met lege of gegokte velden.

Het patroon in code, ruwweg:

type Stage =
  | "triage" | "facts" | "jurisdiction" | "contact" | "booking";

interface StageResult {
  stage: Stage;
  data: Record<string, unknown>;
  next_stage: Stage | "done";
}

async function runStage(
  stage: Stage,
  history: Message[],
  carry: Record<string, unknown>,
): Promise<StageResult> {
  const { system, schema } = STAGE_DEFS[stage];

  const res = await client.messages.create({
    model: "claude-sonnet-4-5",
    system: system + "\n\nKnown so far:\n" + JSON.stringify(carry),
    messages: history,
    tools: [{
      name: "submit_stage_data",
      description: `Submit completed data for stage: ${stage}`,
      input_schema: schema,
    }],
    tool_choice: { type: "auto" },
  });

  // If the model called the tool, the stage is complete.
  const toolUse = res.content.find(b => b.type === "tool_use");
  if (toolUse) {
    return {
      stage,
      data: toolUse.input as Record<string, unknown>,
      next_stage: nextStageAfter(stage),
    };
  }

  // Otherwise the model asked a clarifying question. Pass it to the user.
  return { stage, data: {}, next_stage: stage };
}

De orchestratielus zit hiervoor. Die houdt het chatvenster richting de gebruiker tevreden (één doorlopende thread, typ-indicatoren tussen beurten) en routeert elke beurt naar de fase-handler die op dat moment actief is. Als een fase eindigt, groeit het carry-object met wat die fase indiende, en de systeemprompt van de volgende fase ziet dat.

De cijfers, de week erna

Week één van de nieuwe opzet draaide parallel met de oude prompt: een 50/50-verdeling op binnenkomende chats. We vergeleken dezelfde metric die het kantoor al bijhield, namelijk "gekwalificeerd consult geboekt" (een boeking die niet binnen 24 uur wordt geannuleerd, voor een zaak onder Nederlands recht, met een niet-lege feitensamenvatting in het dossier).

De splittest liep drie weken. De state-machine-variant produceerde gemiddeld 23 gekwalificeerde boekingen per week, tegen 11 voor de prompt-variant. In week drie was het verschil schoon genoeg dat het kantoor de prompt-variant uitzette.

Drie andere dingen bewogen, zoals voorspeld.

  • De gemiddelde gesprekslengte ging omhoog. De oude prompt deed gemiddeld vijf beurten. De state machine zat dichter bij acht. Gebruikers kregen meer vragen, maar het waren de juiste vragen.
  • De no-show-rate op geboekte consulten daalde, omdat de contact-en-toestemming-fase een groep gebruikers ving die anders een slot zou boeken zonder telefoonnummer te bevestigen en daarna zou verdwijnen.
  • De voorbereidingstijd per consult zakte. De behandelend jurist opent het dossier nu op een samenvatting van vier velden in plaats van een transcript van veertig beurten. We hebben het niet met een stopwatch gemeten, maar de partner zei dat het de verandering was die hij het sterkst merkte.

Wat het verhaal van "grotere modellen" mist

Op Hacker News loopt deze week weer het argument dat de voortgang van AI vertraagt. De impliciete boodschap is meestal: productteams moeten wachten op de volgende sprong. Wij zien in het veld het omgekeerde. De meeste agents die we in de praktijk tegenkomen lopen vast op productontwerp, niet op modelcapaciteit. Een frontiermodel uit 2026 dat een Zwitsers-zakmes-prompt van 1.800 tokens draait, presteert niet beter dan een model uit 2024 dat een strakke state machine in vijf fases draait. We hebben dit A/B-getest. De state machine wint, elke keer, en hij wint sterker als het model kleiner is.

De reden is dat een state machine het oppervlak waarover het model moet redeneren verkleint. Elke call heeft één taak, één schema, één exit-conditie. Die structuur doet werk dat het model anders elke beurt gratis zou moeten doen, zonder garantie.

Eén waarschuwing voordat je dit patroon pakt. Een state machine is geen vrijbrief om te over-engineeren. Heeft je agent één taak (een productvraag beantwoorden uit een kennisbank, bijvoorbeeld), dan is één prompt de juiste keuze. De vijf-fases-vorm verdient zich pas terug als er aan de andere kant een echt formulier wordt ingevuld.

Wat we de volgende keer anders zouden doen

Twee dingen, achteraf.

Eén: we hadden de conflict-check ingebouwd in fase drie. In de praktijk verandert de conflictlijst (kantoren waartegen het kantoor niet mag optreden) wekelijks. Door dat in een prompt te zetten, gaat elke wijziging via ons. We hadden de conflictlijst moeten maken als een tool call op een database-tabel die het kantoor zelf beheert. Dat is inmiddels rechtgezet.

Twee: we hadden de fase-schema's in TypeScript geschreven en bij build-time omgezet naar JSON Schema. Prima voor ons. Vervelend voor de in-house developer van het kantoor die een veld aan de feitenfase wil toevoegen zonder onze build-pipeline te leren. Volgende keer zouden we de schema's bij het kantoor laten liggen als platte JSON-bestanden in een Git-repo waarop ze commit-rechten hebben.

Een audit van vijf minuten op je eigen intake-agent

Open een recent transcript dat niet converteerde. Lees het als de jurist die het gesprek had moeten voeren. Tel: hoeveel van de feiten die je nodig hebt om dat consult te beginnen, staan daadwerkelijk in het transcript? Zijn het er minder dan vier, dan extraheert je agent geen formulier. Hij voert een gesprek. Dat gat hebben we bij het kantoor in Haarlem gedicht, en het is het gat dat we het vaakst dichten wanneer we AI-agents bouwen voor klanten van wie de bestaande widget er bijna is maar net niet over de streep komt. Toen we de intake-agent voor dat kantoor bouwden, liepen we tegen het volgende aan: "wees behulpzaam en boek een afspraak" is geen specificatie. Vijf benoemde fases met vijf benoemde exit-condities wel.

Kern

Heeft je chatagent meer dan één taak? Geef elke taak zijn eigen prompt, zijn eigen schema en zijn eigen exit-conditie. Het model wordt scherper en de overdracht leesbaar.

FAQ

Hoe lang duurde het om de state machine in vijf fases te bouwen?

Ongeveer drie weken van kickoff tot live A/B-test. Twee weken aan de fase-schema's en de orchestratielus, één week aan de agenda-integratie en de conflict-check-tool.

Heb je tool use nodig, of werkt gewone JSON-mode ook?

Gewone structured-output JSON-mode werkt. Het state-machine-patroon hangt niet af van tool use. Het hangt af van een strak schema en een heldere exit-conditie per fase.

Werkt het patroon ook op een kleiner, goedkoper model?

Ja. In onze tests draaide de vijf-fases-versie prima op kleinere modellen, omdat elke call een smallere taak heeft. De kosten per gesprek lagen merkbaar lager dan bij de monolithische prompt.

Hoe voorkomt dit dat de agent juridisch advies geeft?

Structureel. Geen enkele systeemprompt van een fase vraagt het model om juridisch advies te geven, dus er is geen opening. Triage classificeert, feiten verzamelt, jurisdictie bevestigt. Geen van die fases vormt een mening.

Wat gebeurt er als de gebruiker iets tegenspreekt wat eerder is gezegd?

De orchestratielus laat terugschakelen toe. Als een latere fase een tegenstrijdigheid detecteert (bijvoorbeeld een niet-Nederlandse jurisdictie), kan die een eerdere fase heropenen met het conflicterende veld leeg.

ai agentschat agentscase studyworkflowarchitectureautomation

Iets bouwen?

Start een project