我是如何实现阅读器「零幻觉」问答的

本文分享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 ,相对阶段二,核心变化有三点:

  1. 预处理产物是结构化索引(目录级摘要 + 精确字符 span),而不是把摘录直接当作问答 Context;
  2. 每次提问由模型通过 Tool Calling 按需检索 ,再 拉取带位置标记的原文 作答;
  3. System Prompt 与前端联动,约束引用格式,并支持点击角标跳转、高亮原文。

三阶段对比:

维度 阶段一(全文直塞) 阶段二(关键句提取) 阶段三(当前)
单次提问 Context 全书(或截断后的前半本) 预提取关键句合集 仅与问题相关的少量 原文 片段
长书准确性 超 40 万字符后严重下降 依赖提取质量,易丢细节 按目录/span 检索,不受全书长度硬截断
响应速度 略好,长书仍慢 检索 + 短 Context,明显更快
Token 成本 极高 中等偏高 预处理摊销 + 按需付费
溯源能力 弱(难标注出处) 有位置标记,但内容已是二次筛选 角标对应 真实原文 span
工程复杂度

为何停在阶段三: 阅读场景的零幻觉,关键不是「让模型看过尽量多的字」,而是 「作答前必须拿到与问题相关的原文证据」 。阶段一、二都在 Context 体积 上做文章;阶段三把链路拆成 「索引(预处理)→ 检索(Tool)→ 取证(原文)→ 作答(约束生成)」,才同时兼顾准确率、成本与可溯源性。

下文展开 阶段三 的实现细节。


二、问题定义:阅读场景下,幻觉比普通 Chat 更致命

普通 ChatBot 偶发错误,用户往往可以容忍。但在 书籍 QA 里,幻觉的代价更高:

  • 用户问的是 这本书 说了什么,不是问模型的 parametric memory;
  • 一句似是而非的「书中观点」,可能误导笔记、引用甚至二次传播;
  • 没有出处,用户无法核实,产品信任很难建立。

因此,「零幻觉」在工程上落地为三条 可执行 的规则:

  1. 书内问题必须先查书:凡可能与当前书籍相关的问题,模型必须先走检索(Tool),再组织答案;
  2. 答案必须可溯源:关键结论附带原文位置标记,前端可解析并跳转高亮;
  3. 查不到就说查不到:书中没有的内容应明确告知,而不是用通用知识冒充「书中观点」。

下文按 阶段三 的数据流,说明上述规则如何落地。


三、整体架构:预处理 → 工具检索 → 约束生成 → 可点击溯源

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,核心五条:

  1. 标准格式 :必须使用 [f_fileIndex-startChar-endChar],三段数字缺一不可;
  2. 只引用当前来源 :角标须 原样复制 自本轮 System/User 消息或 Tool 返回文本中的标记;
  3. 禁止伪造:不得自行计算、修改或编造位置;
  4. 宁缺毋滥 :当前上下文没有合法标记时,正常作答即可,不要输出任何位置标记
  5. 紧跟论述:标记须紧跟相关句段,禁止在文末堆砌引用清单。

前端展示前还会过滤模型偶发输出的 两段位 非法标记(如 [f1-293]),避免无效角标进入 UI。


六、Tool Calling:先检索,再回答

当对话绑定某本书(存在 resourceId,且 chatType === 'chat')时,每次生成前会向模型注册两个 Tool,并挂载对应的 executor。整体遵循 OpenAI 兼容的 function calling 循环

适用于:概念、人物、情节、章节细节等 有明确检索意图 的问题。

流程简述:

  1. 模型将用户口语 改写为书中可能出现的术语(System Prompt 中的「Optimize Search Queries」);
  2. 调用 Tool,传入 question
  3. 将所有片段摘要按 Token 预算 分批(单批约 3 万 Token,最多 5 批);
  4. 每批发起一次 独立的 LLM 请求 ,从 { id, title, summary } 列表中选出相关片段 ID(最多 5 个),返回 JSON,形如 {"Thinking":"...","answer":["1","3"]}
  5. 根据选中 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 点击交互

  1. 首次点击 :解析 [f...] → 取 fileIndex 与字符偏移 → 从 Spine 原文提取文本 → 弹出预览(可带目录标题);
  2. 再次点击同一角标:关闭弹窗;
  3. 确认跳转:打开阅读视图,按字符区间高亮。

从模型复制的标记到用户看到的原文,中间 不经 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 阅读器实践心得,仅供参考。

相关推荐
孟郎郎1 小时前
TimeoutError: The operation was aborted due to timeout at new DOMException
ai·前端框架·npm·vue·pnpm·deepseek
Jing_jing_X1 小时前
我用 Claude Code 搭了一个远程 Claude web:手机发指令,本地电脑自己写代码
ai·agent·个人开发·ai应用开发
z202305081 小时前
RDMA之DCQCN (14)
linux·服务器·网络·人工智能·ai
searchforAI2 小时前
网盘视频转文字后,如何高效做笔记并长期归档?
人工智能·笔记·学习·ai·音视频·语音识别·网盘
腾视科技AI2 小时前
企业调研——工业边缘计算隐形黑马,腾视科技以“硬件+算法”加速出海落地
大数据·人工智能·科技·ai·边缘计算·无人叉车·ainas
sleven fung2 小时前
GPT4All 本地大语言模型运行环境介绍
python·gpt·ai·langchain
VIP_CQCRE2 小时前
Localization Translate API 集成与使用指南
ai
格桑阿sir2 小时前
14-大模型智能体开发工程师:ReAct推理-行动框架
ai·大模型·llm·agent·react·智能体·推理模型
weixin_468466852 小时前
通义千问核心能力与实战表现深度评测
人工智能·深度学习·算法·ai·大模型