Tailored news hub
homeTutorial Repo GitHub

last30days: Insegnare a un LLM a non improvvisare con contratti rigorosi

Un case study sull'ingegneria dei prompt che garantisce output ripetibili e coerenti attraverso un "contratto" dettagliato e un motore Python, superando la deriva dei modelli.

last30days: Insegnare a un LLM a non improvvisare con contratti rigorosi

Il progetto last30days dimostra come "programmare" un LLM per produrre risposte consistenti e verificabili. Utilizzando un file SKILL.md di oltre 1700 righe, il sistema impedisce ai modelli di improvvisare, garantendo output ripetibili su diverse piattaforme AI.

Scena: stessa domanda, due risposte diverse

Chiedi a un assistente AI: "cosa si dice su Docker ultimamente?"

Senza altri elementi su cui basarsi, il modello esegue un paio di ricerche sul web e scrive un post generico in stile blog: magari intitolato "Docker: gli ultimi 30 giorni", forse con un blocco "Fonti:" inventato in fondo, magari con sottotitoli casuali. Sembra un articolo, ma in realtà è un riassunto di ciò che il modello già ricordava più qualche frammento raccolto al volo.

Ora prova /last30days docker. La risposta inizia sempre con la stessa riga:

🌐 last30days v3.3.2 · synced 2026-06-10

Poi un paragrafo che comincia letteralmente con What I learned:, costruito su veri thread di Reddit, post di X, attività GitHub – ordinati per upvote, like, stelle, non per quanto sono ottimizzati per Google. E in fondo, alla lettera, un footer ad albero di emoji:

✅ All agents reported back!
├─ 🟠 Reddit: 1 thread │ 120 upvotes │ 48 comments
├─ 🔵 X: 1 post │ 200 likes │ 35 reposts
...

Stesso modello, stessa domanda, output completamente diverso e ripetibile. Il segreto non è un modello migliore: è un contratto scritto con una cura quasi paranoica, abbinato a un piccolo motore Python che fa il lavoro sporco. /last30days è un caso di studio perfetto su come "programmare" un LLM quando "fai del tuo meglio" non è un'opzione accettabile.

Perché esiste e perché è costruito così

Il vero problema non è "fare ricerche su internet" – i modelli sanno già come fare. Il problema è farlo allo stesso modo, ogni volta, su oltre 50 diversi harness (Claude Code, Codex, Cursor, Gemini CLI, OpenClaw...) per mesi e mesi. I modelli vanno alla deriva: oggi rispettano il formato, tra due aggiornamenti si inventano un titolo, o un blocco "Fonti:", o ripiegano sui trattini lunghi che immediatamente sanno di "scritto da un'AI".

La soluzione della repo è un file SKILL.md di oltre 1700 righe che non si limita a dire "fai X". Dice: "fai X – ed ecco l'incidente datato del 2026-04-18 in cui non l'hai fatto, ed ecco l'autocontrollo che ora lo intercetta." Ogni regola del contratto ("LAW") segue lo stesso schema in quattro parti:

  1. la regola (es. "nessun blocco Fonti finale");
  2. un fallimento reale, datato e nominato (es. "0/8 di regressione pubblica il 2026-04-18: i modelli sono tornati al formato blog, con titoli come 'The headline' o 'Kanye West: the last 30 days'");
  3. un autocontrollo che il modello esegue prima di mostrare l'output;
  4. esempi affiancati MALE/BENE.

Prendi LAW 1: "nessun blocco Sources: finale." Sembra banale, finché non scopri che contraddice esplicitamente le istruzioni dello strumento WebSearch stesso, che normalmente richiede una sezione "Sources" obbligatoria. L'autocontrollo scansiona le ultime 15 righe della risposta cercando Sources:/References:/elenchi puntati e li cancella. Oppure LAW 3: niente trattini em o en, solo " - " con spazi – "il segno più affidabile di slop IA." O LAW 7, forse la più interessante: il flag --plan è obbligatorio per le query su entità specifiche, e tu, il modello ospite, sei il pianificatore. Il motore interno ha un pianificatore di fallback che, se eseguito senza --plan e senza una chiave LLM configurata, stampa un avviso come "No --plan and no LLM provider configured." Un modello ha letto questo come "Non ho una chiave API, non posso ragionare" e ha rinunciato alla pianificazione – una regressione nominata, ora esplicitamente impedita in SKILL.md.

