Torna al blog

Come ho implementato Q&A a zero allucinazioni nel nostro reader

Note tecniche sul Q&A a zero allucinazioni in un reader IA—risposte ancorate al libro aperto, con citazioni in un clic al passaggio esatto.

Copertina: Q&A a zero allucinazioni

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.

mermaid
flowchart LR
    P1[Fase 1: Testo integrale] --> P2[Fase 2: LLM frasi chiave]
    P2 --> P3[Fase 3: Indice segmenti + Tool]
    P1 -.->|Lento, costoso, impreciso su libri lunghi| X1[Abbandonato]
    P2 -.->|Perdita di dettaglio, ancora lento| X2[Abbandonato]
    P3 -->|Attuale| OK[Zero allucinazioni + tracciabile]

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:

  1. Il pre-processing produce un indice strutturato (riassunti a livello TOC + span di caratteri esatti), non estratti usati direttamente come context Q&A;
  2. Ogni domanda usa Tool Calling per cercare on demand, poi recupera testo sorgente con tag di posizione per rispondere;
  3. System prompt + frontend impongono il formato di citazione e supportano clic → salto → evidenziazione nel reader.

Confronto delle tre fasi:

DimensioneFase 1 (testo integrale)Fase 2 (frasi chiave)Fase 3 (attuale)
Context per domandaLibro intero (o prima metà troncata)Frasi chiave pre-estratteSolo frammenti di sorgente pertinenti
Accuratezza su libri lunghiCollasso oltre ~400k caratteriDipende dall'estrazione; perde dettaglioRicerca per TOC/span; niente troncamento rigido dell'intero libro
Velocità di rispostaLentaUn po' meglio; libri lunghi ancora lentiRicerca + context breve—nettamente più veloce
Costo tokenMolto altoMedio-altoPre-processing ammortizzato + pagamento on demand
TracciabilitàDebole (citazioni difficili)Tag presenti ma contenuto filtratoNote a piè di pagina → span sorgente reali
Complessità ingegneristicaBassaMediaAlta

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:

  1. 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;
  2. Le risposte devono essere tracciabili: Le affermazioni chiave portano tag di posizione che l'UI può parsare e raggiungere;
  3. 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

mermaid
flowchart TB
    subgraph prep [Offline / primo pre-processing]
        A[Dividere libro per TOC o lunghezza] --> B[Riassunti segmenti LLM]
        B --> C[Persistere cache Segment in locale]
    end

    subgraph ask [Domanda utente]
        D[Input utente] --> E{Cache Segment esistente?}
        E -->|No| F[Estrarre testo integrale / proporre pre-processing]
        F --> prep
        E -->|Sì| G[Registrare Tool Calling]
    end

    subgraph retrieve [Ricerca Tool]
        G --> H{Tipo di domanda}
        H -->|Panoramica / recensione| I[get_full_book_segment_summaries]
        H -->|Fatti / persone / capitolo| J[get_related_segment_summaries]
        J --> K[LLM sceglie ID segmento dal catalogo]
        K --> L[Recuperare sorgente per span + tag di posizione]
        I --> M[Concatenare tutti i riassunti segmenti]
    end

    subgraph answer [Generare e mostrare]
        L --> N[Risultati Tool al modello]
        M --> N
        N --> O[Regole citazione system prompt]
        O --> P[Risposta in streaming + note di posizione]
        P --> Q[Renderizzare note cliccabili]
        Q --> R[Clic → anteprima → salto ed evidenziazione]
    end

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:

CampoSignificato
startFileIndex / endFileIndexIndice file spine (PDF: un file per pagina)
startOffset / endOffsetInizio/fine in caratteri
sequenceOrdine di lettura lineare
titleTitolo 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:

  1. Formato standard: Deve usare [f_fileIndex-startChar-endChar]; tutte e tre le parti numeriche obbligatorie;
  2. Copiare solo dalle fonti attuali: Note verbatim da messaggi system/user o ritorni Tool di questo turno;
  3. Nessuna fabbricazione: Non calcolare, modificare o inventare posizioni;
  4. Preferire omissione: Nessun tag valido nel context → rispondere normalmente—non emettere tag di posizione;
  5. 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.

Popup tracciamento citazioni


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.

Per: concetti, personaggi, trama, dettagli di capitolo—intento di ricerca chiaro.

Flusso:

  1. Il modello riformula la domanda in termini probabili nel libro («Optimize Search Queries» nel system prompt);
  2. Chiamata Tool con question;
  3. Raggruppare tutti i riassunti segmenti per budget token (~30k token per batch, max 5 batch);
  4. Per batch: richiesta LLM separata sceglie ID pertinenti (max 5) da { id, title, summary }, JSON come {"Thinking":"...","answer":["1","3"]};
  5. 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

  1. Primo clic: Parsare [f…] → fileIndex + offset → estrarre testo spine → anteprima (titolo TOC opzionale);
  2. Stessa nota di nuovo: Chiudere anteprima;
  3. 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:

ScenarioComportamento
Riassunti segmenti non prontiEstrarre prima testo integrale e riassumere
Tool non trova nullaRestituire (No relevant segment excerpts found…); il modello deve dire «non nel libro»
Tag bipartiti invalidi dal modelloFiltraggio frontend; niente note rotte
Chiacchierata informaleIl system prompt consente conoscenza generale fuori dal libro
Esportare chatLe note possono diventare deep link del reader per condivisione/archivio

Esportazione chat


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 demandnessuno 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.

Provider IA personalizzati


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

PassoMetodoRuolo
Pre-processingSuddivisione TOC/lunghezza + cache segmentiLibri lunghi ricercabili e localizzabili
Tag di posizione[fFile-inizio-fine] nella sorgenteProvenienza analizzabile dalla macchina
ToolSegmenti / riassunti libro per domanda, restituire sorgenteForzare prove prima della risposta
System promptLibro prima, niente tag falsi, dire quando mancaVincolare la generazione
FrontendNota → anteprima → salto ed evidenziazioneL'utente verifica le prove
Nessuna ricerca vettorialeUn provider; cambiare modello chat senza re-indiceCosto 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.