
最近把RAG知识库项目从GPU环境迁移到昇腾平台第一轮召回测试跑下来hit rate只有0.52答案不可用率超过30%整个人是崩溃的。后来把索引全流程重新过了一遍——文档清洗、分块策略、embedding在NPU上的部署、混合检索、查询改写、重排序、效果评估——层层优化之后hit rate才涨到0.87。这篇文章就把整套优化链路放在昇腾平台语境下完整复盘一遍包含具体参数配置、踩坑记录和评测方法适合正在做RAG知识库、尤其是准备在国产算力平台上落地的团队参考。先说明一下背景我这边用的是Atlas 300I/3000系列的推理卡软件栈是CANN torch_npu部分推理服务用MindIE封装。整套流程里能用NPU的地方我尽量用跑不了的比如向量数据库检索留在CPU侧。这个架构在不少团队里都有类似形态下面讲的内容可以直接照着迁移。1. RAG项目的瓶颈往往不在推理而在索引1.1 先理解RAG链路里哪一环节决定答案质量很多人立项之初的标准动作是装好LangChain或者Spring AI丢几个PDF进去连上LLMdemo跑通了就觉得完事。但一旦放到生产环境把大模型换成昇腾上的推理服务用户问几个真实问题答案质量立刻打回原形。问题几乎都出在同一个位置——检索结果本身是错的。RAG链路可以粗分成五段文档解析、文本分块、向量化、检索召回、重排生成。前面三段属于索引侧后面两段属于查询侧。我做过一个粗略统计在知识库场景里最终答案错误/不满意的case中大约七成是因为召回的chunk里根本没有正确答案或者上下文碎片化真正属于LLM生成能力不足的不到三成。这就是为什么我说瓶颈在索引不在推理。1.2 昇腾平台上做RAG的生态现状能跑但习惯要改昇腾平台这几年生态确实起来了torch_npu对PyTorch模型的支持度在提升CANN提供的算子覆盖面也够用。像bge-m3这类embedding模型、bge-reranker这类交叉编码器本身结构不复杂在昇腾上跑没有大问题。但有一个核心习惯必须改CUDA生态下写个Python脚本、数据按真实长度直接喂进去的方式在NPU上效率很低。昇腾NPU对动态shape的容忍度比GPU差实际表现为每个batch内部序列长度参差不齐时推理延迟会大幅波动甚至整体变慢。这在索引流程里是致命的——embedding环节的高延迟会直接影响大批量文档的向量化速度。我的经验是凡是上NPU的模型优先考虑固定shape或分桶shape而不是靠框架自动兜底。另外如果你依赖的是LangChain/LlamaIndex的默认组件跑完整流程建议把每个环节拆出来单独验证。默认的文本分割器、默认的向量检索参数是为通用场景设计的不是为你的业务场景设计的。我在迁移过程中把LangChain的Retriever全部替换成了自己的实现只保留了下游的LLM调用。这样做的原因后面会详细讲。2. 数据准备与分块策略索引质量的第一道闸门2.1 文档清洗被忽略的脏数据问题很多团队一上来就分块、embedding完全不做清洗。等到检索结果乱成一锅粥才发现源文档里全是坑。常见的坑包括PDF页眉页脚混入正文、表格内容被切断成碎片、扫描件没有OCR、代码块里的空行和注释干扰分块、英文文档里换行符导致单词被拆开。这些问题在分块之前不解决之后每一步都会被放大。我现在的处理流程是PDF统一用PyMuPDF先抽文本层有文字的直接用没有文字层的扔给OCR服务。OCR我用的PaddleOCR昇腾环境上可以正常跑只是部署的时候注意要用CANN相关的推理配置别用默认的CPU模式硬扛。表格类文档单独处理。PyMuPDF抽出来的是按行排的文本流表格单元格之间关联关系全丢了。现在的做法是转成Markdown表格后再进入分块流程保证每行记录和表头在一起。统一去噪。去掉页眉页脚、页码、多余换行、PDF生成时常见的不可见字符这些用正则就能做掉大半。长文档先做结构感知。按标题层级H1/H2/H3切分让每个候选块尽量对应一个语义完整的章节。2.2 三种分块方式的取舍别迷信某一种文本切分看起来简单实际是索引质量的最大变量。我对比过三种方案方案原理优点缺点固定长度分块按token或字符数硬切实现简单、可控语义断裂严重一句话或一个知识点被拦腰截断递归字符分块按段落、句子、标点逐级切分保留一定语义边界LangChain默认方案对法律、表格、代码等特殊结构不敏感父子分块small2big先切大块用于语义理解检索时返回大块对应的内容命中率高、上下文完整存储和检索链路复杂一点要维护两层chunk映射我推荐的基础方案是结构感知分块 父子chunk。具体做法先用标题层级切出父chunk再把父chunk按段落或句号边界切成子chunkembedding只做在子chunk上检索命中子chunk后把所属的父chunk整体作为上下文交给LLM。这样既保证召回粒度细又保证上下文完整尤其适合政策文件、产品手册这类一个知识点跨多段的文档。至于最近讨论很多的语义分块semantic chunking用embedding相似度判断断点和GraphRAG那套结构化索引我的观点是在数据规模中等、知识结构不稳定的场景下收益不匹配成本。语义分块需要额外的模型推理耗时GraphRAG更是要先构建实体关系图。等你的知识库有明确的实体关系查询需求时再考虑GraphRAG也不迟不要一上来就追热词。2.3 chunk_size和overlap怎么定不要抄默认参数LangChain默认的chunk_size是1024、overlap是200这个值在通用问答场景可能还行但在知识库场景我测下来发现经常导致两个问题chunk太大命中后塞给LLM的上下文里噪音太多chunk太小答案信息被切成两半。我建议以512为起点做网格搜索chunk_size分别测256/384/512/768overlap分别测0/32/64/96。拿你自己的评测集跑一遍hit rate选最优组合。以我做过几个项目的经验客服/FAQ类知识库chunk_size256通常表现最好。因为FAQ条目本身就短大chunk引入的噪音会干扰答案提取。政策法规/产品说明书chunk_size512更稳。这类文档的答案经常跨段落太小的chunk会导致上下文缺失。overlap控制在10%~15%即可不需要很大。overlap的作用是弥补边界切分误差overlap过大只会增加重复存储和检索噪音。顺带提一句切分维度我用的是字符数不是token数。中文场景下token切分和字符切分差异不小如果你用token数一个chunk可能一会儿是1000字、一会儿是600字对后续固定shape的embedding不友好。字符数更直观、更好控制。3. 在昇腾NPU上跑Embedding部署细节与性能调优3.1 模型选型与部署方式Embedding模型我选了bge-m3原因有三条一是中文效果够用在MTEB中文榜单上和同量级的商用模型差距不大二是本身支持稠密向量稀疏向量多向量三种检索方式后面做混合检索有天然优势三是最大序列长度到8192适配长文本场景。如果你只需要中文且追求极致性能bge-large-zh-v1.5也不错输出维度1024检索精度很能打。部署方式有两条路torch_npu直接推理适合离线批量向量化脚本里加载模型model.to(npu:0)即可。灵活性最高方便嵌入自定义的数据处理流程。MindIE封装成HTTP服务适合在线实时embedding查询比如用户query的向量化。MindIE对动态shape和并发调度做了优化吞吐比裸torch_npu高不少。我的线上架构是离线建索引阶段用torch_npu批量向量化在线查询阶段走MindIE服务两者不混用。原因是离线任务要的是吞吐在线任务要的是低延迟和稳定的响应时间用同一个服务模式去扛两个诉求两边都做不好。3.2 动态shape的坑NPU上必须做分桶padding这是我在昇腾上踩得最深的坑之一。bge-m3这类BERT结构模型输入长度天然是不等长的。在GPU上框架可以逐batch动态编译问题不大但NPU上动态shape往往触发重新构图和算子调度单个batch的推理延迟能从5ms飙到30ms长尾延迟极其难看。解决方法很朴素分桶padding。把输入长度按128/256/512/1024划分四个桶不足桶长的padding到桶长超长截断。例如query长度是137就pad到256这个桶再送入模型。def pad_to_bucket(seq_len, buckets(128, 256, 512, 1024)): for b in buckets: if seq_len b: return b return buckets[-1] # 实际使用时batch内全部pad到同一个桶长 bucket_len pad_to_bucket(max_seq_len_in_batch) input_ids pad_sequence(sequences, batch_firstTrue, padding_value0)[:, :bucket_len]分桶之后静态shape带来的好处很明显离线向量化时batch size可以开到64甚至更大吞吐提升约2倍在线推理延迟稳定P95从30ms降到12ms左右。代价是多算了一点点padding token在NPU算力面前可以忽略。3.3 线上推理batch策略与显存管理在线embedding服务我建议按batch做动态排队请求进来先放入队列凑够batch size再统一推理而不是来一个推一个。这能大幅提升NPU利用率。我这边MindIE服务配了并发窗口单个请求返回时间稍有增加但整体吞吐翻了一倍多在知识库场景完全划算。还有一个容易忽略的点embedding模型和LLM共用一张卡时显存会被大模型挤占。embedding模型很小单独跑不占多少显存但共用卡时碎片化严重偶尔会OOM。教训是不要用device_mapauto让框架自己分配手动指定embedding模型固定在NPU的某一块上或者干脆用独立卡位跑embedding。如果你的推理卡紧张至少要做到embedding服务进程设置固定的可用显存上限别让它和大模型抢。4. 索引存储与混合检索别把向量库当唯一答案4.1 向量数据库选型思路昇腾平台不限制向量数据库的选型因为向量检索跑在CPU/内存侧和NPU没有直接耦合。市面上的主流选择是Milvus、Qdrant、Elasticsearch向量插件和FAISS朴素方案。我的建议是看数据量和管理成本百万级向量以下Qdrant足够。部署轻、API直观、Rust写的性能也好docker单机就能跑。百万级以上、对高可用有要求的上Milvus。支持数据分片、多副本、混合查询运维复杂度高一些但可控。公司里如果已经重度使用Elasticsearch直接加向量索引插件是最省事的方案不用多维护一套存储。不管选哪个建索引的参数要调。HNSW的核心参数是M和efConstruction。M控制每个节点的最大连接数M太小召回差太大会增加内存和构建时间我一般设16。efConstruction控制构建时的候选集大小我设200召回率比较稳。线上查询时的ef参数单独设控制在64~128之间即可太大性能会掉得厉害。另外强烈建议保存向量后顺手做一步向量归一化不然后续计算余弦相似度和内积结果会混淆。我习惯统一用余弦距离归一化之后余弦和点积等价后续接rerank时分数处理更干净。4.2 为什么必须加BM25做混合检索纯向量检索有一个经典盲区专有名词、型号编码、人名、代码片段。这些内容在embedding语义空间里经常被淹没比如用户搜CIF-100还是CIF-200好模型可能把语义重心放到好上返回一堆不相关的产品比较。这类场景BM25这种基于词频的稀疏检索反而非常准。所以混合检索不是可选项是必选项。稠密向量负责语义近似BM25负责精确词汇匹配两者互补。具体做法知识库文档先分词走BM25索引可以直接用Elasticsearch的BM25也可以自己实现一个轻量的。查询时稠密向量检索取top50BM25检索取top50。两路结果做融合进rerank。4.3 RRF融合与元数据过滤两路检索结果的分数不能直接相加。BM25的分数分布和余弦相似度完全不在一个量级加权系数调起来很麻烦。我推荐用RRFReciprocal Rank Fusion纯看排名不看分数。def rrf_fusion(ranked_lists, k60): scores {} for ranked in ranked_lists: for rank, doc_id in enumerate(ranked): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) return sorted(scores.items(), keylambda x: -x[1])RRF的k值默认60指纹跟分数绝对值解耦可以跨不同类型的检索器融合而不用操心归一化也比加权求和稳健很多。元数据过滤同样重要。知识库里的文档往往有时间、来源、业务线、文档类型等属性。先按元数据过滤再向量检索能显著减少无关内容的干扰。比如2024年发布的产品手册这个查询先过滤时间范围检索质量会好很多。构建索引时记得把元数据写进chunk的doc_id里别存在外部表做join否则查询链路会多一跳延迟上去了还容易出错。元数据过滤还有一种进阶用法separate索引。把FAQ、长文、表格等不同类型的内容拆成独立的collection或partition。统一检索时改写后的query同时发到多个分区各自召回后RRF融合。这样避免长文档把FAQ短文本的召回挤掉效果立竿见影。5. Advanced RAG最核心的差异化查询改写与重排序5.1 Query Rewriting问题不对索引再强也没用索引再完善如果用户query本身就是模糊的召回质量一定上不去。用户经常问的是那个新出的蓝牙耳机多少钱那个是哪个上下文缺失、指代不明、词汇偏口语化这是常态。查询改写环节要做的事情就是把原始query转化成更适合检索的形式。我实践过的有效方法有三种多轮对话改写结合对话历史把指代词替换成实体。比如上文出现华为Mate 60 Pro用户下一句问它防水吗改写成华为Mate 60 Pro防水吗再检索。HyDE假设性文档让LLM根据query先写一段假想的理想回答然后用这段假想回答的embedding去检索。实际效果不一定每次都好但在知识库内容语言风格和LLM生成风格接近的场景里有明显提升。成本是每次查询多了一次LLM调用延迟多几百ms。query拆解与扩展把复杂query拆成多个子query分别检索后合并结果。适合XX产品兼容性如何和YY相比哪个好这类多意图问题。注意query改写依赖LLM在昇腾平台上意味着多一次NPU推理。我这边用了一个轻量模型做改写参数规模7B左右单次耗时可控。如果延迟敏感可以在改写结果上做缓存同一个query改写结果直接命中不用每次都让LLM跑。5.2 Rerank用精排模型把top50压缩到top5双塔结构的向量检索本质上在做语义近似的粗排。query和doc分开编码交互信息丢失严重。而rerank模型通常是交叉编码器query和doc拼接在一起输入模型能捕捉token级别的交互信息精度更高。我在知识库项目里用bge-reranker-base在昇腾上部署和之前的embedding服务一样走MindIE。流程是召回阶段拿到两路各50条、RRF融合去重后剩大约60~80条统统输入rerank模型精排只取top3~5进入LLM。实测数据很直观没加rerank时hit rate5大约0.68加入bge-reranker后涨到0.83。代价是每次检索多一次batch推理几十条候选的rerank在NPU上的延迟也就几十ms可以接受。一个容易犯的错把rerank分数和向量分数做加权相加。两套模型的分数分布天差地别加起来是灾难。正确做法是直接把RRF融合后的候选交给rerank以rerank分数作为最终排序依据最多对top1做一点微小平滑保证不会因为分数跳变导致同一个问题不同次回答差异过大。另一个细节rerank模型输入长度有限bge-reranker-base最大长度512。如果chunk超过512父子chunk方案里父chunk经常超过会截断丢失上下文。我的做法是截取chunk首尾各保留256个字符再拼接实测效果比只截头部好。5.3 关于Agentic RAG、GraphRAG和Ontology RAG的一点看法这几个方向最近热度很高。我的立场是它们本质都是在解决单一向量检索不够聪明的问题但发力点不同。Agentic RAG让LLM作为agent决定需要查什么、查几次、怎么根据中间结果调整检索策略。适合多跳推理、复杂业务咨询不适合简单FAQ场景成本和延迟会高不少。GraphRAG/Ontology RAG在索引侧构建实体与关系结构让检索链路具备先查关系再查文本的能力。适合企业知识中台里实体关系非常明确的数据比如组织结构、产品依赖、法规条款引用。如果你的数据是一堆半结构化文档实体关系提取本身就是个难题硬上GraphRAG可能事倍功半。RAG as a Service比如AgentScope 2.0里讨论的更多是产品形态问题把RAG能力标准化、服务化对索引侧的要求是一样的。我的建议先把基础的Advanced RAG链路做扎实——混合检索、查询改写、rerank——这些投入产出比最高。等业务积累到一定规模明确了实体关系查询需求再考虑在索引侧叠加图结构。6. 索引质量评估与踩坑记录6.1 评估指标怎么选Hit Rate、RecallK、MRR索引优化最怕没有量化指标凭感觉调参。我建议至少跟踪三个指标Hit RateK正确答案对应的chunk是否出现在前K个检索结果里。这是最核心的业务指标直接反映系统有没有可能答对。RecallK召回结果中相关chunk占所有相关chunk的比例。适合判断索引覆盖率。MRRMean Reciprocal Rank考察正确答案在排序中的位置正确答案排名越靠前越好。MRR和hit rate一起看能区分能找到和能找到且排在最前面的区别。我通常在K5和K20两个档位分别统计。K5对应最终送入LLM的上下文范围K20对应rerank之前的粗排范围。两个数字一起看能定位瓶颈在粗排还是精排。评估集构建是另一个重要工作。我从线上日志里挑选真实用户问题再人工标注每个问题对应的gold chunk id。标准是如果这个chunk被正确检索到并交给回答模型模型能否给出满意回答。初始建50~100条就够后面逐步扩充到200条以上。注意不要在评估集里混入看答案编问题的case那会让指标虚高。评估集要用好还需定期更新。知识库文档更新后old eval上分数可能提升、new eval上反而下降这种退化现象很常见。6.2 我在昇腾平台上踩过的五个坑坑一动态shape导致embedding延迟剧增。前面详细说过解法是分桶padding。这里再说一个延伸问题分桶之后batch内样本长度越整齐性能越好。所以离线向量化时建议按文档长度排序后再分批别让一个256的桶里混着大量只差几个token的样本。坑二表格类PDF按文本流分块后语义断裂。有一次检索设备故障代码E204含义召回全是E204几个字孤零零地出现表头故障含义是另一个chunk。后来把表格转成Markdown、按行分块并保留表头行问题解决。凡是表格结构敏感的文档都建议在清洗阶段显式处理。坑三BM25和向量分数直接相加导致排序混乱。一开始图省事把BM25得分归一化到0~1后和余弦相似度相加。结果几个BM25高分但语义不相关的文档持续霸榜。换成RRF rank融合后这种问题消失。坑四rerank模型FP32在NPU上延迟偏高。bge-reranker用FP32跑单次查询的rerank总耗时要120ms以上。切到FP16后降到40ms左右精度几乎无损失。这里提醒一句FP16格式一定要在量化后重新跑一遍评估集防止个别bad case因精度损失而翻转。坑五FAQ短文本和长文文档混库短文本召回被淹没。长文档chunk数量多、检索分数高FAQ里的精确答案反而排不上来。解法是我前面提到的分区检索FAQ单独建collection长文单独建collection查询时两个分区各自召回再融合。这个改动让FAQ类问题的hit rate提升了近20个百分点。6.3 优化顺序复盘先修基础再上高级手段最后把我整个优化过程的顺序做个复盘方便你对照自己的项目先解决数据侧文档清洗、结构感知分块、父子chunk —— 这一步通常能把hit rate从0.5拉到0.65左右。再优化检索侧混合检索稠密BM25 RRF融合 元数据过滤 —— 这一步能到0.75。然后加查询改写和rerank —— 能稳定到0.85以上。最后才是按业务需要上GraphRAG/Agentic RAG等进阶架构不要一上来就折腾这些。你可能会发现两步之后指标提升不明显。这种情况通常是评测集本身有问题gold chunk标注错误、同一答案对应多个chunk只标了一个、或者评测问题本身超出知识库覆盖范围。先回头审评测集再继续调算法别在一个坏基准上反复空转。还有一个小建议索引全流程优化要建立版本化的评估习惯。每次改动把评估结果记录下来形成一张改动-指标变化的日志表。这样半年后回头看你能清晰地知道当前系统的效果是哪些改动贡献的也能快速回滚到历史最优版本。RAG优化不是一次性工作而是持续迭代的过程。我个人在实际项目里的体会是索引侧多下一点功夫生成侧就少一句这个我暂时无法回答。而这一套方法论在昇腾平台上完全走得通——NPU上部署embedding和rerank、CPU侧跑向量库、混合检索和查询改写按需组合链路清晰每一步都可验证。希望这篇复盘能帮你少踩几个坑。