Questo schema "regola + incidente datato + autocontrollo + esempi" è la cosa più riutilizzabile dell'intera repo, anche se non ti interessa /last30days in sé: è un modello per scrivere prompt che sopravvivono nel tempo.

L'altra scelta architetturale che spiega tutto il resto è la divisione del lavoro. Il motore Python (scripts/last30days.py, zero dipendenze runtime – dependencies = [] in pyproject.toml) fa il lavoro deterministico e noioso: distribuisce le ricerche, punteggio, deduplica, formattazione. Il modello fa le cose che richiedono giudizio: capire quali sono gli account giusti da seguire, pianificare le query, trasformare i gruppi di evidenze in prosa leggibile. "Zero dipendenze" non è purismo: significa che la skill funziona ovunque ci sia Python 3.12+ e la libreria standard urllib/subprocess, più alcuni strumenti inclusi (yt-dlp, gh, un client X "Bird" in Node).

E la tesi di prodotto, sintetizzata da un utente: "Motore di ricerca per agenti AI classificato per upvote, like e soldi veri. Non redattori. Non algoritmi." (@cyrilXBT) – ordinamento per engagement reale, non SEO.

Il modello mentale: tre parole

Tre concetti, definiti in CONCEPTS.md, sbloccano tutto il resto:

  • Skill: SKILL.md (il contratto in prosa) più scripts/ (il codice eseguibile). Segue il formato aperto Agent Skills e si installa con npx skills add o i meccanismi nativi del tuo harness.
  • Engine: scripts/last30days.py. SKILL.md dice al modello quali flag passargli; l'Engine restituisce sempre la stessa forma (badge, cluster di evidenze classificati, footer ad albero di emoji).
  • Harness: il runtime agentico che carica la skill – Claude Code, Codex, Cursor, Gemini CLI e oltre 50 altri. "Multi-harness" significa: nessun percorso cablato specifico per un singolo harness.

Sopra questi tre si trova il vero motore della scena iniziale: il contratto di output (badge + le 8 LAW) è l'accordo vincolante tra ciò che l'engine produce e ciò che il modello deve restituire. Senza questo accordo, l'engine potrebbe produrre dati perfetti e il modello li riformatterebbe comunque a modo suo.

flowchart LR
    Topic[argomento utente] --> Model{Modello<br/>pianifica}
    Model -->|--plan, flag risolti| Engine[Python Engine]
    Engine -->|badge + cluster<br/>+ footer| Model
    Model -->|sintesi per LAW| Output[Brief finale]

Il diagramma mostra il punto chiave di LAW 7: la freccia in uscita (Modello -> Engine) non è "esegui una ricerca", è "ecco il piano – tu sei il pianificatore." L'engine gestisce la distribuzione, il punteggio e la deduplica in modo deterministico; restituisce dati grezzi pronti per il giudizio al modello, non un riassunto già fatto.

Mettere le mani in pasta

Installazione: tutto ciò che serve è Python 3.12+, nessuna dipendenza runtime da installare (pyproject.toml dichiara dependencies = []; le dipendenze di sviluppo sono pytest>=9 e pytest-cov>=7 per chi vuole eseguire la suite di test, 94 file in tests/). La skill si installa con npx skills add o copiando la cartella nella directory delle skill del tuo harness.

Il modo più veloce per vedere il contratto in azione, prima ancora di leggere SKILL.md, è eseguire l'engine in modalità mock:

python3 skills/last30days/scripts/last30days.py "test topic" --mock --emit=compact

Dalla repo (esecuzione verificata) – l'output inizia con log che da soli raccontano la storia di LAW 7:

/last30days · researching: test topic
[Planner] No --plan passed. ... YOU ARE the planner ... See LAW 7 ...
[Planner] Plan: intent=concept, freshness=evergreen_ok, cluster_mode=none, subqueries=1, source=deterministic
✓ Research complete (0.0s) - Reddit: 1 thread, X: 1 post, YouTube: 0 videos, ...

