
Questo articolo descrive l'implementazione tecnica del Q&A a zero allucinazioni nel nostro reader IA: le risposte si basano rigorosamente sul testo del libro aperto e le affermazioni chiave possono essere ricondotte con un clic al passaggio esatto. Se sviluppi lettura IA, Q&A documentale o app in stile RAG, speriamo che tre iterazioni e l'architettura finale possano essere utili.
I. Evoluzione in tre fasi
Il Q&A a zero allucinazioni non è stato progettato perfetto dal primo giorno. È evoluto sotto la tensione tra costo, latenza e accuratezza. Di seguito le tre fasi in ordine cronologico—contesto per capire perché l'architettura attuale ha questa forma.
Fase 1: Inserire tutto il libro nel context (la più semplice—e la prima a cedere)
Approccio: Quando l'utente apre un libro e pone una domanda, mettere tutto il corpo di testo estratto nel system prompt o nel messaggio utente e lasciare rispondere il modello di chat. Se il libro supera circa 400.000 caratteri, troncamento rigido—resta solo l'inizio; i capitoli successivi sono invisibili al modello.
Vantaggi:
- Costo di implementazione molto basso, quasi nessun pre-processing;
- Funziona ragionevolmente su libri brevi e documenti semplici—il modello ha davvero «visto tutto il libro»;
- UX semplice: chiedi e ottieni risposta, senza stato «attendere l'analisi».
Svantaggi (presto inaccettabili):
- Risposte lente: Ogni domanda reinvia un payload enorme; time-to-first-token e latenza totale crescono con la lunghezza del libro;
- Costo token elevato: Si paga l'input dell'intero libro a ogni domanda;
- Libri lunghi fortemente distorti: Oltre 400k caratteri, seconda metà, appendici e conclusioni praticamente non esistono—e l'UI spesso non segnala chiaramente il troncamento;
- Granularità di ricerca zero: Il modello deve «trovare un ago nel pagliaio» su centinaia di migliaia di caratteri—facile perdere dettagli e produrre riassunti plausibili senza fondamento—esattamente ciò che le app di lettura devono evitare.
La fase 1 va bene per un MVP, non per una soluzione di prodotto.
Fase 2: Un LLM leggero estrae frasi chiave (comprimere il context—troppo aggressivamente)
Approccio: Prima del Q&A (o al primo accesso), far girare un modello più economico sul corpo: suddivisione per capitolo spine (o chunk del libro), estrazione di frasi chiave, conservazione di tag di posizione come [fFile-inizio-fine], poi concatenazione in un context più corto per il Q&A successivo.
Pipeline tipica: Extract → Cache → Chat. Estrarre una volta (offline o on demand), salvare un «pacchetto di frasi chiave», riutilizzarlo a ogni domanda—come molti prototipi document Q&A: comprimere prima, rispondere dopo.
Vantaggi:
- Ogni domanda invia molto meno testo; il consumo di token per richiesta cala rispetto alla fase 1;
- Il pre-processing può essere in cache; niente ri-estrazione per domanda sullo stesso libro;
- I tag di posizione gettano le basi per le citazioni.
Svantaggi (ancora insufficienti su libri lunghi):
- Forte perdita di dettaglio: Le «frasi chiave» sono scelte dal modello; qualificatori, controesempi e catene argomentative spesso spariscono—risposte «corrette ma parziali»;
- Context ancora grande su opere lunghe: Anche i pacchetti di frasi chiave sono consistenti—latenza e costo si attenuano, non si risolvono;
- Doppio errore LLM: L'estrazione può omettere; il Q&A può leggere male gli estratti—gli errori si accumulano;
- Context statico: Che la domanda riguardi un capitolo o la struttura globale, il modello riceve sempre lo stesso blob pre-estratto—nessun restringimento dinamico per domanda.
La lezione: il problema non è «se comprimiamo», ma «se la compressione è on demand e se possiamo tornare al testo sorgente».
Fase 3: Indice segmenti + Tool on demand + restituzione testo sorgente (attuale)
Approccio: Ispirato a PageIndex. Rispetto alla fase 2, tre cambiamenti centrali:
- Il pre-processing produce un indice strutturato (riassunti a livello TOC + span di caratteri esatti), non estratti usati direttamente come context Q&A;
- Ogni domanda usa Tool Calling per cercare on demand, poi recupera testo sorgente con tag di posizione per rispondere;
- System prompt + frontend impongono il formato di citazione e supportano clic → salto → evidenziazione nel reader.
Confronto delle tre fasi:
| Dimensione | Fase 1 (testo integrale) | Fase 2 (frasi chiave) | Fase 3 (attuale) |
|---|---|---|---|
| Context per domanda | Libro intero (o prima metà troncata) | Frasi chiave pre-estratte | Solo frammenti di sorgente pertinenti |
| Accuratezza su libri lunghi | Collasso oltre ~400k caratteri | Dipende dall'estrazione; perde dettaglio | Ricerca per TOC/span; niente troncamento rigido dell'intero libro |
| Velocità di risposta | Lenta | Un po' meglio; libri lunghi ancora lenti | Ricerca + context breve—nettamente più veloce |
| Costo token | Molto alto | Medio-alto | Pre-processing ammortizzato + pagamento on demand |
| Tracciabilità | Debole (citazioni difficili) | Tag presenti ma contenuto filtrato | Note a piè di pagina → span sorgente reali |
| Complessità ingegneristica | Bassa | Media | Alta |
Perché ci siamo fermati alla fase 3: In lettura, zero allucinazioni non significa «mostrare al modello il massimo testo», ma «prima di rispondere, ottenere prove sorgente per la domanda». Le fasi 1–2 combattevano la dimensione del context; la fase 3 spezza la pipeline in indice (pre-processing) → ricerca (Tool) → prova (sorgente) → risposta (generazione vincolata)—equilibrio tra accuratezza, costo e tracciabilità.
Di seguito il dettaglio della fase 3.
II. Definizione del problema: Nel Q&A su libri, l'allucinazione costa più che in una chat generica
Gli utenti perdonano errori occasionali in un chatbot generale. Nel Q&A su libri, il prezzo è più alto:
- Chiedono cosa dice questo libro—non ciò che vive nella memoria parametrica del modello;
- Un'«opinione del libro» plausibile può indurre in errore note, citazioni e condivisioni;
- Senza fonti, niente verifica—la fiducia è difficile da costruire.
«Zero allucinazioni» diventa quindi tre regole applicabili:
- Le domande sul libro devono interrogare il libro prima: Tutto ciò che può riguardare il libro aperto passa per il retrieval (Tool) prima della risposta;
- Le risposte devono essere tracciabili: Le affermazioni chiave portano tag di posizione che l'UI può parsare e raggiungere;
- Dire quando non si trova: Se il libro non contiene l'informazione, dirlo—non mascherare conoscenza generale come «ciò che dice il libro».
Il resto segue il flusso dati della fase 3 e l'implementazione di queste regole.
III. Architettura: Pre-processing → Tool → Generazione vincolata → Citazioni cliccabili
Idea centrale: non lasciare il modello «rispondere a memoria»—obbligarlo a «raccogliere prove, rispondere, segnare le fonti».
IV. Pre-processing: Trasformare il libro in un indice di segmenti ricercabile
Se ogni domanda usasse ancora il context fase 1 dell'intero libro, i libri lunghi esplodono il budget token e la ricerca è troppo grossolana. Fase 3: al primo chat IA su un libro, eseguire in background un job di riassunto segmenti—suddivisione per TOC o lunghezza testo in Segment, riassunto di ciascuno, persistenza in IndexedDB locale.
Ogni Segment contiene riassunto più posizione fisica nel corpo:
| Campo | Significato |
|---|---|
startFileIndex / endFileIndex | Indice file spine (PDF: un file per pagina) |
startOffset / endOffset | Inizio/fine in caratteri |
sequence | Ordine di lettura lineare |
title | Titolo TOC |
La suddivisione bilancia precisione e costo: nodo TOC sotto ~20 KB → riassumere solo quel nodo; nodi fratelli fusi in batch (15–20 KB) prima della chiamata LLM; blocchi lunghi non strutturati in intervalli ~30–40k caratteri.
Il system prompt di riassunto richiede di conservare tag di posizione inline ([fNumero-Numero-Numero]) affinché la sorgente recuperata via Tool si allinei agli offset spine. Vincolo centrale:
If summary content relates to a passage, keep the trailing position tag [fNumber-Number-Number] (e.g. [f1-90-109]).
Tags are atomic—do not alter, merge, or omit any character or digit.
Dopo il pre-processing, il Q&A dipende da un indice segmenti strutturato, non dal context dell'intero libro—prerequisito tecnico dello zero allucinazioni su libri lunghi.
V. Sistema di tag di posizione: Codificare il «da dove» nel testo
Zero allucinazioni richiede contenuto dalla sorgente e provenienza analizzabile dalla macchina e raggiungibile in UI. Usiamo tag inline:
[f{fileIndex}-{startChar}-{endChar}]
Esempio: [f5-123-165] = file spine 5 (base 0), caratteri 123–165.
5.1 Come i tag sono scritti nel corpo
Lo strato di estrazione aggiunge [f{fileIndex}-{start}-{end}] a fine segmento:
const position = `[f${fileIndex}-${absOffset}-${absOffset + segment.length}]`;
fileLines.push(segment.text.trim() + position);
Riassunti di pre-processing o estratti Tool: le posizioni si allineano agli offset caratteri spine—non numeri di pagina stimati dal modello.
5.2 Vincoli sull'output del modello
Il system prompt include Position Citation Rules—cinque punti essenziali:
- Formato standard: Deve usare
[f_fileIndex-startChar-endChar]; tutte e tre le parti numeriche obbligatorie; - Copiare solo dalle fonti attuali: Note verbatim da messaggi system/user o ritorni Tool di questo turno;
- Nessuna fabbricazione: Non calcolare, modificare o inventare posizioni;
- Preferire omissione: Nessun tag valido nel context → rispondere normalmente—non emettere tag di posizione;
- Inline con le affermazioni: Tag dopo la frase pertinente; niente elenchi di citazioni a fine risposta.
L'UI filtra anche tag bipartiti invalidi occasionali (es. [f1-293]) prima del render.

