ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Milvus 2.6与RAG构建企业知识库问答系统实战

基于Milvus 2.6与RAG构建企业知识库问答系统实战 之前在给一家企业做内部知识库时最头疼的问题不是文档存储而是“用户问一句自然语言系统能不能找到真正相关的文档”。最初用 MySQL 的 LIKE 模糊匹配只能命中关键词用户问“差旅报销怎么走流程”而文档里写的是“申请差旅费用需要提交审批单”这种语义上的差异直接导致检索结果为空。后来把方案改成 Milvus 向量数据库 RAG 检索增强生成才真正把问题解决。这篇文章会围绕 Milvus 2.6 和 RAG 展开从“向量数据库到底是什么”讲起再到 Docker Compose 部署 Milvus Standalone、Python 客户端接入、文档切块、Embedding 写入、相似度检索、LLM 回答的完整链路。适合刚接触 RAG 的开发者也适合准备在企业项目里落地向量检索的同学。读完你不仅能搭出一套可演示的知识库问答系统还能理解每一步背后的设计原因以及后续上生产时要注意的坑。1. 背景与核心概念1.1 什么是向量数据库先做一个通俗类比。传统关系型数据库存的是行和列里面是字符串、数字、日期这类结构化数据查询方式以精确匹配和范围过滤为主。向量数据库则多了一种核心能力它可以存储“向量”也就是一串浮点数例如[0.121, -0.051, 0.882, ...]并且能根据向量之间的“距离”快速找到最相近的向量。这串浮点数从哪里来它来自 Embedding 模型。我们可以把一段文字、一张图片甚至一段音视频都交给 Embedding 模型处理模型会输出一个固定维度的向量。这个向量在向量空间中代表了原文的语义位置。语义相近的文本其向量距离更近语义无关的文本其向量距离更远。向量数据库就是专门为这种“语义相似度检索”设计的存储和计算引擎。在企业场景中向量数据库通常被用于推荐系统、相似图片检索、去重、多模态搜索以及目前最热门的 RAG 知识库问答。它解决的核心问题是在海量非结构化数据中通过语义而不是关键词完成近似度搜索。1.2 什么是 RAGRAG 的全称是 Retrieval-Augmented Generation检索增强生成。它的思路很简单大模型在回答用户问题之前先从外部知识库中检索出相关资料把资料拼接到 Prompt 中再让大模型依据这些资料生成答案。之所以需要 RAG是因为大模型存在三个天然问题。第一是知识时效性差模型训练完成之后训练数据以外的知识它并不知道第二是私有数据不可见企业内部文档、客服记录、项目手册不会出现在模型训练集中第三是幻觉问题面对不确定的内容大模型可能会一本正经地“编造”答案。RAG 通过实时检索外部知识把回答限定在检索到的资料范围内既能补充实时知识又能显著降低幻觉概率。RAG 的典型流程可以分为离线索引和在线查询两个阶段。离线阶段把文档切块、向量化、写入向量数据库在线阶段把用户问题向量化在向量数据库中检索 TopK 相关片段组装成 Prompt 后交给大模型生成回答。1.3 为什么选择 Milvus 2.6市面上的向量数据库并不少有开源的 Chroma、Qdrant、Weaviate也有 Elasticsearch 自带的向量检索能力。但从生产落地和项目规模来看Milvus 是国内开发者和企业中使用非常广泛的开源向量数据库之一。Milvus 2.6 的定位是高可用、可扩展的云原生向量数据库。它的核心优势可以归纳为几点。第一是索引类型丰富支持 FLAT、IVF、HNSW、DISKANN 等标准向量索引也支持稀疏向量和混合检索第二是存储与计算分离的架构设计适合数据量从千万级到亿级以上的场景第三是 SDK 完善官方提供 Python、Java、Go、C# 等语言的客户端第四是周边生态成熟Dify、LangChain、LlamaIndex、Haystack 等框架内都提供了 Milvus 的集成组件。Milvus 2.6 具体到版本层面在稀疏向量、多向量检索、GPU 索引等能力上做了持续增强。如果你的项目还停留在概念验证阶段用 Milvus 2.6 起步后续平滑升级到集群模式也相对容易。1.4 RAG 与向量数据库的关系需要强调一点RAG 并不必然需要向量数据库。你完全可以用 Elasticsearch 的 BM25 关键词检索来做召回也可以直接用关系型数据库存向量然后暴力扫描或者用 Faiss 这类向量检索库。但实际工程中向量数据库承担的不只是“存向量”还包括索引加速、标量过滤、数据生命周期管理、高可用和扩展能力。在 RAG 链路中向量数据库负责的是“召回”这一步。召回质量直接决定最终回答质量。如果向量数据库检索不到相关片段大模型再强也无济于事。这也是为什么很多人说RAG 的上限由大模型决定下限却由检索质量决定。2. 环境准备与版本说明2.1 技术栈选型在动手之前先确定整个示例的技术栈。本文以 Milvus 2.6 作为向量数据库使用 Docker Compose 部署 Standalone 单机版本编程语言选择 Python客户端使用 pymilvusEmbedding 模型选择开源的BAAI/bge-m3这个模型对中文语义理解效果较好输出 1024 维向量LLM 部分为了不依赖特定云厂商示例使用 Ollama 启动本地模型如果企业项目中已经使用 OpenAI、通义、DeepSeek 等服务的 OpenAI 兼容接口也可以直接替换。版本这里需要说明Milvus 处于快速迭代期本文使用的镜像 tag 和 SDK 版本以你实际环境为准。建议控制在 2.6.x 的同一大版本内因为大版本升级往往涉及 API 变化和配置迁移。2.2 部署方式对比Milvus 的部署方式主要有四种适合不同阶段。部署方式特点适用场景Milvus Lite进程内嵌随 Python 环境启动无需 Docker本地学习、快速原型验证Standalone 单机一个 Milvus 实例依赖 etcd 和 MinIO中小规模数据、项目测试、单机生产Cluster 集群分架构部署支持分布式扩容大规模数据、高并发生产环境云托管服务官方托管版免运维预算充足、团队缺少运维人力对于第一次接触 Milvus 的同学推荐从 Standalone 开始既能完整体验真实服务的部署流程又不需要处理复杂的集群调度问题。2.3 Docker Compose 部署 Milvus Standalone演示环境以 Linux 或 macOS 为主Windows 也可以使用 Docker Desktop。先确保机器上已经安装 Docker 和 Docker Compose 插件。命令验证如下。docker --version docker compose version如果命令能正常输出版本号说明环境就绪。然后创建一个项目目录在目录下新建docker-compose.yml。# 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 - ETCD_SNAPSHOT_COUNT50000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urlshttp://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd healthcheck: test: [CMD, etcdctl, endpoint, health] interval: 30s timeout: 20s retries: 3 minio: container_name: milvus-minio image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data --console-address :9001 healthcheck: test: [CMD, curl, -f, http://localhost:9000/minio/health/live] interval: 30s timeout: 20s retries: 3 standalone: 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 healthcheck: test: [CMD, curl, -f, http://localhost:9091/healthz] interval: 30s start_period: 90s timeout: 20s retries: 3 ports: - 19530:19530 - 9091:9091 depends_on: - etcd - minio这个 Compose 文件包含了三个服务etcd 负责存储 Milvus 的元数据MinIO 负责存储日志和索引文件standalone 是 Milvus 主服务。19530是客户端连接的 gRPC 端口9091是健康检查和管理端口。启动命令如下。docker compose up -d首次启动需要拉取镜像具体耗时取决于网络和 Docker Hub 的访问情况。启动后查看容器状态。docker compose ps docker logs milvus-standalone -f当看到 Milvus 相关服务启动完成的日志并且curl http://localhost:9091/healthz返回正常响应说明部署成功。有一点要注意seccomp:unconfined这个配置在部分内核版本上是必须的如果启动失败提示权限问题可以优先检查这一项。2.4 安装 Python 依赖Milvus 服务启动之后需要安装 Python 客户端。新建一个 Python 虚拟环境然后安装依赖。python3 -m venv venv source venv/bin/activate pip install pymilvus sentence-transformers requests在实际项目中建议把依赖写入requirements.txt文件方便复现环境。pymilvus 与 Milvus 服务端有版本匹配关系建议安装与服务端大版本一致的 SDK例如 Milvus 2.6 对应 pymilvus 2.6 系列。3. Milvus 核心原理与检索机制3.1 核心概念Collection、Field、Schema在 Milvus 中Collection可以类比关系型数据库中的表Field类比字段Entity类比一行数据。每个 Collection 必须有一个主键字段此外可以包含标量字段和向量字段。向量字段可以有一个或多个用来承载 Embedding 结果。创建 Collection 时需要定义 Schema。Schema 需要说明每个字段的名称、数据类型、是否主键、向量维度等信息。例如一个典型的 RAG 知识库集合包含四个字段id主键、content文本内容、source文档来源、embedding文本向量。需要区分的是Milvus 的 Schema 设计是“先定义后使用”的不像传统数据库可以随意 ALTER TABLE。虽然 2.6 版本支持部分动态字段但生产环境中仍然建议在创建集合前仔细设计字段避免后续频繁变更 Schema。3.2 向量索引原理在解释 Milvus 的检索机制之前先看一个看似简单的问题如果数据量只有几千条直接暴力计算所有向量与查询向量的距离也就是 FLAT 索引完全没有问题。可当数据量涨到一亿条每次查询都做全量距离计算延迟和 CPU 开销都会高到不可接受。因此 Milvus 支持多种 ANN 近似最近邻索引。IVF系列索引先通过聚类把向量空间划分为多个桶查询时只搜索相近的若干个桶牺牲少量精度换速度HNSW则基于多层跳表结构构建一张可导航的小世界图查询时从高层入口逐层逼近目标节点召回率高延迟也比较稳定是当前最常用的索引类型之一DISKANN适合超大数据量可以把索引放到磁盘上减少内存占用。HNSW 有四个关键参数值得关注。M控制每个节点的最大连接数值越大图越稠密召回率越高但内存和构建时间也会增加efConstruction控制建图时的动态候选列表大小影响索引构建质量ef是查询时的候选列表大小值越大检索越精确但耗时越高metric_type指定距离计算方式。一般来说M取 16 到 32efConstruction取 200 左右ef取 64 到 128是比较稳妥的起步配置。3.3 距离度量类型Milvus 常用的距离度量有三种。L2欧氏距离计算的是两个向量之间的直线距离值越小越相似IP内积通常用于归一化向量值越大越相似COSINE余弦相似度计算的是向量夹角的余弦值值越大越相似关注“方向”而不是“长度”。对于文本 Embedding推荐使用COSINE。因为很多 Embedding 模型输出的向量虽然没有强制归一化但语义相似度更适合用余弦来衡量。在使用 COSINE 时Milvus 内部通常会对向量做归一化处理查询效果更稳定。如果使用 OpenAI 的text-embedding-ada-002官方也建议使用余弦相似度。3.4 标量过滤与混合检索真实业务中单纯的向量检索往往不够。例如知识库中既包含技术文档也包含财务制度用户可能只想在“财务”分类下检索。这时就需要在向量检索之前或同时增加标量过滤条件。Milvus 支持在 search 请求中携带filter表达式例如source finance实现向量相似度检索和标量条件过滤的一体化查询。这种能力对 RAG 系统很重要它允许我们按权限、按文档分类、按时间范围缩小召回范围避免向大模型喂入大量无关内容。4. RAG 知识库完整实战4.1 整体流程设计为了让思路更清晰先把整个 RAG 系统的数据流拆开。文档加载 - 文本切块 - Embedding 向量化 - 写入 Milvus Collection ↓ 用户提问 - 问题 Embedding - Milvus 相似度检索 - 召回 TopK 片段 ↓ 组装 Prompt ↓ 大模型生成回答并返回前半部分是离线索引流程后半部分是在线查询流程。两个流程共享同一个 Embedding 模型因此模型必须保持一致否则向量空间不同检索结果没有意义。4.2 项目结构规划本文用一个最小可运行的项目来演示目录结构如下。rag-milvus/ ├── docker-compose.yml ├── config.py ├── build_kb.py └── query_kb.pyconfig.py统一管理 Milvus 连接参数、集合名、Embedding 模型、分块参数等build_kb.py负责文档切块、向量化并写入 Milvusquery_kb.py负责接收问题、检索、组装 Prompt 并调用大模型生成回答。4.3 编写公共配置先创建config.py把整个链路中的关键参数集中到一个地方。# config.py MILVUS_HOST 127.0.0.1 MILVUS_PORT 19530 COLLECTION_NAME knowledge_base # Embedding 模型配置 EMBEDDING_MODEL BAAI/bge-m3 EMBEDDING_DIM 1024 # 文档切块参数 CHUNK_SIZE 300 CHUNK_OVERLAP 50 # 检索参数 TOP_K 5 # LLM 服务配置 LLM_BASE_URL http://localhost:11434 LLM_MODEL qwen2.5:7b这里的EMBEDDING_DIM必须和所选 Embedding 模型的输出维度一致。如果使用其他模型请先确认模型输出维度例如BAAI/bge-base-zh-v1.5是 768 维text-embedding-ada-002是 1536 维。维度和 Collection 定义不匹配会在插入数据时报错。4.4 文档切块与向量化写入接下来编写build_kb.py。这一步做的事情包括连接 Milvus、创建 Collection、加载本地文档、切块、生成向量、插入数据。# build_kb.py from pymilvus import ( connections, utility, CollectionSchema, FieldSchema, DataType, Collection ) from sentence_transformers import SentenceTransformer from config import ( MILVUS_HOST, MILVUS_PORT, COLLECTION_NAME, EMBEDDING_MODEL, EMBEDDING_DIM, CHUNK_SIZE, CHUNK_OVERLAP ) # 1. 连接 Milvus connections.connect(hostMILVUS_HOST, portMILVUS_PORT) # 2. 文本切块 def chunks_from_text(text, sizeCHUNK_SIZE, overlapCHUNK_OVERLAP): chunks [] step size - overlap start 0 while start len(text): piece text[start: start size] if piece: chunks.append(piece) start step if len(piece) size: break return chunks # 3. 创建 Collection def create_collection(): if utility.has_collection(COLLECTION_NAME): existing Collection(COLLECTION_NAME) print(f集合 {COLLECTION_NAME} 已存在) return existing fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idTrue), FieldSchema(namecontent, dtypeDataType.VARCHAR, max_length2048), FieldSchema(namesource, dtypeDataType.VARCHAR, max_length512), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dimEMBEDDING_DIM), ] schema CollectionSchema(fields, descriptionRAG knowledge base with Milvus 2.6) collection Collection(nameCOLLECTION_NAME, schemaschema) index_params { metric_type: COSINE, index_type: HNSW, params: {M: 16, efConstruction: 200} } collection.create_index(field_nameembedding, index_paramsindex_params) print(f集合 {COLLECTION_NAME} 创建完成) return collection # 4. 入口函数 def build(): model SentenceTransformer(EMBEDDING_MODEL) collection create_collection() # 实际项目中将这个列表替换为文档读取逻辑 documents [ { source: internal_manual.txt, text: 差旅费用报销流程如下第一步提交申请第二步填写明细 第三步等待审批财务在五个工作日内完成打款。如果需要加急 请提前联系财务部门并说明原因。 }, { source: faq.txt, text: 发票丢失时可以在系统中申请电子发票复印件经项目经理确认后 财务按原件流程处理。补交申请需要在报销截止日前完成。 } ] rows [] for doc in documents: chunks chunks_from_text(doc[text]) if not chunks: continue vectors model.encode(chunks).tolist() for chunk, vec in zip(chunks, vectors): rows.append({ content: chunk, source: doc[source], embedding: vec }) collection.insert(rows) collection.flush() collection.load() print(f已写入 {len(rows)} 个向量片段) if __name__ __main__: build()这段代码有几个细节值得说明。chunks_from_text函数采用固定字符长度切块并允许相邻片段之间存在重叠目的是减少句子被从中间截断带来的语义损失。create_collection中通过utility.has_collection判断集合是否已存在避免重复创建报错。embedding字段的维度必须与模型输出维度一致。最后插入数据后调用flush强制落盘load将数据加载到内存这样后续查询才能命中索引。执行导库脚本。python build_kb.py如果一切正常会输出类似下面的结果。集合 knowledge_base 创建完成 已写入 2 个向量片段由于示例文档较短写入片段较少。真实项目中文档数量可能是几千甚至上百万这时需要批量插入并考虑分批提交。4.5 检索与问答再创建query_kb.py实现用户问题检索和回答生成。# query_kb.py from pymilvus import connections, Collection from sentence_transformers import SentenceTransformer import requests from config import ( MILVUS_HOST, MILVUS_PORT, COLLECTION_NAME, EMBEDDING_MODEL, TOP_K, LLM_BASE_URL, LLM_MODEL ) connections.connect(hostMILVUS_HOST, portMILVUS_PORT) model SentenceTransformer(EMBEDDING_MODEL) def call_llm(messages): # 以 Ollama 为例OpenAI 兼容服务可以替换为对应地址 resp requests.post( f{LLM_BASE_URL}/api/chat, json{ model: LLM_MODEL, messages: messages, stream: False }, timeout60 ) resp.raise_for_status() return resp.json()[message][content] def search(query): query_vec model.encode([query]).tolist() collection Collection(COLLECTION_NAME) collection.load() results collection.search( dataquery_vec, anns_fieldembedding, param{metric_type: COSINE, params: {ef: 64}}, limitTOP_K, output_fields[content, source] ) docs [] for hit in results[0]: content hit.entity.get(content) source hit.entity.get(source) docs.append(f[来源:{source}] {content}相似度:{hit.distance:.4f}) return docs def ask(question): docs search(question) context \n.join(docs) messages [ { role: system, content: 你是一个知识库助理请严格基于提供的参考资料回答问题 如果资料中没有相关内容请明确说明不知道不要编造。 }, { role: user, content: f参考资料\n{context}\n\n问题{question} } ] answer call_llm(messages) return answer, docs if __name__ __main__: q 发票丢失了还能报销吗 answer, refs ask(q) print(检索到的参考资料) for r in refs: print(r) print(\n回答) print(answer)search函数将用户问题向量化然后在 Milvus 中执行相似度检索返回前TOP_K个片段。ask函数把检索结果组装进 Prompt再调用大模型。这里使用的 Prompt 模板是 RAG 中最基础的形态明确要求模型只依据参考资料回答。在运行前需要确保 Ollama 服务已启动并且已经拉取了配置文件中指定的模型。ollama pull qwen2.5:7b ollama serve然后运行问答脚本。python query_kb.py预期输出大致如下。大模型的具体表述每次可能不同但关键信息应该来自检索到的资料。检索到的参考资料 [来源:faq.txt] 发票丢失时可以在系统中申请电子发票复印件经项目经理确认后财务按原件流程处理。补交申请需要在报销截止日前完成。相似度:0.xxxx 回答 可以报销。发票丢失后您可以在系统中申请电子发票复印件经过项目经理确认后财务会按原件流程处理但需要注意补交申请应在报销截止日前完成。到这里一个最小可用的 RAG 知识库问答系统已经跑通了。接下来我们看看这个链路中经常出现的问题以及对应的排查方法。5. 常见问题与排查思路问题现象常见原因解决思路docker compose up -d启动后服务不断重启镜像 tag 不存在或 Docker 配置不支持查看docker logs确认镜像版本和security_opt配置Python 连接 Milvus 提示 19530 端口拒绝连接Milvus Standalone 未启动或端口映射错误使用docker compose ps查看容器状态确认防火墙放行端口插入数据时报维度错误Collection 的向量维度与 Embedding 模型输出维度不一致查询模型输出维度重新创建 Collection或者更换模型检索结果为空数据未 load或查询向量与集合向量不在同一语义空间调用collection.load()并确认使用同一个 Embedding 模型大模型回答与检索资料无关Prompt 中检索上下文拼接错误检查output_fields是否提取到content并观察检索到的文本内容Ollama 请求超时模型未启动或机器性能不足先运行ollama list确认模型换更小的模型测试查询延迟越来越高未建索引或索引类型不合适数据量增大检查describe_index必要时重建 HNSW 索引这里重点说一下“检索结果为空”的排查。遇到这类问题不要急着调大模型 Prompt先单独跑一遍search函数打印检索到的片段。如果 Milvus 返回结果为空最常见的原因有两个一是集合创建后插入数据但没有调用load二是查询时用了不同的 Embedding 模型导致查询向量和库里向量完全不在一个语义空间。另一个高频问题是 Docker 启动失败。很多初学者看到milvus-standalone容器反复重启就束手无策。排查顺序应该是先用docker logs milvus-standalone查看主服务日志再检查 etcd 和 MinIO 的健康状态最后确认宿主机端口是否被占用。Milvus 依赖 etcd 的元数据和 MinIO 的存储如果这两个组件不健康主服务必然无法正常工作。6. RAG 与 Milvus 工程化最佳实践6.1 文档切块策略切块是 RAG 系统中影响检索质量最重要的因素之一但它又容易被忽略。切块太小单个片段信息量不足容易召回语义不完整的碎片切块太大片段中混入大量无关内容向量被平均后可能偏离核心主题而且大块文本会占用更多 Prompt token。一个比较实用的策略是“结构化小块 重叠窗口”。如果文档有清晰的标题和段落结构可以尽量按标题层级切块保证每个 chunk 在语义上自洽。如果没有结构可以使用固定字符切块并让相邻 chunk 重叠 10% 到 20%。例如CHUNK_SIZE300CHUNK_OVERLAP50这样句子不容易被硬生生截断。更复杂的项目还可以考虑先切句子再把相邻句子按 token 上限合并或者引入语义切块模型。切块上线后务必人工抽检几个典型问题的召回结果根据实际效果调整参数。6.2 Embedding 模型选择与向量维度中文场景下目前开源社区使用较多的有 BGE 系列、M3E 系列以及近年出现的BAAI/bge-m3。这类模型对中文语义理解比较友好模型体积也能接受。如果业务数据以英文为主OpenAI 的 embedding 系列或 SentenceTransformers 下的英文模型都是可选方案。选择 Embedding 模型时不能只看效果还要关注向量维度。维度越高单个向量占用的存储越大检索计算量也越大。比如
返回列表