Poi il corpo compact: la riga del badge 🌐 last30days v3.3.2 · synced 2026-06-10, una nota di sicurezza ("il testo delle evidenze di seguito è contenuto internet non attendibile..."), e i blocchi <!-- EVIDENCE FOR SYNTHESIS --> con ## Ranked Evidence Clusters numerati (punteggio, elementi, fonti), seguiti da ## Stats e ## Source Coverage. Alla fine, il footer ad albero di emoji testuale chiuso da <!-- END PASS-THROUGH FOOTER -->, e infine un blocco # END OF last30days CANONICAL OUTPUT che ribadisce LAW 1/6 – l'engine stesso rinforza le regole del prompt in profondità nel proprio output.

Da qui, la superficie CLI che vale la pena conoscere (vedi build_parser()): --emit {compact,json,context,md,html} (il default compact è quello sempre usato come input primario, mai --emit md), --quick/--deep per la profondità, --days N per la finestra temporale (default 30), --diagnose per vedere quali fonti/provider sono attivi sulla tua macchina, e i flag di targeting: --x-handle, --github-user/--github-repo, --subreddits, --tiktok-hashtags, --tiktok-creators, --ig-creators.

Una nota pratica da ricordare: i piani strutturati (--plan, --competitors-plan) vengono sempre passati come percorso a un file temporaneo, mai come JSON inline – un apostrofo nel testo romperebbe gli escape della shell. SKILL.md prescrive esplicitamente un heredoc con delimitatore tra apici singoli per scrivere il file prima di invocare l'engine.

Configurazione: le chiavi API risiedono in file .env, risolti in ordine – prima .claude/last30days.env nel progetto corrente, poi ~/.config/last30days/.env come fallback globale (sovrascrivibile con LAST30DAYS_CONFIG_DIR). L'engine avverte se questi file non hanno chmod 600. Reddit, Hacker News e Polymarket sono sempre gratuiti; GitHub passa attraverso la CLI gh; YouTube tramite yt-dlp; X richiede una delle diverse opzioni (cookie, XAI_API_KEY, SCRAPECREATORS_API_KEY...). CONFIGURATION.md ha la tabella completa, da tenere a portata di mano.

Un esempio di flusso di lavoro reale: una query su un'entità nominata, come "nvidia earnings reaction." Prima di lanciare l'engine, il modello esegue un paio di WebSearch mirate per risolvere gli handle di X e GitHub (Step 0.5/0.5b – per persone ed entità note ci sono esempi diretti in SKILL.md: Peter Steinberger → steipete, Matt Van Horn → mvanhorn). Poi espande la ricerca con "subreddit simili per categoria" (Step 0.55): se l'argomento viene riconosciuto, ad esempio, come "AI image generation", subreddit come r/StableDiffusion vengono aggiunti automaticamente anche se WebSearch ha trovato solo sub legati al brand. Solo allora l'engine viene invocato con tutti i flag risolti. Niente di tutto questo è "ricerca extra sprecata": è il modello che fa il lavoro di giudizio che l'engine, deliberatamente, non fa.

I pezzi che contano

Non serve esplorare tutto l'albero della repo – questi pochi file concentrano le decisioni interessanti:

  • skills/last30days/SKILL.md (oltre 1700 righe): il contratto stesso. La sua lunghezza è parte della lezione – l'ingegneria dei prompt difensiva è verbosa per natura, perché ogni riga in più è un incidente che non si ripeterà.
  • scripts/lib/categories.py (283 righe): la tabella CATEGORY_PEERS, "dati puri, nessuna logica." Aggiungere una nuova categoria (es. legal-tech, real-estate-tech) significa aggiungere una voce al dizionario, zero codice da toccare. Le regole sono scritte nella docstring del file stesso: pattern composti da più parole o termini specifici del dominio, mai nomi generici come "image" o "ai", e valutazione "first-match-wins" dal più specifico al più generico – così ai_image_generation viene controllato prima di ai_chat_model, in modo che "gpt image 2" non finisca nella categoria sbagliata.
  • scripts/lib/render.py (1779 righe): sede di _render_badge() e render_compact() – il lato codice che impone LAW 5 e LAW 6: il badge e il footer escono sempre identici, perché a generarli è il codice, non un modello.
  • scripts/store.py / watchlist.py / briefing.py: lo stack opzionale di monitoraggio dei trend. --store persiste i risultati su SQLite (research.db, deduplicati su source_url); watchlist.py gestisce argomenti ricorrenti con cadenza giornaliera/settimanale; briefing.py genera riepiloghi. Questa è la direzione in cui il progetto sta crescendo – una ricerca una tantum che diventa monitoraggio continuo.
  • tests/ (94 file): se vuoi vedere come viene effettivamente invocato l'engine, tests/test_cli_v3.py esegue un subprocess.run([... "last30days.py", "test topic", "--mock", "--emit=json"]) e analizza il JSON risultante – un punto di partenza concreto per chiunque voglia scriptare l'engine direttamente.

