公司动态

基于Milvus构建RAG企业知识库:从原理到实战

📅 2026/8/26 6:57:02
基于Milvus构建RAG企业知识库:从原理到实战
当企业内部知识库、产品文档、客服问答系统开始大规模依赖大模型时一个无法回避的问题会摆在团队面前大模型没有“记忆”也无法感知企业内部私有文档。即使把文档喂给模型做微调成本高、更新慢效果也很难保证。这时以向量数据库为核心的 RAG检索增强生成架构就成了主流选择。在众多向量数据库中Milvus 是开源社区关注度最高、企业落地案例最多的项目之一。本文将以 Milvus 2.6 为示例版本从 RAG 与向量数据库的原理讲起完整拆解环境搭建、数据入库、相似度检索、大模型问答生成的全流程并结合企业内部项目落地的常见问题给出解决方案。无论你是刚开始接触向量数据库的初学者还是需要把 RAG 工程落地的后端、算法工程师都可以按本文的顺序动手实现一个可运行的示例。1. RAG 与向量数据库基本概念1.1 RAG 是什么RAG 的全称是 Retrieval-Augmented Generation中文通常叫“检索增强生成”。它的核心思路很简单不要直接让大模型凭空回答而是先从知识库中检索出相关的信息片段再把这些片段连同用户问题一起交给大模型由大模型基于这些资料生成回答。通过这种方式RAG 可以带来几个显著好处回答可以引用企业内部文档不再是模型“编造”出来的内容。知识更新不需要重新训练模型只需要更新知识库中的数据。可以在检索环节实现权限过滤实现“谁能看到什么”。可以显著减少大模型在专业领域中的幻觉问题。整个流程可以拆成两条链路离线索引链路和在线查询链路。离线索引链路负责把文档处理后写入向量数据库文档加载 - 文本清洗 - 文本切块 - Embedding 向量化 - 写入 Milvus在线查询链路负责处理用户问题并生成回答用户提问 - 问题 Embedding - 向量检索 TopK - 拼接上下文 - 大模型生成回答1.2 向量数据库为什么重要在 RAG 流程中最关键的操作是“相似度检索”。传统关系型数据库擅长精确匹配例如 WHERE title xxx但用户问“产品的退款流程是什么”文档里可能根本没有“退款流程”这四个字只有“如何申请退货”。这种情况靠关键词无法命中必须把文字转换成向量用数学上的向量距离来衡量语义相似度。向量数据库专门为这种场景设计它负责三件事存储大量高维向量。通过索引结构加速最近邻搜索。支持向量与标量属性如文档来源、部门、时间的混合过滤。相比自己用 Python 写一个“计算余弦相似度再排序”的脚本向量数据库能在千万级、亿级数据量下做到毫秒级响应这就是它不可替代的原因。1.3 Milvus 与同类产品对比市面上向量数据库不少比如 Chroma、Qdrant、Weaviate、pgvector还有云服务产品。Milvus 的主要特点是开源且社区活跃由 Zilliz 团队维护。支持高可用分布式部署适合海量数据场景。支持多种索引类型和相似度度量方式。提供 Python、Java、Go、Node.js、C# 等多种语言 SDK。与 LangChain、LlamaIndex、Dify 等主流 RAG 框架都有官方集成。如果你只是做本地小规模 demoChroma 或 pgvector 会更轻量。但如果目标是企业级知识库、几十万甚至上亿条文本Milvus 是更稳妥的选择。本文实战部分也将会围绕 Milvus 展开。2. 环境准备与 Milvus 安装2.1 环境要求本文示例以 Linux 环境为主Windows 和 macOS 也可以按同样思路运行。前提条件是机器上已安装 Docker 和 Docker Compose因为 Milvus 分布式组件较多使用 Docker Compose 启动是最快的方式。推荐配置如下资源最低配置建议配置操作系统CentOS 7 / Ubuntu 20.04Ubuntu 22.04CPU2 核4 核及以上内存8 GB16 GB磁盘20 GB50 GB SSDDocker20.10最新稳定版需要注意CentOS 7 上 Docker 版本可能较老如果遇到启动问题先升级 Docker。开始安装之前先确认 Docker 可以正常使用docker --version docker compose version如果你的 Docker Compose 是旧版命令docker-compose后面命令注意替换。2.2 使用 Docker Compose 启动 Milvus StandaloneMilvus 有 Standalone单机和 Cluster分布式集群两种部署模式。学习和小规模生产环境通常先用 Standalone。先创建项目目录mkdir -p /opt/milvus cd /opt/milvus官方提供了 standalone 的编排文件推荐从 GitHub 获取。注意完整的官方 YAML 包含 etcd、minio、milvus 三个服务直接下载官方文件即可wget https://github.com/milvus-io/milvus/releases/download/v2.6.1/milvus-standalone-docker-compose.yml -O docker-compose.yml如果网络访问 GitHub 不稳定也可以手动创建 docker-compose.yml核心内容如下version: 3.5 services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.18 environment: - ETCD_AUTO_COMPACTION_MODErevision - ETCD_AUTO_COMPACTION_RETENTION1000 - ETCD_QUOTA_BACKEND_BYTES4294967296 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urlshttp://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd minio: container_name: milvus-minio image: minio/minio:RELEASE.2024-09-22T00-33-43Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data healthcheck: test: [CMD, curl, -f, http://localhost:9000/minio/health/live] interval: 30s timeout: 20s retries: 3 milvus: container_name: milvus-standalone image: milvusdb/milvus:v2.6.1 command: [milvus, run, standalone] security_opt: - seccomp:unconfined environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus ports: - 19530:19530 - 9091:9091 depends_on: - etcd - minio这里简单解释一下三个组件的作用etcdMilvus 的元数据存储记录 collection、partition、索引等元信息。MinIO对象存储负责保存向量数据文件、索引文件和日志快照。Milvus主服务对外提供 gRPC 端口 19530以及 Web 端口 9091。镜像版本请以实际下载时存在的版本为准避免使用latest标签生产环境更要固定版本。启动服务docker compose up -d查看状态docker compose ps正常情况下三个容器都处于Up状态Milvus 容器健康检查通过后即可使用。验证 Milvus 服务是否可用可以使用 Python 客户端连接测试。2.3 安装 Python 客户端建议使用虚拟环境python3 -m venv venv source venv/bin/activate pip install pymilvus2.6.1同时安装后续会用到的依赖pip install langchain langchain-community pymilvus openai注意上述依赖版本会随社区更新变化。实际安装时建议先安装 pymilvus再根据框架文档安装对应版本避免版本冲突。安装完成后用一段简单代码测试连接from pymilvus import connections connections.connect(hostlocalhost, port19530) print(Milvus 连接成功)如果连接成功说明环境已就绪可以开始后续实战。3. 核心原理拆解从文档到可检索向量很多同学在跑通 RAG demo 后会觉得“不就这么点事吗”但一旦数据量上来、业务场景复杂起来问题就出在最初的数据处理和向量化环节。这一节重点讲清楚三个环节的原理与取舍。3.1 文本切块策略大模型有上下文长度限制同时向量检索的精度也受文本粒度影响。如果整篇文章作为一个向量入库查询时很难精确命中某个知识点如果切得太碎又可能丢失上下文语义。常见的切块策略有固定长度切块按字符数硬切例如每 500 个字符一块中间加 100 个字符重叠。实现简单但可能切断语义完整的段落。递归字符切块按分隔符优先级依次切分这是 LangChain 中比较常用的方法。语义切块根据句子向量相似度变化自动切分效果通常更好但计算成本较高。实际项目中切块大小没有统一标准。经验上可以从 200 到 500 个 token 左右开始测试通过检索效果来反推最优值。块太小容易丢失上下文块太大则会引入无关信息降低检索精度。3.2 Embedding 模型选择Embedding 模型负责把文本转换成固定维度的向量。常见选项有OpenAI 的text-embedding-3-small简单方便但数据会发送到外部 API。开源模型如BAAI/bge-large-zh-v1.5中文效果好可以本地部署。M3E 中文模型、Shibing624/text2vec 系列适合企业内部数据不出域的场景。选择 Embedding 模型时需要注意三个问题向量维度维度越高存储和计算开销越大。语言适配性如果知识库以中文为主优先使用中文优化过的模型。部署方式本地化部署可能需要 GPU纯 CPU 推理速度会低一些。需要特别强调的是检索阶段必须使用与入库阶段完全相同的 Embedding 模型否则向量空间不一致检索结果会完全失真。3.3 向量索引与相似度度量Milvus 中Collection 是一个“表”存放向量字段和标量字段。每个 Collection 都要指定向量维度和相似度度量类型IP内积适合向量已归一化的情况。L2欧氏距离值越小越相似。COSINE余弦相似度最符合语义检索直觉值越大越相似。索引方面Milvus 支持多种索引类型。对于绝大多数 RAG 场景推荐使用HNSW。它是一种基于图的近似最近邻索引召回率高、查询速度快适合中小规模到大规模数据。数据量极大且对内存占用敏感时可以考虑DiskANN。创建索引时还需要设置nlist、M、efConstruction等参数这些参数直接影响召回率和构建时间。业务初期不必过度调优使用默认参数即可后面有明确的性能瓶颈再针对性调整。4. 实战构建一个基于 Milvus 的 RAG 知识库系统这一节我们从零开始实现一个完整的企业内部知识库问答系统。示例场景是“企业内部产品帮助文档问答”数据源为 Markdown 或 CSV 文档最终实现的效果是用户提问“怎么申请退款”系统能从文档中检索相关内容并由大模型生成回答。4.1 项目结构推荐的工程目录如下milvus-rag-demo/ ├── data/ │ └── help_docs.csv ├── src/ │ ├── ingest.py │ ├── search.py │ └── query.py ├── requirements.txt └── README.mddata/help_docs.csv模拟企业内部帮助文档包含doc_id和content两列。ingest.py负责读取数据、切块、向量化、写入 Milvus。search.py负责查询向量库返回相似文档片段。query.py把检索结果交给大模型生成最终答案。4.2 准备示例数据我们先准备一个简单的 CSV 数据文件。这里只列三行示例实际使用时替换为你的真实文档。doc_id,content 1,用户可以在订单页面点击申请退款系统将在3个工作日内审核。 2,退款金额将原路返回至支付账户到账时间取决于银行处理速度。 3,如果订单已经发货需要先确认收货后才能申请退款。构建一个能直接运行的 demo建议数据量几十条到上百条效果比较好。4.3 实现离线索引流程离线索引流程是 RAG 系统的地基核心步骤包括加载文档、切块、生成向量、写入 Milvus。创建src/ingest.py完整代码如下# 文件路径src/ingest.py import csv from langchain.text_splitter import RecursiveCharacterTextSplitter from pymilvus import ( connections, CollectionSchema, FieldSchema, Collection, DataType, utility, )这里我们先用一个简化的 embedding 生成函数替代真实模型方便在没有 API Key 的情况下把流程跑通。实际项目中请替换成你选择的 Embedding 服务。import hashlib import random import numpy as np DIM 128 def fake_embedding(text: str) - list[float]: 模拟 embedding实际请替换为真实模型接口 random.seed(int(hashlib.md5(text.encode()).hexdigest()[:8], 16)) vec np.random.rand(DIM).astype(np.float32) norm np.linalg.norm(vec) return (vec / norm).tolist()接下来定义 Milvus 连接和 collection 创建逻辑MILVUS_HOST localhost MILVUS_PORT 19530 COLLECTION_NAME help_docs def connect_milvus(): connections.connect(hostMILVUS_HOST, portMILVUS_PORT) def create_collection(): if utility.has_collection(COLLECTION_NAME): print(fCollection {COLLECTION_NAME} 已存在直接返回) return Collection(COLLECTION_NAME) id_field FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idFalse) doc_id_field FieldSchema(namedoc_id, dtypeDataType.INT64) content_field FieldSchema(namecontent, dtypeDataType.VARCHAR, max_length2000) embedding_field FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dimDIM) schema CollectionSchema( fields[id_field, doc_id_field, content_field, embedding_field], description企业内部帮助文档知识库, enable_dynamic_fieldTrue ) collection Collection(nameCOLLECTION_NAME, schemaschema) index_params { index_type: HNSW, metric_type: COSINE, params: {M: 16, efConstruction: 256} } collection.create_index(embedding, index_params) print(fCollection {COLLECTION_NAME} 创建成功) return collection需要注意几个点id_field用于唯一标识一条记录这里用自增整型。doc_id_field保留原始文档编号方便回溯来源。content_field存原文用于检索后拼接 prompt。enable_dynamic_fieldTrue允许动态字段如果后续需要额外元数据如部门、权限等级可以不用频繁修改 schema。然后是数据切块和写入逻辑def process_and_insert(csv_path: str): splitter RecursiveCharacterTextSplitter( chunk_size200, chunk_overlap50, separators[\n\n, \n, 。, , , , ] ) rows [] with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) for record in reader: doc_id int(record[doc_id]) content record[content] chunks splitter.split_text(content) for idx, chunk in enumerate(chunks): rows.append({ id: len(rows) 1, doc_id: doc_id, content: chunk, embedding: fake_embedding(chunk) }) collection Collection(COLLECTION_NAME) collection.insert(rows) collection.flush() print(f成功插入 {len(rows)} 条向量数据)最后是入口函数if __name__ __main__: connect_milvus() create_collection() process_and_insert(data/help_docs.csv)运行python src/ingest.py如果输出“成功插入 5 条向量数据”根据你的 CSV 行数有所不同说明索引流程已经跑通。4.4 实现相似度检索数据入库后下一步是查询。创建src/search.py# 文件路径src/search.py from pymilvus import Collection, connections, utility COLLECTION_NAME help_docs DIM 128 def get_embedding(text: str) - list[float]: # 与 ingest.py 中相同逻辑实际项目使用同一模型接口 import hashlib import random import numpy as np random.seed(int(hashlib.md5(text.encode()).hexdigest()[:8], 16)) vec np.random.rand(DIM).astype(np.float32) norm np.linalg.norm(vec) return (vec / norm).tolist() def search(query: str, top_k: int 3): connections.connect(hostlocalhost, port19530) if not utility.has_collection(COLLECTION_NAME): raise RuntimeError(Collection 不存在请先执行 ingest.py) collection Collection(COLLECTION_NAME) collection.load() query_vector get_embedding(query) results collection.search( data[query_vector], anns_fieldembedding, param{metric_type: COSINE, params: {ef: 128}}, limittop_k, output_fields[doc_id, content] ) print(f查询: {query}\n) for i, hit in enumerate(results[0]): print(fTop {i 1}: 相似度 {hit.distance:.4f}) print(fdoc_id: {hit.entity.get(doc_id)}) print(fcontent: {hit.entity.get(content)}\n) return results[0] if __name__ __main__: search(用户如何申请退款)运行python src/search.py预期会输出与“退款”相关的几个文档片段并按相似度从高到低排列。注意因为我们用的是随机向量模拟 embedding结果可能并不符合语义这很正常换成真实 embedding 模型后才会体现语义检索能力。4.5 在真实项目中使用 LangChain 简化集成上面我们用原生 pymilvus 实现了全流程如果希望更快接入 RAG 框架LangChain 也提供了 Milvus 的封装。安装依赖pip install langchain-community pymilvus langchain-openai写入和检索的代码可以简化成from langchain_community.vectorstores import Milvus from langchain_openai import OpenAIEmbeddings embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vector_store Milvus.from_texts( texts[用户可以在订单页面点击申请退款], embeddingembeddings, collection_namelc_demo, connection_args{host: localhost, port: 19530} ) retriever vector_store.as_retriever(search_kwargs{k: 3})这种方式非常适合原型验证。但进入生产环境后我更推荐使用原生 SDK 或框架提供的底层封装因为这样可以更精细地控制 schema、索引参数、权限和资源隔离。4.6 接入大模型生成最终回答检索只能返回相关片段最终还需要大模型把片段整合成自然流畅的回答。创建src/query.py# 文件路径src/query.py from openai import OpenAI from search import search client OpenAI(api_keyYOUR_OPENAI_API_KEY, base_urlhttps://api.openai.com/v1) SYSTEM_PROMPT 你是企业知识库助手。请根据提供的上下文回答问题。 如果上下文中没有相关内容请直接说明“根据现有文档无法回答”。 不要编造事实。回答时标注信息来源。 def generate_answer(question: str): hits search(question, top_k3) context_parts [] for hit in hits: doc_id hit.entity.get(doc_id) content hit.entity.get(content) context_parts.append(f[来源 {doc_id}] {content}) context \n.join(context_parts) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f问题{question}\n\n上下文\n{context}} ], temperature0.3 ) return response.choices[0].message.content if __name__ __main__: print(generate_answer(如何申请退款))如果你没有 OpenAI 的 API Key也可以换成本地部署的模型通过 OpenAI 兼容接口访问代码基本保持不变。这里有一个工程细节值得强调检索结果必须附带来源信息。在大模型生成的回答中如果每个关键结论都能关联到文档来源用户在业务上就能追溯到原始材料这也是 RAG 相比纯大模型问答的核心优势之一。5. 常见问题与排查思路在 Milvus RAG 落地过程中下面几个问题是出现频率最高的。按表格整理出来方便直接对照排查。问题现象常见原因解决思路Milvus 容器启动后一直重启etcd 或 MinIO 端口冲突、内存不足查看容器日志释放端口检查内存占用pymilvus 连接超时Milvus 尚未完全就绪或网络不通等待 30 秒后重试检查 19530 端口插入数据时报维度不一致错误Collection 创建时指定的 dim 与 embedding 模型输出维度不一致打印 embedding 向量长度确认与集合定义一致Collection 存在但没有数据插入后没有调用 flush或数据量过小插入后调用collection.flush()查询前调用collection.load()检索结果为空collection 未 load或已有数据未建索引先 load再 search语义检索效果差切块过大/过小embedding 模型不合适尝试不同 chunk_size 和模型构建评测集回答频繁说“无法回答”检索召回率低或 prompt 约束过强调大 top_k检查检索返回内容是否命中多语言文档乱码或错误编码不一致CSV 读取错误统一使用 UTF-8 编码读取时指定 encoding同一个 collection 被多个系统使用数据相互干扰缺少命名空间或分区设计使用 Partition 分区或为不同业务创建独立 collection遇到报错时第一动作永远是看日志docker compose logs -f milvusMilvus 的日志会明确打印出异常模块再结合具体报错关键字搜索大多数问题都能快速定位。关于 Milvus 动态列的问题也经常被问到尤其是 C# SDK 中如何获取$meta动态列的值。在 Milvus 中动态字段是指在 schema 之外额外写入的字段enable_dynamic_fieldTrue时会以$meta形式保存。获取动态字段值时只要在查询结果实体中按字段名取出即可不同 SDK 的 API 差异不大核心原理是相同的动态字段不会出现在output_fields中时结果里不会返回需要显式指定字段名。6. 最佳实践与工程建议6.1 合理设计 Collection 与 Partition在单知识库场景下一个 Collection 就够用了。但如果企业内部有多个部门、多种文档类型建议为不同业务创建独立 Collection或者在同一 Collection 下创建 Partition。Partition 可以在检索时直接限定范围既提高查询速度也方便权限控制。例如collection.create_partition(part_after_sale) collection.insert(rows, partition_namepart_after_sale)检索时collection.search( data[query_vector], anns_fieldembedding, param{metric_type: COSINE}, limit3, partition_names[part_after_sale] )6.2 数据库连接与资源管理Milvus 的连接是长连接不建议每次查询都新建连接。工程上应该把连接管理作为独立的模块复用同一个连接对象。同时要设置合理超时时间避免在 Milvus 异常时接口被长期阻塞。6.3 检索后的重排序单纯的向量检索返回结果可能包含一些“看着相似但实际不相关”的片段。在企业级 RAG 系统中可以在向量检索后增加一个重排序Rerank环节。常见的做法是用向量检索召回 Top 50。用交叉编码器或 LLM 对 Top 50 重新打分。取重新排序后的 Top 5 作为最终上下文。这能显著提升小粒度问题的回答质量代价是多了几十到几百毫秒的延迟。对回答质量要求高的场景这一步非常值得。6.4 数据权限与安全边界RAG 系统最大的隐藏风险是权限绕过。如果所有人都能检索全部文档那么内部敏感信息就可能暴露给不该看到的人。工程上建议在向量数据中加入权限标签字段例如dept、level。查询时根据当前用户权限生成过滤表达式。Milvus 侧开启用户认证不同业务使用不同的用户名和 API Key。Embedding 模型如果调用外部服务注意文档内容是否涉及机密必要时应本地部署模型。写过滤表达式时注意语法例如expr level 3 and dept in [技术部, 客服部] collection.search( data[query_vector], anns_fieldembedding, param{metric_type: COSINE}, limit3, exprexpr )6.5 监控与运维Milvus 提供了健康检测接口可以通过/healthz和/metrics获取服务状态和指标。日常运维至少要关注磁盘使用率尤其是 MinIO 存储目录。etcd 的稳定性和备份策略。查询延迟和召回率趋势。Collection 数据量变化。同时不要忘了给 Milvus 的数据卷做定期备份。元数据依赖 etcd数据文件在 MinIO两者都需要有备份恢复方案。7. 总结与后续学习路线本文从 RAG 的基本概念出发讲清楚了向量数据库在 RAG 架构中的位置然后完整演示了 Milvus 2.6 环境搭建、文档导入、向量检索以及大模型回答生成的流程。你可以在自己的机器上把示例跑通然后逐步替换成真实数据、真实 Embedding 模型和真实大模型接口。如果现在想继续深入建议按下面顺序学习先掌握 LangChain 或 LlamaIndex 中 Milvus 的封装能快速搭建带界面的 demo。再研究 Milvus 集群模式部署理解 etcd、MinIO、Pulsar 在集群中的作用。关注混合检索也就是把 Milvus 向量召回与 Elasticsearch 关键词召回结合用 RRFReciprocal Rank Fusion做结果融合。在数据量上来后分析索引参数对召回率和查询延迟的影响构建属于你自己业务的小型评测集。最后的建议是不要一开始就追求把所有技术都堆上先让一个简单的 RAG 链路跑通再根据业务痛点优化切块、检索、重排序和权限。向量数据库和 RAG 仍在快速演进之中版本差异较大实际部署时一定要以官方文档和当前版本为准。希望本文能帮你少踩一些坑顺利把第一个 Milvus RAG 项目落地。