fast_path.py)canonical_matcher)multi_tool_paths)Quando una richiesta arriva a Metnos, prima di scomodare il pianificatore LLM si cerca di riconoscerla. Se l'abbiamo gia' vista e abbiamo gia' deciso bene come rispondere, la rieseguiamo così com'è e torniamo all'utente in mezzo secondo invece che in dieci. Niente modelli linguistici nel percorso critico, solo memoria.
Il pianificatore di Metnos è un Gemma 4 26B locale: pensa
bene ma ci mette circa dodici secondi a decidere il primo passo di
un turno. Per molte richieste questa attesa è spropositata.
«Che ora è» non ha bisogno di un LLM: ha bisogno di
get_now. Anche «scaricami questa pagina e
descrivila in due righe» non ha bisogno del pianificatore se è
una richiesta che facciamo spesso e di cui sappiamo già la
sequenza: get_urls e poi describe_entries.
Da qui la domanda: come riconosciamo che una richiesta è già nota? E come ci accorgiamo che quasi nota è abbastanza? La risposta è il fast-path introvertivo, organizzato in tre livelli — ciascuno coglie un grado diverso di somiglianza, e tutti convergono allo stesso esito: l'assistente esegue la sequenza giusta senza pensarci due volte.
| Livello | Cosa riconosce | Come | Costo per ogni hit |
|---|---|---|---|
| L0 | Frasi letterali notissime («che ora è», «dove sono») | Tabella chiusa di modelli normalizzati | microsecondi |
| L1 | Una richiesta semanticamente equivalente a una già vista, con UN solo executor | BGE-M3 cosine sul log delle canonical query | ~30 ms |
| L2 | Una pipeline multi-step già eseguita prima, con la stessa intenzione | BGE-M3 cosine sul log delle sequenze + risoluzione di segnaposto | ~30 ms + N invocazioni |
Il flusso è rigorosamente sequenziale: prima si tenta L0, se non matcha si tenta L1, e solo se anche L1 manca si tenta L2. Se tutti e tre mancano, si arriva al pianificatore LLM (L3) come sempre.
Il livello più basso vive in runtime/fast_path.py
e contiene una manciata di pattern testuali codificati a mano. Sono
le richieste che ricorrono a tutti, dette in mille modi
intercambiabili: l'ora, la data, la propria posizione, l'annulla. Per
ognuna abbiamo elencato le varianti italiane e inglesi che ci aspettiamo
realisticamente («che ora è», «che ore sono»,
«dimmi l'ora», «what time is it», «current
time»...) e l'executor canonico da chiamare.
Quando arriva una richiesta, la normalizziamo (minuscolo, apostrofi diritti, punteggiatura via) e cerchiamo una corrispondenza esatta nel dizionario. Se c'è, l'executor parte subito; la risposta esce formattata da un piccolo template italiano/inglese. Zero modello linguistico, zero rete, zero attesa percepita.
È una tabella chiusa: la si estende solo aggiungendo modelli nuovi, mai con espressioni regolari complicate. La regola d'oro: se nel dubbio non sai se va, non aggiungerlo. Conservativo per costruzione.
canonical_matcher)Il secondo livello si occupa delle richieste con UN solo executor di prima scelta: «trova le foto del battesimo», «mostrami gli appuntamenti di oggi», «leggi questa pagina». Sono casi in cui non possiamo enumerare a mano tutte le varianti possibili, perché nel mezzo l'utente cambia argomenti, ordine, lessico. Il pianificatore in pratica chiamerebbe sempre lo stesso executor: vogliamo arrivare alla stessa conclusione senza il giro lungo.
Per riuscirci, ogni volta che il pianificatore decide il primo passo
di un turno, Metnos registra nella tabella canonical_query_log
del mnestoma una riga con tre cose: la richiesta in forma lemma
(versione canonica, minuscola, condensata), l'executor scelto, e i
valori degli argomenti che ha effettivamente usato. Da quel momento
quella riga è un candidato.
Quando una nuova richiesta arriva, la trasformiamo nel suo vettore semantico con BGE-M3 (lo stesso modello multilingua che usiamo per le affinità) e calcoliamo il coseno contro tutti i candidati con almeno tre osservazioni. Se il candidato più vicino supera la soglia 0,95 di coseno, lo accettiamo: la richiesta nuova viene servita con l'executor della riga matchata, e gli argomenti vengono estratti dall'estrattore deterministico (vedi §10) oppure riusati dai valori memorizzati al primo passaggio.
~/.config/metnos/runtime.toml.
multi_tool_paths)
Alcune richieste richiedono più di un executor in sequenza,
con il risultato del primo che alimenta il secondo. «Scaricami
quella pagina e dimmi cosa dice» è due step:
get_urls seguito da describe_entries. Anche
qui il pattern si ripete: per pipeline frequenti il pianificatore
finisce sempre per emettere la stessa sequenza, ma con dodici secondi
di latenza ciascuna volta.
Il terzo livello memorizza le sequenze. Al termine di ogni turno
andato a buon fine con due o più step, Metnos registra in
multi_tool_paths.sqlite la coppia (canonical query,
sequenza di executor + segnaposto degli argomenti). Dopo tre passaggi
identici, la pipeline diventa candidata. Da quel momento, una richiesta
nuova abbastanza simile (coseno ≥ 0,88) viene riconosciuta e
rieseguita per intero: nessun pianificatore, nessuna interruzione.
La soglia 0,88 è più permissiva di 0,95 perché le pipeline multi-step hanno più varianti lessicali della stessa intenzione («scarica e descrivi», «leggi e riassumi», «fammi un sunto» sono la stessa cosa dal punto di vista dell'esecuzione). La protezione contro falsi positivi non è la soglia, ma la risoluzione dei segnaposto: se la nuova richiesta non porta i dati richiesti (per esempio non c'è un URL nel testo ma la pipeline ne richiede uno), il match fallisce e si torna al pianificatore. Niente danni.
enabled = false):
si attiva quando le righe sono sufficienti per essere utili.
Cosa succede a una memoizzazione che non viene usata per mesi? Se Roberto
parte due mesi in vacanza, le sue pipeline non vanno cancellate: gli
serviranno di nuovo al rientro. Per questo le righe di
multi_tool_paths non scadono a calendario, ma in
giorni di attività effettiva.
La tabella system_active_days registra una riga per ogni
giorno in cui Metnos ha visto almeno un turno. A ciascuna riga
è associato un day_rank, un contatore monotono
crescente: il primo giorno utile è 1, il secondo 2, e così
via, saltando i giorni vuoti. Ogni memoizzazione conserva il
day_rank dell'ultima volta che è servita. Quando
il differenziale fra il giorno corrente e quel valore supera N (30
per default), la riga viene rimossa.
La conseguenza interessante: due mesi di assenza non contano. Il contatore avanza solo nei giorni di uso. Una scadenza misurata in «giorni di vita reale dell'assistente», non in «giorni del calendario gregoriano».
C'è un effetto a cascata interessante. Quando un turno contiene step già serviti dal fast-path (perché alcuni sotto-pezzi della richiesta sono noti), Metnos non li scarta dalla registrazione di L2. La sequenza completa — inclusi i passi gestiti da L0 o L1 — viene memorizzata per quella che è: una pipeline che ha funzionato. Dopo tre passaggi, anche quella combinazione diventa un fast-path multi-step.
Significa che il sistema impara a comporre: due fast-path adiacenti diventano un fast-path più lungo, ricorsivamente. La decisione storica è già approvata per ciascun passo, comporne più insieme non aggiunge rischio, solo determinismo.
Una variante opzionale (oggi disattivata di default) permette al fast-path L2 di passare il testimone al pianificatore dopo avere eseguito la sua sequenza. È il caso in cui la richiesta ha più verbi: «scaricami questa pagina e poi mandami il sunto per email». Il prefisso «scarica e descrivi» è un L2 conosciuto; ma «mandami per email» è intenzione residua.
Se la levetta chain_to_planner è accesa e il
rilevatore di continuazione (multi-verbo, congiunzioni) trova
ulteriori azioni nella richiesta, Metnos esegue il prefisso col
fast-path, lo iscrive nella memoria di lavoro del turno, e cede al
pianificatore per gli step rimanenti. Il pianificatore vede una
storia già popolata e decide solo cosa manca, partendo dallo
step successivo.
Riconoscere la richiesta è metà del lavoro. L'altra
metà è ricostruire i valori concreti degli argomenti:
quali path, quali URL, quale data, quale soglia. Per questo Metnos
ha un piccolo estrattore deterministico (args_extractor.py)
che lavora a regole, in tre passi:
canonical_query_log ha valori osservati al primo
passaggio (colonna args_observed), li riusiamo
tali e quali.https://...),
path (~/... o /..., con la scorciatoia
«home» che diventa ~/), email, numeri,
estensioni di file («file PDF» diventa *.pdf),
date (oggi/ieri/domani/dopodomani in italiano e inglese, mappate a
formato ISO), finestre temporali («questa settimana»,
«ultime 24 ore», «last 7 days»).canonical_query_args_llm è accesa, si chiama il
Gemma fast tier con uno schema vincolato e un prompt minimo. Il
risultato viene messo in cache su disco per non ripetere la chiamata.
Spento di default: l'esperienza dice che le regole bastano per il 90%
delle richieste reali.
Tutti i parametri del fast-path vivono in un file TOML
in ~/.config/metnos/runtime.toml, generato al primo
avvio dal setup. Una variabile d'ambiente METNOS_* ha
sempre la precedenza, per il debug e i bench; il file TOML stabilisce
il valore persistente fra un riavvio e l'altro, il default codificato
nel modulo è l'ultima rete di sicurezza.
[fast_path] canonical_query_enabled = true canonical_query_min_uses = 3 canonical_query_threshold = 0.95 canonical_query_args_llm = false [multi_tool_fast_path] enabled = false chain_to_planner = false threshold = 0.88 min_uses = 3 ttl_active_days = 30 k_synth = 50
C'è un ultimo passaggio che chiude il cerchio. Quando una
pipeline L2 viene usata abbastanza spesso da meritare un trattamento
diverso (50 utilizzi, soglia configurabile), un job notturno
(multi_tool_promote) la promuove a candidato per la
sintesi vera e propria. Crea un proto-mnest nel mnestoma, con la
firma desiderata (sequenza di executor, segnaposto, query canonica),
e da quel momento il motore di sintesi può produrre UN nuovo
executor unificato che racchiude tutta la pipeline. La promozione
è idempotente: una riga già promossa viene ignorata
nelle esecuzioni successive.
Il bridge fa due tier conviventi: L2 cattura il volume basso (sequenze viste 3-50 volte, replay deterministico tramite N invocazioni di executor), mentre L3 prende il sopravvento quando il pattern è stabilmente ricorrente e vale l'investimento di sintetizzare un executor monolitico che fa tutto in un colpo. Non si escludono: si completano.
Alcuni punti sono volutamente lasciati al futuro, perché diventano interessanti solo con un po' di volume.
introvertiva: potremmo
generare proposte automatiche di promozione, identificare cluster
di pipeline simili, suggerire executor unificati. Per ora il modulo
introvertiva non legge le memoizzazioni; lo farà in una fase
successiva.© 2026 Roberto Brunialti · documentazione Metnos · ADR di riferimento: 0094 / 0149 / 0150