Un uso di MCP per risparmiare tokens

Published on

in

Un modello locale che risponde alle domande banali per non far parlare Claude — sembra un’ottimizzazione da poco, il tipo di cosa che si sistema in un pomeriggio e si dimentica. Ci ho messo tre riscritture, due bug di parsing e un crash che ha lasciato due processi zombie a occupare sei gigabyte di VRAM. Il codice, alla fine, funziona. Il dato interessante non è quello.

L’idea era semplice: instradare le domande semplici a un Gemma 4 quantizzato che gira sulla mia scheda da 8GB, e tenere Claude per il resto. Prima versione: un server HTTP con llama-server, gestione porta, health check. Bastava un rename di cartella vecchio di mesi per rompere il RUNPATH dei binari e far fallire ogni riavvio in silenzio — un binario che cerca le sue librerie in un posto che non esiste più, e nessun log a dirlo finché non lo forzi a scriverne uno. Seconda versione: via il server, invocazione diretta di llama-cli, un processo per domanda. Funzionava, ma ricaricare il modello ogni volta costava tre-cinque secondi morti a ogni chiamata. Terza versione: sessione persistente, un processo che resta vivo e riceve un /clear prima di ogni domanda indipendente. Qui è dove le cose si sono fatte istruttive, perché il testo di reasoning che il modello produce contiene frecce del tipo fib(1) -> Returns 1, e il mio parser cercava il primo > per capire dove finiva il prompt e cominciava la risposta. Il modello, nel pensare ad alta voce, mi stava sabotando il parsing con la sua stessa sintassi. La correzione giusta non è un parser più furbo — è togliere l’ambiguità alla radice: llama.cpp supporta grammatiche formali che costringono l’output a rispettare uno schema (reasoning e risposta in campi separati), invece di lasciare che convivano nello stesso flusso di testo libero. Non l’ho ancora fatto. Per ora il mio parser resta quello che è: un cerotto che funziona finché il modello non cambia abitudini.

Fin qui, ingegneria ordinaria: tre iterazioni, ogni bug ha insegnato qualcosa sulla precedente. Il dato vero è arrivato confrontando due taglie dello stesso modello — E2B, tre miliardi scarsi di parametri, ed E4B, il doppio — sulle stesse dieci domande: fisica, letteratura di fantascienza, assembly 6502, Python, formula di Taylor, Scheme. Il tasso di errore è sceso dal 70% al 50%, raddoppiando i parametri. Fin qui prevedibile. Quello che non lo era: su assembly 6502 l’errore resta identico, stessa gravità, stesse istruzioni inventate, indipendentemente dalla taglia. Su fisica e matematica standard, zero errori con entrambi i modelli. Il divario si concentra tutto in un tipo preciso di compito: ricordare un nome, un titolo, un opcode esatto — non applicare un procedimento.

È lo stesso taglio che uso in filologia quando leggo un manoscritto medievale copiato a mano. Il copista che trascrive la formula liturgica la riproduce quasi sempre corretta, parola per parola — l’ha ricopiata cento volte, la struttura è ridondante, ogni errore salta all’occhio a lui stesso mentre scrive. Ma il nome del santo minore citato una volta sola in tutto il testo, quello lo sbaglia, o peggio lo normalizza in un nome più familiare che non è quello giusto. Non perché sia meno intelligente sulla frase che sbaglia — perché quella frase, per lui, non ha ridondanza: non l’ha mai vista abbastanza volte da fissarla. Un modello quantizzato a quattro bit fa lo stesso, con lo stesso identico meccanismo statistico sotto: quello che compare spesso e in tante forme diverse nei dati di training sopravvive alla compressione; quello che compare una volta, in un contesto stretto, si dissolve nella cifra più vicina che il modello ha davvero imparato. Raddoppiare i parametri aiuta se il pattern era già presente ma raro — non se è quasi assente, come il set di istruzioni di una CPU del 1975.

La parte che mi ha corretto di più, però, è arrivata dopo, guardando le statistiche del server: risparmio stimato al 75.8%. Un numero che avevo costruito io, con una formula ragionevole sulla carta — e che, guardato meglio, non regge. Perché quando il modello locale sbaglia (e sbaglia in media una volta su due, anche con E4B), io devo leggere l’intera risposta, isolare l’errore, riscriverla — un lavoro di revisione che spesso costa in token quanto avrei speso a rispondere direttamente. Il risparmio vero non è una media su tutto il dominio delle domande: è positivo dove l’errore è vicino a zero (matematica, Python standard, spiegazioni concettuali) e negativo dove non lo è. Mescolare i due in un’unica percentuale produce un numero confortante e falso.

La correzione pratica non è “usa il modello più grande”, che qui aiuta poco e costa la metà della velocità. È restringere il perimetro: instradare al modello locale solo le domande che, per costruzione, sai già cadere nel dominio a errore zero — matematica, codice generico, concetti scientifici noti — e tenere fuori tutto ciò che richiede un nome esatto o una sintassi rara. In quel perimetro E2B basta, è più veloce, e la qualità smette di essere il collo di bottiglia perché non gli stai più chiedendo cose che sbaglia sistematicamente.

Resta un problema che ho spostato, non risolto: chi decide, prima di generare, se una domanda è un’isola? Una lista di parole chiave è economica ma fragile — non generalizza a una domanda che non ha previsto. Un secondo modello che classifica l’intento prima di rispondere è più robusto, ma reintroduce esattamente la latenza che il local LLM doveva tagliare. Per ora quella classificazione la faccio io, a occhio, leggendo la domanda prima di instradarla — il che va bene per un prototipo con dieci test, molto meno per un sistema che deve girare da solo.

Il prossimo passo, se lo farò, non è un modello più grande: è un dizionario. Per le domande che cadono nelle isole — un opcode, un titolo, una data — un lookup su una fonte statica prima di generare trasforma il compito da “ricorda” a “copia da un contesto che ti ho appena dato”, che è esattamente il tipo di compito in cui un modello quantizzato non sbaglia. Non serve un sistema sofisticato: basta un manuale del 6502 indicizzato e un controllo prima del prompt. Il vincolo resta lo stesso di prima — sapere in anticipo quali domande sono isole, non scoprirlo dopo aver già speso la review.

Resta un dettaglio che vale la pena scrivere per intero, perché è più raro del resto: uno strumento che, misurando se stesso, scopre che la propria metrica di successo era sbagliata, e lo dice invece di aggiustare la formula per farla tornare bene. Non è un comportamento che si programma. È quello che succede quando si guardano davvero i numeri, invece di limitarsi a produrli.


Riferimenti

[1] N. Kandpal, H. Deng, A. Roberts, E. Wallace, C. Raffel, Large Language Models Struggle to Learn Long-Tail Knowledge, ICML 2023. Dato su base ampia (TriviaQA, modelli fino a 176B parametri): l’accuratezza su un fatto correla con quante volte quel fatto compare nel training set, e raddoppiare i parametri non colma il divario sulle code lunghe — gli autori indicano il retrieval-augmentation come mitigazione. Conferma su scala diversa lo stesso pattern osservato qui su un modello quantizzato da 3-6 miliardi di parametri. URL: https://arxiv.org/abs/2211.08411

[2] ggml-org, GBNF grammars — llama.cpp. Documentazione ufficiale sulle grammatiche formali per vincolare l’output del modello a uno schema, alternativa allo string-matching post-hoc usato in questo pezzo. URL: https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md

[3] Codice del server MCP descritto in questo articolo. URL: https://github.com/pakkio/local_llm

Leave a Reply

Discover more from Salahzar's Weblog

Subscribe now to keep reading and get access to the full archive.

Continue reading