./run grafo-fermo-a-030-confidence
2194 affermazioni, tutte ferme a 0.30: insegnare a un grafo cosa conta
Ho costruito una pipeline multi-agente che monitora YouTube, Discord e RSS e sputa un digest italiano su WhatsApp. Funzionava tutto tranne la cosa che…
- status
- alive
- project
- knowledge-digest-agent
- updated
- 2026-07-08
- tags
Aprii il pannello KB una mattina e il grafo era una prateria piatta. 2194 affermazioni ingerite da quattro canali YouTube e due feed RSS, tutte con lo stesso identico numero accanto: confidence 0.30, stato staging. Nessun nodo promosso a verified. Mai. Il sistema aggregava contenuti come un raccoglitore diligente, ma non aveva la più pallida idea di cosa fosse notizia e cosa fosse rumore. Questo diario parla di quel numero fermo, e di cosa ci vuole per farlo muovere.
L’idea
L’obiettivo è banale da spiegare e brutale da realizzare: sostituire il lavoro manuale di seguire decine di creator. Un agente che monitora fonti (YouTube, Discord, RSS/Google Alerts), estrae claim atomici, li deduplica dentro una knowledge base vettoriale viva, e ogni mattina spinge su WhatsApp le 3 notizie che contano davvero — con il link alla fonte originale. Non un summarizer: un grafo che cresce, dove la stessa notizia da tre fonti diventa una voce consolidata, non tre duplicati.
Architettura: quattro nodi in fila
La spina è una pipeline LangGraph (StateGraph) che fa scorrere uno stato tipizzato attraverso cinque tappe:
extract_claims → triage_claims → kb_operations → validate_claims → END
- Extract (gpt-4o): spezza un transcript in claim atomici tipizzati —
factual,opinion,prediction,metric. Scarta saluti e filler. - Triage: per ogni claim fa embedding (
text-embedding-3-large), cerca i top-5 vicini in Qdrant, e decideNOVEL/CONFIRM/CONTRADICT. - KB operations: crea un nodo nuovo, oppure arricchisce un nodo esistente (bump di confidence), oppure segnala una contraddizione. Prima passa dal dedup: hash SHA-256 esatto + similarità semantica ≥ 0.95.
- Validate: cross-reference esterno via web search, solo per claim novel o contraddittori.
- Synthesis + Distribution: clustering per topico, un LLM italiano che scrive le voci, poi WhatsApp via Evolution API.
L’orchestrazione condiziona i salti con vere edge function — se l’estrazione non produce claim, si va dritti a END; si valida solo se c’è almeno un NOVEL o CONTRADICT. Tutto asincrono, con Celery/Redis a monte e Postgres come source of truth per il dedup.
La spina: perché niente saliva mai da 0.30
Il bug non era un crash. Era peggio: tutto verde, tutto funzionante, risultato inutile. Ho tirato fuori la calcolatrice e ho ricostruito la catena.
Un nodo nasce a KB_INITIAL_CONFIDENCE = 0.3. Sale solo quando viene confermato, e il boost decresce a ogni conferma:
def calculate_confirmation_boost(self, current_confidence, source_count):
base_boost = 0.15 # confirmation_boost_initial
boost = base_boost * (0.7 ** max(0, source_count - 1))
return min(boost, 1.0 - current_confidence)
Con la soglia di promozione a 0.7, per passare da staging a verified servono ~5 conferme indipendenti: 0.30 → 0.45 → 0.55 → 0.63 → 0.68 → 0.72. Fattibile. Tranne che le conferme erano zero. confirmations_count restava incollato a 0 su tutti i 2194 nodi.
Perché? Il triage decideva così:
if top_score >= self._confirm_threshold: # 0.85
return CONFIRM
if top_score >= self._contradict_threshold: # 0.70 → LLM alignment check
...
return NOVEL
TRIAGE_CONFIRM_THRESHOLD = 0.85 è troppo alto per text-embedding-3-large. Due frasi che dicono la stessa cosa con parole diverse — “Nvidia ha registrato 58,3 miliardi di ricavi” vs “il fatturato del trimestre di Nvidia tocca i 58B” — atterrano tipicamente a 0.6–0.8 di coseno, non 0.85+. Sopra 0.95 scatta il dedup e il claim viene buttato come duplicato esatto. Il risultato è una fascia di conferma stretta e quasi vuota: quasi tutto cadeva sotto 0.85 → classificato NOVEL → nuovo nodo a 0.30 → prateria piatta. Il grafo non cresceva in profondità, si allargava soltanto.
C’era anche un fossile: nel percorso CONFIRM il triage logga ancora confidence_bump_deferred_to_story_3_5, con tanto di commento # Placeholder: Story 3.5 will implement.... In realtà il bump vero lo fa già enrich_node a valle — quel log è una briciola morta di quando la logica era spezzata. Drift README/realtà da manuale: mi ha fatto perdere mezz’ora a cercare un bug dove non c’era.
Insegnare cosa vale una conferma
Abbassare la soglia di conferma (oggi 0.85, troppo alta) sbloccherebbe il flusso. Ma qui c’è la lezione controintuitiva del progetto: non tutte le conferme valgono uguale. Se due “fonti” che confermano la stessa notizia sono lo stesso autore, o citano lo stesso identico URL, non è convergenza — è eco. Contarle come due voti indipendenti gonfia la confidence con aria fritta.
Così il boost passa attraverso un analizzatore di diversità delle fonti, tutto euristico (niente LLM, per costo):
same_author_factor = 0.1 # stesso autore → l'eco vale un decimo
shared_origin_factor = 0.3 # citano lo stesso URL
derivative_factor = 0.2 # testi troppo simili (coseno ≥ 0.85)
# indipendenti → 1.0
effective_bump = base_delta * diversity_score
Due creator diversi che arrivano alla stessa conclusione da percorsi diversi valgono un bump pieno. Lo stesso tizio che ripete la notizia su tre canali vale un decimo. È qui che il grafo comincia a capire qualcosa: distingue tre megafoni puntati sullo stesso comunicato stampa da tre teste che convergono davvero.
Gotcha onesti
- Tre soglie sulla stessa metrica. Dedup 0.95, confirm 0.85, cluster 0.62 — tutte coseno sullo stesso spazio di embedding. Ogni volta che ne tocco una devo ricontrollare che non litighino. La fascia “utile” per una conferma è letteralmente 0.72–0.95, e capire quanto sia popolata richiede di misurare la distribuzione reale sui 2194 claim, non di indovinare.
- L’importance score è scritto, non costruito. Il piano di refocus specifica una formula pesata —
w1·confirmations + w2·source_authority + w3·recency + w4·cluster_size— con tanto di migrationALTER TABLE kb_nodes ADD COLUMN importance_score. Ma un grep insrc/emigrations/non trova la parolaimportanceda nessuna parte. La colonna non esiste ancora. Per ora il digest ordina i cluster per numerosità (clusters.sort(key=lambda c: len(c["nodes"]), reverse=True)), un proxy povero: “più fonti = più importante”. Onestamente: la vera nozione di rilevanza è ancora sulla carta. - La sintesi spreca. Il builder genera
target * 3 = 45chiamate LLM per produrre 15 voci filtrate — ~0.20–0.30 $/giorno buttati per generare candidati poi scartati. Il fix (saltare i cluster da un nodo solo con claim < 60 caratteri) è previsto ma non ancora attivo. - La distribuzione WhatsApp è configurata ma silente. Il codice del publisher c’è, Evolution API è collegata. Non ho ancora premuto il grilletto: preferisco sistemare segnale/rumore prima di spammare notifiche.
Sul self-bot Discord
Una delle fonti è un connettore Discord che legge messaggi da server privati e li inoltra al webhook interno della piattaforma. È deliberatamente read-only — nessun invio, nessuna reaction, nessun cambio di presenza — con rate limiting randomizzato (30–90 s) e backoff di riconnessione. Va detto chiaro: automatizzare un account utente Discord è una zona grigia rispetto ai ToS, e lo tratto come tale. Lo uso solo su server di cui faccio già parte, il processo è stateless e conservativo per non somigliare a un raschiatore. Token e URL del webhook stanno solo in .env, mai nel repo. Non è una feature di cui vantarsi: è un compromesso di cui essere consapevoli.
Come va
POC solido, non ancora in produzione seria. La pipeline gira end-to-end: ingestione (4 YouTube instradati via proxy, 2 RSS), estrazione, KB su Qdrant, triage, clustering, sintesi in italiano. La parte “grafo che impara” è per metà sbloccata: l’analisi eco/convergenza è in codice; abbassare la soglia di conferma, l’importance score e la distribuzione selettiva sono i prossimi pezzi. Definizione di “fatto”: ricevo 3 notizie/giorno ordinate per rilevanza, con link, e posso cercare in KB “cosa ho letto su X”. Manca ancora qualche casella.
Cosa ho imparato
Che il pezzo difficile di un knowledge graph non è ingerire, è giudicare. Estrarre claim è un problema risolto; decidere che questo claim conta più di quello, e che questa conferma è vera convergenza e non un’eco, è il 90% del valore e il 90% della fatica. Un sistema che dice “tutto è nuovo, tutto vale 0.30” è tecnicamente perfetto e praticamente muto. La confidence non è un numero che sale da solo: è un modello di cosa consideri prova. Finché non gliel’ho scritto — soglie tarate sulla distribuzione reale degli embedding, non sull’ottimismo — il grafo è rimasto una prateria piatta, verde e inutile.


