Sistema RAG a tre livelli di conoscenza
Un motore di risposta ancorato ai documenti, alle sintesi curate e ai dati aziendali
- Periodo
- dal 2025
- Ruolo
- progettazione, sviluppo e test
- Tecnologie
- Python · Django REST Framework · PostgreSQL · pgvector · OpenAI · Anthropic · Claude Agent SDK
- Stato
- in produzione
Nel contesto di un sistema LLM aziendale che risponde in modo competente ed esperto alle domande di collaboratori e professionisti, la documentazione su cui il sistema lavora è tanta e cambia: analisi di mercato, normativa dei bandi, relazioni, dati dei progetti in corso. La risposta a una domanda precisa sta quasi sempre dentro un documento che bisogna però prima individuare, e trovarlo è un lavoro che ricomincia da capo a ogni domanda.
Questo servizio risponde a domande in linguaggio naturale su quella conoscenza. Recupera i pezzi di documentazione pertinenti e li passa a un modello linguistico, che scrive la risposta su quei pezzi e non sulla propria memoria: è il metodo che va sotto il nome di RAG (retrieval-augmented generation). La conseguenza pratica è che la conoscenza resta fuori dal modello — si aggiorna cambiando i documenti, e ogni risposta si può verificare risalendo alle fonti da cui proviene.
Un modello linguistico da solo non potrebbe farlo: non conosce i documenti dell'azienda, è fermo alla data in cui è stato addestrato, e di ciò che afferma non permette di risalire alla fonte. Spostare la conoscenza fuori dal modello la rende aggiornabile senza riaddestrare niente, e verificabile da chi legge la risposta.
Il lavoro presente l'ho costruito per l'uso interno dell'azienda in cui lavoro, pensando ad un sistema flessibile, scalabile e facilmente manutenibile.
Le fonti — i documenti grezzi e il wiki curato — confluiscono in un solo indice. Una domanda viene instradata, verso il testo o verso i numeri, recuperata da quell'indice oppure interrogata sullo store analitico, e trasformata in risposta dal modello. Le modalità sono due: sincrona, immediata, e analisi approfondita, affidata a un agente per i casi complessi.
il problema difficile
Recuperare del testo e farne una risposta è la parte facile. La parte difficile è che chi legge non ha modo di accorgersi quando la risposta è sbagliata. E i modelli LLM per loro natura tendono a rispondere comunque in modo assertivo e non dubitativo, anche quando i dati di riferimenti sono incerti.
Due casi reali che ho incontrato erano tutti e due invisibili a chi faceva la domanda. Nel primo il servizio rispondeva «non ho trovato nulla» mentre la conoscenza c'era, e sembrava un'assenza di dati invece che un errore di ricerca. Nel secondo riportava con piena sicurezza un dato che la fonte stessa dichiarava inaffidabile.
Quasi tutto quello che segue serve a una cosa sola: fare in modo che il sistema sbagli in modo visibile. È anche il criterio con cui ho scelto fra le alternative, quando la misura non bastava a decidere.
tre livelli di conoscenza
Domande diverse chiedono conoscenza diversa, e tenerla tutta in un posto solo limita la qualità della risposta. Per tale ragione il motore, che risponde in linguaggio naturale ancorando ogni risposta alla conoscenza aziendale, combina tre livelli:
- il testo grezzo dei documenti segmentato in chunk attraverso il processo di embedding
- il wiki curato
- i dati gestionali ricavati da query SQL su un clone dei DB dei gestionali aziendali
Il primo livello è il RAG, ovvero il testo dei documenti così com'è, spezzato in frammenti e trasformato in vettori numerici che ne rappresentano il significato, così che la ricerca avvenga per somiglianza di senso invece che per parole esatte. È l'ancoraggio ai fatti come sono scritti nella fonte.
Il secondo è un wiki in markdown curato a mano, dove pagine di sintesi organizzano un dominio, per esempio una pagina per ogni mercato. Le pagine rimandano ai documenti anziché ricopiarli, e dichiarano nel frontmatter il canale che ereditano e il documento a cui si riferiscono. Vengono indicizzate negli stessi vettori del primo livello: sintesi e testo grezzo competono insieme per pertinenza, e il ranking non sa quale delle due sta guardando.
Il terzo è uno store analitico, per le domande numeriche basato su query SQL: quanti progetti attivi, quanto è stato speso su un mercato, quanto resta di un budget. È una copia di sola lettura del database dei nostri sistemi gestionali, rigenerate ogni notte, e si interrogano soltanto con query già scritte e provate. Il modello riconosce la domanda numerica, sceglie la query da un catalogo e ne estrae i parametri; non scrive SQL. Se nessuna query del catalogo risponde, il servizio lo dichiara invece di produrre un numero.
il pavimento del verbatim
I tre livelli concorrono alla stessa risposta, e questo crea un problema di equilibrio. Le sintesi curate sono dense e mirate, quindi tendono a vincere il confronto di pertinenza e finirebbero per far sparire il testo grezzo dai risultati.
La leva ovvia sarebbe pesare i due tipi di frammento in modo diverso, premiando il curato. L'ho provata e misurata, e il risultato è stato negativo: premiare le sintesi promuove anche quelle fuori tema, e il guadagno netto sta sotto zero. La leva che funziona è un pavimento — un minimo di frammenti grezzi garantiti in ogni risultato — tenuto separato dal peso, così che ciascuna regoli una cosa sola. In produzione il peso è a zero e il pavimento a un frammento. Era a due, ed è stato abbassato dopo che una revisione del modo di spezzare i documenti ha reso i frammenti grezzi più densi: il secondo, forzato, non aggiungeva niente e a volte toglieva spazio a qualcosa di migliore.
Un esempio. Alla domanda sul posizionamento del vino italiano nel mercato giapponese il servizio recupera cinque frammenti: tre dalla pagina curata di quel mercato, che risultano i più pertinenti, e due dai documenti originali, garantiti dal pavimento. La risposta prende l'impianto dai primi e l'ancoraggio dai secondi.
il percorso di una domanda
Una domanda non va dritta all'indice. Prima si decide di che natura è, poi dove cercare, poi si restringe il campo; la ricerca per somiglianza è l'ultimo passo, non il primo.
Il primo bivio è più grosso di quanto sembri: distingue le domande di testo da quelle di numeri. Lo decide il modello stesso, con una sola chiamata, scegliendo fra le query del catalogo e una funzione di ripiego che dichiara «questa non è una domanda da store». Nel caso comune non costa niente in più, perché quella chiamata la si faceva comunque. Se la domanda è numerica ma nessuna query del catalogo la copre, il modello sceglie il ripiego e si finisce sul percorso testuale, dove il servizio dichiara di non avere il dato invece di inventarlo.
scegliere dove cercare
L'indice è partizionato in canali tematici, e prima di cercare bisogna decidere in quale guardare. La qualità del recupero è vincolata da questa scelta: sbagliare canale significa cercare bene nel posto sbagliato.
All'inizio lo decideva un elenco di parole chiave, fragile per costruzione. Bastava un plurale — «mercati» non contiene «mercato» — o una domanda formulata in modo naturale per non attivare nessun canale, e la risposta diventava «non ho trovato nulla» mentre la conoscenza c'era. In più il primo termine che agganciava vinceva, anche quando puntava al canale sbagliato.
Ora la scelta si fa per somiglianza di significato. Ogni canale è descritto da poche domande-esempio curate; la domanda reale viene confrontata con quelle, con lo stesso meccanismo della ricerca, e vince il canale più vicino se supera una soglia di confidenza. Sotto la soglia il servizio cerca su tutti i canali e lascia decidere la pertinenza.
Tre cose di questa scelta valgono più del meccanismo:
- Quando è incerto allarga invece di svuotare. È la proprietà che mi interessava, e discende dal problema difficile: una ricerca troppo larga produce una risposta mediocre che si vede, un canale sbagliato produce un «non ho trovato nulla» che non si vede.
- Domande-esempio, non centroidi dei frammenti. Gli esempi vivono nella stessa distribuzione delle domande vere: corti, interrogativi. La media dei frammenti di un canale confonderebbe canali che si sovrappongono per tema, e andrebbe ricalcolata a ogni caricamento. Gli esempi si curano dalla configurazione, quindi aggiungere un canale non richiede codice.
- La soglia è tarata su un insieme di prova, non scelta a occhio. Le domande legittime stavano sopra 0,54 e quelle fuori dominio sotto 0,40: la soglia sta in mezzo, a 0,45. Se il fornitore degli embedding non risponde, le vecchie parole chiave restano come ripiego, e concorrono solo i canali che hanno documenti dentro.
Costa una chiamata di embedding in più per domanda, e nessuna chiamata al modello: gli esempi sono vettorializzati una volta e tenuti in memoria, con l'impronta dei testi a decidere quando vanno rifatti.
restringere prima di cercare
Accanto al canale ci sono altri filtri. Tutti insieme formano una sola condizione applicata prima del calcolo di somiglianza: nessuno lavora a valle, e nessun meccanismo a valle può ripescare ciò che è stato escluso a monte.
La domanda che si ripete, ogni volta che qualcuno legge il percorso, è se il passo probabilistico filtri i documenti. Non lo fa: sceglie un parametro — il canale — e solo quando chi chiama non ne ha dichiarato uno valido. Il passo incerto può quindi sbagliare dove si cerca, mai se un filtro viene applicato. Tenere quella riga netta è ciò che rende il sistema spiegabile a chi lo integra.
I filtri sono cinque e, dopo una revisione che ne ha rimossi tre che non filtravano nulla, sono tutti attributi del documento: nessuno agisce sul singolo frammento. Non è una potatura, è un'invariante — un filtro sul frammento oggi richiederebbe una giustificazione esplicita. Quello che conta però non è l'elenco, è come ciascuno si comporta quando manca:
| Filtro | Se manca o non si applica |
|---|---|
| Il committente | non allarga mai: vale il valore di default, e una risposta vive dentro un committente solo |
| Il canale tematico | allarga: la ricerca diventa multi-canale, mai un risultato vuoto |
| L'anno | aperto sull'assenza: un documento senza anno non viene escluso da una domanda per anno |
| Il mercato | chiuso: confronto esatto, senza distinzione di maiuscole |
| Le etichette dichiarate come confine di riservatezza | stringono: una domanda che non le dichiara non vede quei documenti |
Il degrado allarga sul tema e stringe sulla riservatezza. È la stessa regola scritta due volte: dove sbagliare costa poco si preferisce cercare troppo, dove costa molto si preferisce non trovare. Sulle etichette il fallimento è chiuso da entrambi i lati — un'etichetta dimenticata su un documento lo fa sparire dalle risposte, non lo fa trapelare — con un limite che va detto: il servizio non può verificare chi sta chiedendo, conosce il sistema chiamante e non la persona. Ruoli e permessi restano nell'applicazione che chiama.
Solo le etichette hanno un vocabolario governato, condiviso fra chi indicizza e chi interroga. Le altre no, e la conseguenza è che un valore sbagliato da chi chiama non è un errore: è zero risultati in silenzio. Si cura verificando i dati con un comando dedicato, non cambiando meccanismo.
Una nota sul caso che ho sbagliato, perché è il più istruttivo di tutti. Le etichette non erano nell'elenco dei parametri ammessi in ingresso, quindi venivano scartate prima di raggiungere il motore di ricerca: il filtro compariva nella richiesta e non veniva applicato. È sopravvissuto per mesi perché ogni test partiva dal motore in giù, cioè a valle del tratto rotto. La copertura adesso parte da dove chi chiama esprime la sua intenzione, e l'elenco dei parametri ammessi è derivato per introspezione dalla firma della ricerca, così esiste letteralmente in un posto solo: prima era scritto a mano in due, e due elenchi a mano divergono.
i numeri non si recuperano, si calcolano
La ricerca per somiglianza non sa contare né sommare, e su un numero una risposta quasi giusta è una risposta sbagliata. Il terzo livello è quindi costruito con criteri opposti a quelli degli altri due: niente ricerca semantica, niente generazione libera.
- Un archivio separato, di sola lettura. Un cluster PostgreSQL dedicato, rigenerato ogni notte dai backup dei gestionali, con le credenziali ripulite e la data del dato registrata a parte. Il servizio non legge mai il database di produzione a runtime: interroga una copia.
- Il modello non scrive SQL. Le query stanno in un catalogo dichiarativo versionato: descrizione, parametri tipizzati, e la query scritta a mano su una vista. Il modello sceglie quale query usare e ne estrae i parametri; la validazione dei tipi la fa il meccanismo delle chiamate a funzione del provider, non del codice mio.
- Le viste incapsulano la semantica di business. Cosa significa «progetto attivo» o «speso impegnato» è deciso una volta, in SQL, e non nel prompt: se cambia la definizione cambia la vista, e tutte le risposte si adeguano insieme.
- Il confinamento è del database, non dell'applicazione. Il ruolo usato per queste query è in sola lettura a livello di transazione, ha un tetto di tempo per statement, e vede soltanto lo schema delle viste — mai le tabelle sottostanti.
- Ogni risposta numerica cita la sua provenienza: quale query, con quali parametri, e a quale data i dati si riferiscono. Un numero senza la sua data, in un archivio rigenerato ogni notte, è un numero che non si può controllare.
C'è anche un ponte fra il terzo livello e il secondo: una pagina di metriche viene rigenerata ogni notte dallo store e messa nel wiki, ma esclusa dall'indice. Serve a chi cura le pagine per avere i numeri sotto mano; nell'indice non entra, perché dati strutturati dentro la ricerca semantica sono esattamente ciò che questo livello esiste per evitare.
il dubbio che arriva fino alla risposta
Questa è la parte del sistema di cui vado più fiero, e nasce da un difetto trovato in produzione.
Chi cura una pagina del wiki, quando trova un dato che non torna — un numero gonfiato da un'anomalia, due elenchi normativi che si contraddicono — non lo cancella e non lo aggiusta di nascosto: lo dichiara con un'avvertenza. È il valore che il secondo livello aggiunge e che il primo non può restituire, perché nasce dal confronto fra fonti, che è lavoro umano e non sta scritto in nessun documento.
Il servizio però recupera frammenti, non pagine intere. Le avvertenze stavano in fondo alla pagina, in una sezione dedicata, e l'affermazione dubbia altrove: veniva pescato il frammento con l'affermazione e mai quello con l'avvertenza. Peggio, la sezione delle avvertenze è scritta con vocabolario metodologico — tabella, serie, valore anomalo — che non somiglia a nessuna domanda che un utente farebbe davvero, quindi non veniva pescata nemmeno per caso.
Su una pagina la crescita di un mercato era data al 53% annuo, e più sotto era scritto che quel numero è gonfiato da un'anomalia e che il valore realistico è intorno al 3,5%. Alla domanda su quanto cresce quel mercato, il servizio rispondeva 53%.
La correzione sfrutta una separazione che era già nel codice e che nessuno aveva avuto motivo di usare: il testo con cui un frammento viene cercato e il testo che il modello legge sono due argomenti diversi della stessa funzione. Le avvertenze di una pagina viaggiano ora con ogni suo frammento, ma solo nel secondo. Il vettore resta invariato, quindi la ricerca non peggiora di un punto; e il frammento che ospita l'avvertenza non se la ritrova duplicata.
Sopra c'è un secondo strato, che chiamo pavimento di onestà: quando ha fatto una ricerca, il servizio appende alle istruzioni del chiamante alcune righe che il chiamante non può rimuovere, e che coprono tre stati distinti — riporta le avvertenze senza attenuarle; se non hai trovato niente dichiaralo; se le evidenze sono state scartate per limiti di spazio, dichiara anche quello. Si applica solo dove il recupero è stato eseguito: sulle chiamate che non passano dal corpus le istruzioni restano esattamente quelle di chi chiama.
Scoprire dove metterlo è stato metà del lavoro. Il servizio non aveva un prompt di sistema proprio — passava al modello quello del chiamante senza toccarlo — e il punto giusto è risultato essere dopo la compressione del contesto, perché solo lì si sa se le evidenze sono sopravvissute. Nella prova in cui le istruzioni di chi interrogava chiedevano uno stile «sintetico e assertivo», la risposta ha riportato lo stesso il dubbio e scartato il 53%.
Conta perché porta il non-sapere dentro la risposta: il sistema restituisce il problema invece di una soluzione errata. Ed è la proprietà che rende praticabile curare un dominio che non si padroneggia, perché un dubbio irrisolto non svanisce, arriva a chi legge.
Il costo è misurato e l'ho accettato consapevolmente. Il meccanismo è ottuso: attacca tutte le avvertenze di una pagina a tutti i suoi frammenti, anche a quelli che non c'entrano. I frammenti passano da circa 880 a 2.400 caratteri, con un caso peggiore di sei volte, e il costo cresce insieme alla curatela — le pagine con avvertenze erano quattordici quando ho fatto la modifica, diciassette una settimana dopo. Alta copertura, bassa precisione, per scelta: garantisce che il dubbio non si perda, non che sia mirato.
come entra un documento
La qualità dell'indice dipende da come i documenti vengono divisi, e questo è il punto in cui i sistemi RAG si assomigliano tutti in superficie e si distinguono nei risultati.
Prima ancora, però, c'è una decisione che riguarda se un documento debba entrare. Ogni documento passa da un albero chiuso a tre esiti: non entra affatto se non è conoscenza — modulistica, ricevute, email di trasmissione; entra nell'indice così com'è; oppure, se cambia o contraddice una pagina del wiki, passa dalla curatela umana invece che dal testo grezzo, altrimenti il servizio si troverebbe a servire due verità in conflitto sulla stessa domanda.
Il ramo della curatela ha un limite che non è tecnico: regge finché chi cura padroneggia la materia. Sui temi in cui non la padroneggia ho aggiunto un questionario per l'esperto di dominio — un file markdown, non uno strumento — che porta già scritta la formulazione proposta, perché chi è impegnato corregge un testo mentre davanti a un foglio bianco risponde a monosillabi. Tre dettagli che ne fanno un meccanismo invece che un buon proposito: il file sta fuori dalla cartella indicizzata, così una formulazione proposta e poi respinta non può finire fra le risposte per costruzione anziché per una regola da ricordare; la curatela non si ferma ad aspettare, perché la pagina entra con la sua avvertenza, che il pavimento porta già fino a chi interroga; e al ritorno è il grado di sicurezza dichiarato dall'esperto a decidere se l'avvertenza cade, resta più precisa, o la pagina non cambia affatto.
Sul come vengono spezzati, il primo impianto usava come frammento quello che l'estrattore restituiva, e il 33% dei frammenti erano titoli di sezione da una riga: esche vuote per la ricerca per somiglianza, che prendono punteggi alti e non portano informazione. Ora i blocchi vengono impacchettati fra 200 e 1.800 caratteri con una sovrapposizione fra l'uno e l'altro, le tabelle non si spezzano mai, e i PDF si impacchettano dentro la pagina, così il riferimento alla posizione resta esatto. Sui file veri: da 57 frammenti a 19, e nessuno sotto i 100 caratteri.
Le forme che non stanno in questo schema hanno una strategia dichiarata sul file, non indovinata: i documenti a domanda e risposta si spezzano per coppia, ignorando i salti di pagina che altrimenti taglierebbero a metà una risposta; i fogli di calcolo diventano tabelle in markdown invece di una riga per frammento. E se il documento non contiene i marcatori attesi, la strategia produce zero frammenti e lo dice, invece di riuscire in silenzio.
Sul silenzio ho pagato una lezione che vale la pena raccontare. La reindicizzazione di massa inghiottiva le eccezioni: un giro su sorgenti non più raggiungibili ha portato l'indice da 1.595 frammenti a 6, riportando «tutto ok». Ora l'estrazione avviene prima della cancellazione, gli errori interrompono il lavoro, e i frammenti vecchi sopravvivono a un'estrazione fallita.
come si aggiorna la conoscenza
Il wiki curato vive in git, e il ciclo è deliberatamente banale: si scrive in un clone locale, si pubblica, il server si riallinea e reindicizza solo le pagine cambiate. Il servizio consuma una copia in sola lettura: sul server non si scrive mai.
L'invariante è costata mezza giornata per essere capita: un aggiornamento eseguito saltando l'allineamento aveva lasciato tre persone a discutere di quale versione della conoscenza fosse quella buona. Prima i due passi erano separati per costruzione — uno nello script, uno nel comando — e chi ne usava metà saltava l'altra senza che nulla protestasse.
Attorno a questo ci sono tre scelte che valgono più del meccanismo:
- Il registro di ciò che è stato fatto. Ogni esecuzione conserva l'impronta esatta della versione indicizzata, i conteggi per esito, i nomi delle pagine create, aggiornate e cancellate, chi l'ha lanciata e come è andata. Senza l'impronta i numeri dicono cosa è cambiato ma non rispetto a cosa, che è esattamente la domanda che ci si fa quando una pagina sembra mancare.
- Le pagine che hanno cambiato solo etichette si elencano sempre, mai solo contate. Fra le etichette c'è quella che decide a chi la pagina viene servita: un cambio di perimetro non deve passare come un numero in una riga di riepilogo.
- Il blocco contro le esecuzioni sovrapposte è un vincolo di database, non un controllo applicativo: due esecuzioni si contendono le stesse pagine, e un controllo in Python ha sempre una finestra fra la lettura e la scrittura.
C'è infine una trappola che ho scoperto pagandola. Il salto delle pagine invariate confronta l'impronta del testo, quindi rileva le modifiche al wiki ma non i cambi alla logica di indicizzazione: si rilascia una modifica al modo di spezzare le pagine, si lancia l'aggiornamento normale, e si ottiene «nessuna pagina cambiata» — nessun errore, nessun avviso, indicizzazione vecchia. Serve un'opzione esplicita che rigeneri tutto, e la prova che il rilascio sia atterrato è un contatore a zero nel rapporto.
Le sorgenti curate oggi sono più d'una, e non sono due copie della stessa cosa: una è un archivio che esiste per essere conoscenza, l'altra è la documentazione d'uso di un'applicazione, che vive nel repository di quell'applicazione accanto al codice che descrive. Da qui la forma dell'impianto, che è la conseguenza da ricordare: è il servizio a descrivere la sorgente — dove sta, con quale dialetto di intestazioni, quale canale eredita, se attendersi un anno — non la sorgente ad adeguarsi al servizio. Una sorgente nuova non chiede di riscrivere il repository che la ospita: si dichiara e basta.
le identità non escono in chiaro
Fino a un certo punto della storia del servizio, niente di ciò che usciva passava da un filtro: domande, evidenze recuperate e righe dello store arrivavano ai fornitori dei modelli esattamente come stavano. Era un rischio teorico finché il corpus era normativa: una misura sul corpus non ha trovato un solo codice fiscale, partita IVA o IBAN in oltre duemila frammenti. Due cose arrivate insieme lo hanno reso concreto — un quarto fornitore di modelli, fuori dall'Unione Europea, e il ramo numerico, che manda al modello righe dove le ragioni sociali stanno accanto agli importi. Da quel momento quale modello uso e quali dati escono erano la stessa decisione, e non dovevano esserlo.
Il modulo che ho aggiunto sostituisce identità con segnaposto tipizzati prima che qualcosa raggiunga un modello. Le scelte che lo rendono utile invece che rassicurante:
- Identità sì, importi no. Codici fiscali, partite IVA, IBAN e i nomi noti dell'anagrafica vengono sostituiti; importi, date e quantità passano sempre. È il nome ad agganciare una cifra a una persona, non la cifra in sé: una spesa senza sapere di chi è non porta a nessuno. Mascherare l'identità basta a rendere innocuo il numero, e lascia intatta la ragione per cui il ramo numerico esiste.
- È pseudonimizzazione, e va chiamata così. Il segnaposto porta l'identificatore vero del gestionale, non un'impronta: chi ha accesso a quel database risale all'entità nell'istante in cui lo legge, ed è l'effetto voluto — chi risponde per conto dell'applicazione deve poter capire di cosa si parla. La protezione è dal destinatario esterno, non un'anonimizzazione del contenuto, e nessun documento deve descriverla come tale.
- Sulle righe si dichiara l'innocuo, non il sensibile. Le colonne si classificano per nome, e tutto ciò che non è riconosciuto viene oscurato per difetto. È la scelta scomoda apposta: elencare le colonne da proteggere avrebbe significato che una query nuova, scritta senza pensarci, esce in chiaro senza che nulla protesti — la stessa classe di difetto del filtro dormiente. Con l'oscuramento per difetto l'errore possibile è l'opposto, mascherare troppo, e quello si vede nella stessa ora: chi riceve una risposta con un dato utile coperto lo segnala. Una fuga silenziosa non la segnala nessuno, mai.
- Una soglia minima sulla lunghezza dei nomi, imparata sul campo. In anagrafica esisteva un fornitore chiamato con una sola lettera, e ogni marcatore di elenco di un testo normativo veniva scambiato per una ragione sociale: dodici sostituzioni in un solo contesto di risposta. Sotto una certa lunghezza i nomi non si cercano più dentro il testo libero, ma restano sostituiti dove è il nome della colonna a dire che quel valore è un'entità. La soglia è dove passa il confine fra le sigle e le aziende vere, e l'ho scelta guardando i dati, non a intuito.
Due limiti dichiarati, perché senza di essi il meccanismo prometterebbe più di quanto mantiene. Il primo: il ramo di esplorazione libera dell'agente esegue una query scritta dal modello, e le condizioni girano sui dati veri — il mascheramento avviene dopo, sulle righe che tornano. Il valore è protetto, la domanda sul valore no: un modello che lo voglia può ricostruire un nome per tentativi, senza che nessun nome compaia mai in uscita. Non è correggibile spostando una riga, perché è il limite del mascherare in uscita contro uno strumento che sa calcolare in ingresso; si accetta perché quel ramo è raggiungibile solo da chi lancia un'analisi sui propri dati, ma va scritto. Il secondo: la domanda dell'utente arriva al modello prima che il percorso si biforchi, e mascherarla romperebbe l'instradamento, perché i parametri estratti dalla domanda servono a filtrare i dati veri.
Tutto il meccanismo sta dietro tre metodi e un interruttore. Con l'interruttore spento viene iniettato un oggetto che non fa nulla, e gli agganci non contengono un solo if: il comportamento torna identico a prima che il modulo esistesse, e cancellare la cartella riporterebbe il sistema allo stato precedente anche fisicamente. È la ragione per cui il giorno in cui il servizio parlerà con un modello ospitato in casa, la pseudonimizzazione si potrà spegnere senza smontare niente.
la risposta dichiara le sue fonti
Per molto tempo l'informazione su quali documenti avessero prodotto una risposta è esistita solo dentro il blocco diagnostico, e non era quella giusta per essere mostrata a un utente: compariva solo con la diagnostica accesa, veniva troncata ai primi frammenti, e portava il nome del file invece del titolo leggibile.
Ora la risposta ha un campo suo, con il titolo da mostrare e l'identificatore con cui risalire al documento, ordinato dalla fonte più vicina alla domanda. Distanze, posizioni e conteggi restano nella diagnostica: sono diagnosi, non attribuzione, e ammetterli lì avrebbe prodotto un secondo blocco diagnostico con le stesse ambiguità del primo.
La parte che mi interessa di più è un'altra: la presenza del campo è essa stessa informazione. Esce solo dove il recupero è stato tentato, e lì esce sempre, lista vuota compresa. Così «non ho cercato nel corpus» e «ho cercato e non ho trovato nulla» sono due stati distinguibili da chi integra il servizio, invece di due frasi da dedurre leggendo il testo della risposta. È la stessa distinzione che il pavimento di onestà dichiara al modello, resa leggibile anche al programma che chiama.
L'aggregazione — un documento vale una fonte, ordinate per vicinanza — vive in un posto solo, condiviso con il banco di prova interno. Una seconda copia sarebbe divergiata in silenzio, e il sintomo sarebbe stato un banco di prova che mostra fonti diverse da quelle che il servizio dichiara: cioè un banco di prova inutile.
l'analisi approfondita
La risposta sincrona arriva in pochi secondi e copre la maggior parte delle domande. Accanto c'è un agente asincrono, per le richieste che valgono più passi: una sintesi che attraversa più mercati, un confronto fra fonti. Naviga il wiki, incrocia più pagine, interroga lo store, procede per passi e cita le fonti.
- La coda è una tabella, non un sistema di messaggi da mantenere: le richieste vengono prese con un blocco che salta le righe già occupate, e i lavori rimasti orfani si recuperano al riavvio.
- Gli strumenti sono confinati al wiki curato, con un controllo che nega ogni percorso fuori da lì e risolve i collegamenti simbolici prima di decidere. Un documento può contenere istruzioni ostili: il recinto limita dove possono arrivare.
- L'esplorazione libera verso SQL esiste solo qui, dove l'esito è asincrono e rivedibile da una persona. Sul canale sincrono è esclusa, e resta il catalogo di query provate.
- Budget e turni hanno un tetto: esaurito il budget il lavoro fallisce con un motivo scritto, e non restituisce un risultato parziale spacciato per completo.
Un confronto misurato dice che l'agente vince su copertura e citazioni nelle domande di sintesi, mentre la risposta immediata è pari o migliore, e più prevedibile, sulle ricerche puntuali nella normativa. La differenza di costo e di tempo — centesimi e secondi contro qualche decina di centesimi e minuti — fa sì che l'uso si instradi da sé, senza doverlo regolare.
perché non un modello da solo
È la domanda che si fa chiunque guardi un sistema del genere oggi, quando i modelli hanno finestre di contesto enormi e si potrebbe pensare di passargli tutto.
| un modello da solo | questo servizio | |
|---|---|---|
| Da dove viene la risposta | memoria del modello, opaca | documenti e sintesi dell'azienda, tracciabili |
| Dati proprietari | non li conosce | indicizzati e recuperabili |
| Aggiornamento | fermo alla data di addestramento | si cambiano i documenti, cambiano le risposte |
| Verificabilità | difficile risalire alla fonte | ogni risposta dichiara i documenti da cui viene |
| Numeri | plausibili | esatti, dal catalogo di query, con la loro data |
| Incertezza | la smussa: risponde sicuro anche sul contestato | i dubbi della curatela arrivano alla risposta |
| Costo di evoluzione | riaddestramento | scrittura di testo e un aggiornamento |
In breve: mettere la conoscenza dentro il modello la rende rigida, costosa e non tracciabile; tenerla accanto al modello la lascia viva, verificabile e curabile a mano. Per una conoscenza aziendale che cambia e che va giustificata, la seconda vince.
come si misura
Un insieme di domande di riferimento, con la risposta attesa, si esegue prima di ogni rilascio. Ha due livelli, e la distinzione è ciò che rende utile il risultato: uno isola il recupero — sono stati pescati i frammenti giusti? — e l'altro giudica la risposta finale. Quando qualcosa peggiora, i due livelli dicono se la colpa è del corpus o della curatela, che è la domanda che serve per sapere dove mettere le mani.
Un secondo comando verifica i dati dove il primo verifica il codice: che ogni filtro dichiarato abbia documenti da agganciare, e che i valori presenti nel corpus stiano nel vocabolario del registro. È il controllo che intercetta il caso più silenzioso di tutti — un filtro corretto che non seleziona nulla perché nessuno ha mai etichettato i documenti.
Due pratiche contano più degli strumenti:
- Un test verde non basta: va visto fallire. Sulle modifiche delicate provoco di proposito la rottura che il test dovrebbe intercettare, una per volta, e verifico che diventi rosso il test giusto — non un test qualunque. È così che ho scoperto che una prova passava anche rimuovendo la riga che avrebbe dovuto proteggere: era verde per la ragione sbagliata.
- Un secondo modello come giudice. Prima di chiudere un intervento importante lo faccio rivedere da un modello diverso da quello con cui l'ho scritto, in sola lettura sul codice. Sui rilievi che riceve, la regola che mi sono dato è che quelli non recepiti vanno dichiarati insieme al motivo, non lasciati cadere: su un intervento recente erano tre su sei, e i tre respinti hanno una ragione scritta accanto.
C'è infine un anello che chiude il ciclo: da una risposta scadente registrata in produzione si genera la voce pronta da aggiungere all'insieme di riferimento. Serve a evitare che una regressione osservata resti un aneddoto.
limiti dichiarati
Le cose aperte le tengo scritte, ed è parte del metodo: un sistema che risponde a domande deve dichiarare dove non arriva.
- Due modi silenziosi di rispondere a vuoto. Se il testo da passare al modello supera i limiti, le evidenze vengono scartate; se la ricerca fallisce per un guasto, il flusso prosegue lo stesso. Il modello ora lo dichiara nella risposta, il sistema non lo segnala a chi lo amministra. E poiché i frammenti sono diventati quasi tre volte più grandi, il primo caso è più probabile di prima.
- Un guasto tecnico somiglia a un corpus vuoto. L'eccezione viene catturata e il pavimento di onestà risponde con eleganza a un guasto: sicuro, ma muto. Se in produzione compaiono molti «non ho trovato», si guardano i log prima del corpus.
- Il legame fra una sintesi e il documento che riferisce è solo una stringa. Nessun controllo a runtime lo risolve: se il documento viene aggiornato, niente si accorge che la sintesi è invecchiata. Si accenderà quando due versioni dello stesso documento conviveranno nello stesso canale.
- Lo stato per-richiesta vive in oggetti condivisi. I frammenti dell'ultima ricerca si leggono da un attributo di un componente unico invece di essere restituiti dalla funzione: la finestra è di microsecondi e non c'è attesa di rete in mezzo, ma la cura non è una toppa — è togliere quello stato dai componenti condivisi, e resta una voce di lavoro aperta.
- La pseudonimizzazione ha due porte scoperte, entrambe descritte sopra e dichiarate: l'esplorazione libera e la domanda dell'utente prima dell'instradamento.
stato
In produzione, con i tre livelli attivi e le due modalità disponibili. È uno strumento interno: l'indirizzo non è pubblico e non c'è niente da visitare.