如何确保回答有依据
只有在不会胡编乱造的情况下,文档中的助手才值得使用。带着自信语气给出错误答案,其代价比完全没有助手更高:读者会据此采取行动,而你的支持队列会在一天后承担后果。
所以真正重要的问题不是“它是否使用 AI”——而是模型可以基于什么内容回答,以及当正确的段落不在它面前时会发生什么。本页面将从阅读实现代码所能了解的详细程度,给出答案。
你将获得什么#
读者看到的每个答案,都是根据 Docsbook 针对该问题在本次请求中获取的页面文本撰写的。读者可以看到这一过程:小组件会输出 Found N results,并为每个打开的页面输出一行 Reading <page>,每一行都是指向页面本身的链接。答案下方会列出引用;只有当模型在自己的答案中内联引用了该页面的路径,或服务器确实为此问题获取了该页面时,该引用才会保留。模型编造但从未引用的路径,无法成为引用。
当检索不到任何相关内容时,系统会指示模型说明文档未涵盖该内容,而不是编写看似合理的内容。这种拒答会被记录下来,而不会被忽略——它会成为“未回答问题”报告中的一行,并生成一个 chat.no_answer webhook,向你指示下一步应编写哪个页面。
答案是如何生成的?#
七个阶段,依次进行。下面的每个阶段都是请求中的实际分支,而不是示意图。
1. 您的文档会被拆分成单元,然后嵌入#
自动索引以标题粒度运行:页面的每个章节对应一个单元。发送给嵌入模型的文本是该章节的标题面包屑,后面跟着章节正文——Billing > Refunds 位于退款文本之前——因为名为“限制”的章节在“Webhooks”下和在“AI 聊天”下含义不同,而向量必须携带这种差异。每个单元最多包含6,000 个字符。索引器也支持页面级和行级粒度;自动路径使用标题粒度。
一个单元的标识由其页面路径和标题锚点组成。锚点在页面内会重复——一个变更日志可能包含四十个 ### Fixed 标题——因此重复的锚点会获得序号后缀。否则,这些章节中的每一个都会被视为同一个单元,并导致写入冲突。
2. 单元变为向量,未变更的单元无需成本#
嵌入向量为 openai/text-embedding-3-small,维度为1,536,通过 OpenRouter 请求,每次调用批量处理96 个单元。存储列是一个 vector(1536),如果模型返回的宽度不同,系统会直接拒绝,而不是先写入,之后再悄无声息地产生不匹配。
每个单元都携带一个内容哈希值 sha256(model + NUL + text)。重新索引时,哈希值已存储的单元会被跳过,因此编辑一个页面只需为一个页面生成嵌入向量,而不是整个语料库。运行前显示给你的估算包含一个精确的单元数量(分割器是确定性的,并且已经运行)以及一个 characters ÷ 4 的近似令牌数量——请将该数值视为 ±30% 的范围,这也是它被呈现为估算值而非价格的原因。
3. 重新索引跟随您的提交#
您对文档的每次提交都会排队等待重新索引。排队的运行每两分钟由后台运行程序获取一次,而不是由附加到已发送响应的回调来处理——回调正是过去会在执行过程中中途失效、留下带有时间戳却为空的索引的东西。五分钟内的连续提交会合并为一次运行,因为发布流程会先提交内容,然后提交导航,最后提交品牌信息。
是否使用语义检索取决于是否存在向量,从不取决于“上次索引”时间戳。时间戳只是一种声明;一行记录才是证据,而这正是“语义搜索已开启”和“语义搜索正在工作”之间的全部差别。
4. 检索每次都会运行两个检索器#
| 检索器 | 运行时机 | 贡献内容 |
|---|---|---|
读者 @ 提及的页面 |
只要存在就运行 | 强制置于列表前端 |
| 向量搜索 | 工作区有向量且所有者开关已开启时 | 最多 3 个不同页面 |
| Postgres 全文搜索 | 与向量搜索同时始终运行 | 至少 2 个位置;向量搜索找到的结果较少时提供更多 |
| 文档图谱词法搜索 | 仅当上述两者都没有返回结果时 | 最多 4 个页面 |
| 智能体搜索循环 | 仅当上述所有方式都没有返回结果时 | 最多 2 个页面 |
向量搜索会按余弦距离提取最接近的 6 行,将其转换为 1 − distance 的相似度,丢弃相似度低于 0.25 的结果,按页面去重后再截取为 3 个结果(六个命中项通常是同一页面的六个部分),并在最终列表中保留其优先级位置。
这里的全文搜索不是备用方案。它会针对每个问题运行,并合并其页面;同时设置上限,使向量搜索与词法搜索合计不会超过 5 个页面。原因来自我们自己的索引测量:对于问题 "Docsbook 使用什么 URL 模式来提供我的文档网站?",正确页面中最匹配的文本块按余弦相似度排名为 1,341 个文本块中的第 48 位——有 18 个其他页面的得分更高——而词法搜索将其作为首个命中结果返回,因为该页面确实包含查询中的词语。没有任何 top-k 值能修复这一点;对于该查询,错误的是排名本身。一个非空但错误的向量结果正是该合并机制要纠正的故障,而“仅在向量结果为空时运行词法搜索”的规则无法检测到这种情况。
表格的最后两行适用于完全没有索引的语料库。智能体循环会向模型提供一个 search_docs 工具,让它使用不同的措辞重新查询,最多进行 4 次往返,温度设为 0,然后才必须确定最多两个页面路径——这就像第一次猜测没有命中后,你会对代码库执行 grep。
5. 页面从末尾获取并截断#
每个选定的页面都会从您的存储库的默认分支获取,并在提示词中放置其路径和标题作为标题行。超过 12,000 个字符的页面不会从开头截断:保留前 9,000 个字符和最后 3,000 个字符,并在中间标记省略部分。退款政策、“相关内容”和故障排除部分位于页面的末尾,因此从开头截断会恰好丢弃问题最可能涉及的部分。读者当前所在的页面会单独包含在内,截取至 8,000 个字符。
6. 模型在写作前受到约束#
阅读器聊天的默认模型是 openai/gpt-4o-mini ——根据 OpenAI 的模型参考,它具有 128,000 个令牌的上下文窗口和 16,384 个令牌的输出上限。你可以为每个项目选择不同的模型;请参阅 AI 聊天。
系统消息很简短,可以由你替换。依据规则位于随内容一同传入的指令块中,而且这些规则之所以具体,是因为每一条都对应曾经发生过的一次失败:
- 仅以提供的内容为依据。切勿使用预训练知识来定义文档未涵盖的术语或填补文档中的空白。
- 事实存在时,不要声称其不存在。 在说某项内容缺失之前,请重新阅读提供的页面——包括该事实所在页面同时涵盖付费或可选功能的情况。
- 区分免费行为和付费行为。 不要将付费页面中的细节混入对默认行为的描述中。
- 这些页面是通过搜索找到的,未经人工审查。 请根据每个页面是否对应所询问的具体事项进行检查,而不是根据词语重叠来判断。关于区域子目录的页面包含“URL pattern”一词,但并不能回答有关默认 URL 的问题。
- 留意更具体的问题。 如果某个页面回答的是带有限定条件的问题(某种语言、某个套餐、某个附加组件),而提问中没有这样的限定条件,那么该页面回答的是另一件事。
- 设置问题需要准确的句子。 对于“如何设置 X”,请找到一条明确列出用于设置 X 的具体菜单路径、按钮或步骤的句子。如果没有这样的内容,请说明文档没有描述内置的 X 集成——其他地方提到 X,或某种理论上可以连接到 X 的通用机制,都不构成设置流程。
- 前置条件是必需的,并且会被说明两次。 扫描整个页面,查找所需的套餐、角色、前置步骤、版本或配额——文档会将这些内容放在顶部的一行简短粗体文字中,而在前往编号步骤的过程中很容易跳过。生成前会立即进行最终检查,重新阅读每个页面的顶部,因为当模型读到相关小节时,仅在页面简介中说明的前置条件已经没有任何局部内容与之竞争。
答案要求采用严格 JSON 格式——包含一个 markdown 正文和一个 refs 数组——并以温度 0.3 流式传输。
7. 引用由服务器附加,而非信任模型提供的内容#
这一步决定引用是否有意义。
| 模型提供 | 服务器执行 |
|---|---|
pagePath |
仅当该路径在答案自身的 [ref:…] 标记中以内联方式被引用,或该路径对应的页面确实由服务器抓取过时,才保留该 ref。模型既未读取也未引用的路径,会在读者看到之前被删除。 |
pageTitle |
用作链接标签 |
headingText,从页面逐字复制 |
使用渲染器采用的相同 slugifier 自行重新计算锚点 |
| (无) | 从不向模型请求锚点 id,也从不接受模型提供的锚点 id |
锚点规则并非吹毛求疵。手写的 slug 规则(“转为小写、将空格替换为短横线、去除特殊字符”)在普通 ASCII 标点上会与渲染器不一致——Edge cases & errors 在页面上会变成 edge-cases--errors,而在手写规则中会变成 edge-cases-errors——并且会将非拉丁文字标题压缩为空洞的短横线。由一个负责方计算该字符串;其他人都向它请求该字符串。
找不到任何相关内容时会发生什么#
不会编造任何内容来填补空白。当所有检索器都返回空结果时,不会附加任何页面,不会出现 Found N results 行,并且留给模型的指示是明确说明所提供的内容无法回答该问题。
随后,这一拒答会被视为数据:
- 系统会扫描答案文本以查找未回答模式;匹配后会在普通
chat.question_asked事件旁触发一个chat.no_answerwebhook。 - 该问题会进入未回答的问题,这是所有记录的聊天问题中答案未通过该检查的问题的筛选视图。
- 读者可以在小组件中对答案点踩;这些反馈会作为每个页面的点踩计数进入你的分析数据。
文档中的空白,作为报告中的一行,其价值胜过一段编造的文字——这正是整页内容所围绕的权衡。
为什么这是正确的方法(证据)#
| Docsbook 中的规则 | 为何有效 | 来源 |
|---|---|---|
| 根据检索到的页面作答,而不是依据模型记忆 | 检索增强模型“比最先进的纯参数 seq2seq 基线生成更具体、多样且符合事实的语言” | Lewis 等,2020 — 面向知识密集型 NLP 任务的检索增强生成(NeurIPS) |
| 只有明确指向实际阅读过的页面时,引用才有效 | 在四个生成式搜索引擎上的测量显示,“生成的句子中只有 51.5% 得到引用的完全支持”,并且“只有 74.5% 的引用支持其对应的句子”——系统未验证的引用不能算作证据 | Liu、Zhang & Liang,2023 — 评估生成式搜索引擎的可验证性 |
| 对每个问题都运行词法搜索,而不是将其作为备用方案 | 在超过 18 个检索数据集上,“BM25 是零样本设置下的稳健基线”,而稠密检索器“通常表现不佳……凸显了其泛化能力仍有相当大的改进空间”——对于任何嵌入模型而言,你的语料库都属于域外数据 | Thakur 等,2021 — BEIR |
| 1,536 维向量,6,000 字符单元 | text-embedding-3-small 输出 1,536 个维度并接受 8,192 个输入词元;将单元限制为 6,000 个字符后,在该限制以内仍能为面包屑导航留出空间 |
OpenAI — 嵌入指南 |
| 将提示限制为五个页面,从末尾截去过长的页面 | 当相关信息出现在输入上下文的开头或结尾时,模型性能“通常最高;而当模型必须在较长上下文的中间部分获取相关信息时,性能会显著下降”——页面更多并不意味着准确率更高 | Liu 等,2023 — 迷失于中间 |
| 明确要求拒答,并记录拒答 | 普通的指令微调“会迫使模型完成句子,无论模型是否知道相关知识”;必须主动要求模型拒答 | Zhang 等,2023 — R-Tuning:指导 LLM 说“我不知道”(NAACL 2024) |
| 依据资料进行约束可以减少幻觉,但无法消除幻觉 | 对约 18,000 条 RAG 响应进行标注后发现,即使进行了检索,“LLM 仍可能提出与检索内容不相符或相矛盾的主张” | Niu 等,2024 — RAGTruth |
我们测量什么——以及我们不发布什么#
我们不会为 Docsbook AI 聊天发布任何准确率百分比。我们尚未在客户语料库上运行带标签的基准测试,而在我们自己的文档上得出的数字对你的文档毫无意义。引用一个准确率数字将违背本文档其余部分所遵循的规则。
实际存在的是:
| 测量项 | 作用 | 显示位置 |
|---|---|---|
| 答案评估裁判 | 一个 LLM 以温度 0 读取一份已完成的对话记录(上限为 8,000 个字符),并返回严格的 {answered, reasoning} 判定 |
Chat 选项卡中的已回答列 |
| 只存储一次,从不重新评估 | 对话记录不会改变,因此其判定也不会改变——每条记录只写入一次,之后读取 | — |
| 每个请求有上限 | 每次页面加载最多评估6个新对话,因此,即使工作区中有一千个尚未评级的线程,用户第一次打开该选项卡时也不会为一千次调用付费 | — |
| 无答案检测 | 对答案文本进行模式匹配,触发 chat.no_answer,并将结果提供给未回答问题 |
Webhooks、未回答问题 |
| 每个答案的赞踩 | 读者对该答案本身的判定,按页面统计 | 分析 |
| 检索标签 | 每个答案的流都会携带生成其页面的检索器——semantic、fulltext、semantic+fulltext、doc_graph、agentic 或 mentions |
响应流;可通过一次 HTTP 调用验证 |
最后一行是有意为之。“语义搜索已开启”从外部无法证伪,这正是一个盖了章却为空的索引曾经被当作正常运行数月的原因。在每个答案中标明检索器,让任何人(包括你)都能核验这一说法。
Docsbook 还运行两套内部测试——一套包含 40 个案例的黄金测试集,用于在温度 0 下评估模型首先选择哪个工具;以及一套实时场景测试集,包含确定性检查和一个范围有限的 LLM 裁判。两者覆盖的都是你控制面板中的管理员助手,而不是面向读者的聊天;我们明确说明这一点,而不是让它们显示为绿色的勾选被误读为对本页面主题质量的声明。
限制#
- 没有已发布的准确率数据。 见上文。在其语料库、问题集和评分器公开之前,应将任何供应商给出的单一准确率数字——包括我们的数字(如果我们曾引用过)——视为不可用。
- 检索可能会自信地出错。 上述 1,341 个案例中的第 48 个是我们自己的案例,使用的是我们自己的语料库。合并词法检索可以纠正其中很大一类问题,但无法彻底解决。检索在最重要的地方也最不可靠:实测表明,检索对不太热门的事实帮助最大,因为模型没有任何记忆可供回退使用(Mallen 等人,2023)。
- 0.25 的相似度下限是固定常量,不会针对每个语料库进行调优。词汇不寻常的语料库可能需要不同的下限,而目前没有针对单个项目的控制选项。
- 引用过滤器使用的是 OR,而不是 AND。 如果路径已被获取或模型在行内引用了它,则该引用会保留。要求两者同时满足会导致几乎每个答案中的
refs都为空,因为模型会填充数组而跳过标记。其后果是:模型既自行编造路径又在行内引用该路径时,仍会通过过滤器;根据设计,只有已获取的那一半经过了依据验证。行内引用的另一半也需要与语料库进行核对,在此之前仍待验证。 - 有依据并不等于忠实。 服务器可以保证所引用的页面已被读取,但无法保证答案中的每句话都源自该页面。RAGTruth 衡量的正是这一残余问题,而它确实存在。
- 无答案检测器是基于英语模式匹配的。 因此,使用其他语言写成的拒答将无法被识别,所以
chat.no_answer和“未回答问题”报告会低估非英语网站上的数量。在我们发布经过测量验证的替代方案之前,仍待验证。 - 过长的页面会丢失中间内容。 超过 12,000 个字符的页面会以开头部分 + 结尾部分的形式传递给模型,中间内容会被标记为省略。仅存在于超长页面中间部分的事实可能会被遗漏。拆分该页面是解决办法,同时也能改善人类读者的阅读体验。
- 模型行为取决于版本。 默认阅读器模型、其上下文窗口以及拒答行为都由提供商决定,提供商可以对其进行更改。本页面中的机制属于我们;模型是否遵守该机制则不由我们决定。
- 语义检索需要向量。 在索引运行完成之前,检索会退回到全文搜索和文档图。这仍然是可正常工作的聊天,而不是损坏的聊天——但它并不是第 4 阶段所描述的那一种。
相关内容#
- AI 聊天 — 合约:助手可以做什么,以及不能做什么。
- 搜索 — 此流程共享的词法索引。
- 来源 — 除了你的页面之外,允许助手读取哪些内容。
- 聊天钩子 — 拦截问题,或将只有你的系统知道的事实交给模型。
- Docsbook 如何证明其主张 — 本页面遵循的规则。