
本文分享 AI 閱讀器 零幻覺問答 的工程實作:回答嚴格基於當前書籍原文,關鍵論述可 一鍵溯源 到具體段落。若你也在做 AI 閱讀、文件 QA 或 RAG 類應用,希望三次迭代的經驗與最終架構能有所參考。
一、實踐歷程:三個階段的演進
零幻覺問答並非一開始就設計完備,而是在 成本、延遲和準確率 的拉扯中逐步演進。以下依時間順序回顧三個階段,便於理解當前架構為何長成這樣。
階段一:全文直塞 Context(最簡單,也最先暴露問題)
做法: 使用者開啟一本書提問時,將提取出的 全部正文 放進 System Prompt 或 User 訊息,交給對話模型作答。若全書超過約 40 萬字元,則 硬截斷——只保留前面一段,後續章節對模型不可見。
優點:
- 實作成本極低,幾乎不需要預處理;
- 短書、結構簡單的文件效果尚可——模型確實「看到了整本書」;
- 互動簡單:問就能答,沒有「請先等待分析」的等待狀態。
缺點(很快變得不可接受):
- 回應慢:每次提問都要把海量文字送進模型,首 Token 延遲和總耗時隨書長線性惡化;
- Token 成本高:同一本書每問一次就重複付一遍全文的輸入費用;
- 長書嚴重失真:超過 40 萬字元後被截斷,後半本、附錄、結論章節等於不存在,且 UI 往往 沒有明確告知 已截斷;
- 檢索粒度為零:模型要在幾十萬字裡「大海撈針」,容易漏細節,也更容易產生 看似合理、實則無據 的概括——閱讀場景最忌諱這類幻覺。
階段一適合驗證 MVP,不適合作為產品級方案。
階段二:用輕量 LLM 提取關鍵句(壓縮 Context,但壓得太狠)
做法: 在提問前(或首次開啟書時),用 成本更低的模型 對正文做一輪預處理:依 Spine 分章(或整書分段),抽取 關鍵句,輸出時保留 [f檔案-起始-結束] 形式的位置標記,再將摘錄拼成較短文字,作為後續問答的 Context。
典型鏈路是 Extract → Cache → Chat:先離線或按需跑一遍提取並落庫,之後每次提問複用同一份「關鍵句合集」。這與許多文件 QA 原型裡「先壓縮文件、再拿壓縮結果做 QA」的思路相同,也是我們在階段二實際採用過的路線。
優點:
- 每次提問送入模型的文字 明顯縮短,單次 Token 消耗較階段一顯著下降;
- 預處理結果可快取,同一本書不必每次提問都重新提取;
- 已引入位置標記,為後續溯源打下基礎。
缺點(長書場景下依然扛不住):
- 細節大量丟失:「關鍵句」由模型主觀篩選,論證鏈上的限定條件、反例等容易被丟掉,答案容易「正確但片面」;
- 長書 Context 仍然偏大:大部頭作品即便只留關鍵句,拼接後的輸入依然可觀,延遲和成本只是緩解,沒有根治;
- 雙重 LLM 誤差:提取階段可能漏選,問答階段又可能誤讀摘錄,錯誤會 疊加;
- 靜態 Context:無論使用者問的是某一章細節還是全書結構,送進模型的都是 同一份預提取文字,無法依問題動態收窄範圍。
這一階段的教訓很明確:問題不在「有沒有壓縮」,而在「壓縮是否按需、以及能否回到原文」。
階段三:片段索引 + Tool 按需檢索 + 原文回傳(當前方案)
做法: 基本思路參考了 PageIndex,相對階段二,核心變化有三點:
- 預處理產物是結構化索引(目錄級摘要 + 精確字元 span),而不是把摘錄直接當作問答 Context;
- 每次提問由模型透過 Tool Calling 按需檢索,再 拉取帶位置標記的原文 作答;
- System Prompt 與前端聯動,約束引用格式,並支援點擊角標跳轉、高亮原文。
三階段對比:
| 維度 | 階段一(全文直塞) | 階段二(關鍵句提取) | 階段三(當前) |
|---|---|---|---|
| 單次提問 Context | 全書(或截斷後的前半本) | 預提取關鍵句合集 | 僅與問題相關的少量 原文 片段 |
| 長書準確性 | 超 40 萬字元後嚴重下降 | 依賴提取品質,易丟細節 | 依目錄/span 檢索,不受全書長度硬截斷 |
| 回應速度 | 慢 | 略好,長書仍慢 | 檢索 + 短 Context,明顯更快 |
| Token 成本 | 極高 | 中等偏高 | 預處理攤銷 + 按需付費 |
| 溯源能力 | 弱(難標註出處) | 有位置標記,但內容已是二次篩選 | 角標對應 真實原文 span |
| 工程複雜度 | 低 | 中 | 高 |
為何停在階段三: 閱讀場景的零幻覺,關鍵不是「讓模型看過盡量多的字」,而是 「作答前必須拿到與問題相關的原文證據」。階段一、二都在 Context 體積 上做文章;階段三把鏈路拆成 「索引(預處理)→ 檢索(Tool)→ 取證(原文)→ 作答(約束生成)」,才同時兼顧準確率、成本與可溯源性。
下文展開 階段三 的實作細節。
二、問題定義:閱讀場景下,幻覺比普通 Chat 更致命
普通 ChatBot 偶發錯誤,使用者往往可以容忍。但在 書籍 QA 裡,幻覺的代價更高:
- 使用者問的是 這本書 說了什麼,不是問模型的 parametric memory;
- 一句似是而非的「書中觀點」,可能誤導筆記、引用甚至二次傳播;
- 沒有出處,使用者無法核實,產品信任很難建立。
因此,「零幻覺」在工程上落地為三條 可執行 的規則:
- 書內問題必須先查書:凡可能與當前書籍相關的問題,模型必須先走檢索(Tool),再組織答案;
- 答案必須可溯源:關鍵結論附帶原文位置標記,前端可解析並跳轉高亮;
- 查不到就說查不到:書中沒有的內容應明確告知,而不是用通用知識冒充「書中觀點」。
下文依 階段三 的資料流,說明上述規則如何落地。
三、整體架構:預處理 → 工具檢索 → 約束生成 → 可點擊溯源
核心思路可概括為:不讓模型「憑記憶答題」,而是讓它「先取證、再作答、並標註出處」。
四、預處理:把整本書變成可檢索的「片段索引」
若每次提問仍採用 階段一 的全文 Context,長書必然爆 Token,檢索粒度也過粗。階段三的解法是:使用者首次對某本書發起 AI 對話時,背景非同步跑 片段摘要任務,依 目錄結構 或 文字長度 將全書切成若干 Segment,為每個片段產生摘要,並持久化到本機 IndexedDB。
每個 Segment 在資料結構上包含摘要與 正文物理位置:
| 欄位 | 含義 |
|---|---|
startFileIndex / endFileIndex | Spine 檔案索引(PDF 則每頁一個檔案) |
startOffset / endOffset | 字元級起迄偏移 |
sequence | 線性閱讀順序 |
title | 對應目錄標題 |
切分策略兼顧精度與成本:單一目錄正文不超過約 20KB 時只摘要該節點;同級目錄會合併成批(15KB~20KB)再呼叫 LLM;無目錄的大塊正文則依 3~4 萬字元區間切段。
摘要生成時的 System Prompt 會要求 保留原文位置標記(格式 [f數字-數字-數字]),以便後續 Tool 回傳原文時,位置資訊與 spine 字元偏移一致。核心約束如下:
若摘要內容與原文某段相關,須保留段末位置資訊,格式 [f數字-數字-數字](如 [f1-90-109])。
位置標記是整體,禁止修改、合併或省略其中的任何字元或數值。
預處理完成後,問答不再依賴「整書 Context」,而是依賴 結構化片段索引——這是長書場景下零幻覺的工程前提。
五、位置標記體系:把「出處」編碼進文字
零幻覺不僅要求內容來自原文,還要求 出處可機器解析、可在 UI 中跳轉。我們採用內嵌位置標記:
[f{fileIndex}-{startChar}-{endChar}]
例如 [f5-123-165] 表示:第 5 個 Spine 檔案(從 0 起算)中,字元偏移 123~165 的文字區間。
5.1 標記如何寫入正文
正文提取層在輸出片段時,為每個小段在段末寫入 [f{fileIndex}-{start}-{end}]。示意:
const position = `[f${fileIndex}-${absOffset}-${absOffset + segment.length}]`;
fileLines.push(segment.text.trim() + position);
無論是預處理摘要還是 Tool 回傳的原文摘錄,位置資訊都與 Spine 字元偏移 對齊,而不是讓模型「估算頁碼」。
5.2 對模型輸出的約束
組裝 System Prompt 時,我們單獨約定了 Position Citation Rules,核心五條:
- 標準格式:必須使用
[f_fileIndex-startChar-endChar],三段數字缺一不可; - 只引用當前來源:角標須 原樣複製 自本輪 System/User 訊息或 Tool 回傳文字中的標記;
- 禁止偽造:不得自行計算、修改或編造位置;
- 寧缺毋濫:當前上下文沒有合法標記時,正常作答即可,不要輸出任何位置標記;
- 緊跟論述:標記須緊跟相關句段,禁止在文末堆砌引用清單。
前端展示前還會過濾模型偶發輸出的 兩段位 非法標記(如 [f1-293]),避免無效角標進入 UI。

