Hoofdstuk 15 · het gesprek
De chat: praten met je eigen taalmodel
De motor draait en is bewezen juist — maar een motor is nog geen gesprekspartner. Er ontbraken drie dingen: durven gokken, de juiste verpakking, en een geheugen dat tussen de beurten blijft staan. Dit hoofdstuk bouwt ze alle drie, en sluit het boek af waar het hoort: met een computer die terugpraat.
In één alinea
Tot nu toe koos het project áltijd het hoogste hokje. Perfect om te bewijzen dat de motor klopt — en dodelijk voor een gesprek, want hetzelfde begin geeft dan altijd hetzelfde vervolg. Een chat vraagt gecontroleerd toeval, het gesprekssjabloon uit het bestand zelf, en een KV-cache die over de beurten heen doorloopt.
Alle drie samen zijn ze zo'n 350 regels extra Java. En de toets blijft dezelfde als altijd: bij temperatuur 0 moet ons gesprek wóórd voor wóórd gelijk zijn aan dat van llama.cpp — inclusief een tweede beurt die naar de eerste verwijst.
Deel 01Waarom het hoogste hokje geen gesprek is
Laat de motor van hoofdstuk 14 greedy doorschrijven op "De hoofdstad van Nederland is" en er komt "de Nederlandse Kerk (Nederlandse Kerk) (Nederlandse Kerk)…" uit — een grammofoonplaat met een kras. Zo is het echt gemeten, en llama.cpp zegt exact hetzelfde: dit is geen bug maar het wezen van greedy.
Dat is geen domheid van het model, maar een eigenschap van de keuzeregel. Wie áltijd het meest waarschijnlijke token kiest, komt in kringetjes terecht zodra de verdeling twee keer na elkaar hetzelfde aanwijst — en taal die nooit verrast, is bovendien geen taal. De kansverdeling die de softmax aan het einde van hoofdstuk 14 oplevert, is rijker dan haar hoogste hokje: er staat ook in hoe zeker het model is, en welke alternatieven het overweegt.
Sampling is niets anders dan uit die verdeling trékken in plaats van het maximum nemen. Het toeval is daarbij gereedschap, geen vijand — en het blijft controleerbaar: de toevalsbron krijgt een seed, en met dezelfde seed is elk gesprek exact herhaalbaar. Determinisme, de bewijstruc van dit hele project, blijft dus gewoon beschikbaar.
Deel 02De drie knoppen
Drie instellingen bepalen hoe gewaagd het model gokt. Ze werken alle drie op de kansverdeling die je al kent.
Temperatuur deelt de logits door T vóórdat de softmax er kansen van maakt. Dat lijkt een detail, maar het verandert de vorm van de hele verdeling: T < 1 vergroot de verschillen (scherper, voorspelbaarder), T > 1 verkleint ze (vlakker, avontuurlijker), en T → 0 is precies het greedy-gedrag van de vorige hoofdstukken.
Top-k kijkt alleen naar de k beste kandidaten (standaard 40). En top-p knipt daarbinnen de staart af: houd de kléinste kopgroep die samen minstens kans p draagt (standaard 0,9), en verdeel de kansen daarbinnen opnieuw.
Waarom dat afknippen nodig is, zie je pas op schaal: na de softmax over 151 936 hokjes dragen de ruwweg 151 900 onzinkandidaten elk bijna niets, maar sámen alsnog een paar procent. Eens per zoveel tokens zou er dus een wildvreemd token uitrollen — en één gek token is genoeg om een gesprek te laten ontsporen, want het wordt de invoer voor alles wat volgt.
Dezelfde knoppen als ollama
De standaardwaarden (temperatuur 0,8, top-k 40, top-p 0,9) zijn bewust die van ollama, zodat een gesprek hier "hetzelfde soort toeval" heeft als daar. En de sampler is statistisch getoetst: op een klein, met de hand narekenbaar kansprofiel volgen de trekfrequenties de softmax tot binnen één procent, en elke knop doet precies wat hierboven staat.
Deel 03De verpakking: het sjabloon uit het bestand zelf
Hoofdstuk 12 liet één metadata-sleutel bewust links liggen: tokenizer.chat_template. Tijd om die in te lossen.
Elk instructiemodel verwacht zijn gesprek in een eigen verpakking, en die verpakking reist mee in het GGUF-bestand — als sjabloon in de sjabloontaal Jinja. Een volledige Jinja-interpreter bouwen zou hier buiten alle proportie zijn. Daarom doet de chat wat llama.cpp zelf ook doet: het sjabloon herkennen en het bekende equivalent toepassen. Voor Qwen2.5 is dat het ChatML-formaat, met de speciale tokens die je in hoofdstuk 13 al zag staan:
<|im_start|>system
You are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>
<|im_start|>user
What is my name?<|im_end|>
<|im_start|>assistant
En bij een sjabloon dat niet herkend wordt? Dan weigert de chat, luid en met uitleg. Dat is dezelfde filosofie als bij de RoPE-stijl in hoofdstuk 14: een verkeerd toegepast sjabloon geeft geen foutmelding, alleen een model dat nét naast de kwestie antwoordt — en dat is de gevaarlijkste soort fout die er bestaat.
Deel 04Het geheugen blijft staan
De KV-cache uit hoofdstuk 14 was al "het geheugen van het gesprek" — maar tot nu toe gooiden we hem na elke vraag weg. De chat laat hem gewoon staan.
Per beurt wordt alleen het nieuwe stuk door het model gevoerd: jouw vraag plus de sjabloonranden eromheen. Alles wat eerder gezegd is, staat al in de cache als etiketten en inhouden waar de aandacht naar terug kan kijken. Het gesprek groeit dus token voor token:
beurt 1: [systeemblok] [vraag 1] [antwoord 1]
beurt 2: [vraag 2] [antwoord 2]
beurt 3: [vraag 3] …
└────────── alles hiervoor zit al in de KV-cache ──────────┘
Daarom kan het model in beurt twee weten wat je in beurt één vertelde: niet door iets op te slaan of terug te lezen, maar doordat de aandacht van elk nieuw token over de héle cache heen kijkt — precies het mechanisme van hoofdstuk 14, deel 3, nu eindelijk gebruikt waarvoor het bestaat. Tot de context vol is: dan meldt de chat dat eerlijk, en begint /nieuw een vers gesprek.
En de valkuil uit hoofdstuk 13? Voor de derde en laatste keer.
Eén token kan een halve tekencode bevatten. Hoofdstuk 13 waarschuwde ervoor, hoofdstuk 14 trapte er alsnog in (en de nalezing ving het). Een chat die tokens live op het scherm zet, kán niet wachten tot het antwoord af is — dus krijgt hij een stroombuffertje: bytes gaan pas naar het scherm zodra ze samen een compleet teken vormen, de rest blijft hangen tot het vervolg er is. Getest met de gemeenste gevallen: é, 🎉, en het reviewteken 龘 dat in dit model over twee tokens splitst.
Deel 05Het bewijs: vier gesprekken, woord voor woord
De toets van dit hoofdstuk moet drie dingen tegelijk bewijzen: het sjabloon, de motor, én het beurtgeheugen. Er is een toets die alle drie in één klap dekt.
Ollama heeft naast het kale /api/generate ook een echt gesprekseindpunt, /api/chat, dat zelf het sjabloon toepast. Zet de temperatuur op 0 en de repeat penalty uit, en er is geen toeval meer: als onze verpakking, onze forward pass en ons geheugen kloppen, moet elk antwoord wóórd voor wóórd gelijk zijn aan dat van llama.cpp. Eén tekentje verschil in het sjabloon — een ontbrekende regelovergang is al genoeg — en de antwoorden lopen uiteen.
[==] rekensom beurt 1: "2 + 2 equals 4."
[==] hoofdstad beurt 1: "Paris"
[==] geheugen beurt 1: "Hello Willy! I'm Qwen, an AI assistant created by Alib..."
[==] geheugen beurt 2: "Your name is Willy."
[==] tellen beurt 1: "1, 2, 3, 4, 5"
gesprekken volledig gelijk 4 van 4
De regel die alles samenvat is beurt twee van het geheugengesprek. De vraag "What is my name?" is alleen te beantwoorden als de eerste beurt nog in de cache staat — en het antwoord is niet zomaar goed, het is identiek aan wat llama.cpp met de volledige geschiedenis produceert. Sjabloon, motor en geheugen: alle drie in één regel bewezen.
Deel 06Zelf praten
De chat zit bij de broncode van dit boek. Eén JDK, één opdracht, en je praat met een taalmodel dat je — als je het boek van voor naar achter las — nu tot op de byte begrijpt.
# vanuit de map code/ bij dit boek
java --add-modules jdk.incubator.vector src/Chat.java ~/.ollama/models/blobs/sha256-c5396e06af294bd101b30dce59131a76d2b773e76950acc870eda801d3ab0515
# reproduceerbaar gesprek: zelfde seed, zelfde antwoorden
java --add-modules jdk.incubator.vector src/Chat.java … --seed 42
# volledig deterministisch, of juist avontuurlijker
java --add-modules jdk.incubator.vector src/Chat.java … --temp 0
java --add-modules jdk.incubator.vector src/Chat.java … --temp 1.2 --ctx 4096
In het gesprek begint /nieuw opnieuw en stopt /stop. Elke beurt toont de snelheid en de vulling van de context. Zo klinkt het, met de standaardinstellingen en seed 42:
jij> Write a two-line poem about coffee.
model> Coffees, a daily ritual,
Whose flavor soaks in the morning breeze.
(19 tokens, 31,8 per seconde; gesprek: 58/2048)
Eerlijke verwachtingen, nog één keer
Het blijft een model van een half miljard parameters: charmant, snel, en beperkt. Engels gaat het best; Nederlands blijft wankel (het Brussel-incident uit hoofdstuk 14 geldt onverminderd). Maar elk woord dat verschijnt, komt uit een keten die je nu volledig kent — bestand, tokenizer, motor, sampler — en waarvan elke schakel tegen een onafhankelijke referentie is bewezen.
Deel 07Wat er bewust niet in zit
- Een schuivend venster. Als het gesprek de context ontgroeit, zegt de chat dat eerlijk en begin je met
/nieuwopnieuw. Oude beurten stilletjes laten wegschuiven kan, maar verandert waar de aandacht naar kan kijken — dat verdient een eigen verhaal, geen bijzin. - Tools en function-calling. De speciale tokens ervoor (
<tool_call>en verwanten) zag je in hoofdstuk 13 al klaarstaan in de woordenschat; het sjabloon erachter is inmiddels letterlijk een hoofdstuk op zich geworden: hoofdstuk 17. - Grotere modellen. Alles hier werkt onveranderd voor elk qwen2-model dat in het geheugen past — alleen trager. De 7B-koffie wordt in principe gezet — maar alleen op een machine waar die 4,7 GB aan gewichten ruim in het geheugen past; op 8 GB wisselt hij zich dood (hoofdstuk 17 mat het na).
SlotwoordDe cirkel helemaal rond
Dit boek begon bij één neuron met twee gewichten en een fietsbeslissing. Daarna kwam een netwerk van 329 parameters dat leerde optellen, een van ruim tweeduizend dat vier bewerkingen aankon, en de vraag die alles bij elkaar houdt: leert een netwerk het patroon, of onthoudt het zijn huiswerk? In Deel II maakten we een echt taalmodel open — het bestand, de tokenizer, de motor, de snelle kernen — en overal vonden we dezelfde bouwstenen terug: gewichten in roosters, een knik, een softmax over hokjes. Alleen de schaal was anders.
En nu is het traject van het plan-hoofdstuk helemaal afgelopen: alle mijlpalen zijn binnen, elk met zijn eigen harde toets. Een taalmodel van 494 miljoen parameters draait in pure Java, van nul opgebouwd, zonder één bibliotheek — bewezen identiek aan llama.cpp tot op het laatste token, 36 tokens per seconde snel, en sinds dit hoofdstuk kun je er gewoon mee práten. Het antwoord op de vraag waarmee Deel II begon — kan dat überhaupt? — is geen studie meer, maar een programma dat je zelf kunt starten.
Bedankt om mee te bouwen, van nul tot hier.