LLM · embedding · VLMMetnos usa tre famiglie di modelli: un LLM che ragiona e pianifica, un embedder che trasforma testo e immagini in vettori per la ricerca semantica, e un VLM che guarda le foto e le descrive. La domanda di questo documento è semplice: quando voglio sostituire uno di questi modelli, cosa devo toccare?
Fino a poco tempo fa la risposta dipendeva dal modello. Per l'LLM esisteva
già una buona soluzione: un piccolo strato (llm_router) leggeva
da un file di configurazione quale modello servire, così cambiarlo voleva
dire editare quel file. Per l'embedder e il VLM,
invece, no: i vari pezzi del sistema importavano la classe concreta
direttamente — bge_embedding qui, clip_embedding
là — e l'indirizzo del VLM era scritto a mano nel codice.
Il difetto non è estetico, è pratico. Sostituire l'embedder voleva dire andare a cercare ogni punto del codice che lo nominava e cambiarlo uno per uno: il routing semantico, l'indicizzazione, due processi notturni, gli executor delle immagini. Dieci posti diversi, dieci occasioni di sbagliarne uno. E puntare l'embedding a un server esterno? Semplicemente non era previsto: avrebbe richiesto codice nuovo.
virt elimina questa differenza —
porta embedding e VLM allo stesso livello di virtualizzazione che l'LLM aveva
già.
L'idea che sblocca tutto è un cambio di domanda. Invece di chiedere
«dammi BGE-M3», il codice chiede «dammi
l'embedder per il ruolo ‘testo’». Chi chiede non
sa — e non gli interessa — quale modello risponde: sa solo a
cosa gli serve. Tradurre il ruolo nel modello concreto è compito
di un unico strato, il package runtime/virt/, che offre tre porte
d'ingresso (in gergo, tre facciate):
| Facciata | Chiedi un ruolo… | …e ti torna |
|---|---|---|
virt.get_llm(role) | "fast" / "middle" / "wise" / "frontier" | il modello di linguaggio per quel tier (delega a llm_router) |
virt.get_embedder(role) | "text" oppure "image" | l'embedder per quella modalità (BGE-M3 per il testo, SigLIP per le immagini) |
virt.get_vlm(role) | "default" | la configurazione del VLM (provider, modello, indirizzo, limiti) |
Si noti la natura dei ruoli. Per l'LLM sono livelli di capacità
(fast per le risposte rapide, wise per il ragionamento
difficile, frontier per il modello a pagamento usato solo quando
serve). Per l'embedder sono modalità (testo o immagine),
perché lì la distinzione utile non è quanto è potente
il modello, ma che tipo di dato trasforma. Il ruolo è
un’astrazione che si adatta al dominio.
from virt import get_embedder, get_llm, get_vlm
get_embedder("text").embed_texts([...]) # vettori del testo, ruolo "text"
get_llm("middle").chat(system, user).text # risposta del LLM, tier "middle"
get_vlm() # spec del VLM (ruolo "default")
virt lo estende a
tutte e tre le famiglie.
virt traducono quel ruolo leggendo i file di configurazione TOML; a destra i modelli concreti. Nessun consumatore nomina più un modello: lo fa, in un solo posto, la configurazione.La traduzione da ruolo a modello vive in tre file TOML, uno per famiglia, nella cartella di configurazione dell'utente:
~/.config/metnos/llm_tiers.toml — i tier del LLM;~/.config/metnos/embedding_tiers.toml — le modalità dell'embedder;~/.config/metnos/vlm_tiers.toml — la configurazione del VLM.Ogni file ha sezioni piatte, una per ruolo. Ecco com'è fatto davvero quello dell'embedding: due righe per dire «al testo serve BGE, alle immagini SigLIP».
[text] provider = "bge" # BGE-M3, 1024 dimensioni # Per puntare a un embedder REMOTO: # provider = "http" # base_url = "http://host:port" [image] provider = "siglip" # SigLIP, 768 dimensioni, testo+immagine
Quello del VLM è altrettanto piccolo — una sola sezione
[default] con il provider, il modello, l'indirizzo del server e i
limiti dell'immagine:
[default] provider = "llamacpp" # server OpenAI-compat multimodale model = "qwen3vl-2b" base_url = "http://127.0.0.1:8081" timeout_s = 60 max_edge = 1024 # ridimensiona il lato lungo max_tokens = 512
provider in
[text]. Vuoi spostare il VLM su un'altra macchina? Cambi
base_url in [default]. Nessuna riga di Python da toccare,
nessun executor da ri-firmare.
virt) che riproducono esattamente la realtà attuale: BGE per il
testo, SigLIP per le immagini, Qwen3-VL su :8081. I TOML servono solo
quando si vuole deviare dal default. Editarli sovrascrive il valore di
partenza, riga per riga; ciò che non si tocca resta com'era.
Il vero guadagno non è solo la comodità del TOML: è la
segregazione. Prima del cambiamento, dieci punti del codice
importavano l'embedder concreto. Dopo, nessun consumatore importa
più bge_embedding o clip_embedding
direttamente: passano tutti per virt.get_embedder. La conoscenza di
«quale modello» è stata raccolta in un unico imbuto.
| Prima | Dopo | |
|---|---|---|
| Chi nomina il modello | ogni consumatore (routing, indici, notturni, executor immagini) | solo la configurazione, letta da virt |
| Per cambiare modello | cercare e modificare N punti del codice | editare una riga del TOML |
| Endpoint remoto | non previsto (servirebbe codice nuovo) | provider = "http" + base_url |
Da questo discende una libertà che prima non c'era: poiché tutti
chiedono l'embedder allo stesso sportello, si può sostituire
l'implementazione sotto i loro piedi senza che se ne accorgano. In
particolare, l'embedder del testo può diventare un servizio
remoto — un server dietro un indirizzo HTTP — semplicemente
scrivendo provider = "http" e l'indirizzo nel TOML. È la
controparte, per l'embedding, di quel «puntare un tier a un endpoint»
che il LLM faceva già.
virt le restituisce così come sono. L'unica implementazione
aggiunta è HttpEmbedder: l'embedder remoto, che parla a un
server compatibile con l'API OpenAI. Tutto il resto è instradamento.
C'è un secondo motivo, più profondo, per cui questo riordino
conta. Storicamente l'embedding del testo passava — almeno
concettualmente — per una struttura esterna condivisa. Riportandolo dentro
virt è emerso (e si è reso esplicito) che in realtà
gli embedder girano già dentro il processo di Metnos: sono
modelli ONNX caricati in-process, senza dipendere da alcuna struttura
esterna.
| Famiglia | Modello | Dove gira |
|---|---|---|
| embedding testo | BGE-M3 (1024 dim.) | ONNX in-process — nessun server, nessuna dipendenza esterna |
| embedding immagini | SigLIP (768 dim.) | ONNX in-process — lo stesso modello vettorizza testo e immagine |
| LLM (testo) | Qwen | endpoint llama-server su :8080 |
| VLM (immagini) | Qwen3-VL | server su :8081, acceso su richiesta durante l'indicizzazione |
La distinzione è importante. L'embedding — il cuore della ricerca semantica, quella che gira a ogni richiesta — è ora autonomo: vive nel processo, senza chiamare nulla fuori. L'LLM e il VLM restano invece dei server a parte (con il VLM acceso solo quando serve davvero, durante l'indicizzazione delle foto), ma anche per loro «quale modello» e «a quale indirizzo» è ormai una voce di configurazione, non una costante nel codice.
Da dove viene la forma di virt? È modellata su un pattern
già collaudato altrove: dichiarare i contratti — cosa
deve saper fare un embedder, cosa un LLM — come Protocol (in Python,
un'interfaccia che descrive i metodi attesi senza imporre un'eredità). Ma
virt ne prende solo l'osso, restando volutamente un
sottoinsieme leggero.
Il pattern di partenza, più ricco, prevede un registro e
l'iniezione delle dipendenze: un'infrastruttura che costruisce e
distribuisce gli oggetti su richiesta. virt butta via tutto questo.
Tiene due cose sole:
embed_texts, embed_query);llm_router aveva
già per il LLM.Perché ci si può permettere tanta semplicità? Per un motivo preciso: le classi locali che già esistono — quella di BGE, quella di SigLIP, quella del provider llama — rispettano già quei Protocol senza modifiche. Espongono i metodi attesi così come sono. Non serve scrivere alcun adattatore: la factory le restituisce direttamente. Un registro, qui, sarebbe peso morto.
| Per capire… | Leggi |
|---|---|
| i tre tier del LLM, gli alias e il frontier opt-in | multilang (sezione sui tier) |
| dove l'embedding entra nella scelta degli strumenti (vicinanza semantica) | fastpath e autopath |
| il VLM al lavoro: come si descrivono le foto durante l'indicizzazione | executor |
| il confine fra skill e backend (un altro asse di sostituibilità) | skill & backend |
Metnos — virtualizzazione dei modelli, chiedi un ruolo, non un modello