Tillbaka till bloggen

Hur jag byggde noll-hallucination Q&A i läsaren

Tekniska anteckningar om noll-hallucination Q&A i en AI-läsare—svar grundade i den öppna boken, med citeringar i ett klick till exakt stycke.

Omslag: Noll-hallucination Q&A

Det här inlägget delar hur vi implementerade noll-hallucination Q&A i vår AI-läsare: svar grundas strikt i texten i den bok du har öppen, och viktiga påståenden kan spåras i ett klick till exakt stycke. Om du bygger AI-läsning, dokument-Q&A eller RAG-liknande appar hoppas tre iterationers lärdomar och den slutliga arkitekturen vara till nytta.


I. Utveckling i tre steg

Noll-hallucination Q&A designades inte perfekt dag ett. Det utvecklades under spänning mellan kostnad, latens och träffsäkerhet. Nedan följer en kronologisk bild av tre steg—användbar kontext för varför den nuvarande arkitekturen ser ut som den gör.

mermaid
flowchart LR
    P1[Steg 1: Hela texten i kontext] --> P2[Steg 2: LLM extraherar nyckelmeningar]
    P2 --> P3[Steg 3: Segmentindex + Tool retrieval]
    P1 -.->|Långsamt, dyrt, fel på långa böcker| X1[Avvecklat]
    P2 -.->|Tappar detaljer, fortfarande långsamt| X2[Avvecklat]
    P3 -->|Nuvarande| OK[Noll hallucination + spårbar]

Steg 1: Stoppa in hela boken i kontexten (enklast—och först att brista)

Tillvägagångssätt: När användaren öppnar en bok och ställer en fråga läggs all extraherad brödtext i System Prompt eller användarmeddelande och chattmodellen svarar. Överskrider boken cirka 400 000 tecken sker hård trunkering—bara början behålls; senare kapitel är osynliga för modellen.

Fördelar:

  • Mycket låg implementeringskostnad; nästan ingen förbehandling;
  • Fungerar rimligt på korta böcker och enkla dokument—modellen «såg verkligen hela boken»;
  • Enkel UX: fråga och få svar, inget «vänta medan vi analyserar».

Nackdelar (snabbt oacceptabla):

  • Långsamma svar: Varje fråga skickar om en enorm payload; tid till första token och total latens växer med boklängd;
  • Hög tokenkostnad: Du betalar för hela bokens input vid varje fråga;
  • Långa böcker förvrängs: Efter 400 000 tecken kan andra halvan, bilagor och slutsatser lika gärna saknas—och UI:t säger ofta inte tydligt att trunkering skett;
  • Noll retrieval-granularitet: Modellen måste «hitta nålen i höstacken» över hundratusentals tecken—lätt att missa detaljer och lättare att producera plausibla sammanfattningar utan underlag—precis vad läsappar måste undvika.

Steg 1 duger för MVP, inte för produktkvalitet.

Steg 2: Lättare LLM extraherar nyckelmeningar (komprimera kontext—men för hårt)

Tillvägagångssätt: Före Q&A (eller vid första öppning) körs en billigare modell över brödtexten: dela per Spine-kapitel (eller chunka hela boken), extrahera nyckelmeningar, behåll positions-taggar som [fFil-start-slut], sedan sammanfoga utdrag till kortare kontext för senare Q&A.

Typisk pipeline: Extract → Cache → Chat. Extrahera en gång (offline eller på begäran), lagra «nyckelmeningsbunt», återanvänd vid varje fråga—samma idé som många dokument-Q&A-prototyper som komprimerar först och sedan svarar.

Fördelar:

  • Varje fråga skickar mycket mindre text; token per förfrågan sjunker mot steg 1;
  • Förbehandling kan cachas; ingen om-extrahering per fråga på samma bok;
  • Positionstaggar lägger grund för citeringar.

