Quando devi estrarre dati pubblici da più di venti piattaforme, la prima reazione è quasi automatica: creare un unico Post, un unico User e ricondurre a quei modelli ogni risposta ricevuta. Un video bilibili, un video tiktok, una risposta zhihu e un post linkedin sembrano tutti «contenuto più autore». Sulle prime tre piattaforme l'idea funziona benissimo. Alla ventesima, però, quell'astrazione finisce per soffocare il progetto.
Alla fine non ho costruito un modello unificato. Con più di venti piattaforme e oltre duecento comandi, ha retto molto meglio la scelta apparentemente più banale: ogni piattaforma gestisce il proprio dominio.
Perché il modello unificato smette di funzionare
Non crolla di colpo. All'ottava piattaforma, il tuo Post ha già una dozzina di campi opzionali: alcune piattaforme espongono il conteggio dei danmaku, altre no; per alcune la data di pubblicazione è un timestamp al secondo, per altre una stringa come «3 giorni fa». Con più di venti piattaforme, il modello è ormai collassato. Non sotto forma di errore di compilazione: semplicemente non fa più risparmiare nulla. Ogni componente downstream deve prima chiedersi se quella piattaforma abbia valorizzato o meno un campo; la logica necessaria diventa più lunga della lettura della risposta grezza, e il layer unificato si trasforma in un ostacolo da aggirare. Anche in scrittura si paga lo stesso prezzo: ogni nuova integrazione obbliga a tornare lì e infilare nuovi campi in strutture pensate per le piattaforme precedenti.
Una piattaforma, un bounded context
La separazione che regge è l'opposto: niente modello comune, ogni piattaforma resta responsabile del proprio spazio. Nel catalogo, ciascuna piattaforma è un contesto <platform>_reverse/ che possiede quattro aspetti e non li delega:
Validazione degli input. Solo la piattaforma sa che forma hanno i suoi ID e quali combinazioni di parametri siano valide.
Protocollo. Chiamate HTTP dirette o frammenti di JS locale per la firma, domini e header: sono tutti dettagli privati della piattaforma.
Firma. I meccanismi di signing variano enormemente; comprimerli in un signer condiviso produce solo un mostro di if-else.
Normalizzazione delle risposte. La risposta grezza viene ripulita in una struttura posseduta dalla piattaforma, non in un modello globale.
Il quarto punto è quello che si interpreta più facilmente male. «Nessun modello unificato» non significa «nessuna normalizzazione». Ogni piattaforma normalizza eccome, ma definisce la propria struttura di destinazione invece di forzarla dentro un modello condiviso. L'unificazione ha senso soltanto dove la cosa è realmente la stessa: se due endpoint della stessa piattaforma condividono una struttura di post, è corretto, perché si tratta davvero dello stesso oggetto di dominio. L'errore è estendere questa unificazione interna oltre i confini della piattaforma.
Nel layer condiviso entra solo ciò che è davvero comune
Cosa merita di stare nel layer condiviso? Capacità che sono realmente cross-platform e si comportano allo stesso modo ovunque, non quelle che si limitano a sembrare simili. Il mio layer condiviso contiene solo tre elementi:
Read-model dell'interfaccia. Un catalogo unificato delle capacità, derivato dalle dichiarazioni argparse di ogni piattaforma. Ciò che viene unificato è il modo in cui i comandi vengono scoperti e descritti, non ciò che restituiscono. Il primo è davvero trasversale, il secondo è privato della piattaforma. L'idea che «la dichiarazione è l'interfaccia» è approfondita nell'articolo su l'interfaccia come codice.
Trasporto locale in loopback. Le richieste autenticate passano da un servizio di sessione WebSocket locale, che tratta tutte le piattaforme allo stesso modo senza toccarne alcun campo di business.
Punto di dispatch. Individua la piattaforma, inoltra il comando al relativo contesto e non fa altro.
Il criterio è semplice: per entrare nel layer condiviso, qualcosa deve comportarsi davvero nello stesso modo per ogni piattaforma. Trasporto, dispatch e generazione delle descrizioni dell'interfaccia lo fanno; invece «un contenuto» non si comporta affatto allo stesso modo su bilibili e linkedin, quindi non deve entrarci. «Sembrano uguali» è la più grande trappola dell'astrazione: due video si assomigliano, quindi viene spontaneo unificarli. Ma la somiglianza superficiale non è identità di comportamento, e trattarla come un modello di dominio riutilizzabile è all'origine del collasso del modello unificato.
La distribuzione dei comandi mostra dove conviene astrarre
Hai ancora dubbi sul modello unificato? Basta guardare come sono distribuiti i comandi reali: 22 piattaforme, 241 comandi, con una distribuzione estremamente sbilanciata.
Piattaforma | Comandi |
|---|---|
tiktok | 34 |
bilibili | 26 |
18 | |
zhihu | 18 |
douyin | 17 |
xiaohongshu | 16 |
Le altre 16 piattaforme | Da 1 a 13 ciascuna |
Le prime sei piattaforme totalizzano 129 comandi, più della metà del totale. L'altra metà è ripartita tra 16 piattaforme di coda lunga, molte con appena due o tre comandi e alcune con uno solo.
Questa distribuzione definisce l'economia dell'astrazione: il costo di un modello unificato è fisso, perché ogni integratore deve compilare campi, controllare valori null e aggirare le sue limitazioni; il beneficio, invece, si distribuisce piattaforma per piattaforma. Per una piattaforma di coda lunga con due o tre comandi, il vantaggio dell'astrazione è negativo: il codice adapter necessario per adattarla al modello comune finisce per essere più esteso di tutto il suo codice di business.
Non progettare astrazioni per un'implementazione che non esiste
Da quella distribuzione segue un'altra regola: non riservare un'astrazione a una sola implementazione. Se una piattaforma ha una sola implementazione, non aggiungere repository, factory o layer di interfaccia perché «forse in futuro ce ne sarà un'altra». Aggiungere una piattaforma significa aggiungere un contesto <platform>_reverse/, senza dover prima intervenire su una classe base condivisa.
Un layer di interfaccia serve a rendere intercambiabili più implementazioni. Con una sola implementazione, il suo valore è zero mentre il costo di manutenzione è positivo. Riservare spazio a una seconda implementazione inesistente e riservarlo a una presunta comunanza cross-platform inesistente sono lo stesso errore. La migrazione cross-language lo ha confermato ancora una volta: alcune centinaia di comandi del vecchio registry lasciavano esplicitamente non migrato un blocco, senza stub né proxy di compatibilità, perché un guscio vuoto costa più di una lacuna: induce chi arriva dopo a pensare che lì ci sia qualcosa. Un'astrazione riservata fa esattamente lo stesso.
Se normalizzi con un modello, dagli anche il contesto della piattaforma
Il principio «separare per piattaforma, senza modello unificato» vale anche quando usi un modello per normalizzare. Per trasformare le risposte grezze di più di venti piattaforme in una struttura analizzabile, è naturale coinvolgere un modello. L'errore più facile, però, è identico a quello del layer di codice: definire uno schema unificato e passargli il JSON grezzo di ogni piattaforma aggiungendo «mappalo su questo schema». Non funziona, perché il modello non sa se il campo delle visualizzazioni di bilibili e quello di tiktok rappresentino davvero la stessa cosa. Forzarlo verso uno schema al minimo comune denominatore significa perdere un campo essenziale per la piattaforma oppure compilarlo solo parzialmente.
La strada giusta è fornire contesto per piattaforma: dire al modello «questa è bilibili, questi campi significano questo, e per questa piattaforma voglio questa struttura». Si normalizza una piattaforma alla volta, lasciando l'aggregazione cross-platform al layer di analisi. Il flusso richiede alcuni passaggi, ciascuno con esigenze diverse:
Passaggio | Capacità necessaria | Scelta | model id |
|---|---|---|---|
Leggere la struttura dell'intera risposta grezza di una piattaforma | Contesto lungo, capace di assimilare insieme risposta completa e note sui campi | Kimi K3 |
|
Definire il confine della normalizzazione: quali campi sono davvero cross-platform e quali specifici della piattaforma | Ragionamento forte, resistente all'eccesso di unificazione | Claude Opus 5 |
|
Estrarre in massa i campi per piattaforma, mappando elemento per elemento | Economico, con centinaia o migliaia di chiamate ad alta concorrenza | Claude Sonnet 5 |
|
Spiegare perché campi con lo stesso nome su due piattaforme non corrispondono | Ragionamento intermedio, capace di motivare le differenze rispetto ai campi | GPT-5.6 Sol |
|
Il secondo passaggio è l'unico in cui cambiare modello modifica visibilmente il risultato. Mette alla prova la capacità di ammettere che due campi non siano davvero la stessa cosa, proprio come nella sezione sulle controprove nell'identificazione delle famiglie di algoritmi: un modello debole segue il suggerimento di «unificare», uno forte segnala il confine.
Il vero freno è il costo del cambio di modello
I quattro livelli provengono da tre vendor, tre SDK, tre schemi di autenticazione e tre formati di errore. Riscrivere tre volte il client per passare da un modello all'altro tra i diversi passaggi non conviene; per questo molti usano un solo livello per tutto il flusso. Spesso, però, scelgono un livello che fallisce proprio nel passaggio in cui va definito il confine, creando uno schema destinato a collassare di nuovo con più di venti piattaforme.
AIReiter appiattisce questo layer: una chiave, un'interfaccia compatibile con OpenAI, tutti e quattro i livelli dietro le quinte. Per cambiare modello basta modificare il campo model nel body della richiesta.
# Set the normalization boundary: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<one platform response sample + have it mark which fields are platform-specific>"}]
}'
# Extract fields per platform in bulk: change the model field, leave the rest
# "model": "claude-sonnet-5"
# Field-difference attribution:
# "model": "gpt-5.6-sol"
Se usi già l'SDK OpenAI, punta base_url a https://aireiter.com/api/v1 senza cambiare altro. Con l'SDK Anthropic, usa POST /api/v1/messages con la stessa chiave.
Sui prezzi, i modelli Claude hanno uno sconto del 30% sul listino, i modelli GPT costano la metà e Kimi K3 è accessibile con la stessa chiave. Lo sconto incide proprio sulla voce di costo principale: l'estrazione massiva dei campi per piattaforma è il passaggio con più chiamate, su più di venti piattaforme e centinaia o migliaia di record ciascuna, una chiamata per record, eseguita sul Sonnet più economico con un ulteriore 30% di sconto. Leggere una risposta lunga completa con Kimi K3 richiede qualche centinaio di migliaia di token per input, un'altra quota di costo. Il tier di ragionamento per definire i confini richiede poche chiamate, quindi incide appena.
Provalo senza registrarti: passa manualmente la risposta di una piattaforma e verifica se il modello segnala con onestà le differenze o se le appiattisce subito; solo dopo decidi se integrarlo.
In sintesi
Nella raccolta cross-platform, la prima reazione è astrarre un unico modello Post/User. Su piccola scala sembra un'ottima idea, ma con più di venti piattaforme collassa inevitabilmente: ha un costo fisso, un beneficio distribuito per piattaforma e deve fare i conti con una coda lunga di comandi molto marcata. La separazione che funziona assegna un bounded context a ogni piattaforma, responsabile di validazione degli input, protocollo, firma e normalizzazione delle risposte. Il layer condiviso conserva solo ciò che si comporta davvero allo stesso modo ovunque, come trasporto, dispatch e generazione delle descrizioni dell'interfaccia; non un modello di dominio accomunato soltanto dall'apparenza. E non riserva astrazioni a una singola implementazione o a una comunanza inesistente. Con i modelli vale lo stesso principio: normalizza con contesto specifico per piattaforma, non con uno schema unificato, e rinvia l'unione cross-platform al solo layer di analisi. Il workflow completo in quattro fasi approfondisce la suddivisione tra i quattro livelli; collegati da un'unica interfaccia, il costo di passare dall'uno all'altro non è più un motivo per rinunciarvi.