六、Tool Calling:先檢索,再回答
當對話綁定某本書(存在 resourceId,且 chatType === 'chat')時,每次生成前會向模型註冊兩個 Tool,並掛載對應的 executor。整體遵循 OpenAI 相容的 function calling 迴圈。
6.1 get_related_segment_summaries —— 針對具體問題查片段
適用於:概念、人物、情節、章節細節等 有明確檢索意圖 的問題。
流程簡述:
- 模型將使用者口語 改寫為書中可能出現的術語(System Prompt 中的「Optimize Search Queries」);
- 呼叫 Tool,傳入
question; - 將所有片段摘要依 Token 預算 分批(單批約 3 萬 Token,最多 5 批);
- 每批發起一次 獨立的 LLM 請求,從
{ id, title, summary }清單中選出相關片段 ID(最多 5 個),回傳 JSON,形如{"Thinking":"...","answer":["1","3"]}; - 依選中 Segment 的 span,從 Spine 拉取帶位置標記的原文(不是摘要),作為 Tool 結果回傳。
關鍵設計:Tool 回傳原文,而非摘要。 模型作答時看到的是真實段落 + 內嵌 [f…],避免「摘要 → 再概括」帶來的漂移。
6.2 get_full_book_segment_summaries —— 全書概覽類問題
適用於:「總結全書」「點評這本書」「整體結構/主題」等 需要全局視野 的問題。
依閱讀順序拼接所有片段的 summary 回傳,避免逐段相關度篩選遺漏關鍵章節。
6.3 System Prompt:書優先、工具優先
綁定書籍時,System Prompt 注入 Core Principles for Reading Assistant,核心三條:
1. Book First, Tool First
- 任何可能與書籍相關的問題,必須先呼叫工具檢索;
- 答案必須主要依據檢索結果,禁止不檢索就編造「書中內容」。
2. General Knowledge as Fallback Only
- 僅當:純閒聊 / 使用者明確要求不用書 / 工具無結果時,才可使用通用知識;
- 若書中沒有,必須先聲明「書中未提及此內容」,再補充通用知識。
3. Direct Style
- 直入主題,禁止「根據提供的材料…」「綜上所述…」等套話。
生成層實作標準 Tool 迴圈:tool_calls → 執行 executor → 追加 role: tool → 繼續請求,直到輸出最終文字。啟用 tools 時關閉 thinking 通道,避免與 function call 協定衝突。
七、前端溯源:從角標到原文高亮
模型輸出的 [f5-123-165] 不會直接展示,在渲染層轉為可點擊引用。
7.1 角標渲染
展示前將位置標記規範化為 Markdown 連結,例如 [1]([f5-123-165]),再渲染為序號角標;同一位置多次出現時可去重,避免 UI 堆疊。
7.2 點擊互動
- 首次點擊:解析
[f…]→ 取 fileIndex 與字元偏移 → 從 Spine 原文提取文字 → 彈出預覽(可帶目錄標題); - 再次點擊同一角標:關閉彈窗;
- 確認跳轉:開啟閱讀視圖,依字元區間高亮。
從模型複製的標記到使用者看到的原文,中間 不經 LLM 二次加工,溯源鏈路全程 確定、可重現。
八、邊界情況與誠實降級
零幻覺不等於「永遠有答案」,而是 沒有證據時不瞎編:
| 場景 | 行為 |
|---|---|
| 片段摘要尚未產生 | 先提取全文做摘要 |
| Tool 檢索無結果 | 回傳 (No relevant segment excerpts found…),模型應聲明書中未提及 |
| 模型輸出了非法兩段位標記 | 前端過濾,不展示無效角標 |
| 使用者純閒聊 | System Prompt 允許脫離書籍,用通用知識回答 |
| 匯出對話 | 可將角標轉為閱讀器深連結,便於分享或歸檔 |