Nackdelar (håller fortfarande inte på långa böcker):

  • Stor detaljförlust: «Nyckelmeningar» väljs av modellen; kvalificerare, motexempel och argumentkedjor tappas ofta—svar blir «korrekta men ensidiga»;
  • Kontext fortfarande stor på långa böcker: Även nyckelmeningsbuntar för stora verk är betydande—latens och kostnad lättas, inte löses;
  • Dubbelt LLM-fel: Extrahering kan missa; Q&A kan feltolka utdrag—fel staplas;
  • Statisk kontext: Oavsett om användaren frågar om ett kapitel eller helboksstruktur får modellen alltid samma för-extraherade blob—ingen dynamisk avgränsning per fråga.

Lärdom: frågan är inte «om vi komprimerar», utan om komprimering sker på begäran och om vi kan återgå till källtext.

Steg 3: Segmentindex + Tool retrieval på begäran + källtext tillbaka (nuvarande)

Tillvägagångssätt: Inspirerat av PageIndex. Mot steg 2, tre kärnskiften:

  1. Förbehandling ger strukturerat index (innehållsförteckningssammanfattningar + exakta tecken-spans), inte utdrag som direkt Q&A-kontext;
  2. Varje fråga använder Tool Calling för retrieval på begäran, sedan hämtar källtext med positionstaggar för svar;
  3. System Prompt + frontend tvingar citeringsformat och stödjer klick-till-hopp med markering i läsaren.

Jämförelse av tre steg:

DimensionSteg 1 (hel dump)Steg 2 (nyckelmeningar)Steg 3 (nuvarande)
Kontext per frågaHela boken (eller trunkerad första halva)För-extraherade nyckelmeningarEndast käll-utdrag relevanta för frågan
Träffsäkerhet långa böckerKollapsar efter ~400k teckenBeror på extrahering; tappar detaljerHämta via TOC/span; ingen hård helbokstrunkering
SvarshastighetLångsamNågot bättre; långa böcker fortfarande långsammaRetrieval + kort kontext—märkbart snabbare
TokenkostnadMycket högMedelhögAmorterad förbehandling + betala vid behov
SpårbarhetSvag (svårt att citera)Taggar finns men innehåll redan filtreratFotnoter mappar till riktiga käll-spans
Teknisk komplexitetLågMedelHög

Varför vi stannade vid steg 3: För läsning är noll hallucination inte «visa modellen så mycket text som möjligt», utan «innan svar, hämta källevidens för frågan». Steg 1–2 kämpade om kontextstorlek; steg 3 delar pipelinen i index (förbehandla) → hämta (Tool) → evidens (källa) → svara (begränsad generering)—balanserar träffsäkerhet, kostnad och spårbarhet.

Nedan detaljer för steg 3.


II. Problemformulering: I bok-Q&A gör hallucination mer ont än i generisk chatt

Användare förlåter tillfälliga fel i en generell chatbot. I bok-Q&A är kostnaden högre:

  • De frågar vad den här boken säger—inte vad som finns i modellens parametriska minne;
  • Ett plausibelt «bokens synpunkt» kan vilseleda anteckningar, citeringar och vidaredelning;
  • Utan källor kan de inte verifiera—förtroende är svårt att bygga.

Så «noll hallucination» blir tre genomförbara regler:

  1. Bokfrågor måste fråga boken först: Allt som rimligen gäller den öppna boken måste genom retrieval (Tool) före svar;
  2. Svar måste vara spårbara: Viktiga påståenden bär positionstaggar som UI kan tolka och hoppa till;
  3. Säg när du inte hittar: Finns det inte i boken, säg det—klä inte allmän kunskap som «vad boken säger».

Resten följer steg 3 dataflöde och hur reglerna implementeras.


III. Arkitektur: Förbehandla → Tool retrieval → Begränsad generering → Klickbara citeringar

