
← Terug naar blog
Begrensde LLM UX: tweestapsinferentie, SSE-streaming, gestructureerde grounding en de productgrenzen die een portfolio-chat eerlijk houden.
De meeste portfolio's stoppen met evolueren. Ik wilde dat deze zou gedragen als een product: i18n-first, gedeelde UI in een monorepo en een Lucas AI-laag die in de eerste persoon antwoord geeft over mijn werk—alleen vanuit gestructureerde context die is ingebakken in de implementatie. Dit artikel is voor ingenieurs die het systeembeeld willen zien: inferentievorm, streaming, grounding en expliciete niet-doelen—geen rondleiding waar bestanden wonen of hoe een fork lokaal te runnen.
Statische pagina's zijn slecht in follow-ups. Als je randgebied oordeel is—omvang, afwegingen, hoe je onder druk hebt geleverd—dan schaalt een PDF en een contactformulier nieuwsgierigheid niet. Lucas AI is een speciale bestemming (/[locale]/ai): vraag over ervaring, de site of de functie zelf, zonder te doen alsof ik aan de andere kant van de draad in real-time aanwezig ben.
De browser bezit de transcript-UI; de server bezit beleid en kosten. Elk bericht is een POST /api/chat met { message, locale }—geen bijlagen, geen toolaanroepen, geen verborgen velden.
De handler bouwt een systeem, stuurt door naar Groq's OpenAI-compatibele /v1/chat/completions, en proxy's de stroom van bovenstroom als Server-Sent Events (SSE) zodat tokens incrementeel renderen zonder de volledige voltooiing in het geheugen op de rand te bufferen. Dat is hetzelfde patroon dat je zou gebruiken achter elke snelle inferentie-API: behandel de route als een dunne adapter, houd het draadformaat stabiel voor de client.
Grounding leeft in applicatiecode: een getypte carrièreobject wordt eenmaal opgemaakt in een CONTEXT-string en geïnjecteerd in het systeembericht. Er is geen vector database en geen retrievalstap op aanvraagtijd—het model ziet alleen wat je geserialiseerd hebt toen je de prompt bouwde. Dat is een capaciteitshandel (je kunt niet antwoorden vanuit willekeurige documenten) in ruil voor voorspelbare latentie, kosten en auditsurface.
llama-3.3-70b-versatile op Groq (override GROQ_MODEL). Streaming blijft aan; de route staat tot 60s van generatie toe—genoeg voor een zorgvuldig antwoord zonder de rand in een onbeperkte werknemer te veranderen.llama-3.1-8b-instant (configureerbaar als CLASSIFIER_MODEL). Het is een afzonderlijke, niet-streaming oproep met max_tokens: 5, temperature: 0, en een minimaal systeemprompt dat de beslissing tot een enkel token beperkt: CAREER of OFF_TOPIC.Als de uitspraak OFF_TOPIC is, roept de API nooit het grote model aan. Het stroomt een vaste, gelokaliseerde weigering als SSE zodat de clientcodepad overeenkomt met een “echte” voltooiing—geen speciale UI-branch voor kortsluiten. Als de classificatie fouten maakt of time-out, dan faalt de handler open en run het hoofdmodel toch: een goedkope poort mag geen enkel punt van falen worden voor legitiem verkeer.
De poort kan nog steeds worden uitgeschakeld op de host (serveromgeving) wanneer je bewust elke beurt accepteert die het grote model raakt—handig als de kleine eindpunt ongezond is, als je de antwoordpad alleen testbelast, of als productbeleid verandert en je tijdelijk de poort laat vallen. Die knop leeft buiten het zicht van de bezoeker; het is een operatiehendel, geen functieknop in de UI.
Uitgavebudget: assistent max_tokens is begrensd (CHAT_MAX_TOKENS, begrensd 256–8192, standaard 2048). Het koppelt UX (“antwoorden moeten eindigen”) met eenheidseconomie op de providerfactuur.
Elk verzoek draagt:
message: de huidige gebruikersbeurt, getrimd, met een harde limiet van 2 000 tekens (misbruik en prompt-injectieoppervlakreductie).locale: genormaliseerd naar een ondersteunde site-locale. Het systeemprompt eindigt met Bezoeker locale: … zodat het model antwoordt in de site-taal, niet in de taal die de UI-chroom toevallig gebruikt.Wat we niet sturen:
Dus “geheugen” is: gepubliceerde CONTEXT + wat nog zichtbaar is in de client. Dat is een opzettelijke grens: geen server-side chatgeschiedenis, geen cross-apparaatsynchronisatie, geen training op gesprekken.
De UI persisteert berichten in sessionStorage (lucas-ai-messages). Vernieuwen in hetzelfde tabblad houdt de thread; een nieuw tabblad of apparaat begint schoon. Er is een tweede sleutel, lucas-ai-pending, gebruikt wanneer we de gebruiker omleiden na een locale-schakelaar (meer hieronder).
Als GROQ_API_KEY ontbreekt, retourneert de route nog steeds SSE: het stroomt de gelokaliseerde configuratiefoutstring zodat de shell degradeert zonder de lay-out weg te gooien.
De carrièrecorpus begint als getypte records—rollen, stacks, initiatieven, statistieken, type (werk vs vrijwilliger vs onderwijs vs persoonlijk), tijdreeksen en optionele verhalende velden. Een formatter maakt daarvan één markdown-achtig blok gewikkeld in expliciete scheidingstekens, bijv.:
--- CONTEXT (grondwaarheid; niet tegenspreken of uitbreiden) ---
…
--- Einde context ---
Wat erin gaat, is net zo belangrijk als wat eruit blijft:
context / probleem / oplossing / impact verhaalbeats wanneer je wilt dat het model spreekt in uitkomsten, niet in modewoorden.Bovenop, het systeem is strikte wet: eerste persoon, geen doen alsof je live bent op Slack, geen toegang tot privésystemen, geen derdepersoons “Lucas zei…”, CONTEXT als enige feitelijke bron, en geen fabricage voorbij wat CONTEXT specificeert voor implementatiedetail.
Dat is hoe je “niet hallucineren” van een sfeer in testbare reikwijdte verandert: het model is alleen zo slim als het pakket dat je levert, en het pakket is versiebeheer zoals code.
De taak van het kleine model is alleen routing, niet behulpzaamheid. CAREER wordt breed gedefinieerd: achtergrond, vaardigheden, geleverde arbeid, productoordeel, en legitieme vragen over de site zelf—stack, lokalisatiepipeline, hoe de assistent is bedraad—voor zover die informatie bestaat in CONTEXT. OFF_TOPIC vangt alles else op (weer, huiswerk, niet-gerelateerde trivia).
Het behandelen van “meta”-vragen als binnen bereik is een productbeslissing: een portfolio-assistent moet zijn eigen randvoorwaarden uitleggen zonder de hele web als kennisbron te openen.
De route-locale drijft antwoordtaal (via prompt). Maar gebruikers typen soms in een andere taal terwijl ze op bijv. de Engelse UI blijven.
Bij verzenden, voert de client franc-min uit op de invoer (minimale lengte ~15 tekens). Als de gedetecteerde taal niet overeenkomt met de verwachte ISO 639-3-toewijzing van de huidige locale, sturen we niet stil naar de API. We tonen een aanbodkaart: knoppen om router.push(/${targetLocale}/ai) voor overeenkomende locales, plus “vervolgen in huidige taal.” Als ze van locale schakelen, stashen we het uitstaande bericht in sessionStorage, navigeren, en auto-sturen na montage zodat de vraag loopt met de juiste locale in het JSON-lichaam.
Dat is productgedrag: site-taal aligneren met de taal die de gebruiker daadwerkelijk schrijft, in plaats van het model te dwingen te raden of beleid te mengen.
Lucas AI is een nav-destinatie en een home-sectie (badge, koptekst, CTA, voorbeeldprompts)—geen zwevende widget over leesstroom. Volledige pagina-chat houdt het patroon opt-in en vermijdt het “verrassings copiloot”-antipatroon waarbij generatieve UI strijdt met de rest van de lay-out voor aandacht.
llm_auth naar de client—geen onbewerkte bovenstroom (lekken van sleutel of model hints vermijden).Lucas AI is geen algemene assistent die op een marketingpagina is gedropt. Het is een smal product: één feitelijkheidsgebied waar je achter staat, één stroomantwoordpad, een klein model dat alleen “binnen bereik of niet” beslist, en een server die elke beurt opzettelijk vergeet. Het doel is voorspelbaar gedrag—wat er naar de provider wordt gestuurd, wat je de inferentie-API per verzoek betaalt, en wat de bezoeker als feitelijk kan behandelen.
Als je iets dergelijks bouwt, ligt de hefboom niet in het kiezen van het grootste model. Het is het systeembericht en CONTEXT behandelen als een specificatie—eenvoudig, feitelijk, regelbaar—en niet als marketingkopie, en beslissen in de architectuur wanneer het grote model mag lopen (bijvoorbeeld alleen na een onderwerpgerichte poort).