前两周我还在昇腾平台上调一个 RAG 知识库问答系统,遇到一件让我挺纠结的事:文档全部切好了,Embedding 模型也换成了口碑更好的版本,检索出来的结果依旧不太贴题。后来一路排查到索引结构,发现问题根本不在模型,而在文档切分和索引构建这两个“检索前”环节上。这让我意识到,很多人(包括当时的我)对 RAG 优化顺序的理解是反的:总以为先换模型、再调提示词,但实际上索引结构优化才是知识库问答能达到多高召回率的硬上限。
这篇文章不聊大模型调优,专门聊昇腾平台 RAG SDK 在“检索前优化”里最核心的一块——索引结构优化。内容包括分块策略、向量索引参数、BM25 倒排通道、融合检索策略,以及我在昇腾 NPU 环境里实际踩过的坑和复盘过程。适合刚开始在昇腾上搭 RAG 的工程师,也适合已经跑通检索但召回率上不去的同学对照排查。
在进入技术细节之前,先说一个经常被问到的点:“昇腾系列有哪些 GPU?”严格说,昇腾不是 GPU,它是华为的 NPU 加速卡产品线,主流形态包括 310(边缘推理)、510(视频/推理)、910 系列(大模型训练和推理)、以及近期社区里讨论热烈的昇腾 950 测试型号(面向下一代训练推理场景)。对 RAG 来说,Embedding 计算和 LLM 推理都跑在 NPU 上,而索引构建和检索通常由 Host 侧 CPU 配合 SDK 完成——这个分工决定了你在调索引结构时,不能只盯着 NPU 算力,还要重视数据链路和 CPU 内存的使用效率。
1. 检索前优化在RAG链路里的正确定位:先弄清优化对象
1.1 一条典型检索失败链路
举个我调试过的例子。知识库里有一篇文档,标题是“昇腾910B 服务器性能参数”,用户问的是“910B 的 HCCS 组网带宽是多少”。按常理,Embedding 模型应该能理解语义,用户用了“组网带宽”这种词,文档里写的是“HCCS 链路速率”,语义是相近的。但系统召回的前十名里根本没有这篇文档,反而出现了很多讲“服务器整机配置”的无关片段。
把这个问题层层拆开之后会发现,Embedding 模型本身没有异常,检索日志也正常,真正常的是文档切分环节:这篇文档被切成了一段长度约 1800 字的超长块。向量化之后,这条长向量的语义被“服务器整机配置”那部分内容稀释了,导致它和“HCCS 组网带宽”的相似度被拉低;同时 BM25 倒排索引里,“组网带宽”这种用户口语化说法和文档中的“链路速率”之间没有高频词匹配,词汇通道也没有补救能力。于是问题同时出在两头:切分策略让语义太“杂”,词汇通道又没有兜底。
这个案例至少能带出两个教训。第一,检索前优化是整个检索质量的地基,地基不稳,Embedding 模型再换也白搭。第二,索引结构优化不能只理解成“索引文件怎么存”,它是从切分、Embedding 到索引类型和数据组织方式的全流程设计。
1.2 “检索前”到底覆盖哪些环节
RAG 的标准链路大致是:文档解析 → 文本清洗 → 切分 → Embedding → 索引存储 → 召回 → 重排序 → 提示词组装 → LLM 生成。其中“检索前”指的就是生成前的那一段准备链路,尤其从切分到索引存储这一段。
具体拆开看,检索前优化包含四个层级:
- 数据层:文档解析、去重、格式归一化。PDF 里表格转出来的文本经常是乱序的,这会影响后面的句子切分和向量质量。
- 切分层:分块大小、重叠长度、切分策略。这个决定一条向量“语义纯度”的粒度。
- 索引层:向量索引类型、倒排索引参数、索引更新策略。这个决定检索时能用多快的速度找到最相似的多条结果。
- 召回层:召回数量、融合策略、过滤条件。这个决定进入重排序环节的候选质量。
很多人把索引结构优化等同于“把 FAISS 参数调一调”或者“换个向量数据库集合”,其实它横跨了切分层和索引层,而且和召回层的融合策略强相关。少了任何一层,其他几层做得再好都发挥不出来,因为它们之间是串联关系,任何一个环节掉链子,后面全程白干。
1.3 索引结构为什么是召回率的天花板
这里有个很容易被忽略的逻辑。索引结构决定了检索器能看到什么样的“搜索空间”。如果分块把两个语义不同的内容切到同一个块里,那这块向量本身就是“脏”的,不管 Embedding 模型多强,召回时它都不能被准确匹配到具体语义。如果向量索引参数设置不合理,比如 hnsw_m 太小,图结构的搜索通路不完整,原本应该被召回的点也可能被漏掉。
可以这么理解:Embedding 模型决定了一条条向量的“指向精度”,而索引结构决定了这些向量被组织成什么样的地图。地图本身绘制错误或者信息量不足,拿着再精确的方向数据也找不到目的地。所以我习惯把索引结构优化称为检索质量的“第一道闸门”。
另外,索引结构还直接影响了“检索效率”。同一份 100 万段文档,暴力检索也能做到理论上的最大召回率,但延迟完全不可接受;好的索引结构是在召回率几乎不损失的前提下,把检索耗时降两个数量级。昇腾环境下的知识库应用通常对端到端延迟敏感,索引结构的取舍本质上是召回和延迟之间的平衡问题。
2. 索引结构的三层设计:分块、向量索引与倒排索引的取舍逻辑
2.1 分块是第一性参数:先定块再谈参数
“分块”的官方叫法是 chunking。它的本质是把一篇不定长的文档,拆成若干语义相对独立的片段,再对每个片段做 Embedding。分块策略和参数直接决定了检索的最小单元是什么。
常用的切分方式有三种。固定长度切分按字符数切,比如每 512 字一块,不关心语义边界,实现简单但经常把一句话劈成两半;递归切分按段落、句子、标点的优先级递归切割,先用空行切出段落,段落超长再用句号、分号切,这是目前通用场景最稳的方案;语义切分根据句子 Embedding 的相似度动态断句,块边界更“懂语义”,但本身需要额外计算,构建成本比较高。
昇腾 RAG SDK 里一般会提供 chunk_size 和 chunk_overlap 这两个核心参数。我的建议是 chunk_size 不要拍脑袋定成 512,得看实际语料的句子密度:中文技术文档建议 500 到 800 字,重叠 100 到 150 字;问答对数据建议 200 到 400 字;如果是长报告、论文摘要这类自带强章节结构的文档,可以把块容量放大到 800 到 1200 字,但必须配合递归切分,保证块边界落在章节自然断点附近。
块大小对召回的影响非常直观。块太小,单块语义支离破碎,检索匹配到的只是局部关键词,生成阶段拿到的上下文不足;块太大,单条向量语义混合多个主题,相似度被“平均化”,常见的表现就是检索结果“似像非像”。重叠窗口的用处是缓解跨块语义割裂,关键信息如果恰好处在两块交界处,有重叠时至少有一块能完整覆盖它。这个参数看似不起眼,实际价值往往比换一个 Embedding 模型还大。
2.2 向量索引参数:HNSW 的 M、efConstruction 和 efSearch 怎么配
分块确定之后,再来说向量索引。昇腾 RAG SDK 底层对接的向量索引通常是 HNSW 或 IVF 族算法。HNSW 是目前中小数据量下召回率和速度平衡最好的方案,核心参数有三个:M 是每个节点的最大连接数,efConstruction 是构建时的候选队列长度,efSearch 是查询时的候选队列长度。
参数的作用和选择逻辑如下。M 越大,图连接越密,召回率越高,内存占用和查询延迟也越高。M=16 是通用起点,超过 32 会明显吃内存,数据量超过 200 万段时优先考虑 M=24 到 32 的密集图。efConstruction 影响建图质量,一般 100 到 200 足够,它不是越大越好,建图时间成本线性上升,超过 200 后召回收益趋近于零。efSearch 则是查询时最常用的现场调参点,直接控制搜索宽度,efSearch 越大召回越高、延迟也越高。实际场景里可以先固定一个值上线,再用日志里的 P95 延迟反向调整。
还有一种常见做法是把 HNSW 和 IVF 结合,先对向量做粗聚类,再在桶内建立 HNSW 图,适合千万级以上的数据。IVF 模式注意 nlist 聚类数量的设置,经验值是数据量平方根量级,100 万段设成 1000 到 2000 比较合理,nprobe 控制在 8 到 32。理解这些参数的核心价值在于知道:IVF 牺牲一点精度换速度,HNSW 精度好但内存开销大。不存在“最好的索引”,只有适合当前数据量和延迟要求的那一个。
2.3 倒排索引与 BM25:语义检索之外必须保留的词汇通道
很多项目做 RAG 只建向量索引,把倒排索引整个忽略掉了,这是一个典型误区。向量检索擅长语义相似,但对精确词汇匹配比较弱。用户问“昇腾 910B 的电源功率”,文档里恰好有“电源功率 2000W”这样的原文本,如果 Embedding 模型对这个短句理解得不够到位,向量召回不一定把它排在前面。BM25 这类倒排索引基于词频和文档频率打分,能保证“文本里写了这个关键词”的文档一定进入候选集。
BM25 有两个经典参数:k1 控制词频饱和度,通常取 1.2;b 控制文档长度归一化,通常取 0.75。在中文场景里,比这两个参数更重要的其实是分词器。如果用默认的英文分词器去切中文文本,BM25 基本是废的。昇腾 RAG SDK 一般支持自定义分词器,技术类知识库强烈建议准备两样东西:一是中文分词器加领域词典,把“昇腾910B”“HCCS”“CANN”这类专有名词完整切出;二是对标题、摘要、代码标识符等字段做加权,让更重要的词在倒排表中的权重更高。
在医疗、法律、高端制造这类术语密集的领域,倒排索引经常在关键时刻“救场”。用户提问往往带着精确型号、条款编号或者产品名,这些内容在词汇层面的匹配强度远高于语义层面的相似。只做向量检索的系统,在这种场景下的召回率可能只有六成;向量加 BM25 双通道,至少能摸到八成的门槛。这不是夸张,我在这类场景里验证过很多次。
2.4 融合策略:为什么 RRF 通常比分数加权更稳
双通道召回之后,面临一个关键问题:怎么把两条通道的结果合并成一个有序列表。最直觉的做法是把向量相似度和 BM25 分数直接加权求和,但实践中很容易翻车。原因很简单,两个分数不在同一个量纲、同一种分布上,权重很难定准,换个数据集就得重新调。
更稳的方案是 RRF,Reciprocal Rank Fusion。RRF 的思路不看绝对分数,只看每条结果在各通道中的排名。假设一份文档在向量通道排第 2,在 BM25 通道排第 7,融合分就是 1 除以 60 加 2,再加上 1 除以 60 加 7,其中 60 是经验常数 RRF_K。这样两个通道的分数被抽象为秩信息,天然具备可比性,对权重不敏感,实现还简单。
RRF 也不是没有缺点。如果某个通道整体质量很差,它的排名噪声会直接进到融合结果里。所以在用 RRF 之前,先分别评测单通道效果,确认两条通道各自都有可用性,再开启融合。融合之后还要处理好 top_k 和候选集的衔接:一般向量通道和 BM25 通道各自取 20 到 30 条,融合后再取 top 10 进重排序。这个微小的候选数量设置,比我盲目把两通道各取 top5 的效果要好得多,因为重排序环节需要更充分的候选池才能发挥价值。
3. 昇腾NPU上的索引构建实战:RAG SDK配置与算力分布
3.1 昇腾系列硬件在RAG里的角色:NPU 不是 GPU
先解决很多人刚接触时都会问的问题:“昇腾系列有哪些 GPU?”严格说,昇腾不是 GPU,它是华为的 NPU 加速卡产品线。昇腾产品线覆盖了从边缘到数据中心的多种形态:
- 昇腾 310:主打边缘推理场景,算力相对小,做向量 Embedding 的批量推理任务时吞吐有限,适合轻量级服务。
- 昇腾 510:视频分析和推理场景更常见,和通用文本 RAG 的耦合度不高。
- 昇腾 910 系列:大模型训练和推理的主力,跑 7B 到 70B 级别的 LLM 和 Embedding 模型都比较稳。
- 昇腾 950 系列:最近社区讨论的昇腾 950 测试型号,多指下一代训练推理加速卡,算力规模和显存容量比 910 系列有进一步提升。
对 RAG 来说,NPU 主要负责两类计算:一类是 Embedding 模型的批量推理,把文本转换成向量;另一类是最终生成阶段的 LLM 推理。向量索引的构建和检索本身,通常是在 Host 侧 CPU 内存里完成的。这意味着在昇腾平台上做 RAG,索引结构优化的两个主战场分别落在 NPU 上的 Embedding 吞吐,以及 Host 侧 CPU 的索引构建和检索延迟上,两者不能偏废。
3.2 用昇腾RAG SDK配置混合索引
昇腾 RAG SDK 的接口封装方式在不同版本里会有差异,但核心配置项的语义是通用的。下面这段代码展示的是我当前项目里使用的配置骨架,你对照自己的 SDK 版本调整一下 API 名称即可。
from ascendsdk.rag import ProcessorConfig, ChunkConfig from ascendsdk.rag.index import VectorIndexConfig, Bm25IndexConfig from ascendsdk.rag.retriever import HybridRetriever, RetrieverConfig # 文档解析、清洗后的切分配置 chunk_cfg = ChunkConfig( strategy="recursive", chunk_size=768, chunk_overlap=128, separators=["\n\n", "\n", "。", ";", ",", " ", ""], ) # 向量索引走 HNSW vector_cfg = VectorIndexConfig( algorithm="hnsw", metric="cosine", hnsw_m=24, ef_construction=200, ef_search=64, ) # 倒排索引:K1=1.2, B=0.75,外加领域词典路径 bm25_cfg = Bm25IndexConfig( k1=1.2, b=0.75, tokenizer="zh_custom", custom_dict="/data/lexicon/ascend_terms.txt", ) # 双通道融合 retriever = HybridRetriever(RetrieverConfig( vector=vector_cfg, bm25=bm25_cfg, fusion_method="rrf", rrf_k=60, top_k=10, channel_top_k=25, )) # 批量灌入已切分的文档块,内部会自动调 NPU 做 Embedding retriever.build_index(documents=chunks, batch_size=128)几点说明。第一,metric 选择上,中文文本和通用语义模型搭配,cosine 是主流选择,除非你的服务已经对向量做了明确的 L2 归一化处理。第二,BM25 配置里的领域词典是我强烈建议加的,后面的避坑清单里会展开讲为什么。第三,build_index 的 batch_size 直接决定 NPU 利用率,我在昇腾 910 上常用 64 到 128,具体看输入模型的 max_seq_len 和 NPU 显存容量。批次太小,芯片吃不饱;批次太大,如果文本长度参差不齐,反而出现等待和显存碎片化。
3.3 索引构建的算力瓶颈:NPU提速但Host CPU不能拖后腿
昇腾平台上批量 Embedding 的吞吐其实不是最大瓶颈。真正容易卡住的是数据预处理和索引写入环节。Embedding 之前要做分词、归一化、padding,这些都在 Host CPU 上来回搬数据;索引写入时,HNSW 的插入操作对图结构有依赖,多线程批量插入并不像想象中那么美好。
我常用的优化思路有三个。第一,把文本预处理做成流水线。用 Python 多进程把文档解析、清洗、切分的结果先落盘到中间缓存,Embedding 环节只读缓存,避免主线程被磁盘 I/O 阻塞。这一步做完,昇腾 910 上的构建吞吐能提高百分之三十到五十。第二,HNSW 构建时不要开太多线程。HNSW 插入算法本身是分层的,全局并发写容易造成图结构不一致,SDK 内部一般靠锁保护,线程太多反而锁竞争严重。经验值是 8 到 16 线程,把剩余 CPU 留给数据预处理。第三,增量构建优先于全量重建。知识库文档更新频繁时,每回都全量重建索引,时间成本完全扛不住。我通常的做法是:新增文档直接插入索引,删除项打软删标记,每天低峰期做一次索引整理。昇腾 RAG SDK 底层如果用向量数据库做持久化,建议把“批量插入 + 软删除 + 定期 compact”作为标准节奏。
4. 从75%检索满意度到可用的三轮回调复盘
4.1 初始配置与问题症状
我本地有一套企业知识库问答系统,跑在昇腾 910B 平台上,语料大概 120 万段,覆盖产品文档、工单记录和技术博客。初始配置是 chunk_size=1024、overlap=0,纯向量检索,HNSW M=16。检索满意度只有 75% 左右,典型症状有三类:问型号参数时返回的是泛泛的整机描述;问故障处理步骤时返回多条重复内容;部分新入库的工单第二天就搜不到。
这三类症状分别对应索引结构的三类问题:长块语义稀释、缺少去重策略、增量索引一致性差。单纯换 Embedding 模型解决不了这些问题,因为症状全都出在索引组织方式上。
4.2 第一轮优化:分块与向量索引参数
第一轮改动只动两个地方。把 chunk_size 从 1024 降到 768,overlap 设为 128,切分策略从固定长度改成递归切分;HNSW 的 M 从 16 提到 24,ef_search 从 32 调到 64。改动之后,长块语义稀释的问题明显缓解,检索结果第一条的命中率提升了。
为什么这样调?因为 1024 的块对中文技术文档偏大,一个块里往往混着概述、参数表和注意事项三个话题;768 配合递归切分,大多能落在章节断点附近。overlap 128 覆盖了说明文字的首尾重复区域,跨块检索的丢命中率自然下降。M=24 让图连接得更密,ef_search=64 扩大搜索面,当时观察到的 P95 延迟只增加了约百分之二十,但召回率明显改善。这说明索引参数的调整是有杠杆效应的,花费的算力不多,换来的是检索质量的大幅度提升。
4.3 第二轮优化:引入BM25通道和RRF融合
第一轮之后,语义相关问题的命中率上去了,但精确词匹配问题还是明显。用户这里是工单场景,通常直接输入“ASCEND 910B 网卡丢包”,而文档里的“网卡丢包率”如果没有被语义模型精准关联,向量通道就找不到。于是第二轮加入 BM25 倒排索引,构建了一个行业术语词典,把所有产品型号、协议名、报错码都放了进去,同时开启 RRF 融合。
这一轮的效果最明显。检索满意度拉到 85% 左右,尤其是故障处理和参数查询类问题的 top5 命中率大幅提升。BM25 通道保证了精确词一定进候选,向量通道负责同义改写和语义泛化,两条通道的互补关系很好地建立起来了。RRF 融合还有个附带好处:对两个通道排名都很靠前的文档,融合分会有非线性加成,正好解决了纯向量检索里“文档有多个相关段落但每个都不是最像”的情况。
4.4 第三轮优化:元数据过滤与增量一致性
第三轮做的更多是工程收尾。我给每个文档块打了知识域、来源类型、更新时间的元数据标签。检索时如果用户选择了“只看产品文档”或“只看工单”,直接在召回前做过滤,而不是等结果都返回之后再人工筛选。效果非常直接:候选集变小,后续重排序和生成的噪声也少了。
增量一致性也做了调整。原来的做法是每天全量重建索引,成本高而且延迟大,新工单第二天搜不到的问题一直存在。改成新文档实时入库、删除项打墓碑、每天凌晨三点做一次索引整理和 compact 之后,这个问题基本消失。到这里,系统的可用性才算真正立住了。
4.5 优化前后的对比结果
下面这组数据是我自己记录的(数据集固定为当月 120 万段语料,100 条人工评测问题,每条问题跑三次取均值):
| 方案阶段 | Top5 命中率 | P50 检索延迟 | 人工满意度 | 索引构建耗时 |
|---|---|---|---|---|
| 初始方案 | 52% | 38ms | 75% | 约 2.5 小时 |
| 第一轮后 | 61% | 46ms | 79% | 约 3 小时 |
| 第二轮后 | 74% | 58ms | 85% | 约 3.5 小时 |
| 第三轮后 | 78% | 52ms | 88% | 约 3.5 小时 |
第三轮之后延迟不升反降,主要得益于元数据过滤减少了无效候选的检索范围。最终人工满意度 88%,距离理想状态还有差距,但比起最初已经是一个可用的状态。这个案例说明一个道理:索引结构优化不是一步到位的,它是分块、索引参数、通道融合、元数据过滤和工程策略层层叠加的结果。
5. 索引调优过程中踩出来的坑和注意事项
5.1 分块参数“一调就乱”的根因
很多人调分块参数只看全局平均指标,结果发现怎么改都没效果。我的经验是先看失败案例的分布。如果失败的 case 集中在“太长太杂”的长文档上,优先调 chunk_size 和递归切分;如果失败 case 是“关键词被切到两块边缘”,优先加 overlap,而不是继续压缩 chunk_size。分块参数高度依赖语料结构,没有一个万能值能通吃所有场景。上线之前一定要做小样本切分质量评估,把切出来的块随机抽几十条人工看一眼,别凭感觉调参。
5.2 中文BM25的分词:不建词典等于白搭
我见过不少项目兴冲冲开启 BM25 通道,结果效果很差,最后直接放弃。常见原因就一个:分词器不认识专业术语。“昇腾910B”如果被切成了“昇腾 910 B”,用户搜“昇腾910B”时 BM25 分数会变得很低,因为这个词的完整形态根本没有进入倒排表。解决办法是在分词阶段用领域词典强制保留专有名词。词典维护成本不高,从历史工单和产品文档里抽取高频词,每两周更新一次就够了。索引优化的很多收益来自这些基础工程,而不是那些听着酷炫的算法替换。
5.3 增量更新的“墓碑”和一致性问题
知识库更新场景下,索引文件里已经失效的向量如果还占着位置,检索时就会持续召回过期内容。最简单的方案是删除时执行真正的物理删除,但 HNSW 的物理删除容易造成图结构空洞,反复删除插入之后检索质量会下降。实践里更稳的是“墓碑”方案:删除操作先打标记,召回阶段过滤掉;累积到一定比例或固定周期后,再统一重建或 compact。这个节奏要结合索引构建耗时来定,我在昇腾平台上做日更语料,选择每天凌晨执行一次维护,白天只做实时插入和软删除。这套机制看起来不起眼,但能不能保持长期稳定,全靠它。
5.4 用评测集而不是单条Case来验证索引调整
最后这条经验最重要。我在调试过程中曾经因为某一条 case 命中了就沾沾自喜,结果跑全量回归发现整体指标反而下降。索引调优必须有评测集思维:固定 50 到 100 条有代表性的问题,每轮调整后跑一遍回归,既看命中率也看延迟分布。单条 case 只能用于定位问题,不能作为决策依据。
实际项目里,我把评测集分成三类:参数查询类,强调精确匹配;语义问答类,强调同义改写能力;流程步骤类,强调多跳召回和位置信息。跑完一轮评测就能立刻判断当前调参影响了哪种检索能力,再针对性地推进下一步。没有评测集,索引调优就是瞎猫碰死耗子,碰巧调好也不知道是因为什么。
最后再分享一个实操体会。很多人会把优化精力全部放在 Embedding 模型选型上,总想着找一个“更强的语义模型”来弥补索引的缺陷。我在昇腾平台上反复对比过之后发现,在同样的算力预算下,把时间花在索引结构优化上,往往比盯着模型榜单换模型性价比高得多。先把索引结构调整到健康状态,再回头审视模型,这个顺序一定不要搞反。