NPC-Forge en de ontwikkeling van TERMy
Vanaf begin juli kreeg ik de kans om twee maanden lang te focussen op mijn persoonlijke projecten, tijdens een vreemde periode van AI-prijsstijgingen en het einde van gesubsidieerde 'tokenmaxing'. Ik was nieuwsgierig of ik vanaf nul een terminal-assistent zou kunnen ontwikkelen die eenvoudige natuurlijke taalverzoeken kon afhandelen. Omdat ik een slecht geheugen heb, was ik gewend geraakt om Copilot te vragen om bijvoorbeeld "de virtuele omgeving te activeren" of andere triviale handelingen, wat me maandelijks een niet-onbeduidend bedrag kostte. Ik begon me af te vragen of ik mijn workflow efficiënter kon maken. Heb ik echt biljoenen parameters nodig om deze taken uit te voeren?
Transformers thuis
Ik startte een open onderzoek naar de haalbaarheid van het implementeren van een generatief model aan huis, waarbij ik het vanaf nul trainde op de computer die ik begin jaren 2010 gebruikte om Kerbal Space Program te spelen. Deze was "geüpgraded" met 16 GB RAM, een NVIDIA GTX 1050 Ti (4 GB VRAM) en een i7-4790K (4.0 GHz, 8 cores) CPU. In mijn experimenten streef ik naar minimalisme, dus ik stelde mezelf een beperkte omgeving op om gedwongen te worden tot een elegante en efficiënte oplossing.
Eerst ontwikkelde ik een framework om transformers te trainen en te evalueren, wat ik volledig in Python implementeerde. Ik begon met iets dat erg leek op NanoGPT met 100-200 miljoen parameters, waarna ik flash attention en alle verwachte optimalisaties toevoegde. Ik probeerde zelfs vernieuwende architecturen zoals Mamba. De resultaten waren over het algemeen onbevredigend, en zelfs angstaanjagend, zoals blijkt uit het volgende voorbeeld:
Prompt: What is an alien?
Gegenereerde tekst:
He's not a member of the world. He can't believe anything anymore.
All of those animals are looking like excrements, but every mouth is not a bad one.
They look alike, all of which are really terrible.
They get their own life, and each is a sign that they're not.
They're all right, they say.
They're just some different things they can make.
They've never seen them.
They were better not. Some of them've got their rights.
Some of them have to go.
But we're not looking for the future on how many.
The last two of them are all in the universe.
(Het woord excrements werd hier gebruikt in plaats van een krachtterm van vier letters die ik hier niet wil publiceren).
Al mijn tests produceerden modellen die geneigd waren in lussen te belanden waarin dezelfde zin werd herhaald. Zelfs wanneer ze waren getraind op vraag-en-antwoordsets, konden ze zelden consistent antwoorden geven, zeker niet op technische vragen. Ik trainde modellen op een collectie rechtenvrije boeken van Project Gutenberg, veel open-source software en diverse datasets van Huggingface.
Ik begreep al snel dat deze aanpak niet haalbaar was; een fatsoenlijke run zou minstens een maand ononderbroken training vereisen. Ik was verbijsterd door hoe mijn modellen "levend" en magisch leken, maar ik schaamde me tegelijkertijd omdat ze ongelooflijk verspillend en effectief nutteloos waren.
Lokale modellen
Ik stapte over op Ollama en open-weight modellen en ontwikkelde 'howto', een terminal-harness die een pre-prompt gebruikt om het model te dwingen alleen met terminalcommando's te antwoorden. De resultaten waren over het algemeen onbevredigend vanwege de tijd die nodig was om een respons te krijgen. Modellen zoals ornith:9b, mistral:7b of cogito:14b kunnen het werk soms doen, maar ze zijn niet snel en betrouwbaar genoeg voor algemeen gebruik, zeker niet met slechts 4 GB VRAM.
De overstap naar determinisme
Toen herinnerde ik me de blockchain-gekte, waarin iedereen een blockchain overal in wilde passen om het als het volgende grote ding te verkopen. Ik wilde mijn tijd en geld niet verspillen zoals die mensen deden in de vorige hype-cyclus, dus ik begon een terminal-assistent vanaf nul te bouwen met een nieuwe set restricties:
- Geen embeddings
- Geen machine-learning
- Geen LLM's
Dataset-formaat
Het eerste dat ik nodig had, was een set conventies. Daarom stelde ik NDF 0.0 op (NPC-Forge Dataset Format), dat het dataset-formaat van NPC-Forge specificeert. Het volgende object bevat de categorie, inputzinnen, tekstuele respons, denkprocessen (thinking traces), toestemmingsbeheer (permission gating) en tool-aanroepen die in een formaat compatibel met VS Code worden uitgevoerd:
{
"category": "linux_files",
"input": [
"list files",
"list files and directories"
],
"tools": [
{
"name": "run_in_terminal",
"arguments": {
"command": "ls -lah",
"explanation": "Lists the files in the current directory.",
"goal": "Display current directory contents",
"mode": "sync"
}
}
],
"message": "Done",
"thinking": [
"That is quite simple!",
"This is boring..."
],
"permission": "yolo"
}
Ik ben echt weg van dit systeem; het is een zelfstandig atoom van kennis dat eenvoudig bewerkt en gedeeld kan worden. Het is zeer eenvoudig om de mogelijkheden van conversationele agenten uit te breiden als je je aan deze conventie houdt. Stel dat ik mijn terminal-assistent docker-commando's wil laten leren; dan schrijf ik simpelweg een lijst met objecten in dataset_docker.json, plaats ik het bestand in de dataset-directory, en de NPC leert ze onmiddellijk, zoals Neo Jujitsu leerde in The Matrix.
Het volgende probleem was: hoe ga ik om met vragen als "create file test.txt"? Ik moest de "variabele" daarin kunnen parsen en de werkelijke betekenis van het verzoek begrijpen. Daarom bedacht ik dit:
{
"intent": "file_creation",
"category": "linux_files",
"type": "template",
"structure": [
[
{
"tag": "<||vocab_create||>",
"type": "vocab",
"required": true
},
{
"tag": "<||vocab_file||>",
"type": "vocab",
"required": false
},
{
"tag": "<||file||>",
"type": "filename",
"required": true
}
]
],
"message": "<||completion||>",
"tools": [
{
"name": "run_in_terminal",
"arguments": {
"command": "echo '' > '<||file||>' && termy_set_context 'active_file' '<||file||>'",
"explanation": "Writes <||string||> in file <||file||>.",
"goal": "Directory Allocation",
"mode": "sync"
}
}
],
"permission": "ask",
"thinking": [
"Ok, I am asked to create the file <||file||>."
]
}
Elke tag, zoals <||vocab_create||>, vertegenwoordigt een concept (in dit geval de actie van creatie), dat wordt weergegeven door meerdere synoniemen:
{
"<||vocab_create||>": [
"create",
"make",
"generate",
"craft",
"forge"
]
}
Op één of meerdere posities kan een tag worden verwacht, en elke tag kan verplicht of optioneel zijn. De "variabelen" of benoemde entiteiten worden geëxtraheerd op basis van hun type en een bijbehorende reguliere expressie:
{
"<||filename||>": "[\\w\\-]+\\.[a-zA-Z0-9]{2,4}",
}
Ik moet mijn goede vriend Kevin bedanken voor zijn hulp bij het uitdenken hiervan.
Overwegingen over veiligheid
Kijkend naar de permission-sleutel concludeerde ik dat de tool inherent veilig wordt om te gebruiken door het gebruik van "permission": "ask" af te dwingen voor alle potentieel destructieve commando's. Uiteraard blijft er ruimte voor menselijke fouten, zoals een bug in de implementatie of in de dataset, maar de risico's worden hiermee sterk beperkt.
De implementatie
Ik schreef de klassen FlintParser en FlintNPC om gebruik te maken van het hierboven beschreven dataformaat, een NLU-pipeline (Natural Language Understanding) te implementeren en alle benodigde functies voor de terminal-assistent in ongeveer 1000 regels code. Ik schreef deze klassen in identieke, cross-compatibele implementaties voor zowel Python (voor lokale OS-omgevingen) als JavaScript (voor client-side gebruik in browser-tabs of Node.js-instanties).
Het moeilijkste deel was bepalen wat er gedaan moest worden en in welke volgorde. Ik heb veel gewerkt aan een compiler voor mijn eigen programmeertaal, BIPLAN. Tijdens die ontwikkeling leerde ik dat het eerste wat je moet doen bij het vertalen van code, het verwijderen van ruis is, waarna je de minst kostbare paden eerst probeert.
Dit is de pipeline die ik heb geïmplementeerd:
- Verwijder krachttermen, interjecties en aanmoedigende, ontmoedigende of dankwoorden (ruis verwijderen).
- Sentimentanalyse.
- Exact Match (zeer snel).
- Template Match (trager).
- Probabilistische Match (nog trager).
Stap 5 vertrouwt op:
- IDF (Inverse Document Frequency) om zeldzame woorden te identificeren.
- BOW (Bag Of Words) om omkeringen in woordvolgorde op te vangen.
- IDF-gewogen Levenshtein om spelfouten veilig af te handelen.
Voor het eerst in bijna twee maanden waarin ik "spaghetti tegen de muur gooide in de hoop dat het zou plakken", voelde ik opnieuw de vreugde van het werken aan iets begrijpelijks en voorspelbaars. Ik had eindelijk een betrouwbare terminal-assistent op mijn computer!
Ik besloot het TERMy te noemen.
Vergelijking met bestaande NLU-frameworks
Hoe verhoudt dit zich tot gevestigde NLU-frameworks? Rasa en NLP.js zijn zwaar en vertrouwen op machine-learning-classifiers en trainingspipelines; ChatScript is enorm en staat bekend om een steile leercurve. NPC-Forge stript dit alles weg: er is nul training nodig, het maakt gebruik van een krachtig en flexibel dataformaat, en het beschikt over een verrassend capabele parser die klein genoeg is om op een micro-controller te draaien.
Koppeling met Copilot
Ik heb TERMy ontwikkeld en gekoppeld aan Copilot om een deel van de prompts af te handelen die ik voorheen naar Claude stuurde. Het is geen LLM, maar het doet het werk en het is onmiddellijk. Ik vermoed dat dit de eerste keer is dat velen van ons een deterministische agent via een harness zien, hoewel ik geloof dat dit in de nabije toekomst een dominant thema zal worden.
Het is ironisch dat de recente prijsstijgingen van Copilot me uiteindelijk hebben gepusht om tijd aan deze software te besteden. Misschien is dat gewoon de levenscyclus van corporate SaaS? In ieder geval geloof ik dat harnesses zoals Copilot en Pi zouden moeten vertrouwen op deterministische NPC's zoals TERMy, en pas in laatste instantie doorrouteren naar een zwaar LLM. Het blijven verspillen van rekenkracht en elektriciteit aan triviale taken is duur en onverantwoord.
NPC-Forge
NPC-Forge is een framework, een server en een CLI die de volgende functionaliteiten biedt:
- Beheer van OpenAI API-servers
- NPC-beheer
- Diagnostiek en testen
Met NPC-Forge kan iedereen nu snel een NPC bouwen en deze delen met de community. Deze NPC's draaien op de CPU van elke Linux-machine, zoals de RPI Zero, en reageren in milliseconden. Stel je voor: nu kan zelfs je energiemeter of je router gratis een conversationele agent hosten. Deze aanpak is ook democratischer; jouw NPC zal zonder problemen iets ongemakkelijks zeggen als je hem zo programmeert, wat veel beter is dan corporate alignment-filters en guardrails die de publieke opinie, cultuur en taal vormgeven.
Ik vraag om wat coulance: dit is mijn eerste Python-project, gemaakt in twee maanden van gepassioneerde ontwikkeling. Ik ben me er volledig van bewust dat de code nog niet productieklaar is. Ik wed dat veel Linux-veteranen me tot het einde der tijden zullen vervloeken vanwege de keten van commando's die TERMy soms uitzendt; er moet veel werk worden verzet aan de dataset en de broncode om volledige veiligheid en productierijpheid te bereiken.
Beschouw dit als een vroege release om te laten zien wat we in een niet zo verre toekomst kunnen gebruiken. Als de gezamenlijke inspanning van de community zich richt op NPC-Forge en TERMy, denk ik dat ze ons meer vrijheid kunnen geven en ons kunnen helpen geld, elektriciteit en tijd te besparen. Als je dit artikel met plezier hebt gelezen en het met me eens bent, overweeg dan om bij te dragen aan het project. Veel succes met knutselen!
Groetjes,