
我们内部把整套RAG系统拆了好几期来做第一期解决数据从哪来的问题第二期就是我这次要聊的RAG-02数据与索引模块。说实话这个模块在项目排期里看起来不是最亮眼的那块但它决定了后面检索和生成的天花板。索引之前每一道工序都在给召回结果打分数据没洗干净、切分切断了语义、向量和文档对不上号后面接什么样的生成模型都救不回来。这篇东西我会按实际做过的项目来讲不整虚的。适合正好在搭RAG系统、或者已经跑通demo但发现检索结果不靠谱的人参考新手也能照着流程一步步来老手可以跳过基础部分直接看后面的调参和排查章节。1. 为什么把数据与索引单独拆成一个模块1.1 模块边界与职责划分我们这边的系统分了几期RAG-01负责数据采集和归档RAG-02负责把采集到的原始数据变成可检索的索引RAG-03做检索与重排RAG-04才轮到生成和评估。RAG-02这个模块的边界很明确输入是原始文档输出是既能做语义检索又能做关键词检索的索引以及配套的元数据。它不关心用户最终问什么问题也不关心生成模型怎么组织答案这些是其他模块的事。这样的模块拆法有个明显好处每一期可以独立测试和上线。数据与索引模块可以先跑通然后把索引文件作为一个固定产物交给下游下游开发不用等我们反复改接口。即使后面发现生成模块要换模型索引这一层完全不用动因为生成模型拿到的永远是检索结果跟索引内部怎么构建没关系。1.2 这个模块最容易翻车的地方大部分RAG项目最后效果不好罪魁祸首往往不是模型不行而是索引层出了问题。我复盘过几个案例问题集中在四个地方一是数据样式太杂PDF里扫描件和文字版混在一起有的还有页眉页脚和多栏排版解析出来就是一团乱码二是切分策略一刀切把一句话从中间劈开导致后面召回的时候上下文语义不完整三是嵌入模型选得随意换模型之后向量维度变了索引直接废掉四是索引更新不及时文档改版后旧内容还在库里检索出来的全是过期信息。这四个问题都不是模型层能兜住的。所以RAG-02这个模块在设计阶段就要把数据接入、清洗、切分、向量化、索引构建、更新维护串成一条完整链路每一步都留出校验和监控的接口而不是让数据流了一地没人管。2. 数据接入与清洗别让脏数据进索引2.1 多格式文档解析的小坑数据接入这步听着简单做起来全是细节。我们实际碰到的输入格式包括PDF、Word、HTML、Markdown、纯文本还有少量的图片。PDF是最折腾的尤其那些从打印店出来的扫描版不跑OCR的话连一个字都抽不出来。第一版我们天真地用普通文本提取去处理所有PDF结果好几份文档提取出来的内容是空白有的提取出乱码。后来我们加了一层格式识别逻辑先看PDF是否自带文本层如果自带就直接抽取没有文本层就走OCR管线。OCR会产生识别错误所以还要在清洗环节做近义词和错别字的归一处理。Word文档相对友好一些但是要注意表格嵌套和文本框这些内容很容易在提取时丢失。HTML页面则需要剥离脚本、样式和导航栏只保留正文区块。我在处理HTML时习惯用区块抽取而不是整页转文本。整页转文本会把页脚版权信息、推荐阅读链接全部带进来这些噪音在向量化之后会对语义检索产生干扰而且很难定位是哪条数据带进来的。2.2 去重、格式归一与敏感内容过滤数据清洗的逻辑是层层过滤不是一步到位。我们的顺序是先做格式归一再做去重最后做敏感内容过滤。格式归一主要处理编码问题和空白字符。有些旧文档是GBK编码读取之后要转成UTF-8全角和半角符号统一连续的换行和空格压缩成单字符。这个阶段不做内容判断纯粹是让文本变得规整。去重这里我推荐用MinHash或者SimHash这类算法不要简单地对标题判重。因为很多文档内容相同但标题不同比如同一份技术方案在不同文件夹里被改了文件名重复归档。SimHash可以做到近似去重对文本稍微改动也不敏感适合处理这种场景。敏感内容过滤在RAG系统里很关键。我们用正则规则库匹配手机号、身份证、邮箱这类信息一旦命中就先脱敏再入库。规则库要维护因为数据源的写法千奇百怪电话号码有加区号的、有带分机号的正则写得不够细就会漏。上生产环境之前我建议人工抽检一批清洗后的数据看看规则库有没有误伤正常内容比如把“联系电话”这种说明文字也当成敏感信息给删了。3. 文档切分决定召回上限的关键动作3.1 三种切分策略的取舍切分是RAG-02里我最看重的环节。向量检索在大段文本上效果并不好因为一个句子的语义很容易被一长段无关内容稀释掉。所以文档要先切成小块再逐个向量化。固定长度切分最无脑按字符数或者token数切实现简单速度飞快。但问题也明显切断句子、切断语义单元切出来的块可能是一堆不完整的片段。递归字符切分比它聪明一点会优先按照段落、句子、标点这样的层级去寻找切分点保证块内文本尽量完整。语义切分更进一步用模型判断哪些句子应该分在一起效果最好但成本也最高。我这边的建议是默认从递归字符切分起步先把流程跑通沉淀一批真实的用户查询后再评估要不要上语义切分。不要一开始就追求复杂的切分方案因为切分效果最终要靠检索结果来验证不跑完整链路很难判断是切分问题还是向量化问题。3.2 块大小与重叠窗口怎么定块大小直接决定召回精度和检索成本。块太大一个块里包含多个主题查询向量只能匹配到其中一小部分很多无用内容被当成背景噪音块太小块内信息量不足语义表达不完整召回率会掉。最常用的做法是按token数控制块大小因为嵌入模型对token数有限制。我们的经验值是块大小设置在300到500个token之间这个范围基本能覆盖一段完整的技术描述又不至于让向量被稀释。重叠窗口设置成块大小的10%到20%左右作用是避免一个关键句子正好落在两个块的边界上导致两边都不完整。切分逻辑大概长这样def split_document(text, chunk_size400, overlap_size64): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end find_split_point(text, start chunk_size) chunks.append(text[start:end]) if end len(text): break start end - overlap_size return chunks注意这里的关键点是find_split_point它的作用不是硬切到start chunk_size就完事而是往前找最近的段落边界或句号尽量让切分点落在语义完整的位置。重叠的部分虽然会重复向量化存储成本稍微高一点但换来的是召回稳定性这笔账是划算的。4. 向量化与嵌入模型选型4.1 自部署模型的关键参数嵌入模型的选择对RAG效果的影响非常大但这个影响不是立刻暴露的往往要等检索结果烂到一定程度才会回头查。我的建议是优先考虑中文语料效果好的开源双语模型不要一上来就接商业API。自部署的好处是数据不出内网批量向量化的时候成本也可控。选模型时我最关注三个参数最大序列长度、向量维度、推理速度。最大序列长度决定了它能吃下多大的文本块如果块大小是400个token那模型的序列长度至少得是512。向量维度影响存储大小和检索计算量维度越高通常表达越精细但索引占用空间也线性增长。推理速度直接关系离线批量向量化的时间十万篇文档和百万篇文档的耗时差好几倍。嵌入模型还有一个容易忽略的点向量归一化。很多模型训练时采用余弦相似度推理出来向量长度不归一的话计算相似度时就等于在做点积数值范围和余弦相似度的排序结果会有偏差。我们在批量向量化之后、写入索引之前会做一次向量归一化处理这一步虽然简单但能让后续检索的分数更可解释。4.2 批量向量化与存储成本估算批量向量化的过程要注意显存和吞吐量的平衡。显存够大就把batch size拉高但一次性把几千条文本塞进模型显存很容易爆掉。更稳的做法是动态batch根据文本长度调整单批数量短文本多塞几条长文本少塞几条。存储成本这块可以用公式粗算。假设有十万个文本块每个块向量化后是768维的float32向量那么4字节乘768维乘10万条大概是307MB。如果加上索引内部的图结构或倒排结构整体还会再膨胀一些。如果用了降维或者量化比如int8量化存储可以控制在不到一百MB。所以选型之前先估算规模再决定要不要上量化。我遇到过团队选了一个1024维的模型向量化所有文档之后发现索引内存暴涨服务一启动就OOM最后不得不换模型重新来。这种问题完全可以在选型阶段用上面那个公式算清楚别等上了生产才返工。5. 索引构建与混合检索调优5.1 向量索引从暴力扫描到HNSW索引阶段最基础的做法是暴力扫描每次查询都遍历所有向量算相似度。十万条数据以下勉强能跑百万条以上就扛不住了。我们生产上用的是HNSW图索引它把向量组织成多层图查询时从高层粗选、逐层细化速度和召回之间可以靠参数调节。HNSW有三个关键参数M控制每个节点的邻居数efConstruction控制建图时的搜索宽度efSearch控制查询时的搜索宽度。M越大图越稠密召回越高但内存越大efConstruction越大建图越精细但耗时越长efSearch是查询时的调节旋钮需要实时调优。从我们的测试看参数配置建议如下参数推荐范围说明M16~32小数据集取16百万级取32efConstruction200~400建图阶段离线任务取大值更稳efSearch64~128在线查询取大值召回更高但延迟增加这些参数不要照搬实际应该用一份带标注的查询集去测Recallk来回调节找到平衡点。我见过有人把efSearch调到512召回确实高了一截但接口P95延迟从30毫秒涨到150毫秒这种代价就看业务能不能接受了。5.2 混合检索BM25和向量不是二选一向量检索擅长语义匹配但关键词精确匹配是它的弱项。比如用户搜一个产品型号“RAG-02”向量检索可能会把它跟“RAG系统模块划分”混在一起因为语义上很像。这时倒排索引的BM25能精确命中型号字符串给出正确结果。所以成熟的做法是混合检索向量检索召回一批BM25召回一批然后融合排序。BM25的参数不多k1控制词频饱和度b控制文档长度的影响程度。词频太高会过度影响分数所以k1一般取1.2到1.5把词频的影响压一压。b取0.75左右是比较常见的默认值。我们实际调参时会准备一组查询样本对比不同参数组合下的MRR参数变化对最终效果的影响挺直观的。融合方式我们用的是RRF也就是倒数排名融合简单有效。具体做法是把两种检索结果的排名取倒数再叠加分数这样即使某个检索器没有召回到某个文档也不会因为分数为零而完全丢掉它。相比之下直接加权求和的方式需要不断调两个检索器的权重很麻烦RRF基本不需要怎么调。5.3 重排模块不能省索引层跑出来的Top20候选直接送给生成模型是不合适的。向量检索和BM25都是粗排它们只保证候选跟查询有点相关但候选列表里往往夹杂着一些似是而非的内容。我们在这两层之后加了一个重排模块用一个更强的交叉编码器模型对Top20逐个打分排序最后取Top5喂给生成模块。重排模块的代价是增加了一点在线延迟二三十条文本过一次模型大概多几十毫秒。但它对最终答案质量的提升非常明显可以说是整个RAG链路上性价比最高的部分。很多RAG项目检索效果差不是粗排不行而是把粗排结果直接当成最终结果用了。6. 索引更新维护与监控6.1 增量更新与全量重建的适用场景索引不是建完就万事大吉数据一变化索引就得跟着变。这里最容易忽略的问题是文档被删除或修改后旧的向量块怎么处理。全量重建最简单也最可靠每天凌晨跑一次离线任务把所有文档重新清洗、切分、向量化、建索引。文档量不大、更新频率不高的时候全量重建完全没有问题。但如果库有上百万个文本块全量重建要跑几个小时在线服务就没法及时反映当天新增的内容。增量更新思路是只处理变更的文档新增文档直接切分向量化后追加进索引修改文档需要把旧版本对应的向量删掉再写入新版本删除文档需要精准定位并移除对应的向量和元数据。增量更新的难点在于删除和修改时必须要知道哪些向量属于哪个文档版本所以构建索引时每个向量块都要记录文档ID和块序号。这里的关键实践是元数据版本号。每次文档变更都生成一个新的版本号向量块和元数据都带上版本号。查询时只召回最新版本的数据历史版本在索引里占地方但不参与检索等夜间全量重建时再彻底清掉。这样既保证了增量期间的数据准确性又不用频繁动索引结构。6.2 需要盯住的几个监控指标索引服务上线之后我最关心的指标有三个查询延迟、召回质量和索引大小。查询延迟看P50和P95P95要控制在200毫秒以内超过这个阈值说明索引参数太激进或者服务资源不够。召回质量不能只看在线指标因为用户不会主动告诉你检索对不对需要用一批带标准答案的评测集定期跑离线回归观察RecallK和MRR的变化。索引大小和内存使用要监控曲线如果发现周期性增长大概率是增量更新时该删的向量没删干净索引里积攒了大量过期数据。有一次我们线上索引内存一天涨了500MB排查下来发现是删除逻辑写错了只删了元数据表没删向量索引里的记录导致每次文档更新都残留旧向量。后来我们加了一个对账任务定期比对元数据库和向量索引的数量差了就报警。7. 常见问题与排查实录7.1 问题速查表这半年处理过的和同行交流过的问题整理成一张表可以直接对着排查现象可能原因处理方式检索结果大量无关切分块过大语义被稀释调小块大小或改用语义切分精确型号搜不到只有向量检索没有关键词索引加BM25倒排索引向量维度不一致导致索引失败嵌入模型换过但索引没重建统一模型重建全部索引服务内存持续增长增量更新时旧向量未删除检查删除逻辑加对账任务同一问题答案不稳定重叠窗口或重排参数导致候选波动固定检索参数评估TopN稳定性召回结果都是旧版本元数据版本号没有参与过滤检索时按最新版本过滤7.2 两段真实的排查过程第一个问题挺典型的。客户反馈系统对某个专业术语的搜索效果很差基本上查不到相关内容。我们一开始以为是嵌入模型对术语理解不到位换个更强的模型也没有明显改善。后来把召回结果拉出来看发现术语本身包含一个短横线清洗阶段把短横线当标点去掉了文档里和查询里的术语都变成了不完整的字符串向量检索当然对不上。解决办法也很简单把这类术语加入清洗阶段的保护名单不让它被误删。第二个问题是索引文件莫名其妙损坏。某一次服务重启之后查询接口一直报错索引加载到一半就崩。查了半天发现是构建索引的离线任务在写文件时磁盘空间满了没及时告警索引文件只写了一半。修复方式是把索引构建改为先写临时文件再原子替换正式文件这样即使构建过程异常线上服务加载的始终是上一份完整索引不至于因为半成品索引文件导致服务不可用。这两个问题都不是什么高深的技术难题但都说明了一个道理数据与索引模块的坑往往藏在看似不起眼的数据处理细节里排查的时候不能只盯着模型和算法要把整个链路从原始数据开始一路查下来。最后就我个人经验来说RAG-02这个模块最值得投入的地方不在花哨的算法而在把每个环节做扎实数据清洗规则要维护切分策略要测试索引参数要调优更新链路要监控。这个模块一旦出了问题反馈周期很长从数据源头到最终生成结果要经过好几道工序定位问题往往比解决问题更耗时所以必须在前期就把监控埋好。另外一个小建议从项目一开始就准备一份带标准答案的评测集不要等到上线前才匆匆准备。没有评测集你永远说不清楚是切分改坏了还是重排调差了。有评测集之后每次改动都能量化评估这个习惯能帮你减少大量返工时间。