Wiki narrativo AI-maintained: architettura di conoscenza compilata per cataloghi tecnici
Pattern per costruire un chatbot tecnico interno su un manuale fornitore PDF voluminoso, senza allucinazioni sui codici articolo. Pipeline OCR doppia, wiki autoritativo a 4 categorie, single source of truth, cinque pattern di context engineering, eval set come gate. Nello stack v2.0 questo pattern descrive il livello *Knowledge editabile* (Knowledge Tools per brand + schema PIM + Rule Engine), una delle componenti del modello bi-dimensionale.
Questo studio descrive un pattern progettuale che ho costruito per un chatbot tecnico interno aziendale, costruito sopra un manuale fornitore di circa 750 pagine. L'utenza e' interna e tecnica (commerciali, addetti al magazzino, tecnici di showroom): tollera un'interfaccia testuale ma non tollera codici articolo inventati o numeri sbagliati. Il dominio e' arredamento di precisione (componenti meccanici di precisione per cassetti e ante a ribalta), ma il pattern si applica a qualunque catalogo tecnico denso con codici verificabili.
Lo chiamo wiki narrativo AI-maintained: una struttura di conoscenza compilata sopra agli estratti grezzi del manuale, autoritativa, editabile da un esperto interno, e progettata per essere consumata da un LLM senza allucinazioni.
Quando applicare il pattern (e quando no)
Si applica bene se:
- Esiste un manuale fornitore PDF voluminoso (centinaia di pagine, tabelle, formule grafiche, schemi) che fa fede sul piano tecnico.
- Il dominio ha codici articolo verificabili (nomenclatura strutturata:
ART-001,XYZ7M70E2,750.5001) di cui e' possibile costruire un dataset enumerabile. - L'utenza e' interna e tecnica, non end-user generico.
- Il volume di query e' basso/medio (decine al giorno, non migliaia al secondo): permette routing dinamico dei modelli e prompt caching senza ottimizzazioni infrastrutturali estreme.
- Il manuale fornitore cambia annualmente (nuova edizione catalogo): serve un percorso di rigenerazione ripetibile, non un import una tantum.
Non si applica bene se:
- Il catalogo e' piccolo (sotto qualche centinaia di prodotti) o senza nomenclatura verificabile: in quel caso un semplice RAG su FAQ basta, e tutto l'apparato di gate / template / single source of truth e' over-engineering.
- I prodotti sono descritti a parole, non a codici: senza un identificatore che il modello possa "verificare contro DB" non si neutralizza l'allucinazione.
- L'utenza e' end-user generico (consumer): qui serve un'esperienza UX piu' ricca (immagini guidate, configuratore, e-commerce) e l'accuratezza tecnica passa in secondo piano.
- Esiste gia' un'API strutturata del fornitore (BMEcat, schema standard): il punto di partenza e' l'API, non il PDF.
1. Pipeline OCR-ingest: dal PDF ai chunk searchable
Trasformare un manuale tecnico in qualcosa che un LLM puo' consultare richiede due estrattori complementari, perche' nessun singolo strumento copre il 100%:
- MinerU: ottimo su layout, tabelle, struttura. Esce un markdown leggibile + immagini segmentate. Perde pero' alcune formule grafiche rese come SVG/glifo (es. altezze quotate in disegni tecnici).
- pdfplumber: estrae il raw text delle stesse pagine in modo brutale: tutte le formule grafiche tornano come testo flat. E' rumoroso, ma cattura cio' che MinerU perde.
I due output vivono affiancati nella stessa cartella per famiglia, con due fonte distinti nel DB (mineru e supplementare). Il modello vede entrambi tramite search_semantic, e nei casi limite il chunk "supplementare" recupera cio' che il chunk "mineru" non aveva.
PDF master (manuale fornitore) | +-- pdftoppm 200dpi --> pagine PNG (per vision in-session su pagine critiche) | +-- MinerU CLI -------> families/<id>/extracted.md (tabelle + struttura) | +-- pdfplumber -------> families/<id>/extracted_supplementary.md (raw text) | +-- pdftoppm 150dpi --> manuale_pages/pag-NNN.jpg (citazioni inline) extracted*.md | v chunker per fonte (script Python) | v DB (SQLite o Postgres) -- tabella chunks (testo + fonte + metadata) | v sentence-transformers (mpnet 768d) | v DB embeddings BLOB wiki_curato/D* G* R* F*.md (autoritativo, vedi sezione 2) | v reindex_wiki.py | v DB chunks (fonte: distinta/guide/regola/famiglia) DB --(cosine sim 768d)--> tool search_semantic esposto al LLM
Artefatti intermedi che vale la pena conservare anche dopo build:
pagine_breadcrumb.csv(numero pagina, sezione manuale): serve per cross-ref e per popolare il frontmatterpag_start/pag_enddelle schede wiki.albero_manuale.md: tabella di contenuti ricostruita; utile per diff fra edizioni del catalogo._status.jsonper famiglia: marker che la pipeline ha completato senza errori (verde/rosso): serve per sapere cosa rilanciare in caso di interruzione.
2. Wiki autoritativo a 4 categorie
Le pagine MinerU e pdfplumber sono read-only e immutabili (rispecchiano il PDF). Sopra ad esse vive un wiki MD curato, autoritativo, editabile via UI, organizzato in 4 categorie:
| Categoria | File | Cosa contiene | Quando crearne uno |
|---|---|---|---|
Distinte (D*.md) | wiki/distinte/D001_<famiglia>.md | Composizione canonica per famiglia: lista componenti, pattern dei codici, varianti, optional, criteri di scelta | Una per famiglia "configurabile" (es. cassetto = guide + spondine + supporto + attacco) |
Guide (G*.md) | wiki/guide/G002_<scelta>.md | Decision tree narrativo per scelte ("scegli X vs Y vs Z") | Una per asse decisionale ricorrente |
Regole (R*.md) | wiki/regole/R002_<regola>.md | Regola tecnica con frontmatter YAML (formula, range, fonti) e body MD discorsivo | Una per regola/formula trasversale (peso anta, numero cerniere, dimensionamento) |
Famiglie (F*.md) | wiki/famiglie/F001_<famiglia>.md | Scheda narrativa "cos'e' X, quando sceglierla, quando NON sceglierla, codici tipici" | Una per famiglia user-facing (anche solo "anagrafica narrativa") |
A questo si affiancano gli artefatti di indicizzazione (pattern dei 3 layer di knowledge management AI-maintained):
wiki/index.md: catalogo navigabile rigenerato automaticamente. Il modello lo riceve inline nel system prompt come mappa di orientamento (cached).wiki/log.md: append-only, audit trail di chi ha modificato cosa. L'editor wiki UI scrive qui automaticamente.
Idempotenza ed editing
Ogni MD e' autocontenuto, ha un frontmatter YAML stabile (id, famiglia_id, pag_start, pag_end) e un body markdown libero. Il salvataggio dall'editor UI crea un .bak.<ts> con retention 10. Modificare un valore in un MD si propaga al DB con un singolo reindex_wiki.py --only <kind> (richiede ~2 min per categoria).
Frontmatter eseguibile (estensione v2.0 per Rule Engine)
Le R*.md (regole tecniche) possono essere lette in due modi dalla stessa fonte: come narrativa (esperto umano legge tabelle/note) e come rule object eseguibile (Rule Engine deterministico applica vincoli). L'estensione v2.0 introduce nel frontmatter una sezione condizione + azione + test_cases parsabile da un evaluator Python.
---
id: R005
nome: "Cassetto LEGRABOX: set guida 750 vs 753 (high-load)"
ambito: blum.legrabox
versione: 1.2
fonte_pagine_fis: [248]
# parte ESEGUIBILE dal Rule Engine
condizione:
all_of:
- { fatto: famiglia, op: "=", valore: legrabox }
- { fatto: portata_kg, op: ">", valore: 40 }
- { fatto: NL_mm, op: ">", valore: 500 }
azione: warn
messaggio_template: "Per portata {{portata_kg}}kg e NL {{NL_mm}}mm usa 753.*, non 750.*"
# embedded test (lint-friendly, pre-commit hook)
test_cases:
- { in: {famiglia: legrabox, portata_kg: 70, NL_mm: 600}, expect: warn }
- { in: {famiglia: legrabox, portata_kg: 40, NL_mm: 400}, expect: pass }
---
# R005 - Set guida LEGRABOX 753 (high-load)
Quando il cassetto supera 40 kg di portata e ha NL > 500 mm, il set guide
standard 750.* (40 kg) non e' adeguato. Va usato 753.* (70 kg).
## Tabella decisione
| Portata | NL | Codice |
|---------|-------------|--------|
| 40 kg | qualsiasi | 750.* |
| 70 kg | qualsiasi | 753.* |
Doppia lettura della stessa fonte:
- L'esperto umano modifica via UI body narrativo + tabella + (con minimo training) le 4 righe di
condizione. - Il Rule Engine al boot legge tutti gli R*.md, parsa il frontmatter, costruisce in memoria la lista di Rule eseguibili.
- Il validator AI (gia' in uso pre-save) verifica: syntax MD, codici inventati, sigle interne, plus
condizioneben formata etest_casesche passano.
DSL della condizione: combinatori all_of/any_of/none_of, foglie {fatto, op, valore}, operatori =/!=/>/</>=/<=/in/not_in/contains. Engine evaluator ~150 righe Python, no Drools, no CLIPS.
Stesso pattern (frontmatter eseguibile + body narrativo) si applica al CategorySchema del PIM (file C*.md in wiki_<sistema>/categorie/): definisce attributi tipizzati per categoria prodotto + body narrativo che spiega il dominio. Knowledge come fonte editabile, struttura tipizzata come derivata.
3. Single source of truth: MD → Python → system prompt
Il pattern piu' costoso e' la divergenza silenziosa: lo stesso parametro vive in 3 posti, viene aggiornato in 1, e nessuno se ne accorge per settimane.
Caso reale del progetto: i parametri tecnici delle 4 famiglie principali (altezze, range numerici ammessi, portate, pattern del codice articolo, colori validi, attacchi) erano inizialmente duplicati in:
wiki/distinte/D001-D004.md: fonte autoritativa per l'editor wiki (umano).backend/system_prompt.py: sezione "REFERENZE PARAMETRICHE", 3 tabelle hardcoded che il modello vedeva inline nel prompt cached.backend/tools.py: dict PythonPATTERNS, consumato dal tool compositoassemble_distinta.
Ogni cambio = 3 file da modificare. In pratica: chi editava l'MD non toccava il Python, e viceversa.
Soluzione
Un solo modulo (backend/distinta_parser.py) parsa i D*.md e produce sia il dict Python (load_patterns()) sia il markdown della sezione del system prompt (render_referenze_parametriche()). Le altre due "copie" diventano derivate.
wiki/distinte/D*.md <- UNICA fonte editabile
|
v
backend/distinta_parser.py <- parser MD permissivo
|
+--> load_patterns() -> consumato da tools.py
+--> render_referenze_parametriche() -> sostituito in system_prompt.py al boot
Verifica di idempotenza
Cambia un valore numerico in D001, riavvia. Sia la distinta generata dal tool sia il system prompt mostrano il nuovo valore. Definizione di fatto: 1 valore cambiato in 1 file, nessun altro file da toccare.
4. Cinque pattern di context engineering
Sono il cuore trasferibile del progetto. Cinque pattern che si sono dimostrati determinanti per abbattere l'allucinazione e contenere il costo in un dominio dove il modello "sa abbastanza" da inventare codici plausibili-ma-fasulli.
4.1 Catalogo inline nel system prompt cached, non tool da chiamare
Un primo tentativo prevedeva un tool read_wiki_index() che il modello avrebbe dovuto chiamare per orientarsi. Non funzionava: i modelli piccoli (Haiku) non lo chiamavano, quelli grandi (Sonnet) lo chiamavano raramente, e in ogni caso aggiungeva un round di tool-use (latenza piu' token).
Soluzione: il catalogo wiki (index.md, ~4500 token) e' incluso direttamente nel system prompt, dentro un blocco cache_control: ephemeral. Costo ammortizzato dopo il primo turno (sconto 90% sui token cached), zero round di tool-use, e il modello non puo' "dimenticarsi" di consultarlo.
Generalizzabile a: tassonomie di prodotto, glossari, decision tree, qualunque "mappa di orientamento" sotto i 5-10k token che il modello deve avere SEMPRE sotto gli occhi. Sopra quella soglia, il prompt diventa difficile da debuggare e va ripensato.
4.2 Push automatico della distinta-template (non pull)
Per le query di tipo "configurazione cassetto" il modello deve avere sotto gli occhi la composizione canonica della famiglia (5-6 gruppi di componenti, pattern dei codici, varianti). Un primo design lasciava al modello la chiamata read_distinta(famiglia). Non funzionava affidabilmente: Haiku saltava il tool, partiva a memoria, sparava codici plausibili ma inventati.
Soluzione: classifier deterministico Python (regex su keyword famiglia + keyword "distinta/configurazione") che riconosce l'intento e inietta la distinta-template direttamente nel messaggio user, prima di chiamare il modello. Marker [ctx:Dxxx] per idempotenza (non si inietta due volte nella stessa sessione).
# semplificato
fam = detect_distinta_intent(user_msg) # regex intent classifier
if fam:
distinta_md = read_distinta(fam)["contenuto"]
user_msg = f"[ctx:{d_id}]\n{distinta_md}\n\n[Richiesta utente]\n{user_msg}"
Generalizzabile a: ogni volta che esiste un "documento di riferimento corto" (sotto 5k token) la cui presenza e' necessaria per rispondere correttamente a una classe di query. Push deterministico e' piu' affidabile che sperare nel tool-use del modello.
4.3 Gate logic con marker (controllo del flusso server-side)
Per casi in cui mancano dati chiave (es. "distinta cassetto" senza profondita' e senza portata) il modello tende ad assumere default e produrre comunque una risposta verosimile. Il problema: l'utente non si accorge che il valore e' inventato.
Soluzione: lo stesso classifier rileva i dati mancanti (_detect_NL_mm, _detect_portata su tutto lo storico user, non solo l'ultimo turno) e inietta una direttiva interna:
[gate:portata] (direttiva interna - non mostrare all'utente)
REGOLA RIGIDA: l'utente ha chiesto una distinta ma manca la portata.
- NON chiamare assemble_distinta.
- NON inventare default.
- Chiedi SOLO la portata con [OPZIONI: 40 kg | 70 kg].
[Richiesta utente]
distinta cassetto 500
Quando invece i dati ci sono tutti, viene iniettata una direttiva opposta ([gate:done]): "produci subito la distinta, non chiedere accessori".
Generalizzabile a: qualsiasi flusso conversazionale dove esistono "dati chiave senza i quali la risposta e' inventabile". La logica deterministica vive in Python (testabile, debuggabile), non nel prompt (opaco, non testabile, costoso).
4.4 tool_choice forzato API-side per task ad alto rischio
Anche con tutta la prompt engineering del mondo, istruire il modello "DEVI chiamare il tool X" in linguaggio naturale e' inaffidabile, soprattutto su modelli piu' piccoli. Antipattern.
Soluzione: usare il parametro nativo dell'API:
if rounds == 1 and force_tool_name:
create_kwargs["tool_choice"] = {"type": "tool", "name": force_tool_name}
Quando il gate e' in stato done (tutti i dati chiave presenti), forziamo assemble_distinta al primo round. Il modello deve chiamarlo (lato API), poi nei round successivi tool_choice non e' piu' impostato e puo' rispondere liberamente.
Generalizzabile a: ogni "punto di non ritorno" del flusso dove la chiamata di un tool specifico e' obbligatoria per mantenere la verifica dei dati. Non delegare al modello scelte che l'API permette di forzare.
4.5 Routing modello per rischio allucinazione, non per costo
Il primo istinto e' "modello piccolo per tutto, modello grande solo se serve". Sbagliato: il rischio di allucinazione e' inversamente proporzionale alla taglia del modello. Su query complesse (decision tree multi-step, calcoli combinati, distinte) il modello piccolo non e' "lento", e' piu' allucinatorio.
Routing implementato:
- Modello piccolo (Haiku): lookup secchi (codice, sigla, glossario), pattern regex
^cos['e']|cosa è|...o presenza di codice articolo. Cache ben sfruttata. - Modello grande (Sonnet): distinte, configurazioni, "scegli tra X e Y", calcoli, "differenza", "compatibile". Forzato anche dal gate
doneo dall'iniezione automatica di una distinta-template. - Default: modello piccolo (la maggioranza delle query e' lookup banale).
L'eval set ha permesso di calibrare le regex: 4 bug reali sono stati catturati passando da 5 a 15 casi di test (gate non riconosceva alcuni valori numerici per regex troppo stretta; parsing famiglia non catturava nomi senza suffisso).
Generalizzabile a: ogni volta che si offrono modelli di diverse taglie sullo stesso flusso. Il driver del routing e' la natura della query, non solo il costo. E il routing va testato come si testa il codice.
5. Eval set come rete di sicurezza
Il salto qualitativo tra "demo che funziona" e "sistema su cui basare un refactor" e' avere un eval set deterministico.
Sviluppatore --> evals/run_evals.py
|
v
loop per ogni caso (~30 casi)
|
+--> agent.chat() --> Anthropic API
| |
|<---------------------+
|
v
assertions:
- must_call_tools
- must_not_call_tools
- must_inject_marker
- response_must_contain (codici reali)
- response_must_not_contain (codici allucinati)
- response_must_not_contain_regex (pattern allucinazioni)
- model_used
|
v
report 30/30 PASS o N/30 FAIL con diff
PRIMA del refactor: snapshot baseline
DURANTE il refactor: ogni 15 min
DOPO il refactor: gate per il deploy
Tipologie di assertion che hanno valore reale
| Tipo | Esempio | Cosa cattura |
|---|---|---|
must_call_tools | ["assemble_distinta", "get_media"] | Il modello segue il flusso atteso |
must_not_call_tools | ["assemble_distinta"] quando manca un dato | Il gate sta bloccando |
must_inject_marker | [gate:done], [ctx:D008] | I classifier deterministici Python funzionano |
response_must_contain | Codici articolo specifici | Il modello cita codici reali (verificati in DB) |
response_must_not_contain | Codici plausibili ma fasulli | Regression test per allucinazioni storiche |
response_must_not_contain_regex | Pattern di codici mai esistiti | Cattura allucinazioni "famiglia" |
model_used | "claude-haiku-4-5-..." | Il router sta scegliendo il modello giusto |
Costo e cadenza realistica
~30 casi end-to-end completi (API reale, no mock) -> ~5 min wall-clock, ~$0.30. Si lancia prima di ogni refactor non-banale, prima di ogni deploy, e nuovi bug "in produzione" diventano nuovi casi (regression testing organico). Il costo e' trascurabile rispetto al costo di un'allucinazione in produzione su un dato tecnico.
6. Anti-pattern incontrati e abbandonati
Da onesti, molte delle prime soluzioni non hanno funzionato. Documentarle aiuta a non ripeterle.
6.1 Prompt testuale "DEVI chiamare il tool X"
Tentativo: scrivere nel system prompt "se l'utente chiede una distinta, DEVI chiamare assemble_distinta prima di rispondere".
Esito: il modello piccolo ignorava la direttiva nel ~30% dei casi e partiva a memoria. Il modello grande la rispettava di piu', ma comunque non al 100%.
Sostituito da push deterministico server-side della distinta-template (sezione 4.2) e tool_choice API-side per forzare il tool al primo round (sezione 4.4).
Lezione: il prompt e' un suggerimento, non un contratto. Per garanzie hard servono meccanismi hard (codice piu' parametri API).
6.2 Tool che il modello non chiamava mai
Tentativo: esporre un tool read_wiki_index() per dare al modello una mappa del wiki da consultare a discrezione.
Esito: il modello piccolo non lo chiamava praticamente mai, quello grande lo chiamava sporadicamente. In entrambi i casi un round di tool-use sprecato.
Sostituito da catalogo wiki inline nel system prompt cached (sezione 4.1). Il modello non sceglie, ce l'ha gia' sotto gli occhi.
Lezione: i tool che il modello "potrebbe" chiamare sono inutili. Tool = azioni che hanno effetti collaterali (DB, calcoli, lookup specifici). Per documenti di riferimento, push.
6.3 Hardcoding parametri in 3 posti
Vedi sezione 3. Il pattern del MD-fonte + Python-derivato + prompt-derivato e' trasferibile a tutto: ogni volta che lo stesso valore appare in piu' file, e' solo questione di tempo prima che diverga in silenzio.
6.4 Iniettare la distinta-template SOLO sull'ultimo messaggio user
Bug catturato dall'eval set: la query "distinta famiglia X" -> "40 kg" (secondo turno). Il classifier deterministico cercava la famiglia solo nel turno corrente ("40 kg" non contiene il nome famiglia) -> nessuna iniezione -> modello a memoria.
Soluzione: il classifier ha fallback all'intera history user concatenata. Il chain "ricorda il contesto della distinta" nei turni successivi. E' esattamente il tipo di bug che senza eval-set passa inosservato.
7. Stack runtime tipico
Non e' lo scopo di questo studio prescrivere lo stack: il pattern e' indipendente dall'infrastruttura. Per riferimento, lo stack su cui ho costruito il sistema:
Browser (LAN-only, in produzione interna)
v
FastAPI (uvicorn, NSSM service Windows)
- /chat -> API LLM (rate limited)
- /editor -> editor wiki UI (whitelist email)
- /feedback -> wizard segnalazione + AI auto-fix
- /admin/* -> backup, logs, restart, reindex
Backend Python:
- sentence-transformers paraphrase-multilingual-mpnet-base-v2 (768d, 1.1 GB cached)
- Anthropic SDK con prompt caching ephemeral
- SQLite (~20 MB: chunks + embedding BLOB + chat_session + feedback)
- Rate limiter sliding window in-process (no Redis, no deps esterne)
Scelte di stack non ovvie
- SQLite + BLOB embedding 768d anziche' vector DB dedicato: a 4-5k chunks la cosine similarity in-process con NumPy va piu' che bene (~50 ms su CPU, no setup). Sotto i 100k chunks e' la scelta piu' semplice e debugabile. Per scale piu' grandi: pgvector su Postgres, sempre preferito a soluzioni esterne.
- Routing modello regex-based, NON tramite un classifier LLM (vedi sezione 4.5).
- Prompt caching ephemeral sui blocchi
systemetools: sconto ~90% sui token "fissi" (~7-8k/turno), ammortizza il costo di tenere catalogo, distinta-template e tabelle parametriche inline nel prompt anziche' come tool da chiamare.
8. Ciclo di vita annuale
Un sistema di questo tipo regge nel tempo solo se la rigenerazione del dataset e' ripetibile. Quando il manuale fornitore cambia (annual update), serve un percorso documentato.
[Nuovo PDF fornitore]
|
v
[Branch git vYYYY-YYYY]
|
v
[Backup pre-ingest: repo + DB]
|
v
[Rigenera estratti: MinerU + pdfplumber]
|
v
[Snapshot codici pre/post + diff]
|
v
[Codici rimossi menzionati nel wiki?] --SI--> [Sostituire nei MD con nuovo equivalente]
| |
NO |
|<---------------------------------------------+
v
[Revisiona MD R*/D*/F*/G*]
|
v
[Vision in-session per regole sensibili]
|
v
[Rebuild DB + embedding]
|
v
[Reindex wiki]
|
v
[Eval set 30/30 PASS?] --NO--> [Triage diff: fix MD o codice]
| |
SI |
|<-----------------------------+
v
[Smoke test: 5 query benchmark]
|
v
[Deploy + smoke prod]
|
v
[Tag v.YYYY.0.0]
I punti critici che fanno la differenza tra "ingestione che fila" e "ingestione che genera bug silenti":
- Snapshot codici PRIMA dello step OCR. La rigenerazione
families/*/extracted.mdsovrascrive: perdere lo snapshot pre = niente diff possibile. - Triage del diff codici: i codici "rimossi" vanno cercati nei
D*.mde neiG*.mde sostituiti, non lasciati orfani. Senza questo passaggio, il wiki cita codici fantasma fino al primo report utente. - Vision in-session sulle regole sensibili: se il body di una regola e' cambiato, e' piu' affidabile prendere uno screenshot ad-hoc della pagina nuova e farlo vedere al modello in conversazione, piuttosto che fidarsi del puro OCR per testi di policy.
- Eval set come gate di deploy: 30/30 PASS prima del deploy. Un singolo fail su un test "core" (lookup, distinta, gate) e' motivo sufficiente per rinviare.
- Doc update post-ingest: log nel wiki/log.md (audit trail), entry in handoff doc, eventuale aggiornamento conteggi nel doc di context. Non e' ceremonia: e' la mappa per il prossimo che ci mettera' mano.
Stima realistica per un'edizione del catalogo intera: 1-2 giornate effettive (3-4 h compute + 6-12 h focused-time umano).
9. Lessons learned
Cinque punti che vale la pena interiorizzare prima di partire con un progetto analogo.
9.1 Pezze e organicita'
Le pezze risolvono il problema oggi. L'organicita' lo previene domani. Servono entrambe, ma in tempi diversi: la pezza va in _archive/scripts_one_shot/ con un commento esplicito, l'organicita' entra a pacchetti, e ha senso solo se hai un eval set che ti dice se il pacchetto ha rotto qualcosa. Senza eval set, l'organicita' e' un atto di fede.
9.2 Eval before refactor
Il primo deliverable di valore non e' la feature: e' l'infrastruttura di test. Senza, ogni cambio e' una scommessa, e il debito di "non posso refactorare perche' non so cosa rompo" cresce esponenzialmente. Costruire 20-30 casi end-to-end deterministici e' un investimento di mezza giornata che si ripaga la prima volta che eviti una regressione in produzione.
9.3 Context engineering ≠ prompt engineering
Il prompt e' la punta dell'iceberg. Sotto ci sono: cosa e' inline e cosa e' un tool, quale modello vede quale query, quale contesto viene iniettato in che momento, quali marker tracciano lo stato della conversazione, dove vivono le regole deterministiche (Python) e quelle discorsive (system prompt). Context engineering e' il design del sistema attorno al modello; il prompt ne e' solo un componente. Perdere settimane a tweakare le parole del system prompt quando il problema e' architetturale (es. tool sbagliato, modello sbagliato, dato non iniettato) e' il modo piu' rapido di non andare da nessuna parte.
9.4 Modello sbagliato e contesto sbagliato sono concomitanti
Quando una risposta e' cattiva, raramente la causa e' univoca. Tipico stack-up: il router ha mandato la query al modello piccolo invece che a quello grande, e il classifier non ha iniettato la distinta-template, e il prompt non ha l'info parametrica giusta nelle referenze. Ognuno dei tre fix isolati avrebbe contribuito a peggiorare di poco; tutti e tre insieme spiegano il -90% di qualita'. Bisogna essere disposti a investigare a tutti e tre i livelli prima di concludere "il modello non capisce".
9.5 Single source of truth o si paga in debt
Ogni valore che vive in 2+ posti diverge prima o poi. Non e' un'eventualita' remota: e' una garanzia statistica. La domanda non e' "se" ma "quando" e "quanto male me ne accorgo". L'investimento nel pattern MD-fonte + Python-derivato e' di 2-3 ore per le famiglie principali; il costo del debt non corretto e' infinito perche' si manifesta come piccoli errori sparsi che nessuno collega all'origine comune. Pagare l'organicita' una volta sola, all'inizio.
9.6 Knowledge Karpathy non basta da solo (lezione v2.0)
Il pattern descritto in questo studio funziona benissimo per il caso che lo ha generato (chatbot tecnico interno single-brand su catalogo PDF voluminoso). Quando arrivano scenari multi-brand con filtri strutturati cross-brand (es. "lavastoviglie 60 classe A" su 3 brand di elettrodomestici), un wiki narrativo per brand non basta piu':
- "60 cm" finisce in vector search e matcha "60 watt" su un altro prodotto.
- "classe A" non e' normalizzato (l'utente puo' scrivere "A", "A+", "energy A").
- I vincoli cross-modulo (lavastoviglie 60 + cassetti retrostanti -> nicchia 560mm) non emergono perche' nessun knowledge brand "vede" la cucina intera.
Il pattern Karpathy resta vincente per la rappresentazione del knowledge editabile da umani (wiki narrativi per brand, regole come R*.md con frontmatter eseguibile, schema attributi categoria come C*.md). Sopra serve l'orchestrazione architetturale: Typed Query Layer (slot filling pre-LLM), Supervisor Filter-then-Validate (PIM filtra cross-brand + Knowledge Tools brand validano), Configuration Context (stato di sessione persistito tipizzato), Rule Engine deterministico (che legge regole dai R*.md Karpathy ma le applica con evaluator Python).
Lo studio principale Agentizzare una PMI: stack, architettura, strumenti v2.0 descrive questa cornice. Il pattern wiki narrativo qui descritto e' la fondazione di rappresentazione del knowledge, non l'architettura completa di un sistema multi-brand consulenziale.
Detto in altro modo: BlumCat in produzione e' un Knowledge Tool brand mascherato da chatbot. Funziona perche' il dominio e' single-brand. Il pattern v2.0 lo riformula come uno dei N Knowledge Tools dello stack, con ruoli e contratti chiariti.
Checklist di partenza
Se stai per partire con un chatbot tecnico interno su catalogo PDF denso (altro fornitore, altro settore), questo e' il minimo praticabile per non rifare gli stessi errori.
Per la cornice architetturale completa dentro cui questo pattern vive (stack v2.0 a 5 livelli con modello bi-dimensionale knowledge verticale + cross-cutting orizzontale), vedi Agentizzare una PMI: stack, architettura, strumenti v2.0. Per applicazioni concrete dei flussi, vedi Agentizzare un'azienda: timeline e flussi reali v1.2.
Registro aggiornamenti
- v1.1
Allineamento allo stack v2.0. Aggiunto Callout in apertura che chiarisce il ruolo nello stack v2.0: questo studio descrive il livello Knowledge editabile (Knowledge Tools per brand + schema PIM in
C*.md+ Rule Engine che legge daR*.md), una delle componenti del modello bi-dimensionale, NON l'architettura completa. Aggiunta sotto-sezione "Frontmatter eseguibile (estensione v2.0)" alla sezione 2 (Wiki autoritativo): il frontmatter delleR*.mdpuo' essere esteso concondizione+azione+test_casesper essere letto come rule object da un Rule Engine deterministico Python (~150 righe). Doppia lettura della stessa fonte: narrativa per l'esperto umano + struttura tipizzata per la macchina. Stesso pattern applicato alCategorySchemadel PIM lite (fileC*.md). Aggiunta lessons learned 9.6 "Knowledge Karpathy non basta da solo": chiarisce dove il pattern smette di essere sufficiente (scenari multi-brand cross-categoria) e quale orchestrazione serve sopra (vedi studio principale v2.0). - v1.0
Prima stesura. Pattern completo del wiki narrativo AI-maintained: pipeline OCR dual-extractor, wiki autoritativo a 4 categorie (Distinte/Guide/Regole/Famiglie), single source of truth MD -> Python -> system prompt, cinque pattern di context engineering, eval set come gate di refactor, anti-pattern abbandonati, ciclo di vita annuale, lessons learned. Lo studio nasce dall'esperienza diretta di costruzione di un chatbot tecnico interno su un manuale fornitore di circa 750 pagine.