
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.
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:
- Förbehandling ger strukturerat index (innehållsförteckningssammanfattningar + exakta tecken-spans), inte utdrag som direkt Q&A-kontext;
- Varje fråga använder Tool Calling för retrieval på begäran, sedan hämtar källtext med positionstaggar för svar;
- System Prompt + frontend tvingar citeringsformat och stödjer klick-till-hopp med markering i läsaren.
Jämförelse av tre steg:
| Dimension | Steg 1 (hel dump) | Steg 2 (nyckelmeningar) | Steg 3 (nuvarande) |
|---|---|---|---|
| Kontext per fråga | Hela boken (eller trunkerad första halva) | För-extraherade nyckelmeningar | Endast käll-utdrag relevanta för frågan |
| Träffsäkerhet långa böcker | Kollapsar efter ~400k tecken | Beror på extrahering; tappar detaljer | Hämta via TOC/span; ingen hård helbokstrunkering |
| Svarshastighet | Långsam | Något bättre; långa böcker fortfarande långsamma | Retrieval + kort kontext—märkbart snabbare |
| Tokenkostnad | Mycket hög | Medelhög | Amorterad förbehandling + betala vid behov |
| Spårbarhet | Svag (svårt att citera) | Taggar finns men innehåll redan filtrerat | Fotnoter mappar till riktiga käll-spans |
| Teknisk komplexitet | Låg | Medel | Hö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:
- 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;
- Svar måste vara spårbara: Viktiga påståenden bär positionstaggar som UI kan tolka och hoppa till;
- 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
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ält | Betydelse |
|---|---|
startFileIndex / endFileIndex | Spine-filindex (PDF: en fil per sida) |
startOffset / endOffset | Tecken start/slut |
sequence | Linjär läsordning |
title | TOC-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:
- Standardformat: Måste använda
[f_fileIndex-startChar-endChar]; alla tre numeriska delar krävs; - Kopiera endast från aktuella källor: Fotnoter ordagrant från denna rundas system/user-meddelanden eller Tool-returer;
- Ingen fabricering: Beräkna, redigera eller hitta inte på positioner;
- Föredra utelämnande: Finns ingen giltig tagg i kontext, svara normalt—skriv inga positionstaggar;
- 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.

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.
6.1 get_related_segment_summaries — Riktad segmentuppslagning
För: begrepp, figurer, handling, kapiteldetaljer—tydlig retrieval-avsikt.
Flöde:
- Modellen skriver om användarens formulering till termer som sannolikt finns i boken («Optimize Search Queries» i System Prompt);
- Anropa Tool med
question; - Batcha alla segment-sammanfattningar per tokenbudget (~30k tokens per batch, max 5 batchar);
- Varje batch: separat LLM-begäran väljer relevanta segment-ID (max 5) från
{ id, title, summary }, JSON som{"Thinking":"...","answer":["1","3"]}; - 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
- Första klick: Tolka
[f…]→ fileIndex + offsets → extrahera Spine-text → förhandsgranska (valfri TOC-titel); - Samma fotnot igen: Stäng förhandsgranskning;
- 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:
| Scenario | Beteende |
|---|---|
| Segment-sammanfattningar inte klara | Extrahera full text och sammanfatta först |
| Tool hittar inget | Returnera (No relevant segment excerpts found…); modellen ska säga att det inte finns i boken |
| Ogiltiga tvådelade taggar från modell | Frontend filtrerar; inga trasiga fotnoter |
| Vardagschatt | System Prompt tillåter allmän kunskap utanför boken |
| Exportera chatt | Fotnoter kan bli läsar-deep links för delning/arkiv |

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äran—ingen 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.

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
| Steg | Metod | Roll |
|---|---|---|
| Förbehandla | Dela efter TOC/längd + segment-sammanfattningscache | Långa böcker sökbara och lokaliserbara |
| Positionstaggar | [fFil-start-slut] i källa | Maskintolkbar härkomst |
| Tool retrieval | Segment per fråga / helbokssammanfattningar, returnera källa | Tvinga evidens före svar |
| System Prompt | Bok först, inga falska taggar, säg när saknas | Begränsa generering |
| Frontend | Fotnot → förhandsgranska → hoppa och markera | Användare verifierar evidens |
| Ingen vektorretrieval | En provider; byt chattmodell utan om-index | Lä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.