mermaid
flowchart TB
    subgraph prep [Offline / första gången förbehandling]
        A[Dela bok efter TOC eller längd] --> B[LLM segment-sammanfattningar]
        B --> C[Persist Segment-cache lokalt]
    end

    subgraph ask [Användarfråga]
        D[Användarinput] --> E{Finns Segment-cache?}
        E -->|Nej| F[Extrahera full text / be om förbehandling]
        F --> prep
        E -->|Ja| G[Registrera Tool Calling]
    end

    subgraph retrieve [Tool retrieval]
        G --> H{Frågetyp}
        H -->|Översikt / recension| I[get_full_book_segment_summaries]
        H -->|Fakta / personer / kapitel| J[get_related_segment_summaries]
        J --> K[LLM väljer segment-ID från sammanfattningskatalog]
        K --> L[Hämta källa per span + positionstaggar]
        I --> M[Sammanfoga alla segment-sammanfattningar]
    end

    subgraph answer [Generera och visa]
        L --> N[Tool-resultat tillbaka till modell]
        M --> N
        N --> O[System Prompt citeringsregler]
        O --> P[Strömma svar + positionsfotnoter]
        P --> Q[Rendera klickbara fotnoter]
        Q --> R[Klick → förhandsgranska → hoppa och markera]
    end

Kärnidé: låt inte modellen «svara ur minnet»—tvinga den att «samla evidens, svara och markera källor».


IV. Förbehandling: Gör hela boken till ett sökbart segmentindex

Om varje fråga fortfarande använde helbokskontext från steg 1 spränger långa böcker tokenbudget och retrieval är för grov. Steg 3: vid första AI-chatt för en bok körs segment-sammanfattningsjobb i bakgrunden—dela efter TOC eller textlängd i Segment, sammanfatta varje, persist i lokal IndexedDB.

Varje Segment innehåller sammanfattning plus fysisk position i brödtext:

FältBetydelse
startFileIndex / endFileIndexSpine-filindex (PDF: en fil per sida)
startOffset / endOffsetTecken start/slut
sequenceLinjär läsordning
titleTOC-titel

Delning balanserar precision och kostnad: om en TOC-nods brödtext är under ~20KB, sammanfatta bara den noden; syskon kan slås ihop i batchar (15–20KB) före LLM-anrop; ostrukturerade långa block delas i ~30–40k teckenintervall.

Sammanfattnings-System Prompt kräver behålla inline positionstaggar ([fNummer-Nummer-Nummer]) så Tool-hämtad källa stämmer med Spine-offsets. Kärnbegränsning:

Om sammanfattning relaterar till ett stycke, behåll avslutande positionstagg [fNummer-Nummer-Nummer] (t.ex. [f1-90-109]).
Taggar är atomära—ändra, slå ihop eller utelämna inte tecken eller siffror.

Efter förbehandling beror Q&A på strukturerat segmentindex, inte helbokskontext—tekniskt förutsättning för noll hallucination på långa böcker.


V. Positionstaggsystem: Koda «var» i text

Noll hallucination kräver innehåll från källa och maskintolkbar, UI-hoppbar härkomst. Vi använder inline-taggar:

[f{fileIndex}-{startChar}-{endChar}]

Exempel: [f5-123-165] = Spine-fil 5 (0-baserad), tecken 123–165.

5.1 Hur taggar skrivs in i brödtext

Extraheringslagret lägger till [f{fileIndex}-{start}-{end}] i segmentets slut:

const position = `[f${fileIndex}-${absOffset}-${absOffset + segment.length}]`;
fileLines.push(segment.text.trim() + position);

Oavsett förbehandlingssammanfattningar eller Tool-utdrag stämmer positioner med Spine-teckenoffset—inte modellens gissade sidnummer.

5.2 Begränsningar på modellutdata

System Prompt inkluderar Position Citation Rules—fem kärnpunkter:

  1. Standardformat: Måste använda [f_fileIndex-startChar-endChar]; alla tre numeriska delar krävs;
  2. Kopiera endast från aktuella källor: Fotnoter ordagrant från denna rundas system/user-meddelanden eller Tool-returer;
  3. Ingen fabricering: Beräkna, redigera eller hitta inte på positioner;
  4. Föredra utelämnande: Finns ingen giltig tagg i kontext, svara normalt—skriv inga positionstaggar;
  5. Inline med påståenden: Taggar följer relevant mening; inga citeringslistor i slutet.

UI filtrerar också tillfälliga tvådelade ogiltiga taggar (t.ex. [f1-293]) före render.

Popup för citeringsspårning


VI. Tool Calling: Hämta först, svara sedan

