ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Milvus 2.6 企业级 RAG 实战:从向量检索到知识库系统

2026/8/26 10:55:59 拓冰建站 浏览量
Milvus 2.6 企业级 RAG 实战:从向量检索到知识库系统 去年做内部知识库问答系统时团队用了很短时间就搭出第一个 demo把一批技术文档切成小块做向量化塞进一个轻量向量数据库再让大模型基于检索结果回答。演示效果不错大家一度以为这事已经结束了。真正的问题出现在一周之后。同事上传了更多资料文档结构从“几十个小文件”变成“几千个长文档”有人要求按部门隔离知识范围需要按时间筛选最新版本还要支持增量同步。这时候原先那套“单机 Demo”开始频繁暴露问题检索结果不稳定、数据重复、过滤条件很弱、索引重建慢更别提并发和权限。我们重新审视架构最后把向量存储切到了 Milvus 2.6整套 RAG 流程才逐步稳定下来。这篇文章不是 Milvus 的官方文档翻译也不是一套可以直接抄走的代码。我的目标是把“Milvus 2.6 RAG 实战”这件事拆开来讲为什么向量数据库在 RAG 项目里更像“记忆系统”而不是“搜索引擎”为什么 Milvus 这类重一点的基础设施适合放到企业项目中以及从单机原型到可维护系统有哪些环节必须补齐。1. 向量数据库在 RAG 项目里的角色比你想的更接近“记忆”1.1 RAG 效果好不好一半取决于“知识回取”RAG 的基本链路并不复杂文档切块 → 向量化 → 向量检索 → 拼接上下文 → 大模型生成。很多团队把注意力放在最后一步也就是提示词和大模型本身结果却总在“检索不到”或“检索到的东西不对”上翻车。我现在的判断是RAG 的效果上限很大程度上由“知识回取”决定。大模型能回答得多准确取决于它有没有从向量数据库里拿到足够相关的上下文。如果向量库里根本没有合适内容提示词写得再精细也没用。这里有一个容易误解的地方向量检索不是“关键词搜索”它比较的是语义相似度。用户问“这个接口怎么鉴权”系统应该召回“登录认证流程”相关段落而不是只召回包含“鉴权”两个字的片段。这是向量数据库的价值但也带来一个代价召回结果天然有“概率性”不像传统数据库那样严格。因此在生产系统里我们不仅要关心能不能检索到还要关心召回质量、排序和过滤条件是不是可预期。1.2 轻量实现与重基础设施先别急着给团队上“重武器”如果你只是做几十个文件的个人知识库选择一个轻量向量数据库、一个 notebook 脚本完全够用。因为没有并发、没有权限、没有增量更新也没有“系统性失败”需要处理。但企业项目是另一回事。它的特征通常是文档数量大且持续增长文档有部门、标签、时间、版本等属性需要多人同时使用检索链路可能出现慢查询需要定期更新向量库而不是每次清空重建有时候还需要跨文档回答以及结果可溯源。这时选型就非常关键。轻量方案往往赢在“上手快”但输在“工程能力”。Milvus、Elasticsearch 这类系统虽然部署更重、学习曲线更陡但它们具备数据模型、索引管理、过滤条件、分布式扩展、监控备份这些能力。换句话说它们是“数据系统”而不是“内存里的相似度计算工具”。1.3 选型前先问三个问题在决定用 Milvus 还是其他向量数据库之前我会让团队先过一遍三个问题数据量大概会涨到多少百万条以上时索引和查询性能会开始拉开差距需不需要和结构化成对筛选如果知识库带部门、标签、时间范围过滤向量库的标量过滤能力就很重要是想解决“今天的问题”还是想解决“半年的问题”如果只是验证想法轻量方案更合适如果要放进业务系统就要从第一天考虑运维和扩展。我见过不少团队先用了轻量方案做演示三个月后因为权限和增量更新重构。也有团队一上来就搭三台机器跑 Milvus结果数据只有几百条很多运维能力完全用不上。这里没有绝对的对错只有阶段不匹配。2. Milvus 2.6 的关键设计不只是向量搜索引擎而是数据系统2.1 从集合Collection到分区Partition数据模型决定了工程边界Milvus 2.6 的数据模型继承自 Milvus 2.x 体系核心概念包括Collection可以理解为关系数据库中的表同一套字段结构的数据放在一个集合里Field字段除了向量字段还可以定义主键、字符串、整数、JSON 等标量字段Partition集合内部的分区可以按业务维度拆分数据比如按月份或部门分区。这个设计对 RAG 项目非常重要。因为在纯向量检索方案里你通常只有一个“向量列 文本列”过滤能力很弱。但在 Milvus 里你可以把doc_id、doc_type、department、publish_date、page_index等字段一起存进去然后在检索时通过表达式过滤。我最常用的一种结构是字段类型用途idINT64 主键唯一标识doc_idVARCHAR文档 ID方便关联原始文档chunk_idVARCHAR切块 IDcontentVARCHAR文本片段departmentVARCHAR所属部门或知识域publish_dateINT64发布时间戳page_indexINT64来源页码方便溯源embeddingFLOAT_VECTOR向量字段维度对齐 Embedding 模型这样设计的好处是检索时不做“全库暴力扫描”而是在指定分区或过滤条件下做向量相似度查找。企业场景中最常见的过滤是“只看本部门资料”或“只看 2024 年以后发布的文档”。如果没有标量字段这个需求会变成一场灾难。2.2 索引不是越复杂越好先理解参数Milvus 2.6 支持多种索引类型比如 FLAT、IVF_FLAT、IVF_PQ、HNSW以及 GPU 索引等。对 RAG 项目来说最常遇到的是 IVF_FLAT 和 HNSW。FLAT暴力计算最准但数据量大了之后很慢适合小数据验证IVF_FLAT先聚类再检索需要调nlist和nprobe参数HNSW基于图的近似最近邻召回质量高查询快但内存占用更高构建时间也更长。很多人一上来就想选 HNSW觉得“更先进”。但我会建议先看数据量十万条以内IVF_FLAT 足够百万级以后再考虑 HNSW。原因很简单HNSW 召回好代价是内存和参数调优复杂度工程上必须同时考虑部署资源。另一个容易被忽略的是距离度量方式。常见的有 L2、IP、COSINE。文本向量通常用 COSINE 更直观因为向量化模型很多已经对向量做了归一化处理。如果模型没有归一化COSINE 还能避免向量长度对相似度产生干扰。落地时一定要确认两件事Embedding 模型的输出是否归一化向量字段和查询时的metric_type是否一致。不一致的后果是从结果表面看有时正常有时明显偏差。2.3 为什么标量过滤在 RAG 系统里如此重要很多 RAG 项目的初始版本没有权限和维度过滤因为大家在做一个“搜索框”。但企业知识库天然带组织边界销售不应看到研发内部文档实习生不应看到薪资流程。你要么在文档切块时就按权限分区存储要么在向量检索时带过滤条件。Milvus 2.6 的查询支持表达式过滤比如expr department rd and publish_date 20240101这个能力让知识库从“一个所有内容混在一起的大仓库”变成“可按业务维度隔离的知识系统”。但也带来一个工程要求文档入库时必须把元数据提取正确。很多团队把向量化写好了却在“元数据质量”上栽了跟头——字段缺失、部门统计口径不一致、正文中提取的标题和文件名不匹配。这些都会直接影响检索过滤效果。2.4 可扩展性从单机到集群的平滑路径Milvus 的架构分成了多个组件包括接入层、查询节点、数据节点、索引节点等。单机模式下用 Docker Compose 就能启动一套环境数据量上来后可以扩展为分布式集群模式。对普通开发团队这意味着一个很重要的好处你不需要在项目初期就决策“我到底要部署多大集群”。先跑单机数据量增长后再把组件拆分这个路径比“轻量方案做大了再整体迁移”要平滑得多。当然分布式不是免费的它引入了更多运维组件需要监控 query node 的负载、segment 的数量、索引构建的进度等等。所以如果数据量很小我仍然建议先单机不要为了“高可用”而过度设计。3. 一次跑通Milvus 2.6 RAG 的最小落地流程3.1 先规划环境再写代码我建议先做一张最小环境清单避免中途被依赖问题打断。环节建议说明Docker已安装 Docker 和 Docker ComposeMilvus 2.6 可以用 Docker Compose 启动Milvus 版本以官方最新稳定版为准写文章时 2.6 是大版本但实际落地要确认当前版本Embedding 模型先选一个中文效果稳定的模型比如常见开源 Embedding 模型或 API 模型输出维度要记下来Python 版本Python 3.9兼容性更好SDK安装pymilvus以 Milvus 官方 SDK 为准如果是本地验证Docker Compose 启动是成本最低的方式# 拉取项目仓库或配置文件后启动 docker compose up -d具体配置文件以官方仓库为准这里不展开。启动后检查端口19530是否监听然后开始写 Python 脚本。3.2 设计集合结构并创建集合举个例子假设我们有一套产品手册需要按文档类型过滤。我们先连接 Milvus创建集合。from pymilvus import ( connections, CollectionSchema, FieldSchema, DataType, Collection, utility ) connections.connect(hostlocalhost, port19530) files [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idFalse), FieldSchema(namedoc_id, dtypeDataType.VARCHAR, max_length256), FieldSchema(namecontent, dtypeDataType.VARCHAR, max_length65535), FieldSchema(namedoc_type, dtypeDataType.VARCHAR, max_length64), FieldSchema(namepublish_date, dtypeDataType.INT64), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dim1024), ] schema CollectionSchema(files, descriptionrag_docs) collection_name doc_kb if utility.has_collection(collection_name): collection Collection(collection_name) else: collection Collection(collection_name, schema)这里最需要确认的是dim1024必须和 Embedding 模型输出维度一致。不同模型可能是 384、768、1024 或更高不一致时插入数据会直接报错。3.3 文档切块和向量化文档切块是另外一个非常重要的话题后面专门讲。现在我们先假设已经得到若干文本块每个文本块对应一行记录。from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-m3) # 仅示例具体模型按实际情况选择 chunks [ {doc_id: manual_001, content: 系统登录支持账号密码和 SSO 两种方式。, doc_type: manual, publish_date: 20240101}, {doc_id: manual_001, content: 调用鉴权接口时需要先将 token 放入 Header。, doc_type: manual, publish_date: 20240101}, ] batch_embeddings [] for chunk in chunks: vector model.encode(chunk[content]).tolist() batch_embeddings.append({ **chunk, embedding: vector, }) collection.insert(batch_embeddings) collection.flush() print(collection.num_entities)这里有一个工程常识不要逐条插入和flush尤其数据量大时性能会很难看。正确的做法是积攒一批向量批量插入需要可见时再 flush或者用定期脚本统一刷新。3.4 检索并拼接上下文检索时先做查询向量化再调用search接口最后把返回的content拼接到上下文里。question 如何获取鉴权 token q_vector model.encode(question).tolist() collection.load() results collection.search( data[q_vector], anns_fieldembedding, param{metric_type: COSINE, params: {nprobe: 16}}, limit5, exprdoc_type manual, output_fields[doc_id, content, publish_date], )返回结果中每条记录有id、distance和entity。entity里可以看到content等字段。再把content按顺序拼接生成最终的大模型提示词。context \n\n.join( hit.entity.get(content) for hit in results[0] ) prompt f请基于以下资料回答问题\n\n{context}\n\n问题{question}到这一步最小的 RAG 链路已经跑通了。3.5 这段流程最容易踩的几个坑第一忘记load()。创建索引后必须先把集合加载进内存才能执行查询。否则会报“collection not loaded”或类似错误。第二向量维度不匹配。常见来源是换了一个 Embedding 模型但没改dim。第三COSINE 归一化没确认。可以打印两个向量直接计算余弦相似度和 Milvus 返回的distance对比如果出现明显偏差就要检查模型是否归一化、距离度量是否一致。第四limit不是越大越好。RAG 场景里检索 5 到 10 块文本通常已经足够超出后不仅上下文过长还会引入噪声大模型反而更容易答偏。注意先跑通“单条查询”再处理批量任务不要一上来就把所有文档灌进去。先确认 3 条测试数据能查到再扩展到全量。4. 文档切块决定 RAG 质量的上限但最容易被低估4.1 切块到底在做什么向量化之前要把长文档切块这是 RAG 链路里最影响效果但最容易被忽略的环节。切块的目的不是简单地把文件按长度截断。它要做的是尽量保证每个块是一个语义完整、可独立理解的知识单元。为什么不能直接把整篇文档塞给模型因为当文档超过模型上下文限制或者里面包含多个互不相关的话题时检索系统很难返回“最相关的那段”。反过来如果切得太碎每一块包含的信息量太少模型可能缺少上下文。4.2 常见切块策略对比策略做法适合场景风险固定长度切块按 token 或字符数切带重叠通用场景快速验证容易截断句子语义断裂递归字符切块先按段落分再处理大块Markdown、纯文本需要按文档类型调整分隔符按文档结构切块根据标题、章节、表格切分PDF、Word、Markdown依赖解析质量父子块父块保存更大语义子块用于检索长文档问答实现复杂度高索引量更大语义切块用 Embedding 判断边界内容主题跳跃明显计算成本高边界仍不确定从我自己的经验看大多数 RAG 项目不需要一开始就用高深的语义切块。先把固定长度 重叠跑通再观察哪些文档类型表现差再针对性地改成结构切块或父子块这个顺序更稳。如果用递归字符切块常见配置是from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , ], )注意chunk_size是按字符还是 token不同库定义不同而且不同语言、不同文档结构的最佳值差异很大。与其抄一个参数不如先抽 20 条有代表性的文档人工验证切割结果看有没有把关键句子拆断。4.3 切块质量如何验证一个很实用的验证方法把切完的块打印出来随机抽取问三个问题这个块里的信息是否完整是不是一句话被截成两半这个块的主题是否单一如果一段包含三个不同的功能点检索时很容易“答非所问”如果只靠这一个块回答用户问题上下文够不够很多人会在这里偷懒原因是很直接切块结果肉眼看起来似乎没毛病。但检索系统的问题往往出现在边界比如一个流程的“步骤 2”被切断或者某个表格被截断成一个残缺的竖列。这类问题是提示词工程解决不了的。还有一个点值得注意切块时最好在元数据里保留来源信息比如page_index、doc_id、标题路径。当大模型引用错误内容或用户质疑结果时系统能快速定位到原始文档。这也是企业级 RAG 和 Demo 的重要区别。5. 从 Demo 到企业项目还需要补的工程拼图5.1 增量更新与数据一致性做好一次全量导入并不难难的是每天都有新文档、文档有修改、文档有删除。这时你需要一个“内容管线”检测源文件是否变化对新增文件切块并向量化对修改过的文档删除旧块插入新块对已删除的文档级联清理向量数据定期重建部分索引处理碎片。Milvus 支持按表达式删除数据比如根据doc_id删除collection.delete(fdoc_id in [manual_001])但删除之后要意识到一个问题删除操作不会立刻重排所有索引数据段会有碎片。如果频繁增删性能可能会退化需要定期执行 compact 合并数据段。这个操作在生产环境中要有运维计划不能等到查询慢到不可忍受才处理。5.2 权限过滤与租户隔离RAG 系统一旦对外开放使用权限和租户隔离就不可避免。有两种常见思路所有文档存在同一个集合通过标量字段做过滤每个租户或部门用独立 Collection 或 Partition。第一种方式优点是一条查询路径缺点是过滤条件一旦写错可能发生越权第二种方式隔离更彻底但需要管理更多集合资源占用更大。在多数中大型项目里我更建议先按业务域做 Partition再在 Partition 内部用字段过滤。比如按部门分区查询时先锁定分区再通过department字段进一步过滤。这样即使过滤条件漏掉横向越权的大面积泄露风险也会更低。5.3 可观测性不能让向量库变成“黑盒”企业项目里你迟早会遇到“用户说检索不到”的问题。如果系统没有日志和指标排查会非常痛苦。至少要采集这几类信息查询耗时、召回数量、候选数量distance分布判断相似度得分整体是偏高还是偏低Collection 的实体数量、索引状态、segment 数量导入任务的成功率、失败原因过滤条件是否命中预期的分区。有一个很容易被忽视的监控点相似度阈值。很多团队没有记录每次查询的相似度分数一旦用户反馈“结果不对”只能重新运行脚本猜测。如果从一开始就把distance写进日志就能很快发现“结果不相关是因为相似度普遍偏低”还是“ 过滤条件写错导致排除了正确结果”。5.4 混合检索与重排不是每个项目都需要但值得知道典型问题用户问“怎么配置 Nginx 反向代理”向量检索可能召回一些语义接近但不精确的内容。因为纯向量检索对“专有名词、编号、代码片段”这类精确匹配并不敏感。解决思路是做混合检索向量检索负责语义召回关键词/全文检索负责精确匹配再用 RRFReciprocal Rank Fusion或重排模型把结果融合排序。在一些需要精确引用的场景比如产品文档、法务条款、代码示例混合检索的效果通常比纯向量好。不过混合检索会引入更多组件比如一个全文搜索引擎或者额外的索引。如果数据量不大、问题以开放式问答为主纯向量也够用。我的建议是“先有监控再做增强”等确实出现召回不足的案例再决定是否上混合检索。5.5 一个可复用的落地顺序把前面的经验收束成一个框架我通常建议按下面顺序推进先做 20 条数据的“最小链路验证”确认能插入、能检索、能拼上下文再选 200 条业务真实文档人工检查切块质量和检索结果设计元数据字段、权限过滤口径、更新策略跑通全量导入记录耗时和失败率上线后只做小流量验证同时记录查询日志和召回分数根据日志暴露的问题反向调整切块策略、过滤条件和索引参数。这个顺序的原则是不要在一开始追求完整的企业级架构但也不要带着 Demo 心态上线。先让链路可控再逐步补齐工程能力。6. 常见问题排查链路6.1 检索不到任何数据先判断是哪一层断了遇到“查不到结果”不要立刻怀疑向量数据库。按顺序排查检查集合里有没有数据collection.num_entities是否为 0检查flush()是否执行过Milvus 中数据插入后会先进入内存未 flush 时有些查询可能看不到检查向量维度插入时报错维度不匹配查询时可能报错检查expr过滤条件如果元数据字段名或值写错过滤会清空结果检查相似度阈值如果阈值设置得过于苛刻比如要求distance 0.1但真实数据普遍在 0.3 左右也会查不到检查模型是否加载没有调用load()或load()失败查询也会失败。这套顺序的关键是“先看数据再看条件最后看阈值”。很多新手第一反应是调索引参数结果数据根本没进去。6.2 检索到了但结果明显不相关这是更棘手的问题因为链路通了但质量不对。我一般按这个顺序排查可能原因检查方式优先调整方向切块粒度不合适打印几个检索命中块看语义是否完整调整 chunk_size 或换结构切块查询词本身太复杂拆成子问题测试考虑 query 改写或分解Embedding 模型不适合领域在领域文档上做召回人工评测换模型或对比多种模型过滤条件过强去掉过滤条件对比结果调整权限过滤口径相似度计算方式不符检查 metric_type 和归一化统一 COSINE 或根据模型调整另外一个很常见的原因是索引参数和查询参数不匹配。比如索引用的 IVF_FLAT 但查询参数里缺失nprobe或索引构建时nlist设置得过小在小数据集上导致聚类效果不好。遇到这种情况先用 FLAT 索引做一个基线如果 FLAT 下召回明显更好说明问题大概率出在近似索引参数上。6.3 查询变慢不是“加机器”能直接解决的查询变慢的原因通常有这么几类数据量增长但没有重新 compactsegment 碎片太多查询中使用了不带索引的标量字段实际上在做全量扫描expr过滤条件没命中分区导致跨 segment 扫描并发高节点资源已经打满索引类型和查询参数不匹配导致搜索范围过大。排查顺序建议先看查询日志的耗时分位数再看节点 CPU 和内存再看 segment 数量然后看慢查询对应的过滤表达式。不要一慢就加节点先把资源浪费的根源找到。Milvus 提供了一些性能监控指标比如查询耗时、数据段数量、索引构建进度等。项目上线前就把这些指标接到告警平台否则出了问题只能靠猜。提醒生产环境里任何“优化”都要先记录改动前后的查询日志。没有基线优化就是碰运气。写在最后工具最终为流程服务从 Milvus 2.6 的选型到 RAG 的完整落地我一直警惕一件事不要为了用一个更“重”的组件而制造复杂。Milvus 2.6 的价值只有在“知识量增长、检索条件复杂、需要长期维护”的背景下才会显现。如果你的项目还处在验证阶段用轻量方案完全合理如果你的知识库已经进入生产或者即将承接核心业务那么从数据模型、索引参数、元数据设计、监控日志这些角度提前规划才是更稳妥的做法。这篇文章里提到的最有价值的方法不是某个代码片段而是一套推进节奏先跑通最小链路再验证真实数据的切块和召回再补权限和监控最后根据日志反向迭代。这套顺序适用于大多数 RAG 项目也适用于你下一次接到的知识库需求。把向量数据库当成“长期记忆系统”把 RAG 当成“记忆与生成之间的协作流程”很多选择就变得清晰了。剩下的是持续迭代。