从零搭建私有文档问答系统:向量数据库选型、向量化与检索调优全链路拆解
1. 从零搭建私有文档问答系统向量数据库选型、向量化与检索调优全链路拆解把一堆内部文档丢给大模型指望它直接回答“我们去年Q3的采购流程是什么”结果它一本正经地胡说八道——这个场景我猜不少人都遇到过。大模型本身的知识截止于训练数据它压根没见过你硬盘里那些PDF、Word和Markdown。想让它基于私有文档回答问题就得走检索增强生成这条路先把文档切片、向量化、存进向量数据库用户提问时检索出最相关的片段再连同问题一起喂给大模型生成答案。这套流程听起来不复杂但真正落地时向量数据库选型、向量化模型选择、检索调优这三个环节处处是坑。选错了数据库数据量一上来查询就卡死向量化模型没选对中文语义检索效果惨不忍睹检索参数不调优要么召回一堆无关内容要么漏掉关键信息。这篇内容我会把这三个环节的实操细节全部拆开讲从环境搭建到参数调优从踩坑记录到性能对比争取让你看完就能复现一套能用的私有文档检索增强应用。整套方案适合谁参考如果你手头有一批私有文档技术手册、内部规范、产品文档等想让大模型基于这些文档做问答同时又不希望数据出本地环境那这套方案就是为你准备的。不需要你是算法专家但需要你会基本的Python操作能看懂Docker命令剩下的跟着做就行。2. 整体架构设计与技术选型思路2.1 为什么是“向量检索大模型生成”而不是微调很多人第一反应是让大模型学会我的文档微调不就行了我一开始也这么想过但实操下来发现微调这条路对私有文档问答来说性价比极低。微调需要准备大量问答对作为训练数据标注成本高每次文档更新都得重新训练周期长而且微调后模型容易“遗忘”通用能力回答其他问题时反而变差。检索增强生成的思路完全不同文档更新只需要重新向量化入库几分钟就能搞定大模型本身不动通用能力不受影响检索到的片段作为上下文注入提示词答案有据可查还能附上引用来源。打个比方微调像是让学生把整本教材背下来再考试检索增强像是开卷考试——学生不需要背但需要知道去哪本书哪一页找答案。对于文档频繁更新、问答范围明确的场景开卷考试显然更实用。2.2 核心链路的四个环节整套应用的核心链路可以拆成四步文档加载与切片把PDF、Word、Markdown等格式的文档读进来按语义或固定长度切成小块。切片大小直接影响检索精度切太大噪声多切太小语义不完整。向量化用嵌入模型把每个文本块转成高维向量。这一步决定了语义检索的天花板——模型不行后面怎么调都白搭。向量存储与检索把向量存进数据库查询时做相似度搜索返回Top-K个最相关的文本块。生成增强把检索到的文本块和用户问题拼成提示词交给大模型生成最终答案。这四个环节里向量化模型和向量数据库的选择是地基检索调优是上层建筑。地基没打好调优空间很有限。2.3 技术选型对比为什么选这套组合先说我最终落地的选型再解释为什么。环节选型备选方案选择理由向量化模型BGE-M3text-embedding-ada-002、M3E、GTE中文语义效果好支持多语言本地部署无API费用向量数据库MilvusChroma、Qdrant、Weaviate、FAISS支持亿级向量分布式扩展社区活跃文档完善大模型Qwen2.5-7B-InstructLlama3、ChatGLM4中文能力强7B量化后单卡可跑部署简单编排框架LangChainLlamaIndex、Haystack生态成熟组件丰富调试方便向量化模型这块我试过好几个。text-embedding-ada-002效果确实稳但走API有网络延迟和数据出域的顾虑而且按量计费长期用成本不低。M3E中文还行但多语言支持弱。BGE-M3是目前开源里中文检索效果第一梯队的支持8192 token长文本还能同时输出稠密向量和稀疏向量做混合检索很方便。本地部署用一张消费级显卡就能跑推理速度也够用。向量数据库选Milvus主要看中它的扩展性。Chroma上手确实快几行代码就能跑起来但数据量超过百万级后查询性能下降明显而且不支持分布式。Qdrant性能不错但生态相对小。Milvus支持IVF、HNSW等多种索引类型能根据数据规模和查询延迟要求灵活选择单机版部署也不复杂后续数据涨了可以平滑扩展到集群。大模型选Qwen2.5-7B-Instruct是因为中文问答场景下它的指令遵循能力比较强7B参数量化后显存占用约6-8GB一张RTX 3090或4090就能跑起来。如果硬件条件更好上14B或32B效果会更稳。注意向量数据库的选型没有绝对优劣关键看数据规模、查询延迟要求和运维成本。数据量在十万级以下Chroma完全够用百万级以上再考虑Milvus或Qdrant。不要为了“用新技术”而过度设计。3. 核心细节解析与实操要点3.1 文档切片切得好不好直接决定检索质量文档切片是整条链路里最容易被忽视但影响最大的环节。我见过不少人直接把整篇文档丢进去向量化结果检索时要么召回整个文档太长噪声大要么因为向量维度压缩丢失细节。切片的核心原则是每个切片应该是一个语义完整的单元。具体怎么切取决于文档类型。对于Markdown和技术文档按标题层级切最自然。比如一个二级标题下的内容作为一个切片如果内容太长再按段落切。对于PDF先做版面分析把表格、图片说明、正文分开处理。对于纯文本按固定字符数切但要在句子边界断开别把一句话切一半。我实际用的切片策略是这样的from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size512, # 每个切片的目标字符数 chunk_overlap64, # 相邻切片的重叠字符数 separators[\n## , \n### , \n\n, \n, 。, , , , , , ], length_functionlen, )chunk_size512是我反复测试后的选择。太小比如256会导致语义碎片化检索时可能召回多个不完整的片段太大比如1024会引入无关内容稀释关键信息。512字符大约对应300-400个中文字刚好能容纳一个完整的段落或几个相关句子。chunk_overlap64是为了防止关键信息刚好落在切片边界上被切断。重叠部分让相邻切片有上下文衔接检索时不会因为边界问题漏掉内容。分隔符的顺序很重要。优先按标题切其次按段落再按句子最后才按字符。这样能最大程度保证切片在语义边界断开。实操心得切片大小没有万能值。技术文档可以小一点384-512叙事性内容可以大一点768-1024。建议先用一批典型问题测试看召回片段是否完整覆盖了答案所需信息再反过来调整切片参数。3.2 向量化模型部署BGE-M3的本地化实践BGE-M3的部署比想象中简单。用FlagEmbedding库几行代码就能加载模型from FlagEmbedding import BGEM3FlagModel model BGEM3FlagModel( BAAI/bge-m3, use_fp16True, # 半精度推理显存减半速度提升 devicecuda # 有GPU就用cuda没有就cpu ) # 批量向量化 texts [文档切片1, 文档切片2, 文档切片3] embeddings model.encode( texts, batch_size16, # 根据显存调整 max_length8192, # BGE-M3支持的最大长度 return_denseTrue, # 返回稠密向量 return_sparseTrue, # 返回稀疏向量用于混合检索 return_colbert_vecsFalse # ColBERT向量按需开启 )use_fp16True这个参数很关键。BGE-M3全精度加载约2.2GB显存半精度只要1.1GB左右推理速度还快不少。实测在RTX 4090上半精度模式下每秒能处理约200个切片512字符长度一万个切片的文档库几分钟就能全部向量化完。batch_size要根据显存调整。16是一个比较安全的默认值显存充裕可以调到32或64吞吐量更高。如果爆显存就降到8。BGE-M3同时输出稠密向量和稀疏向量这是它的一大优势。稠密向量擅长语义匹配比如“如何申请报销”和“费用审批流程”能匹配上稀疏向量擅长关键词精确匹配比如产品型号、专有名词。两者结合做混合检索效果比单用稠密向量好很多。3.3 向量数据库选型对比从Chroma到Milvus的迁移实录我最初用的是Chroma原因很简单——上手快。三行代码就能建库、插入、查询import chromadb client chromadb.Client() collection client.create_collection(docs) collection.add(embeddingsembeddings, documentstexts, idsids) results collection.query(query_embeddings[query_emb], n_results5)但数据量涨到50万切片后问题开始出现。查询延迟从几十毫秒涨到几百毫秒批量插入速度也明显下降。Chroma的底层是SQLite单机单线程的架构决定了它不适合大规模数据。迁移到Milvus的过程比预想中顺利。Milvus的Python SDK和Chroma的API风格差异不大主要改动在于需要先定义Collection的Schemafrom pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType connections.connect(hostlocalhost, port19530) # 定义Schema fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idTrue), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dim1024), # BGE-M3稠密向量维度 FieldSchema(nametext, dtypeDataType.VARCHAR, max_length65535), FieldSchema(namesource, dtypeDataType.VARCHAR, max_length512), ] schema CollectionSchema(fields, description文档检索库) collection Collection(namedoc_retrieval, schemaschema) # 创建索引 index_params { metric_type: COSINE, # 余弦相似度 index_type: HNSW, # 图索引查询快 params: {M: 16, efConstruction: 200} } collection.create_index(field_nameembedding, index_paramsindex_params)索引类型的选择有讲究。Milvus支持多种索引索引类型适用场景查询速度召回率内存占用FLAT小数据量10万慢100%高IVF_FLAT中等数据量中等高中等IVF_SQ8大数据量内存受限快较高低HNSW大数据量查询延迟敏感很快很高高DiskANN超大数据量磁盘存储中等高低我选HNSW是因为查询延迟要求高用户等不了几秒数据量在百万级内存也够用。M16控制每个节点的连接数越大召回率越高但内存占用也越大efConstruction200是建索引时的搜索范围越大索引质量越好但建索引越慢。这两个参数在查询时还可以通过ef参数进一步调节。注意Milvus的索引参数没有万能配置。M值一般取8-64efConstruction取100-500。建议先用小批量数据测试不同参数组合的召回率和延迟找到适合自己场景的平衡点。3.4 混合检索稠密稀疏的协同策略单用稠密向量做检索遇到专有名词、产品型号、代码函数名这类查询时经常翻车。比如用户搜“ERR_CONNECTION_TIMEOUT怎么解决”稠密向量可能召回一堆讲网络连接原理的文档但真正包含这个错误码的文档反而排不到前面。混合检索的思路是稠密向量负责语义匹配稀疏向量负责关键词匹配两路结果加权融合。BGE-M3输出的稀疏向量可以直接用于Milvus的稀疏向量检索# 查询时同时获取稠密和稀疏向量 query_output model.encode( [query_text], return_denseTrue, return_sparseTrue ) dense_vec query_output[dense_vecs][0] sparse_vec query_output[lexical_weights][0] # 稠密检索 dense_results collection.search( data[dense_vec], anns_fieldembedding, param{metric_type: COSINE, params: {ef: 128}}, limit20, output_fields[text, source] ) # 稀疏检索需要单独建稀疏向量字段和索引 sparse_results collection.search( data[sparse_vec], anns_fieldsparse_embedding, param{metric_type: IP}, limit20, output_fields[text, source] ) # 加权融合RRF或加权求和 def hybrid_fusion(dense_results, sparse_results, alpha0.7): scores {} for rank, hit in enumerate(dense_results[0]): scores[hit.id] scores.get(hit.id, 0) alpha * (1 / (rank 60)) for rank, hit in enumerate(sparse_results[0]): scores[hit.id] scores.get(hit.id, 0) (1 - alpha) * (1 / (rank 60)) sorted_ids sorted(scores.keys(), keylambda x: scores[x], reverseTrue) return sorted_ids[:10]这里用的是RRFReciprocal Rank Fusion融合算法60是平滑常数防止排名靠前的文档权重过大。alpha0.7表示稠密检索权重占70%稀疏占30%。这个比例可以根据实际效果调整——如果查询里关键词特征明显可以调高稀疏权重。实测下来混合检索在专有名词查询上的召回率比单用稠密向量提升了约25%在语义查询上基本持平。代价是查询延迟增加约30%因为要跑两路检索再做融合。如果延迟敏感且查询以语义为主可以只用稠密检索。3.5 重排序让最相关的片段排到最前面检索返回Top-20后直接取前5个喂给大模型往往不够精准。因为向量相似度高不代表内容真正相关——有些片段可能只是用词相似实际答非所问。重排序模型就是来解决这个问题的。它把查询和每个候选片段拼在一起做更精细的相关性打分。BGE-Reranker是常用的选择from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-v2-m3, use_fp16True) pairs [[query_text, hit.text] for hit in retrieved_hits] scores reranker.compute_score(pairs, normalizeTrue) # 按重排序分数重新排列 reranked sorted(zip(retrieved_hits, scores), keylambda x: x[1], reverseTrue) top5 [hit for hit, score in reranked[:5]]重排序的代价是延迟。20个候选片段做重排序大约需要200-500毫秒取决于GPU但换来的是Top-5的精准度大幅提升。我的经验是检索阶段召回20-50个候选重排序后取3-5个给大模型这个组合在效果和延迟之间平衡得最好。实操心得重排序模型和向量化模型最好用同一系列的。BGE-M3配BGE-Reranker-v2-m3是官方推荐的组合两者在训练数据上有一致性配合效果更好。混用不同系列的模型可能导致打分尺度不一致。4. 完整实操流程与核心环节实现4.1 环境准备与依赖安装整套系统需要以下组件Python 3.10Milvus 2.4单机版用Docker部署CUDA 11.8GPU推理需要大模型推理框架vLLM或OllamaMilvus单机版用Docker Compose部署最省事# 下载Milvus standalone的docker-compose配置 wget https://github.com/milvus-io/milvus/releases/download/v2.4.0/milvus-standalone-docker-compose.yml -O docker-compose.yml # 启动 docker-compose up -d # 检查状态 docker-compose psPython依赖pip install pymilvus2.4.0 pip install FlagEmbedding1.2.10 pip install langchain0.2.0 pip install langchain-community0.2.0 pip install pypdf4.2.0 pip install python-docx1.1.0 pip install vllm0.5.0大模型用vLLM部署Qwen2.5-7B-Instructpython -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --dtype float16 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --port 8000--gpu-memory-utilization 0.9表示用90%的显存留10%给向量化模型和重排序模型。如果显存紧张可以降到0.8。--max-model-len 8192是上下文长度检索增强场景下提示词通常不会太长8192够用。4.2 文档加载与预处理不同格式的文档加载方式不同。PDF用pypdfWord用python-docxMarkdown直接读文本from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader, TextLoader import os def load_documents(doc_dir): documents [] for filename in os.listdir(doc_dir): filepath os.path.join(doc_dir, filename) if filename.endswith(.pdf): loader PyPDFLoader(filepath) elif filename.endswith(.docx): loader Docx2txtLoader(filepath) elif filename.endswith(.md) or filename.endswith(.txt): loader TextLoader(filepath, encodingutf-8) else: continue documents.extend(loader.load()) return documentsPDF加载有个坑扫描版PDF没有文字层pypdf读出来是空的。这种情况需要先做OCR。我一般用PaddleOCR做预处理把扫描版转成文字版再加载。加载后的文档还要做清洗去掉页眉页脚、合并断行、去除多余空白。这些细节不做的话切片里会混入大量噪声。4.3 向量化与入库完整代码把前面的环节串起来from FlagEmbedding import BGEM3FlagModel from pymilvus import Collection, connections from langchain.text_splitter import RecursiveCharacterTextSplitter # 初始化 connections.connect(hostlocalhost, port19530) collection Collection(doc_retrieval) model BGEM3FlagModel(BAAI/bge-m3, use_fp16True) # 加载并切片 documents load_documents(./docs) splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap64, separators[\n## , \n### , \n\n, \n, 。, , , , , , ] ) chunks splitter.split_documents(documents) # 批量向量化 texts [chunk.page_content for chunk in chunks] sources [chunk.metadata.get(source, unknown) for chunk in chunks] embeddings model.encode( texts, batch_size16, max_length8192, return_denseTrue, return_sparseTrue ) # 插入Milvus data [ embeddings[dense_vecs].tolist(), texts, sources ] collection.insert(data) collection.flush() print(f已入库 {len(texts)} 个切片)collection.flush()这步不能省。Milvus是异步写入不flush的话数据可能还在内存里没落盘查询会查不到。4.4 检索与生成链路串联完整的问答流程from openai import OpenAI client OpenAI(base_urlhttp://localhost:8000/v1, api_keynot-needed) def ask(question): # 1. 向量化查询 query_output model.encode([question], return_denseTrue, return_sparseTrue) dense_vec query_output[dense_vecs][0] # 2. 稠密检索 results collection.search( data[dense_vec], anns_fieldembedding, param{metric_type: COSINE, params: {ef: 128}}, limit20, output_fields[text, source] ) # 3. 重排序 candidates [hit.entity.get(text) for hit in results[0]] pairs [[question, c] for c in candidates] scores reranker.compute_score(pairs, normalizeTrue) reranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) top_chunks [c for c, s in reranked[:5]] # 4. 构造提示词 context \n\n---\n\n.join(top_chunks) prompt f基于以下文档片段回答问题。如果文档中没有相关信息请明确说明。 文档片段 {context} 问题{question} 回答 # 5. 调用大模型 response client.chat.completions.create( modelQwen/Qwen2.5-7B-Instruct, messages[{role: user, content: prompt}], temperature0.1, max_tokens1024 ) return response.choices[0].message.contenttemperature0.1是为了让回答更确定、更贴近文档内容。检索增强场景不需要创造性需要的是忠实于检索结果。4.5 参数调优实录从能用 to 好用系统跑通只是第一步调优才是真正花时间的部分。我记录了几个关键参数的调整过程切片大小最初用1024发现召回片段经常包含多个不相关段落大模型容易被带偏。降到512后精准度明显提升但有些长答案被切断了。最终用51264重叠兼顾两者。检索数量Top-K从5调到20再重排序取5。直接取Top-5时有些问题的最佳答案排在6-10位漏掉了。先召回20个再重排序召回率提升约15%。ef参数HNSW的查询参数ef从64调到128召回率提升约8%延迟增加约20毫秒。128是一个比较好的平衡点。重排序阈值重排序分数低于0.3的片段直接丢弃不喂给大模型。这样能过滤掉明显不相关的内容减少大模型幻觉。提示词模板最初没加“如果文档中没有相关信息请说明”大模型遇到检索不到的问题时会强行编造。加上这句话后拒答率明显提升幻觉减少。5. 常见问题与排查技巧实录5.1 检索效果差的排查思路检索效果差是最常见的问题表现是“答案明明在文档里但就是搜不出来”。排查按以下顺序第一步检查切片是否包含答案。把问题对应的原文找出来看它被切到了哪个切片里。如果切片里没有说明切片策略有问题需要调整chunk_size或分隔符。第二步检查向量化是否正常。把问题和答案切片分别向量化算余弦相似度。如果相似度低于0.5说明向量化模型对这类语义不敏感可能需要换模型或加查询改写。第三步检查索引参数。如果相似度正常但检索排不到前面可能是HNSW的ef值太小召回不充分。调大ef试试。第四步检查是否有稀疏向量参与。如果查询包含专有名词单靠稠密向量可能不够需要开启混合检索。5.2 大模型幻觉的抑制方法即使检索到了正确片段大模型也可能“自由发挥”。抑制幻觉的几个手段提示词约束明确要求“仅基于以下文档回答”、“文档中没有的信息不要编造”。降低temperature0.1甚至0让输出更确定。引用来源要求大模型在回答中标注信息来自哪个片段增加可追溯性。重排序过滤低分片段直接丢弃不进入提示词。答案校验生成后用另一个模型或规则检查答案是否在检索片段中有依据。5.3 性能瓶颈定位与优化系统变慢时按环节定位症状可能原因排查方法优化手段查询延迟高索引参数不当看Milvus查询日志调小ef换IVF索引向量化慢batch_size太小看GPU利用率调大batch_size重排序慢候选数量太多看重排序耗时减少召回数量生成慢大模型推理慢看首token延迟量化模型换更小模型内存占用高索引全量加载看内存监控换DiskANN索引5.4 常见问题速查表问题原因解决方案Milvus连接失败Docker未启动或端口占用检查docker-compose ps确认19530端口向量维度不匹配模型输出维度与Schema不一致BGE-M3稠密向量是1024维检查Schema定义检索结果为空数据未flush或索引未建执行collection.flush()和create_index()中文乱码文件编码问题加载时指定encodingutf-8显存不足模型太多或batch太大降低batch_size用fp16分时加载模型答案不完整切片太小或Top-K太少调大chunk_size增加召回数量专有名词搜不到纯稠密检索不擅长关键词开启混合检索加稀疏向量重复内容召回切片重叠过多减小chunk_overlap大模型答非所问提示词模板不当优化提示词加约束条件查询速度波动大索引未预热启动后先跑几次预热查询避坑技巧Milvus的Collection在第一次查询时会加载索引到内存第一次查询特别慢是正常的。可以在服务启动后先跑几个预热查询把索引加载到内存后续查询就稳定了。5.5 几个我踩过的坑坑一切片时把表格切碎了。PDF里的表格被按字符切分后行列关系完全丢失检索出来的片段根本没法看。后来我单独处理表格把表格转成Markdown格式再切片保留结构信息。坑二向量化模型和重排序模型抢显存。两个模型同时加载到GPU上显存直接爆了。解决方案是向量化用GPU重排序用CPU或者分时加载——检索时加载重排序模型向量化时卸载。坑三Milvus的VARCHAR长度不够。默认max_length是65535但有些切片超过这个长度会报错。要么调大max_length要么确保切片不超过限制。坑四大模型把检索片段当成了对话历史。提示词模板里如果不用分隔符明确区分文档和问题大模型可能把文档内容当成用户之前说的话回答时产生混淆。用---或###明确分隔。坑五批量插入时ID冲突。Milvus的auto_idTrue会自动生成ID但如果手动指定ID且重复插入会失败。要么用auto_id要么确保ID唯一。6. 效果评估与持续迭代6.1 怎么判断系统好不好用评估检索增强系统不能只看“能不能回答”要看三个指标检索命中率对于一组测试问题正确答案所在的切片是否在Top-K中。这个指标反映检索环节的质量。我一般准备50-100个测试问题人工标注每个问题的答案在哪个文档哪个位置然后跑检索看命中率。目标是把Top-5命中率做到90%以上。答案忠实度大模型的回答是否完全基于检索到的片段有没有编造。这个可以用人工评估也可以用另一个大模型做自动评估。忠实度低说明提示词约束不够或重排序没做好。端到端延迟从用户提问到收到回答的总时间。检索重排序生成每个环节的延迟都要监控。我的目标是首token延迟控制在2秒以内完整回答控制在5秒以内。6.2 持续优化的几个方向系统上线后不是终点还有不少优化空间查询改写用户的问题往往口语化、模糊直接检索效果不好。可以用大模型先把问题改写成更适合检索的形式再去做向量检索。比如“报销怎么弄”改写成“费用报销流程和所需材料”。多路召回除了向量检索还可以加BM25关键词检索、按文档标题检索等多路召回再融合排序。召回路径越多覆盖越全但延迟也越高需要权衡。缓存机制高频问题的检索结果可以缓存避免重复计算。用Redis存查询向量和对应的检索结果命中缓存直接返回。增量更新文档更新时不需要全量重建索引只向量化新增或修改的文档插入Milvus即可。删除文档时按source字段批量删除。用户反馈闭环记录用户的点赞/点踩把点踩的问题收集起来分析原因针对性优化切片策略或检索参数。6.3 硬件配置建议不同数据规模下的硬件配置参考数据规模GPU内存存储说明10万切片RTX 3060 12GB32GB100GB SSD单卡可跑所有模型10-100万切片RTX 4090 24GB64GB500GB SSD向量化和生成分卡100-1000万切片A100 40GB x2128GB2TB NVMeMilvus集群推理集群1000万切片A100/H100集群256GB分布式存储需要专业运维大部分私有文档场景的数据量在10万切片以内一张消费级显卡加32GB内存就够了。不要一上来就追求高配先跑通再按需扩展。6.4 一个实际案例的参数配置最后分享一个我实际项目的配置文档库约5万切片日均查询2000次左右切片chunk_size512chunk_overlap64向量化BGE-M3fp16batch_size16索引HNSWM16efConstruction200查询ef128检索稠密Top-20 稀疏Top-20RRF融合取Top-10重排序BGE-Reranker-v2-m3取Top-5生成Qwen2.5-7B-Instructtemperature0.1max_tokens1024硬件单张RTX 409064GB内存这套配置下端到端延迟约1.5-2.5秒Top-5检索命中率约92%答案忠实度人工评估约95%。对于内部文档问答场景这个效果已经足够用了。后续如果数据量涨到50万切片计划把Milvus从单机版迁移到集群版向量化和生成分到两张卡上预计延迟能控制在2秒以内。