
本文分享AI阅读器 零幻觉问答 的工程实现:回答严格基于当前书籍原文,关键论述可 一键溯源 到具体段落。如果你也在做 AI 阅读、文档 QA 或 RAG 类应用,希望三次迭代的经验与最终架构能有所参考。
一、实践历程:三个阶段的演进
零幻觉问答并非一开始就设计完备,而是在 成本、延迟和准确率 的拉扯中逐步演进的。下面按时间顺序回顾三个阶段,便于理解当前架构为何长成这样。
flowchart LR P1阶段一:全文直塞 --> P2阶段二:LLM 提取关键句 P2 --> P3阶段三:片段索引 + Tool 检索 P1 -.->|慢、贵、长书不准| X1淘汰 P2 -.->|丢细节、仍偏慢| X2淘汰 P3 -->|当前方案| OK零幻觉 + 可溯源
阶段一:全文直塞 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),再组织答案;
- 答案必须可溯源:关键结论附带原文位置标记,前端可解析并跳转高亮;
- 查不到就说查不到:书中没有的内容应明确告知,而不是用通用知识冒充「书中观点」。
下文按 阶段三 的数据流,说明上述规则如何落地。
三、整体架构:预处理 → 工具检索 → 约束生成 → 可点击溯源
flowchart TB subgraph prep 离线/首次预处理 A按目录或长度切分全书 --> BLLM 生成片段摘要 B --> C本地持久化 Segment 缓存 end subgraph ask 用户提问 D用户输入问题 --> E{已有 Segment 缓存?} E -->|否| F提取全文 / 询问是否预处理 F --> prep E -->|是| G注册 Tool Calling end subgraph retrieve 工具检索 G --> H{问题类型} H -->|全书概览/书评| Iget_full_book_segment_summaries H -->|具体事实/人物/章节| Jget_related_segment_summaries J --> KLLM 从摘要目录中选相关片段 ID K --> L按 span 拉取原文 + 位置标记 I --> M拼接全书片段摘要 end subgraph answer 生成与展示 L --> NTool 结果回传模型 M --> N N --> OSystem Prompt 约束引用格式 O --> P流式输出答案 + 位置角标 P --> Q渲染可点击引用角标 Q --> R点击 → 预览原文 → 跳转高亮 end
核心思路可以概括为:不让模型「凭记忆答题」,而是让它「先取证、再作答、并标注出处」。
四、预处理:把整本书变成可检索的「片段索引」
若每次提问仍采用 阶段一 的全文 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 字符偏移一致。核心约束如下:
text
如果总结内容与原文某段相关,须保留段末位置信息,格式 [f数字-数字-数字](如 [f1-90-109])。
位置标记是整体,禁止修改、合并或省略其中的任何字符或数值。
预处理完成后,问答不再依赖「整书 Context」,而是依赖 结构化片段索引------这是长书场景下零幻觉的工程前提。
五、位置标记体系:把「出处」编码进文本
零幻觉不仅要求内容来自原文,还要求 出处可机器解析、可在 UI 中跳转。我们采用内联位置标记:
[f{fileIndex}-{startChar}-{endChar}]
例如 [f5-123-165] 表示:第 5 个 Spine 文件(从 0 起算)中,字符偏移 123~165 的文本区间。
5.1 标记如何写入正文
正文提取层在输出片段时,为每个小段在段末写入 [f{fileIndex}-{start}-{end}]。示意:
typescript
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 按需检索 这条演进路径,以及 内联位置标记 + 原文回传 的做法,能作为可参考的一种实现。
以上是我们在开发令狐兄(Foxycape)AI 阅读器实践心得,仅供参考。