VI. Tool Calling: Prima cercare, poi rispondere
Quando la chat è legata a un libro (resourceId presente, chatType === 'chat'), registriamo due Tool con executor prima di ogni generazione—ciclo function calling compatibile OpenAI.
6.1 get_related_segment_summaries — Ricerca mirata di segmenti
Per: concetti, personaggi, trama, dettagli di capitolo—intento di ricerca chiaro.
Flusso:
- Il modello riformula la domanda in termini probabili nel libro («Optimize Search Queries» nel system prompt);
- Chiamata Tool con
question; - Raggruppare tutti i riassunti segmenti per budget token (~30k token per batch, max 5 batch);
- Per batch: richiesta LLM separata sceglie ID pertinenti (max 5) da
{ id, title, summary }, JSON come{"Thinking":"...","answer":["1","3"]}; - Per i segmenti scelti, estrarre testo sorgente taggato dallo spine—not i riassunti—come risultato Tool.
Design chiave: il Tool restituisce sorgente, non riassunti. Il modello risponde da paragrafi reali con [f…] inline, evitando la deriva «riassunto → ri-riassunto».
6.2 get_full_book_segment_summaries — Panoramica dell'intero libro
Per: «riassumi il libro», «recensisci questo libro», «struttura/temi globali»—vista globale.
Concatenare tutti i campi summary dei segmenti in ordine di lettura—evitare di perdere capitoli chiave solo per rilevanza per chunk.
6.3 System prompt: Libro prima, tool prima
Con libro legato, si applica Core Principles for Reading Assistant:
1. Book First, Tool First
- Any question possibly about the book must call tools first;
- Answers must rely mainly on retrieval—never invent “book content” without retrieval.
2. General Knowledge as Fallback Only
- Only for: casual chat / user explicitly skips the book / tools return nothing;
- If the book lacks it, say “not mentioned in this book” before general knowledge.
3. Direct Style
- Get to the point—avoid “based on the provided materials…” and similar filler.
La generazione esegue il ciclo Tool: tool_calls → eseguire → append role: tool → continuare fino al testo finale. Con tools attivi, il canale thinking è spento per evitare conflitti di protocollo.
VII. Tracciabilità frontend: Dalla nota all'evidenziazione
L'output [f5-123-165] del modello non è mostrato grezzo; lo strato di render converte i tag in citazioni cliccabili.
7.1 Render delle note
Normalizzare i tag in link Markdown come [1]([f5-123-165]), mostrare come note numerate; deduplicare la stessa posizione.
7.2 Interazione al clic
- Primo clic: Parsare
[f…]→ fileIndex + offset → estrarre testo spine → anteprima (titolo TOC opzionale); - Stessa nota di nuovo: Chiudere anteprima;
- Confermare salto: Aprire vista lettura, evidenziare intervallo caratteri.
Dal tag copiato dal modello al testo sorgente visibile all'utente, la catena non passa mai da un'altra chiamata LLM—deterministica e riproducibile.
VIII. Casi limite e degradazione onesta
Zero allucinazioni ≠ «c'è sempre una risposta»—è nessuna prova, nessuna invenzione:
| Scenario | Comportamento |
|---|---|
| Riassunti segmenti non pronti | Estrarre prima testo integrale e riassumere |
| Tool non trova nulla | Restituire (No relevant segment excerpts found…); il modello deve dire «non nel libro» |
| Tag bipartiti invalidi dal modello | Filtraggio frontend; niente note rotte |
| Chiacchierata informale | Il system prompt consente conoscenza generale fuori dal libro |
| Esportare chat | Le note possono diventare deep link del reader per condivisione/archivio |