När chatt är bunden till bok (resourceId finns, chatType === 'chat') registrerar vi två Tools med executors före varje generering—standard OpenAI function calling-loop.

För: begrepp, figurer, handling, kapiteldetaljer—tydlig retrieval-avsikt.

Flöde:

  1. Modellen skriver om användarens formulering till termer som sannolikt finns i boken («Optimize Search Queries» i System Prompt);
  2. Anropa Tool med question;
  3. Batcha alla segment-sammanfattningar per tokenbudget (~30k tokens per batch, max 5 batchar);
  4. Varje batch: separat LLM-begäran väljer relevanta segment-ID (max 5) från { id, title, summary }, JSON som {"Thinking":"...","answer":["1","3"]};
  5. För valda segment, hämta taggad källtext från Spine—inte sammanfattningar—som Tool-resultat.

Nyckeldesign: Tool returnerar källa, inte sammanfattningar. Modellen svarar från riktiga stycken med inline [f…], undviker «sammanfattning → om-sammanfattning»-drift.

6.2 get_full_book_segment_summaries — Helboksöversikt

För: «sammanfatta boken», «recensera den här boken», «övergripande struktur/teman»—global vy.

Sammanfoga alla segment summary i läsordning—undvik att missa nyckelkapitel enbart via chunk-relevans.

6.3 System Prompt: Bok först, tools först

Med bunden bok gäller Core Principles for Reading Assistant:

1. Book First, Tool First
   - All fråga som kan gälla boken måste anropa tools först;
   - Svar måste huvudsakligen bygga på retrieval—hitta aldrig på «bokinnehåll» utan retrieval.

2. General Knowledge as Fallback Only
   - Endast för: vardagschatt / användaren uttryckligen hoppar över boken / tools utan resultat;
   - Saknas i boken, säg «nämns inte i den här boken» före allmän kunskap.

3. Direct Style
   - Gå till sak—undvik «baserat på tillhandahållna material…» och liknande utfyllnad.

Generering kör tool-loopen: tool_calls → kör → lägg till role: tool → fortsätt tills sluttext. Med tools på stängs thinking-kanal av för att undvika protokollkonflikter.


VII. Frontend-spårbarhet: Från fotnot till markering

Modellutdata [f5-123-165] visas inte rå; renderlager gör klickbara citeringar.

7.1 Fotnotrendering

Normalisera taggar till Markdown-länkar som [1]([f5-123-165]), rendera som numrerade fotnoter; deduplicera samma position för att undvika UI-klutter.

7.2 Klickinteraktion

  1. Första klick: Tolka [f…] → fileIndex + offsets → extrahera Spine-text → förhandsgranska (valfri TOC-titel);
  2. Samma fotnot igen: Stäng förhandsgranskning;
  3. Bekräfta hopp: Öppna läsvy, markera teckenintervall.

Från kopierad modelltagg till användarsynlig källa passerar kedjan aldrig genom ytterligare LLM-anrop—deterministisk och reproducerbar.


VIII. Kantfall och ärlig degradering

Noll hallucination ≠ «alltid finns svar»—det betyder ingen evidens, ingen fabricering:

ScenarioBeteende
Segment-sammanfattningar inte klaraExtrahera full text och sammanfatta först
Tool hittar ingetReturnera (No relevant segment excerpts found…); modellen ska säga att det inte finns i boken
Ogiltiga tvådelade taggar från modellFrontend filtrerar; inga trasiga fotnoter
VardagschattSystem Prompt tillåter allmän kunskap utanför boken
Exportera chattFotnoter kan bli läsar-deep links för delning/arkiv

Chattexport


IX. Designavvägning: Varför inte «vector RAG»?

Kollegor som bygger dokument-Q&A frågar ofta: om ni gör retrieval-augmented generation, varför inte Embedding + vector DB Top-K?

Vi gör RAG—hämta före generera. Skillnaden: «RAG» i community-språk innebär ofta vektorsimilaritet; vårt steg 3 är segmentindex + Tool med källhämtning på begäraningen vektorlager medvetet. Nedan arkitekturskäl, inte förnekande av vector RAG:s värde.