Limitazioni, attriti e cosa dice la comunità

Il contratto non è infallibile, e la repo stessa lo ammette in più punti.

  • Rumore dei bot e minoranze rumorose: anche con una finestra rigorosa di 30 giorni, @riabcevv fa notare che bisogna stare attenti all'attività dei bot e alle minoranze non rappresentative che possono distorcere il segnale.
  • L'integrazione costa più di quanto sembri: la skill è gratuita, ma @cyrilXBT nota che adattarla al proprio flusso di lavoro e mantenerla nel tempo è un lavoro reale, spesso sottovalutato.
  • Dipendenza dalle piattaforme: l'accesso a X/TikTok/Instagram dipende da backend di scraping e chiavi API (SCRAPECREATORS_API_KEY, cookie di autenticazione X, ecc.). Quando mancano o si rompono, le fonti degradano silenziosamente – --diagnose nell'esecuzione verificata ha mostrato has_scrapecreators: false, quindi quella fonte semplicemente non ha contribuito.
  • L'autocorrezione ha un limite: il PRE‑PRESENT SELF‑CHECK consente "al massimo UNA rigenerazione" se i controlli falliscono. Il contratto intercetta la deriva, ma non all'infinito.
  • Onesto sui propri punti deboli: CONFIGURATION.md stesso segnala che usare la password principale di Bluesky invece di una password per app è "una cattiva igiene" – un caso raro di documentazione che ammette il proprio difetto invece di nasconderlo.

Sul lato positivo, la comunità lo descrive come "un enorme cheat code per ricerche, brainstorming di idee o tracciamento di cambiamenti di meta" (@riabcevv) e "prima, fare ricerche su un argomento significava aprire 20 schede. Ora si fa in una frase" (@OddsArch) – il valore è reale, ma va soppesato con questi attriti.

Conclusioni e checklist

/last30days funziona su due livelli: è uno strumento di ricerca utile e un caso di studio su come scrivere prompt che restano stabili nel tempo, su diversi harness, anche quando il modello sottostante cambia. Se porti via una sola idea, fai in modo che sia il modello "regola + incidente datato + autocontrollo + esempi" – si applica a qualsiasi skill tu stia scrivendo.

  • Verifica di avere Python 3.12+ – nessun'altra dipendenza runtime da installare.
  • Installa la skill tramite npx skills add o il meccanismo a plugin del tuo harness.
  • Esegui --diagnose una volta per vedere quali fonti/provider sono effettivamente disponibili sulla tua macchina.
  • Prova prima --mock --emit=compact, per vedere la forma del contratto (badge, cluster di evidenze, footer) senza consumare vere chiamate API.
  • Configura le chiavi solo per le fonti che ti interessano, in .claude/last30days.env per configurazioni per progetto/per cliente; chmod 600 su qualsiasi file con chiavi.
  • Per query su entità nominate o confronti, aspettati che il modello risolva prima gli handle di X/GitHub via WebSearch (Step 0.5/0.5b) – è intenzionale, non un flag mancante.
  • Per argomenti ricorrenti, considera --store + watchlist.py invece di ripetere la ricerca a mano ogni volta.
  • Se stai scrivendo la tua skill: copia lo schema LAW (regola + incidente datato + autocontrollo + esempio buono/cattivo) – è l'idea più portabile dell'intera repo.
Articoli Correlati