IX. Compromesso di design: Perché non il «RAG vettoriale»?
I colleghi nel Q&A documentale chiedono spesso: se fai retrieval-augmented generation, perché non Embedding + vector DB Top-K?
Facciamo RAG—cercare prima di generare. La differenza: «RAG» nel discorso comunitario implica spesso similarità vettoriale; la nostra fase 3 è indice segmenti + Tool con sorgente on demand—nessuno strato vettoriale per scelta. Sotto: ragioni architetturali, senza negare il valore del RAG vettoriale.
Ambito: non «nessuna ricerca», ma «nessuna ricerca vettoriale»
- RAG ampio: cercare → generare → lo facciamo;
- RAG vettoriale: recall via similarità embedding → non in questa versione.
Il pre-processing costruisce un indice di riassunti segmenti; il modello sceglie segmenti via Tools e ottiene testo sorgente. C'è ricerca senza modello embedding separato né manutenzione indice vettoriale.
Motivo 1: Provider LLM personalizzati—superficie di integrazione ridotta
Gli utenti possono collegare le proprie API key, base URL personalizzate o Ollama locale—il modello di chat è loro scelta; costo e percorso dati restano sotto controllo.
Il RAG vettoriale tipico allarga l'integrazione:
- Oltre al modello di chat, serve di solito un modello di embedding (altro nome, a volte altro endpoint);
- Ollama locale richiede modello embedding separato più compatibilità dimensione/API;
- Più modi di guasto: chat OK ma ricerca vuota—embedding, indice o dimensione; debug più difficile di un provider end-to-end.
Qui, scelta segmenti e risposta condividono una config provider—niente «chat su A, indice su B». Per app LLM pluggabili, spesso conta più di qualche punto di recall.