Omfattning: inte «ingen retrieval», utan «ingen vektorretrieval»

  • Bred RAG: hämta → generera → vi gör detta;
  • Vector RAG: recall via embedding-similaritet → inte i denna version.

Förbehandling bygger segment-sammanfattningsindex; modellen väljer segment via Tools och får källtext. Retrieval finns utan separat embedding-modell och vektorindex-underhåll.


Skäl 1: Anpassade LLM-providers—håll integrationsytan liten

Användare kan koppla egna API-nycklar, anpassade base URLs eller lokal Ollama—chattmodell är deras val; kostnad och datapath under kontroll.

Typisk vector RAG vidgar integration:

  • Utöver chattmodell behövs oftast embedding-modell (annat namn, ibland annan endpoint);
  • Lokal Ollama behöver separat embedding-modell plus dimension/API-kompatibilitet;
  • Fler fel lägen: chatt OK men tom retrieval—embedding, index eller dimensionsmismatch; svårare att felsöka än en provider end-to-end.

Här delar segmentval och svar samma provider-konfiguration—inget «chatt på A, index på B». För utbytbar LLM-appar slår det ofta några recall-poäng.

Anpassade AI-providers


Skäl 2: Embeddings binder till index—providerbyte är dyrt

I vector RAG är vektorer inte universellt mellanformat—de är koordinater under en embedding-modell. Index med A, fråga med B: similaritet är vanligt inte jämförbar—ofta full om-embedding, och dimensioner (768 / 1024 / 1536 …) låser lagringsschema.

Steg 3 persisterar strukturerade sammanfattningar + tecken-spans, inte vektorer; byte av chattmodell bygger inte om index; evidenskedja (källpositioner) oförändrad—i linje med «prova olika LLM när som helst».


Skäl 3: Strukturerad routing räcker ofta för TOC-tunga långa dokument

E-böcker och PDF har vanligtvis kapitelstruktur; förbehandling ger segmenttitlar + sammanfattningar. För «vad säger kapitel X» eller «hur definierar boken Y» fungerar val från katalog och källhämtning bra i praktiken; Tool returnerar källa med [f…], noll hallucination förankras i tecken-spans.

Vektorer hjälper vid diffus semantik, flerspråk, långspann literal mismatch; för läsare med TOC + förbehandling + stark spårbarhet ger investering i Tool + källretur + citeringsregler ofta bättre ROI.


Framtid: Hybrid recall, inte omskrivning

Vi kan lägga till grov vektorrecall (embedding bara för Top-N kandidatkapitel), fortfarande slut i välj segment → källa → klickbart spår—noll-hallucination-regler oförändrade. Om tillagt: embedding valfritt, tydliga om-indexera-meddelanden vid modellbyte—undvik tyst fel retrieval.

Tills dess: vilken OpenAI-kompatibel chatt-API som helst fungerar; byte av chattmodell bygger inte om lokalt index.


X. Sammanfattning

StegMetodRoll
FörbehandlaDela efter TOC/längd + segment-sammanfattningscacheLånga böcker sökbara och lokaliserbara
Positionstaggar[fFil-start-slut] i källaMaskintolkbar härkomst
Tool retrievalSegment per fråga / helbokssammanfattningar, returnera källaTvinga evidens före svar
System PromptBok först, inga falska taggar, säg när saknasBegränsa generering
FrontendFotnot → förhandsgranska → hoppa och markeraAnvändare verifierar evidens
Ingen vektorretrievalEn provider; byt chattmodell utan om-indexLägre integrations- och migrationskostnad

«Noll hallucination» betyder inte att modellen aldrig felar—det betyder teknik som låser utdata till evidenskedja: ingen retrieval → utge sig inte för bokinnehåll; med retrieval → ge verifierbara källpositioner.

Om du bygger AI-läsning eller dokument-Q&A hoppas vi att vägen hel dump → nyckelmeningar → Tool-first på begäran, plus inline positionstaggar + källretur, är en användbar referensimplementation.

Detta är lärdomar från att bygga Foxycape AI-läsare—endast som referens. Prova läsaren på nedladdningssidan.