九、設計取捨:為什麼不用「向量 RAG」?
做文件 QA 的同行常會問:既然要做檢索增強,為什麼不走 Embedding + 向量庫 Top-K 這條標準路線?
實際上 我們也在做 RAG——每次回答前都會先查書、再生成。差別在於:社群語境裡的 RAG 往往預設包含 向量化與相似度檢索;當前方案是 「片段索引 + Tool 按需拉原文」(階段三),刻意不引入向量層。以下從 架構約束 說明取捨,並非否定向量 RAG 的價值。
界定範圍:不是不用檢索,而是不用「向量檢索」
- 廣義 RAG:檢索相關材料 → 再生成 → 我們在做。
- 向量 RAG:召回依賴 Embedding 相似度 → 當前版本不做。
全書預處理為 片段摘要索引;提問時模型透過 Tool 選段,再 回傳原文。檢索增強存在,但不依賴單獨的 embedding 模型與向量索引維護。
原因一:支援自訂 LLM Provider,設定鏈路要盡量短
產品允許使用者自由接入 自有 API Key、自訂 Base URL,或使用 本機 Ollama——對話模型由使用者自選,成本和資料路徑可控。這對許多自託管、多模型對比的場景是硬需求。
疊加典型向量 RAG 後,整合面會明顯變寬:
- 除 Chat 模型 外,通常還需 Embedding 模型(另一個 model name,有時還是另一個 endpoint);
- Ollama 等本機部署還要單獨拉 embedding 模型,並處理維度、介面相容;
- 故障域變複雜:Chat 正常但 檢索為空 時,可能是 embedding、索引或維度不一致,排查成本高於「單 Provider 全鏈路」。
當前方案裡,選段與作答共用同一套 Provider 設定,避免「Chat 用 A、建索引用 B」。若你在做 可插拔 LLM 的應用,這往往比多幾個點的召回率更重要。

