./run llm-wiki-prompt-centomila-righe
LLM Wiki: quando un prompt di una pagina diventa un'app da centomila righe
Esplorazione di nashsu/llm_wiki: come un pattern astratto di Karpathy per wiki mantenute da LLM diventa un'app desktop Tauri da 86 moduli, tra grafi, ingest a due passi e parser difensivi.
- status
- draft
- project
- LLM-wiki
- updated
- 2026-07-07
- author
- terzi · esplorazione
- tags
Ho aperto nashsu/llm_wiki aspettandomi un prompt. Ci trovo un file, llm-wiki.md, che è esattamente questo: una pagina e mezza di prosa, la descrizione astratta di un’idea — il pattern di Karpathy per costruire knowledge base personali con gli LLM. Il documento dice, testualmente, che è “intenzionalmente astratto” e che “il tuo agente costruirà i dettagli”. Poi guardo accanto: package.json versione 0.4.24, un backend Rust con Tauri, React 19, sigma.js per i grafi, LanceDB, un server MCP, un’estensione Chrome. Ottantasei moduli TypeScript nella sola src/lib/, e 101 file di test a fargli compagnia. Il solo ingest.ts sono 2.993 righe.
Questo è il gancio che mi ha tenuto qui: la distanza tra un’idea che sta in una pagina e la sua istanza reale. Il pattern promette che “l’LLM fa tutto il lavoro sporco, near-zero maintenance”. Il codice racconta un’altra storia — quella di quanto lavoro sporco serve scrivere a mano perché quella promessa non crolli. Non è mio, e vale la pena leggerlo proprio per questo.
L’idea, in una riga
Il RAG classico rifà tutto ad ogni domanda: recupera chunk, sintetizza, dimentica. Qui invece l’LLM costruisce e mantiene una wiki persistente — file markdown interlinkati che si accumulano. Aggiungi una fonte e l’LLM non la indicizza: la legge, aggiorna le pagine-entità esistenti, segna dove contraddice ciò che c’era. La conoscenza è compilata una volta e tenuta viva. Obsidian come IDE, l’LLM come programmatore, la wiki come codebase.
L’architettura sono tre strati: raw sources (immutabili, la verità), wiki (markdown generato, di proprietà dell’LLM), schema (le regole). Tre operazioni: ingest, query, lint. Fin qui il documento di Karpathy. Il resto è dove nashsu ci ha messo il lavoro.
Il pezzo che mi ha convinto: il modello di rilevanza a 4 segnali
Il pattern parla di [[wikilink]] ma non ha nessuna analisi del grafo. Qui invece c’è un vero motore di rilevanza tra pagine. La funzione calculateRelevance combina quattro segnali con pesi espliciti (graph-relevance.ts):
const WEIGHTS = {
directLink: 3.0, // pagine collegate via [[wikilink]]
sourceOverlap: 4.0, // pagine che condividono la stessa fonte grezza
commonNeighbor: 1.5, // Adamic-Adar sui vicini comuni
typeAffinity: 1.0, // bonus entity↔concept, ecc.
}
Il dettaglio che apprezzo è che il segnale più pesante non sono i link espliciti (3.0) ma il source overlap (4.0): due pagine nate dalla stessa fonte grezza sono considerate più affini di due pagine che si linkano a vicenda. È una scelta di dominio precisa — la provenienza conta più della citazione. E l’Adamic-Adar è implementato per bene, pesando i vicini comuni per l’inverso del logaritmo del loro grado:
adamicAdar += 1 / Math.log(Math.max(degree, 2))
un hub collegato a tutto vale meno di un vicino di nicchia condiviso. Sopra ci girano la community detection di Louvain (con cohesion scoring: cluster sotto 0.15 di densità interna vengono segnalati come “sparsi”) e gli insight sul grafo — connessioni sorprendenti cross-community e knowledge gap. Roba che nel pattern astratto non c’è nemmeno accennata.
La spina: “l’LLM scrive i file” è la parte difficile, non quella facile
Il pattern dice, con leggerezza disarmante, che l’LLM “tocca 10-15 pagine in un colpo solo”. Nella pratica, l’LLM sputa fuori un unico stream di testo e qualcuno deve tagliarlo in file veri. Quel qualcuno è parseFileBlocks, e il suo commento in testa è la cosa più onesta che ho letto in tutto il repo. Elenca gli hazard, numerati, ciascuno con la sua fixture di test:
- H1 — CRLF di Windows: il regex ancorato su
\nmancava ogni blocco. - H2 — stream troncato: l’ultimo
---END FILE---non arrivava mai e la pagina spariva in silenzio. Non risolvibile (è un problema di budget dello stream), ma almeno ora emette un warning. - H5 — il delimitatore
---END FILE---dentro un code fence, tipico quando l’LLM scrive una pagina-concetto sul formato di ingest della wiki stessa. Il match lazy si fermava lì e troncava tutto.
È il ciclo di vita reale dell’output LLM trattato come un protocollo di serializzazione fragile. E spiega perché quel file è enorme: metà della complessità di “far scrivere markdown a un modello” è difendersi da come il modello lo scrive davvero.
Lo stesso spirito difensivo sta nel backend Rust. Il Cargo.toml è compilato con panic = "unwind" — non abort — apposta, e c’è un panic_guard.rs che spiega il perché:
I parser di terze parti (pdf-extract/lopdf, docx-rs, calamine…) sono noti per andare in panic su input malformati invece di ritornare Err. Sotto
panic = "abort"questo uccide l’intera app.
Quindi ogni comando Tauri passa da un catch_unwind che converte il panic in un errore mostrabile a schermo. Un PDF corrotto non deve poter buttare giù la finestra. Sono le decisioni che non vedi mai in un README ma che separano un demo da un’app.
Il gotcha onesto: README contro codice
Il README descrive la pipeline di query con uno split del context budget “60/20/5/15” (wiki / history / index / system). Ma se apro context-budget.ts, i numeri veri sono altri:
const RESPONSE_RESERVE_FRAC = 0.15
const INDEX_BUDGET_FRAC = 0.05
const PAGE_BUDGET_FRAC = 0.5
Il 50% va alle pagine, il 5% all’index, il 15% è riservato vuoto per far spazio alla risposta del modello. History e system prompt non hanno un budget imposto — il commento nel codice lo dice chiaro: “non è enforced come budget unico, l’avanzo fa da headroom”. È un drift classico README/realtà, del tipo che rende un progetto credibile invece che sospetto: il codice è più preciso e più modesto della documentazione.
Altro numero da prendere con le pinze: il README dichiara che la vector search fa salire la recall “dal 58,2% al 71,4%”. Nel repo non ho trovato né il benchmark né la metodologia — è un claim, non un risultato riproducibile. Lo segnalo perché la guida chiede onestà, e questo è esattamente il tipo di cifra che va verificata prima di ripeterla.
Come sta messo
È spedito e vivo: binari per macOS (ARM+Intel), Windows e Linux via GitHub Actions, GPL-3.0, l’ultimo commit sistema il fetch di PDFium per le build Intel. C’è un’API HTTP locale su 127.0.0.1:19828, solo loopback, protetta da token con confronto a tempo costante (constant_time_eq) contro i timing attack e con un kill-switch. Sopra ci gira un server MCP bundle-ato, così un agente come Claude Code può interrogare la tua wiki locale. Cinque provider LLM (OpenAI, Anthropic, Google, Ollama, custom), ingest queue seriale con retry fino a 3 volte e cache SHA256 per saltare le fonti immutate. Non è un giocattolo.
Cosa ho imparato guardando la casa di un altro
Tre cose, concrete. La prima: un pattern astratto e la sua istanza vivono in universi di complessità diversi. Karpathy scrive una pagina che dice “l’LLM fa il bookkeeping”; nashsu scrive 86 moduli per far sì che quel bookkeeping non produca file corrotti. La distanza tra i due è tutto il progetto.
La seconda: il confine con l’LLM è un protocollo, e i protocolli vanno parsati con paranoia. parseFileBlocks con i suoi hazard numerati è più istruttivo di qualsiasi tutorial su “structured output” — perché mostra i modi reali in cui l’output si rompe: CRLF, stream troncati, delimitatori dentro i code fence.
La terza, la più utile per me: leggi il codice, non il README. Lo split 60/20/5/15 promesso diventa 50/5/15 nella realtà, più onesto e più semplice. Nei progetti che non sono tuoi, quel divario è dove impari qualcosa.
Non è mio: github.com/nashsu/llm_wiki — GPL-3.0, basato sul pattern LLM Wiki di Karpathy.