索引时RAG的质量:片段类型、数据清洗与检索标签
索引时的RAG质量:构建文本、表格和图像块,过滤解析器产生的噪声,并添加由大语言模型生成的检索标签,从而使失败查询的数量减少近一半。
演示如何转变为生产系统
RAG解析器会生成一份有序的节点列表,每个节点都明确标注其来源部分。从此时到模型读取查询语句之前的所有处理都属于索引构建阶段的工作,正是这一阶段决定了某个演示版本能否用于实际生产环境。该过程可分为三步:首先将节点拆分为不同类型的块,每种块都有对应的生成工具;接着去除解析器虽已提取但模型无需处理的冗余信息;最后为每个块由语言模型生成简短的标签。在针对银行文档的内部测试中,这些标签使检索失败率降低了大约一半,成为整个处理流程中最显著的收益来源。
这也正是它最缺乏吸引力的地方。几乎没人撰写关于噪声去除的内容,而这恰恰是为何会有如此多相关成果出现的原因。下方的数据来自一次小规模的内部评估,并非严格的基准测试,因此应以此视角来理解。
三种块结构,三种构建工具
简单的处理流程仅使用一个分块器:将文本分割后就直接处理。而实际文档中至少包含三种行为完全不同的内容。一段散文、一张费用表以及一张截图都需要不同的处理方式,如果将所有内容都通过同一个分块器处理,就会出现半截表格和缺失的步骤。
文本块遵循结构
按字符数分割的方式不可行。相反,每行内容都会先被输入,作为标题、编号步骤或普通句子,合并规则则根据这些类型来确定。这样就能避免将编号步骤与无关段落绑定在一起。过长的文本会在句子末尾截断,只有在万不得已的情况下才会选择在句子中间切断。相邻的文本块会略有重叠,因此跨越边界的流程从任意一侧都能被理解。
表格保持完整
表格是单独生成的,因为其含义体现在布局之中。生成工具会找到表头行,如果表格需要分割,则在每个部分重复该表头行,并整理各行。同时,它也会排除那些没有实际内容的表格,最常见的就是目录,解析器通常会将其以整齐的标题和页码网格形式输出。
该系统的上一版本在这里存在缺陷。目录是通过一种简单的测试来检测的,很多行的数值最终都会落在较小的数字上。费用表也符合这一特征,因此实际的费用明细可能会在毫无预警的情况下从索引中消失。新的测试方法更为精准:在真实的目录页面中,页码只会递增,而像35美元、30美元和7美元这样的费用数值则会上下波动。只有当表格中的数值始终不下降时,它才会被视作索引。这样一来,费用明细依然会保留,目录页面也能正常显示,仅通过一个条件就消除了这个漏洞。
图片是逐个处理的
每张图片都可能产生费用,因此系统会按顺序尝试三种方法,一旦有一种有效就会立即停止。
- 自由路径会使用解析器已提取的文本。如果文档提供了可用的标题或注释,且并非“image”之类的占位符,就会使用这些文本,而无需调用任何视觉模型。在许多文档中,有三分之一到一半的图片会走这条路径,这无疑能节省资源。
- 付费版本会调用视觉模型。当没有可用文本且图像尺寸大于小型分隔线时就会使用此功能。表述精确的提示词能促使模型优先进行分类。如果图片是标志或装饰元素,模型会回复“DECORATIVE”这一个词,随后该部分内容会被丢弃。否则模型会填写两个字段。“SCREEN”字段为一行文字,说明屏幕的用途,例如企业账户的对外来电接入屏幕,其表述方式会使其在向量空间中更接近用户的问题。“CONTENT”字段则按阅读顺序列出所有可见的字段标签、按钮及代码,为搜索者提供他们可能输入的准确字符串,这样就能通过用途或显示的文字来找到截图。
- 备用路径会使用标题。如果这两种方式都失败但该部分内容已知,系统会存储一个占位符,说明图片的内容及其所在位置,这样在查询时仍可通过该部分访问到图片。
尺寸检查存在一个小缺陷。极小的图片会因宽度和高度均低于阈值而跳过视觉识别流程,但由于两个维度都必须很小,因此非常细长或非常宽的分隔线仍会通过检查并被发送给模型。这虽然会在装饰效果上稍有浪费,但不会影响准确性,已被列入待改进清单。
图表按顺序展示了尝试的三种图片路径。视觉模型在索引生成时运行一次,之后的每次查询都是免费的。
优化解析器已正确处理的部分
正是这种忠实性使得解析器在这里成为麻烦。它们会提取页面上的所有内容:200页中反复出现的“机密”标识、页码、版权信息,以及被分页符截断的句子。这些内容每一项都会变成一个数据块,而所有这些数据块都是冗余信息。冗余信息还会影响排名,因为用户的查询同样可能匹配到广告横幅而非实际流程。
因此,一旦生成了这些数据块,就会进行一次清理处理,每一步都针对实际文档中常见的某种冗余内容。具体步骤如下:
- 模板过滤会去除单纯的页码、修订标记,以及页面开头的“联系我们”或“阅读更多”之类的链接文字。不同的文件类型需要不同的规则集,因为PDF、Word文档和幻灯片文件各自都有不同的冗余内容。
- 删除重复内容。在多个页面上完全相同的文本属于冗余内容。“CONFIDENTIAL, INTERNAL USE ONLY”这一表述若出现在15个页面上就会被识别并删除,而同一页面上出现两次的“见第3页”则不会被删除,因为它并不跨越多个页面。
- 修复断裂的片段。那些在句子中间开始或以悬空项目符号结尾的片段可能是被错误地分割开了。如果这些片段能够与同一章节中的前一个片段合并,它们就会合并在一起;否则则会被标记为续篇,其标签会保留相应的上下文信息。
- 合并孤立的片段。位于章节边界处的40字符左右的孤立片段可以被相邻的片段吸收,但前提是两者必须属于完全相同的章节和标题。不同章节之间的片段无法合并,而那些真正独立的简短内容则保持原样。
- 去重功能会将同一标题下重复出现的句子合并为一个片段。如果该句子出现在两个不同的标题下,则会保留两个副本,因为带有对应章节路径的它们包含不同的信息。图片则完全不受此规则影响,因为两张外观相似的屏幕截图仍属于两个独立的截图。
这些规则都不复杂。每一条都是通过观察真实文档如何优于简化版本而总结出来的,它们共同解释了为何查询会进入处理流程而非版权相关环节。
检索标签
最具价值且大多数系统都会忽略的步骤接下来要介绍。以这样一段简单的文本为例:
输入金额后按F4键。
它包含自身的路径信息 Wire Transfer > Initiation > Amount Entry,这已经很有帮助了。但在嵌入之前,语言模型还会为该内容编写一段18到22个单词的简短检索标签,并与对应内容一起存储。该内容的标签大致如下:
操作步骤:输入转账金额,然后在启动电汇时按F4确认。
考虑一下其效果。没有标签时,这条单纯的指令会与其他所有要求输入数值并按键的步骤一起存在于嵌入空间中。而加上标签后,它就会出现在电汇和确认相关的内容附近,这些都是真实用户会使用的词汇。像“如何确认电汇金额”这样的重新表述的问题就能找到这一部分内容。在内部评估中,标签减少了大约一半的检索错误。这只是针对单一语料库进行的小规模测试,因此应将其视为有力的提示,而非普遍适用的规律。
该技术源自Anthropic的上下文检索技术,该技术发现,在对每个文本块进行索引处理之前在其前添加特定于该块的上下文句子,能显著降低检索失败率。此处则针对那些需要精确标识符的内容进行了优化。交易代码和功能键必须原样复制,绝不能改写,且每个标签都以“操作步骤”或“参考表”之类的角色词开头,这样各文本块就能与相应类型的问题归为一组。
标签编写者能看到什么
标签的质量取决于传递给编写它的模型的上下文。仅有的文本块本身是不够的,因为正是在从页面中截取出来时其含义才丧失的。因此,每个标签请求都会包含以下几项内容:
- 文本块本身的内容
- 该部分所在的路径及标题
- 当内容为表格时,显示列标题
- 显示前一个和后一个内容块,以便了解其周围的内容
- 提供整个文档的简短摘要,以便把握整体情况
文档摘要是在标记生成开始之前通过单独调用获得的。它会从文档的开头、中间和结尾抽取内容样本,再加上一个表格和一张图片,然后返回两到三句话,说明文档类型、目标受众以及涉及的产品和代码。之后的每个标记请求都会包含这一摘要。在电汇指南中,“输入金额”这一表述应与贷款披露文件中的表述有所区别,而摘要正是模型用来判断当前正在阅读何种文档的依据。
需要仔细查看相邻块,因为“前一个”和“后一个”并不一定指物理上相邻的两个块。相邻的块可能属于完全不同的流程。因此,会根据相关性分三个层级选取相邻块,优先选择最先匹配的:
- 同一路径段中最近的块
- 否则,同一父路径段中最近的块
- 否则,同一标题下最近的块
会从当前块开始向外扫描候选块,超出相应路径范围的块将被忽略。这样模型就能找到真正与当前块相关的下一个块。每个相邻块的长度被限制在几百个字符左右,这样的上下文量已经足够,同时避免了传输完整块带来的开销。
以F4示例而言,模型能够识别出其所在部分的前一个片段是关于选择收款人,下一个片段则是关于确认转账。这使它能够生成关于输入电汇金额的明确标签,而非模糊的总结。恰当选择的相邻片段能让标签更加精确。
该图展示了标签模型如何读取同一部分的相邻片段,然后生成一句具体的句子,将其存储在对应片段旁,并同时提供给两个搜索路径。
为何这种方法可行
有三个特性使得这一做法既实用又不会造成过高成本。
- 一次性成本。标签在索引过程中生成并保存,因此查询时无需为此付费。
- 单位成本低。该模型体积小、处理速度快,可每25个片段为一组进行标记,多组可同时运行,每个片段的成本远低于一分钱。
标注信息存储在独立的字段中,与数据块主体分开。这种分离在后续的查询阶段非常重要,因为在关键词搜索时标注会获得更高的权重。目前的关键在于,最耗资源的推理工作会在任何人进行查询之前完成,这也是改写后的查询能够正常工作的原因。
确定性与时序版本提示
标注生成是确定性的:将温度设置为零并使用固定种子,可确保对文档重新进行索引时能生成相同的标注。这正是“成本仅支付一次”这一说法的依据。重新索引不会重新排序向量,只是重新生成它们而已。
确定性还取决于提示语的版本控制。种子值仅能在输入完全相同时产生相同的输出,而指令文本也是输入的一部分。在版本更新之间若修改了丰富提示语,后续的重新索引就会生成不同的标签,因此索引会悄然分为两代,却没有任何提示。因此,提示语会附带版本标签保存,在索引生成时通过该标签加载,并且只能通过有记录的、刻意进行的更新来修改。跨版本的可重复性需要人为构建,因为仅靠种子值是无法实现这一点的。
嵌入与存储
经过清洗、定位和标注后,最终的索引处理步骤就是生成向量。有一个嵌入模型可以处理所有类型的文本块:text-embedding-3-large,其维度被压缩到1536,这样文本、表格和截图都可以在同一个空间内根据查询结果进行排序。
嵌入器中输入的内容是按固定顺序组合的字符串:结构前缀、标签,再接着正文内容。该前缀会利用已知的关于该内容块的信息,这些信息包括从章节路径推断出的文档类型,如费用表或电汇流程,以及标题和章节路径;对于图片,则还包括页面和位置信息。将类似流程的内容块标记为流程,会使其更倾向于出现在以“如何做某事”开头的问题中。
在此阶段,段路径也会被添加到文本中,而这正是旧版本存在缺陷的地方。过去在清理过程中前缀会被写入数据块,之后的任何编辑都可能再次添加前缀,从而悄悄破坏向量结构。当前的代码会先移除所有已存储的前缀,然后在嵌入时生成新的前缀。该前缀仅存在于一个位置,并且在模型读取数据之前被添加一次,这样就消除了各类隐性双前缀错误。
处理完成的向量会与数据块内容、标签、结构元数据以及用于关键词搜索的全文列一同存储起来。之后该文档就会进入活跃状态。如果某个处理步骤出现异常,该文档会被标记为已拒绝,同时会记录原因以便调试,而之前的活跃版本则始终可以被搜索。半索引化的文档永远不会显示出来。
完成后的行呈现的样子
为便于理解,以下是该功能在系统中实现时的“输入金额并按F4”模块。其中的每个字段都来自上一步骤。
text : "Enter amount and press F4."
(built + cleaned + section-prefixed)
retrieval_context : "Procedure step entering the amount
and pressing F4 during wire transfer
initiation." (the label)
chunk_type : TEXT
section_path : "Wire Transfer > Initiation >
Amount Entry" (Part 2)
preceding_heading : "Amount Entry"
table_headers : null (TABLE only)
page_start / end : 12 / 12
image_object_key : null (IMAGE only)
embedding_model : text-embedding-3-large
embedding_dim : 1536
embedding : [ ...1536 floats... ] (body+label)
text_search_vector: label weighted high, body lower
在表格数据块中,列标题用于填充文本数据块中值为空的位置。图像数据块则存储二进制数据键、MIME类型以及页面位置,而内容数据以text格式存储,屏幕相关信息则存放在retrieval_context中。这三种类型都采用相同的行结构。检索功能依赖两列数据:embedding用于向量搜索,text_search_vector用于关键词搜索,这两列数据均来自内容主体与标签信息。其余列则用于定位数据块、显示内容或限制其仅对特定用户可见。
这就是索引生成时的全部输出:每個数据块对应一行,经过清洗、标注和向量化处理,等待首次查询的到来。
相关阅读
- 实用笔记:将RAG投入生产:基于实际案例的端到端隐患分析 — 为采用该模式的团队提供的详细操作指南,涵盖合同、检查项以及可直接使用的代码模板。
- 生产环境中会悄悄降低检索质量的RAG错误及修复方法 — 从分块大小与重叠度到混合搜索、RRF、重排序、嵌入模型选择以及持续集成评估,深入解析导致错误RAG结果的十种检索错误及其解决方案。