In Deel 1 schreef ik waarom we een AI-reisinterface vanaf nul bouwden, wat we leerden van onze eerste mislukte poging in 2019, en waarom een plugin je nooit een volledig geïntegreerde conversationele ervaring geeft. Dit deel gaat over het hoe. De architectuur, de zoekpipeline, het geheugenprobleem dat vier pogingen kostte om op te lossen, en het UI/UX-werk dat 25 seconden aanvoelt als 5.
De architectuur
Omdat dit een techblog is en geen marketingpagina, laat ik je zien wat er echt draait. Dit is geen weekendproject. Dit is productie-infrastructuur.
Frontend (React + TypeScript + Zustand)
│
│ WebSocket (real-time, bidirectional)
│
FastAPI Backend
├── Query Orchestrator
│ ├── LLM Client (multi-provider with automatic fallback)
│ ├── Tool Registry (9 specialized tools)
│ └── Observer System (async post-turn analysis)
├── RAG Pipeline
│ ├── Vector Database (3 content collections)
│ ├── Embedding Engine
│ └── Reranker (dual provider, switchable)
├── Conversation Memory (dual storage)
│ ├── SQL (message history)
│ └── Vector DB (semantic search over past messages)
├── Security Layer
│ ├── Input Guard (prompt injection defense)
│ ├── Output Guard (credential leak prevention)
│ └── Rate Limiting + IP Management
├── MCP Server (Model Context Protocol)
│ └── External AI assistant access to our data
└── Lead Management
├── Proposal Generation
└── Specialist HandoffDe frontend is een React-applicatie met real-time WebSocket-communicatie. Geen polling. Geen request-response-cycli waarin je op een compleet antwoord wacht. Elke token streamt in real-time, gebufferd op 50ms-intervallen om flikkering in de UI te voorkomen. Productresultaten worden naar de interface gepusht zodra ze gevonden zijn, nog voordat de LLM aan zijn antwoord begint.
De backend is Python/FastAPI met een modulair toolsysteem. De LLM genereert niet alleen tekst. Hij heeft toegang tot 9 gespecialiseerde tools en beslist zelfstandig welke hij gebruikt. Reizen zoeken? Dan roept hij search_products aan. Informatie over een bestemming nodig? Dan roept hij search_knowledge aan. Terugroepen wat eerder besproken is? Dan doorzoekt hij het gespreksgeheugen semantisch.
De LLM genereert geen antwoorden. Hij orkestreert tools en stelt een antwoord samen op basis van echte data.
De LLM-laag draait met een multi-provider fallback-systeem. Valt de primaire provider uit, dan schakelt het systeem automatisch over naar de secundaire. Geen downtime. Geen foutmeldingen. De klant merkt er niets van.
Er draait al een MCP-server, het protocol dat ik beschreef in mijn Web 4.0-post. Externe AI-assistenten kunnen onze reisdata er al door bevragen. Het is nog vroeg, maar de infrastructuur voor AI-to-business-communicatie staat live. Als de AI-assistent van Lisa de producten van 333travel wil doorzoeken, bestaat het endpoint al.
De RAG-pipeline: waar het echte werk gebeurt
RAG, Retrieval-Augmented Generation, is de ruggengraat van het hele systeem. Het is wat “GPT met een kennisbank” scheidt van “een AI die je product echt kent.” Maar slecht uitgevoerde RAG is bijna erger dan geen RAG.
We onderhouden drie aparte vectorcollections:
| Collection | Wat erin zit | Wat het doet |
|---|---|---|
| Travel Products | Roundtrips, hotels, tours, cruises | Drijft de productzoekopdracht aan |
| Travel Details | Dagprogramma’s, excursies, inbegrepen services | Beantwoordt specifieke vragen over reisschema’s |
| Travel Knowledge | Blogs, bestemmingengidsen, reisinformatie | Levert context en inspiratie |
Stelt een klant een vraag, dan wordt de query ge-embed en tegen de relevante collection gegooid. Maar hier komt het cruciale deel: we pakken niet gewoon de beste vector-matches en noemen dat een resultaat.
Vector similarity alleen is middelmatig. Het brengt je in de juiste richting, maar levert niet de beste resultaten. Daarom doen we een brede fetch, aanzienlijk meer resultaten dan nodig, en halen die door een reranker. De reranker beoordeelt elk query-documentpaar op werkelijke semantische relevantie, niet alleen op embedding-nabijheid.
Het verschil is enorm. Het is het verschil tussen “hier zijn reizen die een paar van je woorden bevatten” en “hier zijn reizen die daadwerkelijk matchen met wat je zoekt.”
Maar het gaat verder. Nog voordat de query de vectordatabase raakt, gaat hij door een fuzzy normalisatielaag. Elke land- en plaatsnaam in ons systeem wordt bij het opstarten gecachet. Typt een klant “Bali”, dan weten we dat Indonesië bedoeld wordt. Schrijft iemand “Tailand”, dan weten we wat er bedoeld wordt. Aliassen, spellingvarianten, taalverschillen: allemaal opgelost voordat de zoekopdracht begint. Zonder dit krijg je lege resultaten op queries die wel hadden moeten matchen. En daarbovenop combineert de zoekopdracht semantische similarity met metadatafilters. Land, duur, producttype: dat zijn geen velden in een database. Het zijn actieve filters die samen met de vectorsearch de resultaten verfijnen, nog voordat de reranker begint.
Na het reranken splitsen de resultaten zich in twee paden. De volledige set gererankte resultaten gaat meteen naar de frontend, maar geblurd. De klant ziet productkaarten verschijnen terwijl Joy nog aan het nadenken is. Het toont voortgang zonder te overweldigen. Ondertussen krijgt de LLM een uitgedunde versie van die resultaten, alleen de velden die hij nodig heeft om te redeneren. Deze projectie bespaart ruwweg een derde van de tokens, wat snellere antwoorden en lagere kosten betekent. Daarna stelt Joy haar antwoord samen en bespreekt ze de 4 of 5 reizen die echt passen bij de vraag. Alleen die besproken producten worden zichtbaar voor de klant. De rest verdwijnt. Wat de klant ziet is een gecureerde, doordachte selectie. Geen lijst met 10 zoekresultaten. Een aanbeveling van 5 die echt logisch zijn.
Brede fetch → Rerank → Project. Drie stappen die het verschil maken tussen een zoekresultaat en een aanbeveling.
Het geheugenprobleem
Dit is het onderdeel dat de meeste iteraties kostte om goed te krijgen.
Conversationele AI heeft een smerig geheimpje: het onthoudt niets. Elke LLM-call is stateless. Het model heeft geen idee wat er eerder gezegd is, tenzij je de context expliciet aanlevert. En er is een grens aan hoeveel context je kunt aanleveren.
Voor een simpele Q&A-chatbot maakt dat niet uit. Iemand stelt een vraag, krijgt een antwoord, klaar.
Maar voor een reisassistent? Geheugen is alles.
Een klant zegt: “Ik wil twee weken naar Thailand.” Drie beurten later: “Maak er eigenlijk drie weken van. En ik wil Laos erbij.” Vijf beurten later: “Wat was die reis die je eerder liet zien, met de kookles?”
Zonder geheugen is elke beurt een leeg blad. Het systeem weet niets van Thailand. Niets van de wijziging naar drie weken. Niets van Laos. En al helemaal niets van de reis met de kookles van zeven berichten geleden.
Zonder geheugen is elk bericht een eerste date. Je AI heeft geen idee wat er eerder gebeurd is.
Dit losten we niet in één poging op. Niet in twee. Zelfs niet in drie.
De eerste versie was simpel: de laatste 10 vraag-antwoordparen in de LLM-context stoppen. Dat werkte voor korte gesprekken. Maar onze gesprekken zijn niet kort. Klanten verkennen, vergelijken, komen terug op eerdere ideeën, bedenken zich. Tien beurten is niet genoeg als iemand twintig minuten over drie verschillende landen heeft gechat.
Dus probeerden we op de achtergrond samenvattingen te genereren. Het idee was goed: het gesprek samenvatten tot kernpunten, zodat de LLM altijd het volledige beeld heeft zonder de hele context window vol te gooien. De praktijk was anders. De LLM begon details te hallucineren in de samenvattingen. Landen door elkaar halen. Voorkeuren toedichten die de klant nooit had genoemd. De samenvatting zou de source of truth zijn, en hij verzon dingen.
Toen ontwierpen we een geheugensysteem in drie lagen: kortetermijn, tijdelijk en langetermijn. De tijdelijke laag moest bijhouden welke onderwerpen bij welke bestemming hoorden, welke voorkeuren op welk deel van de reis golden. Op papier elegant. In de praktijk een nachtmerrie. Het vereiste dat de AI complexe relationele context begreep, zoals “toen ze zeiden dat het budget flexibel was, bedoelden ze dat voor Thailand, niet voor Vietnam.” Dat kon hij niet. Te veel edge cases. Te fragiel.
Wat we uiteindelijk hadden, was simpeler en robuuster. Elk bericht wordt als vector geëmbed in een aparte memory collection in dezelfde database die we voor product search gebruiken. Als de klant zegt “die reis met de kookles”, draait het systeem een semantic search over alle eerdere berichten in die sessie. Het vindt het relevante bericht, ook als de exacte woorden niet overeenkomen. Misschien noemde Joy het drie beurten eerder een “culinaire ervaring”. Vector search geeft niets om woordmatching. Het matcht betekenis. Gecombineerd met de laatste 10 uitwisselingen in de prompt voor directe context, hoeft Joy niet voor elke vraag die tool call te doen. Ze heeft het recente gesprek direct bij de hand, en de volledige historie doorzoekbaar wanneer ze die nodig heeft.
Vier pogingen. Elke poging leerde ons iets over wat AI wel en niet kan doen met context. De definitieve versie werkt niet omdat het de slimste opzet is, maar omdat het het eerlijkst is over de beperkingen.
AI-geheugen is niet één probleem. Het zijn er twee: terugroepen wat er gezegd is, en begrijpen wat er bedoeld is. We hadden twee verschillende systemen nodig om dat op te lossen.
UI/UX: het onzichtbare werk
Hier sterven de meeste AI-projecten, zonder dat ze het zelf doorhebben.
Je kunt de meest geavanceerde RAG-pipeline ter wereld hebben. De slimste retrieval. De beste reranking. Maar als de interface traag, log of verwarrend aanvoelt, doet dat er allemaal niet toe. De klant geeft niets om je architectuur. Het gaat ze om hoe het voelt.
Als je AI slim is maar je interface traag, heb je een genie gebouwd dat opgesloten zit in een kast.
Hier zijn we diep op ingegaan. En “diep” betekent tientallen iteraties op dingen die de meeste mensen details zouden noemen. Maar de details zijn het product.
Streaming die natuurlijk voelt. LLM-antwoorden streamen token voor token over een WebSocket. Maar rauwe streaming geeft flikkerende tekst die schokkerig en onaangenaam is. We bufferen met intervallen van 50ms. Snel genoeg om real-time te voelen, langzaam genoeg om constante re-renders te voorkomen. Het verschil is subtiel, maar enorm voor de ervaren kwaliteit.
De afweging in snelheid. Laten we eerlijk zijn: een product search met volledig antwoord duurt 20 tot 30 seconden. Dat is lang. We weten dat het lang is. Maar we hebben een bewuste keuze gemaakt: kwaliteit boven snelheid. De brede fetch, de reranking, de semantic matching, elke stap in de pipeline bestaat omdat hij de resultaten beter maakt. Haal er één weg en je krijgt snellere antwoorden met slechtere aanbevelingen. Liever dat Joy 25 seconden werkt en 5 reizen aflevert die echt passen, dan 10 middelmatige matches in 8 seconden.
Dus de vraag werd: hoe zorgen we dat 25 seconden niet aanvoelen als 25 seconden? Daar kwam het UX-werk. Joy laat zien wat ze doet. Producten zoeken. Bestemmingsinformatie opzoeken. Gesprekshistorie checken. Elke tool call is zichtbaar terwijl hij gebeurt. Productkaarten schuiven vervaagd naar binnen terwijl zij nog haar antwoord opbouwt. Zodra het antwoord klaar is, worden alleen de reizen zichtbaar die Joy daadwerkelijk bespreekt. De rest vervaagt. De klant staart nooit naar een leeg scherm. Ze zien het proces ontvouwen. En het blijkt dat ergens naar kijken werken totaal anders voelt dan ergens op wachten.
Een deel van de latency is ook een taalprobleem. Nederlands is slecht vertegenwoordigd in de meeste embedding- en taalmodellen. Het semantische begrip dat in het Engels prachtig werkt, degradeert als je klanten en productdata Nederlands zijn. Dat betekent meer verwerking, zorgvuldiger matchen, meer moeite voor hetzelfde kwaliteitsniveau.
Wat is GPT-NL? GPT-NL is een Nederlands initiatief om een large language model te bouwen dat specifiek getraind en geoptimaliseerd is voor de Nederlandse taal.