原因二:Embedding 與索引強綁定,切換 Provider 成本高
向量 RAG 裡常被低估的一點:向量不是通用中間格式,而是某個 embedding 模型下的座標。 建庫用模型 A、查詢用模型 B 時,相似度通常 不可比——換模型往往意味著 全書重新向量化,且不同模型的 向量維度(768 / 1024 / 1536 …)會綁死儲存 schema。
階段三持久化的是 結構化摘要 + 字元 span,不存向量;切換 Chat 模型時 無需重建索引,證據鏈(原文位置)不變。這與「使用者隨時對比不同 LLM」的目標更一致。
原因三:有目錄的長文件,結構化路由往往已夠用
電子書、PDF 通常有 章節結構;預處理已產出 段標題 + 摘要。對「某一章講了什麼」「書中如何定義某概念」類問題,在摘要目錄上選段再 拉回原文,實務中效果穩定;且 Tool 回傳的是 帶 [f…] 的原文,零幻覺仍錨定在字元 span 上。
向量檢索在語意模糊、跨語言、長段落字面匹配等場景仍有優勢;在 有 TOC、可預處理、要強溯源 的閱讀器裡,優先把複雜度放在 Tool + 原文回傳 + 引用約束 上,ROI 通常更高。
後續方向:混合召回,而非推倒重來
不排除將來增加 向量粗召回(例如 embedding 只篩 Top-N 候選章節),最終仍走 選段 → 原文回傳 → 可點擊溯源,零幻覺規則不變。若引入,會盡量滿足:Embedding 可選、換模型時 顯式提示重建索引,避免 silent wrong retrieval。
在此之前,優先保證:任意 OpenAI 相容 Chat API 即可工作,換 Chat 模型不必重建本機索引。
十、小結
| 環節 | 手段 | 作用 |
|---|---|---|
| 預處理 | 依目錄/長度切分 + 片段摘要快取 | 長書可檢索、可定位 |
| 位置標記 | [f檔案-起始-結束] 寫入原文 | 出處可機器解析 |
| Tool 檢索 | 依問題查片段/全書摘要,回傳 原文 | 作答前強制取證 |
| System Prompt | 書優先、禁止偽造角標、查不到要說 | 約束生成行為 |
| 前端溯源 | 角標 → 預覽 → 跳轉高亮 | 使用者可核驗證據 |
| 不用向量檢索 | 單 Provider、換 Chat 模型無需重建索引 | 降低整合與遷移成本 |
「零幻覺」不是指望模型從不犯錯,而是 用工程結構把輸出鎖在證據鏈上:沒有檢索結果就不應冒充書中內容;有檢索結果則應給出可核驗的原文位置。
若你也在做 AI 閱讀或文件 QA,希望 全文直塞 → 關鍵句提取 → Tool-First 按需檢索 這條演進路徑,以及 內嵌位置標記 + 原文回傳 的做法,能作為可參考的一種實作。