Motivo 2: Gli embedding legano l'indice—cambiare provider costa caro
Nel RAG vettoriale, i vettori non sono un formato intermedio universale—sono coordinate sotto un modello di embedding. Indice con A, query con B: la similarità di solito non è comparabile—spesso re-embedding completo, e dimensioni (768 / 1024 / 1536 …) bloccano lo schema di storage.
La fase 3 persiste riassunti strutturati + span caratteri, non vettori; cambiare modello chat non ricostruisce l'indice; la catena di prova (posizioni sorgente) resta—allineato a «provare LLM diversi in qualsiasi momento».
Motivo 3: Il routing strutturato spesso basta per documenti lunghi con TOC
E-book e PDF hanno di solito struttura a capitoli; il pre-processing fornisce titoli segmento + riassunti. Per «cosa dice il capitolo X» o «come il libro definisce Y», scegliere segmenti dal catalogo e tirare la sorgente funziona bene in pratica; il Tool restituisce sorgente con [f…], lo zero allucinazioni resta ancorato agli span caratteri.
I vettori aiutano per semantica fuzzy, multilingue, match letterale lungo; per reader TOC + pre-processing + forte tracciabilità, investire in Tool + restituzione sorgente + regole citazione ha spesso ROI migliore.
Futuro: Recall ibrido, non riscrittura
Potremmo aggiungere recall vettoriale grossolano (embedding solo per Top-N capitoli candidati), terminando sempre in scegli segmento → sorgente → traccia cliccabile—regole zero allucinazioni invariate. Se aggiunto: embedding opzionale, prompt espliciti di re-indicizzazione al cambio modello—evitare silent wrong retrieval.
Fino ad allora: qualsiasi API chat compatibile OpenAI funziona; cambiare modello chat non ricostruisce l'indice locale.
X. Sintesi
| Passo | Metodo | Ruolo |
|---|---|---|
| Pre-processing | Suddivisione TOC/lunghezza + cache segmenti | Libri lunghi ricercabili e localizzabili |
| Tag di posizione | [fFile-inizio-fine] nella sorgente | Provenienza analizzabile dalla macchina |
| Tool | Segmenti / riassunti libro per domanda, restituire sorgente | Forzare prove prima della risposta |
| System prompt | Libro prima, niente tag falsi, dire quando manca | Vincolare la generazione |
| Frontend | Nota → anteprima → salto ed evidenziazione | L'utente verifica le prove |
| Nessuna ricerca vettoriale | Un provider; cambiare modello chat senza re-indice | Costo integrazione e migrazione ridotto |
«Zero allucinazioni» non significa che il modello non sbaglia mai—significa che l'ingegneria lega l'output a una catena di prove: nessuna ricerca → non fingere contenuto del libro; con ricerca → posizioni sorgente verificabili.
Se sviluppi lettura IA o Q&A documentale, speriamo che il percorso testo integrale → frasi chiave → Tool-first on demand, più tag di posizione inline + restituzione sorgente, sia un'implementazione di riferimento utile.
Queste sono lezioni dal reader IA Foxycape—solo a titolo di riferimento. Prova il reader nella pagina download.