Waar huidige modellen zoals GPT en Claude Nederlands behandelen als een niet-bestaande taal, wil GPT-NL Nederlands een first-class citizen maken in AI. Voor toepassingen zoals de onze, waar zowel de klant als de productdata Nederlands zijn, kan een model dat de taal echt begrijpt de kwaliteit van semantisch zoeken aanzienlijk verbeteren en de extra verwerking verminderen die we nu nodig hebben om dat gat te compenseren. We volgen dit project op de voet.
En dan is er de keuze van het model. De kwaliteit van het gesprek hangt sterk af van welke LLM je gebruikt. We draaien op Sonnet 4.6 als primair model, en het verschil met kleinere of goedkopere modellen is niet subtiel. Het is het verschil tussen een gesprek dat natuurlijk voelt en een dat gerepeteerd aanvoelt. Haiku, de snellere en goedkopere optie, voelt als een downgrade zodra je overschakelt. GPT dient als onze automatische fallback, maar het is een stap terug in gesprekskwaliteit. Dit zijn geen inwisselbare commodity’s. Het model dat je kiest bepaalt de ervaring die je klant krijgt.
Directe paginaladingen. De hele conversatie blijft bewaard in local storage. Als je de pagina ververst of later terugkomt, staat je gesprek er al. Direct. Geen spinner. Geen “opnieuw verbinden…"-melding. De sync met de server gebeurt geruisloos op de achtergrond.
Markdown-normalisatie. LLM’s hebben meningen over opmaak. Claude is gek op vette koppen met emoji. GPT is gek op genummerde lijsten. Wij normaliseren alles server-side. Vet-met-emoji wordt omgezet naar schone koppen. Horizontale lijnen worden gestript. Font weights worden teruggeschroefd. Het resultaat oogt alsof het ontworpen is, niet gegenereerd. De klant moet nooit het gevoel hebben dat hij AI-output zit te lezen.
Sessiecontinuïteit. Je sessie overleeft paginaverversingen, gesloten browsers en herverbindingen. Kom een dag later terug, je gesprek staat er nog. Je opgeslagen reizen staan er nog. En als een sessie verloopt, krijg je een nette overlay die uitlegt wat er gebeurd is, geen kapotte pagina of een cryptische foutmelding.
De lead flow. Als een klant klaar is om met een specialist te praten, genereert het systeem een reisvoorstel dat al vooraf is ingevuld met wat het uit de conversatie weet. Besproken bestemmingen, reisvoorkeuren, bekeken producten. Minimale wrijving. Maximale context. De menselijke specialist pakt precies op waar Joy stopte. Dat is geen handoff. Dat is precies het punt. Joy bestaat om dat menselijke gesprek beter te maken, niet om het te vermijden.
Elk van deze details kostte meerdere iteraties. En geen enkele is zichtbaar voor de klant. Dat is de bedoeling.
Security: een eerlijke noot
Ik houd deze sectie kort en eerlijk. Security is niet iets wat je afmaakt. Het is iets waar je continu aan werkt.
We hebben een dubbel guardsysteem. Een input guard vóór de LLM die prompt-injection-pogingen onderschept, en een output guard ná de LLM die credential leaks of system prompt-fragmenten in antwoorden onderschept. Beide zijn deterministisch en voegen vrijwel geen latency toe.
De system prompt is verhard met anti-jailbreak-instructies. Er is rate limiting, botbescherming en IP-beheer voor hardnevig misbruik.
Is het bulletproof? Nee. Niets is dat. Maar het is gebouwd met defense in depth in gedachten, en we itereren erop naarmate nieuwe aanvalspatronen opduiken. Als je een AI-interface bouwt die met het publiek praat en je hebt nog niet nagedacht over input/output guards, dan ligt er een probleem op de loer.
Het grotere geheel: een extra kanaal, geen vervanging
Laat me heel duidelijk zijn over wat Joy is en wat ze niet is.
Joy is geen vervanging van onze website. Ze is geen vervanging van telefoon of e-mail. Ze is een extra kanaal. Een nieuwe manier voor klanten om onze producten te ontdekken, naast alles wat er al bestaat. Ze zal nooit een vervanging van menselijk contact zijn. Dat vinden we bij ons bedrijf het belangrijkst. Joy is een middel naar menselijk contact. Ze helpt klanten uitvinden wat ze willen, zodat het gesprek met een van onze specialisten op een compleet ander niveau begint.
Joy is niet de bestemming. Ze is de weg die leidt naar een echt gesprek met een echt persoon.
De MCP-server staat al live, wat betekent dat AI-assistenten onze data al programmatisch kunnen bevragen. Als de Web 4.0-visie werkelijkheid wordt en Lisa’s AI-assistent zelfstandig op zoek gaat naar haar perfecte Thailand-reis, dan bestaat het endpoint al. Niet omdat we er haastig mee aan de slag zijn gegaan. Omdat we al meer dan een jaar met deze infrastructuur aan het experimenteren zijn.
Joy is niet het eindproduct. Ze is het begin van een kanaalstrategie die zowel mens-tot-AI- als AI-tot-AI-interacties omvat. En ze is een test, een heel serieuze, heel grondige test, of conversationele AI vandaag de dag daadwerkelijk waarde kan toevoegen voor reisklanten.
De resultaten tot nu toe zijn beter dan ik verwachtte. Niet vlekkeloos, niets is dat, maar echt indrukwekkend in hoe natuurlijk de gesprekken voelen en hoe relevant de aanbevelingen zijn. Als je een klant een vaag idee ziet beschrijven en Joy komt met reizen die daadwerkelijk passen, dan besef je: dit is geen gimmick. Het werkt. En het werkt dankzij alles wat eronder ligt.
Dus, welk probleem lossen we ook alweer op?
Ik begon met deze vraag omdat dit de vraag was die alles heeft geleid wat we bouwden. Niet “hoe gebruiken we AI?” Niet “wat doen onze concurrenten?” Niet “hoe komen we innovatief over?”
Alleen: welk probleem lossen we op, en werkt deze oplossing daadwerkelijk?
We bouwden Joy om daarachter te komen. Met echte architectuur. Echte data. Echte klanten. En de bereidheid om “dit is nog niet klaar” te zeggen als dat zo zou blijken.
Als je nadenkt over het toevoegen van AI aan je product, begin daar. Niet met de tool. Niet met de pitch van een leverancier. Niet met de druk om bij te blijven.
Begin met de vraag. Bouw vanuit het antwoord.
Je klanten verdienen beter dan een FAQ met een gezicht. Ze verdienen een echt gesprek. En uiteindelijk een echt mens. Zorg dat je AI ze daar naartoe leidt, en niet ervandaan.
Als je Deel 1 nog niet gelezen hebt: daar begint het verhaal. Waarom we dit bouwden, wat we leerden van ons mislukken in 2019, en waarom je hier niet komt met een plugin.






