基于事实的回答
文档如何转化为可供智能体回答的内容——分块、上下文头、查询重写和相关性阈值。
“来自文档的答案”很容易宣称,却很难做好。本页描述了平台在文件上传和基于事实的回答之间实际执行的操作,包括通常被省略的部分。
从文件到分块
摄取:每个文档一次
文件被解析为文本——对于包含图表的文档,图表也会被读取(见下文)。特定格式的提取在此处完成;下游所有环节看到的都是纯文本。
按 tokens(词元)大小拆分为分块——但文本是按字符拆分的,因此两者之间的比率是从文档自身的脚本中估算的。如果将拉丁语系的比率应用于中文,产生的分块会比预期大数倍。
每个分块前缀加上其文档标题(以及可选的生成笔记),仅用于嵌入。存储的文本保持不变;标题的存在是为了让向量反映该段落所属的部分。
分块及其标题被嵌入为一个向量。
加密存储并限定在空间范围内。从这里开始,它可被检索——但仅限于该空间内。
在上传文档时运行。下面的查询路径在每次提问时运行。
查询:每个问题一次
客户提出问题——通常是一个只有在上下文中才有意义的后续问题:“那一个的价格是多少?”
如果问题依赖于之前的对话轮次,它会被重写为一个自包含的问题,仅用于检索向量。模型仍会收到客户的原始措辞,任何失败都会回退到原始措辞。
使用与分块相同的模型对(可能已重写的)问题进行嵌入。
余弦最近邻,按距离排序,在数据库层面限定在该最终用户空间内,而不是通过应用过滤器。
超出最大距离的匹配结果会被丢弃。返回的分块少于请求的数量是正确的结果——为了凑够固定数量而填充结果是检索系统产生流畅但错误答案的方式。
幸存下来的内容会被解密并发送给模型。如果没有幸存下来的内容,智能体会说它没有答案。
阈值是决定智能体是否回答的步骤。
上传的文档被解析为文本,拆分为分块,嵌入并加密存储。拆分过程中的两个细节比看起来更重要。
分块大小以 tokens 为单位测量,但文本是按字符拆分的——因此平台必须在它们之间进行转换,且转换依赖于语言。大约 4 个字符对应 1 个 token 的拉丁语系比率对于中文、日文和韩文来说是完全错误的,因为在这些语言中,单个字符更接近一个完整的 token。使用一个全局常量意味着 CJK(中日韩)文档的分块会比预期大数倍:分块超出了它们被设定大小的上下文,检索返回的是大段文本。
因此,拆分器会从文档内容本身估算比率,根据文本中 CJK 内容的比例在 CJK 和拉丁语系比率之间进行插值。一份混合了中文和英文的合同会落在两者之间,而不是处于任一极端。
段落尽可能保持完整。 分块按段落累积,直到达到目标大小;只有本身过大的段落才会被硬拆分。连续的分块之间有一个可配置的重叠量,将前一个分块的尾部保留下来,以便跨越边界的知识仍可从两侧检索到。
默认值为平台配置中的 chunk_target_tokens 和 chunk_overlap_tokens。它们是租户可调的,而非固定保证——合适的大小取决于您的文档。
图表,而不仅仅是文本层
大多数文档提取仅停留在文本层。上传数据表、安装手册或报告,典型的管道会提取正文,并 silently 丢弃所有绘制而非键入的内容——接线图、电路板布局、页面中间的图表。文件被“处理”了;它所说的一半内容从未被读取。询问“CPU 风扇在哪个接口上”,答案不在索引中,因为它只存在于图片中。
该平台也会读取这些内容。
- 扫描或仅含图像的 PDF——完全没有文本层——会逐页渲染并由视觉模型转录,因此拍摄的照片合同或扫描表格会变成可搜索的文本。
- 混合文档——文本层 加上 图表——是其他系统遗漏的情况。带有真实图表的页面会被渲染并由视觉模型描述,描述内容会被折叠到文档的文本中,以便分块、嵌入,并与正文一起被检索。电路板布局页面会返回连接器名称及其位置;图表会返回趋势和数字。
- Word 文件中的嵌入式图像会被从文档中提取并以相同方式描述。
困难的部分不在于调用视觉模型——而在于不在同一个装饰性侧边栏上调用四十次。手册在每一页重复一个徽标和页面边框;描述这些将是纯粹的成本和纯粹的噪音。因此,图表检测区分了真正的图表(许多矢量路径、大照片、图像蒙太奇)和模板装饰(内容相同的图像在页面间重复出现),并仅发送承载信息的页面。
描述图表使用视觉模型,因此其成本高于纯文本提取。两个护栏:图表密集的文档在产生高额费用前会请求确认,且图像描述支出会作为单独的行——vision_image——显示在用量中,因此您始终能看到钱花在哪里。
上下文标题
单独嵌入的分块会丢失它来自特定文档的事实。“当借款人违约时适用第 7.2 条”在查询为“如果我在桥接产品上逾期付款会发生什么”时检索效果很差——该分块从未说明它属于哪个产品。
在嵌入之前,每个分块前缀加上上下文标题——文档标题,以及可选的 LLM 生成的情境笔记——以便向量反映段落及其所属部分。存储的文本保持不变;只有嵌入输入携带标题。
查询重写:大多数系统跳过的部分
检索质量通常被讨论为一个排序问题。通常并非如此——通常查询根本不包含任何信息。
客户问“那一个的价格是多少?”。代词指的是两轮前说过的内容。嵌入这句话会产生一个接近任何特定内容的向量,该轮次的检索几乎是随机的。没有任何重排序能修复它,因为信号从未存在过。
因此,在嵌入之前,依赖于先前上下文的后续问题会被重写为一个自包含的问题。三个 deliberate(深思熟虑)的选择:
- 它倾向于过度触发。 一个指代表达式的正则表达式决定是否尝试重写。不必要地触发成本仅为一个廉价的调用,且自包含的问题会原样返回;未能触发则导致该轮次的检索完全失败。
- 它重写为更完整的句子,而非关键词。 嵌入模型是在句子上进行训练的。简化为关键词会丢失关系(“借款人对贷款人的责任”及其反向表述坍缩为相同的向量)和否定(“哪些情况不需要担保”)。
- 它仅影响检索向量。 发送给模型的消息始终是客户的原始措辞。重写从未进入提示词、对话历史或存储——任何失败都会回退到原始句子,因此最坏情况正是旧行为。
搜索,以及相关性阈值
查询向量与限定在该最终用户空间内的分块进行余弦距离比较。这种限定不是可能被遗忘的应用层过滤器——它在数据库层面强制执行。
然后按最大距离过滤顶部匹配结果。这是区分有用智能体和自信智能体的部分:
接待处周一到周五 9:00 至 17:00 有人值班。
预约至少提前一个工作日安排。
24 小时内的取消将收取一半费用。
由两位合伙人于 1998 年创立,现拥有九人团队。
Illustrative. 虚构诊所的示例距离。实际值取决于您的文档和嵌入模型——这里没有测量值,且阈值由租户配置。
返回的分块数量少于请求的数量是正确的结果,而非不足。 如果您的知识库中没有任何相关内容,智能体将一无所获并如实告知,而不是被分配最不相关的段落并从中编造联系。将结果填充到固定数量是检索系统产生流畅、看似有来源但错误的答案的方式。
过滤器是在排名获取之后应用的,而不是在 SQL 谓词中——结果已经是按距离排序的,因此结果相同,但这保持了干净的索引扫描,而不是退化为全表后过滤。
当检索成功但仍失败时
相关性阈值捕捉到了没有任何内容足够接近的情况。有一个它无法看到的更困难的情况:每个分块都通过了阈值,主题正确,但问题所关注的那个事实并不在其中。
在该站点自己的售前智能体上进行测量。询问“如果我处于 Starter 套餐且每月使用量增长 30%,六个月后我要付多少钱”,检索返回了三条分块:Starter 套餐页面、令牌使用指南和服务条款。都相关。但它们都没有说明价格——套餐页面是关于该套餐面向谁的介绍。给定一个标题为“Starter”且其中没有数字的文档,模型编造了一个数字。在五次运行中,它产生了 $5、$250、$99、$0 和 $29,每个都附带了引用标记。真实数字是 $49。
没有任何东西以可检测的方式失败。距离很好,因此没有检索遗漏需要报告。引用指向的分块确实被检索到了。只有答案是错误的。
第二次传递,由代码决定
因此,在第一次搜索之后,对返回的内容提出两个问题——两者都可以通过字符串比较回答,都不委托给模型:
被问到的东西甚至被提及了吗? 问题中的专有名词,加上从知识库中文档标题中提取的术语——这是一个由某人策划的词汇表,而不是我们猜测的。如果一个术语出现在问题中,但不在任何检索到的分块中,则该术语会被单独搜索。
被问到的那方面存在吗? 这是第一次检查遗漏的一半,也是上述测量落脚的一半:Starter 确实在 检索到的文本中;价格不在。因此,每个问题也会针对它所询问的内容进行匹配。其中一些具有严格的格式——价格带有货币符号,持续时间带有时间单位,联系方式看起来像电子邮件或 URL——真正涵盖该方面的材料几乎从不省略格式。其他(退款、合规、试用、集成)没有格式,因此信号是主题词本身,按语言写出,而不是留给向量相似性来桥接。
当任一检查未通过时,派生术语会被再次搜索,结果会合并进来。仅一次,永不循环。误报成本为一个向量查询——没有额外的模型调用,没有额外的往返——而遗漏成本为一个附带引用标记的编造数字。
- Starter — 面向谁一个小型、定义明确的业务组合——几十位客户。无价格
- 令牌使用 — 什么算数您的仪表板将使用量显示为两种颜色,提示和完成。无价格
- 服务条款您可以按公布费率提前购买额外令牌。无价格
有两件事值得坦诚相告。这是防止编造的下限,而非正确性的证明——它注意到问题的某一方面未被代表,而非答案正确。而且,它并没有消除知识库实际包含该事实的需求:使上述示例在生产环境中生效的修复措施也是给每个套餐一个简短文档以说明其价格,因为关于增长和超额使用的长问题每次都会胜过单一的合并定价表。
当问句点名了条号
语义相似度是核对引证时用错了的工具。「第 18 条」「Part 9」「ISO 9408」——它们不表达意思,它们指认对象。一张标准表里相差一个数字的两行,在向量看来是同一句话。
所以问句里的标识会被抽出来当作文本去查,而带着被点名那个标识的片段会被钉在语义排序之前,并且语义排序不许把它挪走。最后这半句才是重点,值得直说,因为这里曾经不是这样:那层保护写在注释里、从来没有在代码里实现,于是重排把钉住的片段又排了下去。在一张相邻行只差一个数字的标准表上实测:唯一写着被问那一部分的那行,关掉重排排第 1,开着重排连前 20 都进不去—— 而回答用的是相邻那一行。
把标识当身份而不是意思,会带来两个后果:
当这个标识出现的地方多到上下文装不下时,引证不再被整组丢弃。 以前是丢的:如果「第 18 条」命中的片段超过能放下的数量,整组就按「样本不是答案」的理由扔掉。这句话对"按文件顺序取前几个" 成立,对"取离问题最近的几个"不成立。实测的一例里按后一种取法排完,能回答它的两片余弦距离是 0.27 和 0.31,噪音从 0.51 开始——那道坎自己说明了该在哪里停,不必预先猜一个阈值。而当距离是一道平滑的斜坡而不是一级台阶时,一片都不救:分布均匀说明这些片段彼此没有分别,那才真的是样本。
向量距离最近的那一片一定会被带进上下文。 重排可以重新排序,但不许把最接近的那一片丢掉。实测中重排把它挤出上下文的比例是七次里有一次,而在人工核对的样本里,被挤掉的那一片有时是 唯一能回答这个问题的。它是补进去的,不是替换——不会为了给它腾位置而挤掉别的片段。
当模型自己要更多材料
有时候摆在模型面前的片段定不了这道题,于是它不作答,而是自己去查知识库。这个动作就是信号。 它读完了手上的材料并且伸手要更多,所以它需要的是下一批,而不是同一批——后者正是朴素实现会返回的东西,而那等于什么也没告诉它。
下一批本来就在手上。检索取回的候选池比它交出去的更宽,排一次序,只交出最前面的一段,剩下的就留在这一刻用。把它交出去不需要第二次检索、第二次排序,也不需要第二次相关性评估——只多一次它本来就要打的模型调用。第一版实现确实重跑了检索,在一道题上把 22 秒变成了 71 秒;一次"再看一眼" 的代价是这样量出来的,不是估出来的。
每一轮只发一次。之后重查恢复原样,因为"再查还是这些"本身也是一个信号,拿掉它模型就会一直挖下去。
解密发生在最后
分块内容按空间加密存储。只有实际匹配的分块才会被解密,使用为该租户和用户加载的密码——检索在向量上运行,纯文本仅出现在即将使用的分块中。
数字不是意义
以上内容都是机械装置。这部分由您控制,它对结果的影响比本页上的任何设置都大。
检索匹配意义。单独的值携带的意义很少,因此一个事实可能存在于您的文本中但仍不可达——悄无声息地,因为搜索继续返回某些内容。
这一行出现在真实客户目录的 1,500 个分块中的 1,480 个中:
Reading level: 1 · Age: 5 · Words: 1951而“什么书适合刚开始独立阅读的孩子”返回了一本关于音乐神童的书。1 与 beginner reader(初学者读者)没有 resemblance(相似性),就像单词与单词之间的相似性一样。
在值旁边添加一句话修复了它,在该记录上测量:
Reading level: 1 · Age: 5
Suitable for around age 5, for children just starting to read on their own.| 查询 | 之前 | 之后 |
|---|---|---|
| 什么书适合刚开始独立阅读的孩子 | 0.625 | 0.540 |
| 什么书适合刚开始自己读的孩子 | 0.626 | 0.552 |
| beginner reader age 5 | 0.598 | 0.498 |
越低越接近,并且——见下文——两个无关的分块在此模型上位于 0.61 左右。因此,最后一行从“略好于随机”变为真实匹配,且增益跨越语言保持。
一般规则:如果问题是通过属性而非正文来回答的——价格、日期、级别、可用性、库存——请将其以文字形式以及字段形式呈现。 结构化值用于过滤;检索找到的是写下的内容。
有一件事不值得 effort(努力),因为我们测量过:导出经常重复自身(summary 和 description 携带相同的正文)。删除重复项将检索距离改变了 +0.002 across 60 records(跨 60 条记录)——毫无影响。嵌入是一个方向,而不是一个使用重复文本的预算。
距离不是分数,且在不同模型之间不可比较
在阅读任何距离数字之前,还有一件事值得了解。嵌入模型将文本压缩到一个狭窄的锥体中,而锥体的宽度因模型而异。在此平台今天运行的模型上,两个完全无关的分块相距约 0.61——因此 0.52 的命中是一个强匹配,而非弱匹配,且 0.5 在任何地方都不是有意义的截止值。
两个实际后果:
- 将距离与无关基线进行比较,永远不要与您从某处记住的数字进行比较。控制台中的知识星图在其刻度上标记了该基线,正是出于这个原因。
- 长问题比关键词检索得更好。 单个词与 900 字符的分块相比是一个弱信号,即使该分块字面上是关于该词的;完整问题是更紧密的匹配。真实用户写句子,这是好的情况——但如果您使用单字查询进行测试,请预期数字看起来比系统实际表现更差。
检索无法做什么,以及什么回答这些问题
以上内容都是关于找到回答问题的分块。三类问题根本不涉及分块,没有任何调整能使相似性搜索回答它们:
- 计数——“你们有多少”,“有多少是非小说类”
- 数值过滤——“哪些少于 200 字”,“在 1000 到 2000 之间”
- 分组——“每类有多少”,“哪个作者出现最多”
这些以容易忽视的方式失败。被问“是否有大约 500 字的”,仅有向量搜索的智能体将报告其前几名中碰巧出现的记录——关于四份文档的真实陈述,被呈现为关于整个集合的陈述。
结构化索引
因此,知识库可以携带一个第二张小表,与向量并列:每份文档一行,由文档已包含的字段构建。值被逐字符复制——没有生成,没有重写。
有了它,智能体选择适合问题的工具:
| 问题 | 由...回答 |
|---|---|
| “关于恐龙的任何内容?” | 向量搜索 |
| “你们有多少?” | 表格 |
| “关于恐龙,适合五岁儿童” | 表格过滤,向量搜索排名 |
| “链接是什么?” | 表格,完全引用 |
字段是通过阅读您的几份文档提出的,然后针对整个集合进行验证:仅在五分之一的文档中发现的字段会被丢弃并报告,因为在其上过滤会无声地省略其余部分。构建或更改索引不会重新嵌入任何内容——值是从已存储的文本中读取的——因此它不会干扰已经正常工作的检索。
没有共享机器可读标头的文档没有索引,平台会如此说明,而不是构建一个值都不同的表。这不是一个需要规避的限制:这样的表无法有意义地计数或分组,拥有它只会引发它回答糟糕的问题。
为什么答案中的链接曾经属于错误的项目
有一个同样由该表修复的、更安静的失败。
检索返回分块,但标题、产品代码、链接和图像位于文档标题中——即第 1 个分块,而回答问题的分块可能是第 7 个。因此,模型收到来自三到四个文档的四到五个片段,并必须确定哪个链接属于哪个标题。
当它出错时,看起来没有任何错误。链接是真实的,它打开,图像渲染——它只是属于不同的项目。在 4,500 份文档的目录中,20 个测试答案中有 11 个将标题与另一个文档的链接配对。
您标记为精确的字段——链接、图像、标识符——现在永远不会显示给模型。它接收一个占位符,写入占位符,平台在输出时替换存储的值。模型无法拼写它从未见过的地址。同样的 20 个问题,20 个全部正确。
哪个链接是发送给客户的链接是一个您的数据未陈述的业务事实,因此您自己命名这些字段;其他一切由您提议。更多信息见 结构化索引。
当您的材料确实没有答案时
有时诚实的回答是“这不在您的材料中”,有时这就是有用对话的终点。公共来源回退是一个可选的、每个智能体的替代方案:当检索为空时,智能体可以搜索网络,阅读它找到的页面之一,并从中回答。
除非您打开它,否则它是关闭的,并且当它打开时,有三件事是真实的。
阅读页面是其中的一部分,而不是额外的。 搜索结果是一个标题和两行摘要,而重要的数字——有效期、费用、截止日期——通常只在正文中。我们评估集中的一份页面在摘要中陈述“有效期五年”,而续订前:9 个月仅在页面本身中。仅凭摘要,模型有提示但没有事实,并从记忆中填补空白。因此,一个开关同时启用搜索和阅读;仅启用搜索是可测量的最糟糕配置,而不是更保守的配置。
搜索而不阅读是不允许的结果。 如果智能体搜索然后在不打开页面的情况下回答,平台会要求它在交付答案之前回去阅读一个页面。这种区别不是学术性的:被问谁目前管理日本的 PMDA,仅凭摘要产生了一个自信的错误名称;阅读页面后的同一问题产生了正确的名称。
来自公共页面的任何内容都会被标记,您可以看到它搜索了什么。 引用标记是不同颜色的,面板显示域名并明确说明这是公共来源而非您的材料,这种区分由平台应用——而不是要求模型,因为模型会在恰好需要的时候忘记。
智能体阅读的页面和它仅找到的结果都列出了,绘制方式不同(实线 versus 虚线)和标签不同,因为它们是不同等级的证据。早期版本仅显示它阅读的页面,效果与意图相反:在获取失败的一轮中,答案来自摘要并完全没有来源标记——视觉上与来自您自己材料的答案相同。隐藏较弱的证据隐藏了证据薄弱的事实。
每个这样的答案都会进入审核队列,在控制台的 知识审核 下。问题、答案、来源 URL、它阅读时的页面快照,以及出站事实检查无法追溯到来源的任何数字。
当您批准时写入的是一个新条目,从问题和材料中重写——而不是访客收到的答案。该答案是为一个人定制的,并由该对话塑造:它以他们的名字开头,引用“您的产品线”,并以行动号召结束。逐字归档,一个访客的私人上下文成为每个人的答案。因此,条目在不包含任何这些内容的情况下重新生成,标题从材料中而非从问题中写出——措辞为“...关于我的产品”的问题是一个标题,它悄悄地将某人的上下文带入标题在检索中权重很高的地方。电子邮件地址和电话号码在存储之前会被屏蔽。您会看到重写后的条目,如果需要可以编辑它,然后批准它。
拒绝会保留记录,因此同一页面不会在下个月重新审核。
我们希望您知道的数字
在知识库确实无法回答的 24 个问题上进行测量,运行三次:可追溯到真实来源的答案从 8% 增加到 33%,我们无法追溯到任何来源的数字从 13 下降到 6。第二个数字不是零。大约每十二个答案中仍有一个携带我们无法解释的数字,这就是为什么事实检查随功能一起提供而不是在其之后,以及为什么存在审核队列。
这对实践意味着什么
- 多语言文档集不会无声地降级——分块器适应脚本。
- 文档内的图表或图表是可回答的,而不是与图片的其他部分一起被无声丢弃。
- 后续问题与开场问题一样检索良好。
- 智能体诚实的“我没有那个”是相关性阈值的设计结果。