ARTICLE DETAIL

资讯详情

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

RAG知识库向量数据库选型与落地:从Chroma到Qdrant的工程实践

RAG知识库向量数据库选型与落地:从Chroma到Qdrant的工程实践 这两年AI应用遍地开花智能体这个提法谁都能聊两句但真正落到工程层所有人都会遇到同一个问题项目里的企业知识、个人文档到底存在哪里才合适我前后试过Chroma、Milvus、pgvector也在线上跑过一段时间最后稳定用下来的是 Qdrant 这个向量数据库。它解决的是典型的RAG存储与检索需求把非结构化文本切成块用embedding模型转成向量再通过语义相似度把相关内容找回来交给大模型。如果你正在做知识库增强、智能客服、智能体记忆这类应用这篇文章应该能帮你省掉不少试错时间。下面我不讲空泛概念单从项目落地和线上运维的角度把Qdrant从选型、核心模型、部署上手到知识库落地的完整套路以及那些文档里不会写清楚的坑一次说完。1. 为什么知识库需要专门为向量设计的数据库1.1 传统数据库卡在哪语义匹配不是精确匹配先说一个很基础的观察。传统关系型数据库擅长的是精确匹配主键、外键、等值查询、范围查询这一套在业务系统里没有任何问题。但知识库场景里用户问的是“报销流程有哪几步”文档里写的是“员工提交申请后由部门负责人审批再交财务复核”这两段话没有任何一个词是重复的用SQL的LIKE去匹配永远匹配不上。你上全文索引比如Elasticsearch能解决一部分分词的问题但还是卡在“同义表达”和“语义相近”上。向量数据库的思路是把文本交给embedding模型生成一串几百维的浮点数向量。语义相近的文本在高维空间里的距离就近语义差的就远。用户查询时先把问题也转成向量然后在库里找“离它最近的”一批向量再把对应的原文取出来。这个逻辑本质上不是一个SQL问题而是一个高维空间最近邻搜索问题关系型数据库从存储模型到索引结构都不适配。1.2 向量检索的本质与ANN/HNSW可能有人会问数据量小的时候我用Python把库里所有向量都读出来逐个算内积也能找“最近的”为什么非要数据库这正是核心区别。当你有几十万条文档块每个块是768维向量暴力扫描一次的耗时是线性增长的到了千万级基本不可用。而且这还只是单次查询线上QPS一上来服务直接就垮了。所以向量数据库干的核心事情是近似最近邻检索英文缩写是ANN。Qdrant默认实现的索引结构叫HNSW全称是Hierarchical Navigable Small World分层可导航小世界图。理解它可以用一个生活场景你在陌生的城市找一个人最快的办法不是挨家挨户敲门而是先问你认识的朋友朋友再问他认识的朋友一层一层找到目标。HNSW把向量组织成多层图上层边稀疏但跨度大用来快速定位大概区域下层边密集用来精确定位邻居。查询时从最上层往下走每一层都找离查询点最近的若干节点逐步逼近真正的近邻。这个设计牺牲了一点点精度换来了数量级的速度提升。而Qdrant在HNSW取回候选集之后还会用原始向量做一次精确距离重排等于把近似检索的误差又拉回了一大截。这也是我后来选定它而不是自己造轮子的原因你单独写一个暴力检索脚本容易但想把索引、过滤、重排、持久化、并发查询全都做扎实不是一个周末能搞完的开销。2. 选型花了好几天Qdrant、Milvus、pgvector与Chroma的实测对比2.1 先看一张能直接用的大表我知道很多人在动手之前会卡在选型上。这里我直接把四类常用方案的实测体会整理成一张表维度是我在实际项目中真正关心的不是官网宣传语。方案部署方式过百万向量后的表现过滤能力运维成本最合适场景Qdrant单容器可直接跑也支持集群Rust实现内存控制好延迟稳定内置Payload过滤支持复杂条件组合低二进制单一RAG知识库、中小规模到亿级Milvus依赖etcd、对象存储、消息队列等组件分布式能力强适合超高并发、超大集群支持标量过滤但配置复杂高组件多要专门人维护大数据平台、大规模生产集群pgvectorPostgres扩展随实例跑几百万内能用再上去索引构建和查询衰减明显依赖SQL表达复杂条件很啰嗦低但调优空间小已有Postgres、数据量可控Chroma本地文件零配置原型没问题数据一多查询和持久化开始吃力有基础过滤但生产级能力弱最低本地demo、小规模脚本我身边很多团队从Chroma起步因为它写demo确实快三五行代码就能把PDF灌进去。可一旦遇到权限过滤、版本升级、数据迁移、高并发这些问题Chroma的抽象就开始不够用了。而Milvus又走到另一个极端功能全面但组件太多光是把环境变量和依赖服务理清楚就得花上一两周对一个小团队来说有点重。2.2 我放弃Chroma和Milvus的真实原因我最初在Chroma上做原型一天就搭完了检索流程。但准备接生产时发现几个硬问题一是数据量大之后本地文件和底层表结构变成了黑盒出了问题很难排查二是权限过滤场景越来越复杂我想按文档所属部门、密级、时间范围做组合筛选写起来很不顺手三是备份和迁移的方案不够成熟我对直接把数据目录复制到另一台机器多少有点不放心。Milvus我也认真试过。它确实强大尤其是大规模分布式场景业界很多头部企业都在用。但对我们的团队来说维护这套依赖链本身就是负担而且我的项目数据量还没大到需要分布式集群的程度。这个时候Qdrant的定位就很舒服它有独立向量数据库该有的正经能力比如Payload过滤、快照备份、分片、REST/gRPC接口但部署上又是一个Rust写成的单容器资源占用低启动也快。从原型平滑过渡到生产中间不用换技术栈。2.3 什么场景下其实不用选Qdrant选型这件事没有银弹我也碰到过不适合用Qdrant的情况。如果团队已经重度使用Postgres业务数据本身就在里面而且向量数据规模不大我倾向于直接用pgvector少一套组件就少一堆事如果要检索的数据到了十亿级以上并且有专门的平台团队支撑Milvus的分布式能力会更对路如果只是想在笔记本上验证一下RAG想法Chroma依然是最快的方式。我现在的判断标准很简单先看数据规模和团队运维边界再看过滤和混合检索需求。Qdrant适合的是“需要一个正经向量数据库但不想养一堆基础组件”的团队这也是它在这轮选型里胜出的根本原因。3. Qdrant的核心模型Collection、Point、Vector与Payload怎么协作3.1 从“表”“行”“字段”去理解Collection与Point接触Qdrant时最容易懵的是一堆名词Collection、Point、Vector、Payload。其实拿关系型数据库做类比就很好理解。Collection相当于一张表Point相当于表里的一行Point里的Vector是这行数据的主向量列Payload则是这一行上挂的其它Json字段用来存元数据和过滤条件。举个例子。建一个叫wiki的Collection往里写一条Pointfrom qdrant_client import QdrantClient client QdrantClient(urlhttp://localhost:6333) from qdrant_client.models import PointStruct client.upsert( collection_namewiki, points[ PointStruct( id1, vector[0.12, -0.34, 0.56], payload{ title: 请假制度, department: HR, page: 3, is_public: True } ) ] )这里的id可以是一个无符号整数也可以是一个UUID。实际项目中我习惯用文档块的唯一ID比如“文档ID_切块序号”这样重复写入时同一个ID会被覆盖天然实现了幂等更新。Vector是检索的核心Payload则是过滤的核心。两者分开设计的好处很明显检索走的是高维向量索引过滤走的是Payload上的标量索引两者在查询时可以组合起来先过滤再检索也可以在全部数据里检索后再根据Payload结果筛选。这个组合能力对知识库特别重要因为真实场景里几乎不存在“无条件的全局检索”。3.2 维护多种向量命名向量、稀疏向量与多向量一个容易被忽略的点是Qdrant允许一个Point里挂多个向量这就是命名向量。比如一条文档块既可以用“标题向量”也可以用“正文向量”来检索两个向量存在同一个Point里查询时可以指定用哪一个。命名向量的配置方式是vectors_config传一个字典from qdrant_client.models import Distance, VectorParams vectors_config { title: VectorParams(size384, distanceDistance.COSINE), content: VectorParams(size384, distanceDistance.COSINE), } client.create_collection( collection_namewiki, vectors_configvectors_config, )查询时再指定用哪个向量。这个特性在做标题优先召回、正文补充召回时很实用。Qdrant还支持稀疏向量。稠密向量是每维都有值的浮点数组稀疏向量则只记录非零位置和值常见于BM25、SPLADE这类关键词或词权重模型。稠密向量擅长理解语义但遇到专业缩写、型号、人名时容易“想当然”稀疏向量恰好擅长精确的词匹配。很多知识库项目会在一个Collection里同时启用两种向量查询时做混合召回再用RRF之类的融合算法把两路结果合并效果比单路要好。3.3 距离度量选错结果直接废向量之间的“距离”怎么定义Qdrant提供了三种常用度量Cosine、Dot、Euclid。使用embedding模型时最好去看模型文档里推荐哪种距离。比如很多中文语义模型默认使用CosineOpenAI的text-embedding-ada-002也是推荐余弦相似度而一些专门训练用于向量搜索的模型可能推荐Dot因为向量已经做过归一化Dot和Cosine等价但计算更快。选错度量的表现很典型检索出来的结果“好像相关但排序不太对”或者差不多质量的文档分数分布压在一起阈值怎么调都不舒服。解决的办法也不复杂在创建Collection前确认好度量然后固定下来。但要注意一旦Collection创建完成向量维度和距离度量就不能改了后面想换只能重建Collection。所以建库之前请一定先确认一遍embedding模型的输出维度和推荐的相似度算法。4. 第一次跑通Qdrant部署、写数据、查询的完整动作4.1 Docker起服务Dashboard在哪里Qdrant官方提供了Docker镜像这也是我最推荐的起步方式不用纠结编译和依赖问题。一条命令就能把服务跑起来docker run -p 6333:6333 -p 6334:6334 \ -v $(pwd)/qdrant_storage:/qdrant/storage \ qdrant/qdrant这里我把本地的qdrant_storage目录挂载到容器里的/qdrant/storage这样数据持久化在宿主机上容器删了重建也不会丢。6333是REST API端口6334是gRPC端口Dashboard默认在浏览器打开http://localhost:6333/dashboard。Dashboard不是摆设我实际使用中经常用它做三件事看一眼Collection状态、手动写一条测试向量、快速验证某个过滤条件有没有生效。很多人一上来就只写代码遇到查询结果不对时来回调试其实在Dashboard里点两下就能定位问题。服务启动后建议先用docker logs确认没有启动报错再敲一下REST接口curl http://localhost:6333/collections返回一个空集合列表就说明服务正常。4.2 Python客户端建库、写入、检索三连Python日常操作我用的库是qdrant-client直接pip安装pip install qdrant-client建库时指定向量维度和距离度量。以384维的Cosine距离为例from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams client QdrantClient(urlhttp://localhost:6333) client.create_collection( collection_namewiki, vectors_configVectorParams(size384, distanceDistance.COSINE), )写入数据和查询数据的完整流程大概是这样的。先用embedding模型把文本转成向量构造PointStruct列表调用upsert批量写入from qdrant_client.models import PointStruct points [] for idx, chunk in enumerate(chunks): vec embed_model.encode(chunk[text]).tolist() points.append( PointStruct( idchunk[doc_id] _ str(idx), vectorvec, payload{ text: chunk[text], source: chunk[source], department: chunk[department], }, ) ) client.upsert(collection_namewiki, pointspoints)查询时同样把用户问题转成向量调query_pointshits client.query_points( collection_namewiki, queryquery_vec, limit5, ).points for hit in hits: print(hit.id, hit.score, hit.payload[source])这里有个细节新版本推荐用query参数代替老版本的query_vector如果你在用旧客户端写成query_vectorquery_vec也能工作但建议升级到新版本。查询返回的score就是余弦相似度我一般会根据实际业务设定一个阈值低于阈值的直接丢弃避免给大模型塞入一堆不相关内容。4.3 从单机到分片先别急着踩油门很多教程会一上来就让你配置分片、副本、TLS我的建议是不要。数据量还没起来的时候分片只会增加不必要的复杂度。Qdrant的单机能力其实很强百万级向量、几千QPS的读取场景单容器完全扛得住。等真到了需要分布式扩展的时候再按照官方文档开启分片这个演进路径很平滑。有个和分片无关但容易忽略的经验写入大量数据后不要马上反复查询Qdrant的后台optimizer会在后台做segment合并和索引构建刚写入的数据可能需要一点时间才能真正进入优化的索引结构。如果你发现刚灌完数据时查询稍慢先等一下等Collection状态稳定了再压测结论才真实。5. 检索质量与速度的平衡点过滤、索引与重打分5.1 Payload过滤是知识库的“行级权限”基础知识库场景里有一个要求比“搜索快”更优先那就是“不能搜到不该看的内容”。Qdrant的Payload过滤就是解决这个问题的。比如我只允许当前用户看到HR部门公开的文档块可以在查询时带上Filter条件from qdrant_client.models import Filter, FieldCondition, MatchValue query_filter Filter( must[ FieldCondition(keydepartment, matchMatchValue(valueHR)), FieldCondition(keyis_public, matchMatchValue(valueTrue)), ] ) hits client.query_points( collection_namewiki, queryquery_vec, query_filterquery_filter, limit5, ).points这里的逻辑是先在满足Payload条件的文档块集合里做向量检索而不是在全部数据里检索完了再过滤。这个顺序非常关键它保证了过滤的严格性——如果先全局检索再过滤权限低的数据仍然参与了计算极端情况下检索结果可能因为卡在边界而漏掉合法内容但更危险的是文档内容被带回了服务器内存里属于不该有的暴露。5.2 HNSW参数与Payload索引的调优节奏Qdrant的HNSW有几个核心参数我调优时最常看的是m和ef_construct。m决定每个节点连接的邻居数量越大图越稠密召回率会好一点但内存占用也会涨ef_construct控制建图时的搜索宽度越大建图越慢但索引质量更高。默认值在多数场景下够用但如果你发现“看起来内容相关但查不到”可以在建Collection时显式调一下ef_construct。更常见的性能问题出在Payload字段上。如果你经常按某个字段过滤但没给它建索引Qdrant就只能在这个字段上做全扫描数据量一大查询延迟直接从几毫秒掉到秒级。解决办法是先分析高频查询条件再给必要字段建索引client.create_payload_index( collection_namewiki, field_namedepartment, field_schemakeyword, )注意不要给所有字段都建索引。字段索引同样有内存和磁盘成本建多了反而拖慢写入。我的习惯是只给“出现在线上Filter里的字段”建索引其它字段保持普通Payload就行。5.3 score_threshold与混合检索把召回结果再筛一遍HNSW返回的结果默认都附了一个score含义就是相似度分。实际项目里我会设置score_threshold把相似度明显偏低的结果直接过滤掉client.query_points( collection_namewiki, queryquery_vec, query_filterquery_filter, limit5, score_threshold0.5, )这个阈值怎么定没有通用标准我一般用一批真实问题跑一遍看“正确文档”最低的score是多少再留出余量。阈值定太高会漏定太低会导致大模型上下文被噪声填满回答质量明显下降。混合检索是另一个提升质量的办法。我项目里会在同一个Collection里配一个稠密向量和一个稀疏向量用于BM25式关键词匹配查询时两路并行召回再用RRF融合算法合并排序。纯语义检索对同义词友好但遇到精确型号、编号、人名这类实体时容易翻车关键词检索反过来。两者一结合知识库召回的稳定性会好很多。6. 把它接到AI智能体上企业知识库的完整落地方案6.1 一条链路串起来切块、向量化、入库、召回总有人问“AI智能体的企业知识库是存放在向量数据库中的吗”我的回答是核心的检索底座通常是向量数据库但知识库不等于向量数据库它是一条完整的数据流水线。我目前在生产环境跑通的链路是五步。第一步是文档解析把PDF、Word、网页转成干净的Markdown或纯文本去掉页眉页脚和导航噪音第二步是切块这里不要太死板地按固定512字切最好是按标题、段落、表格边界来切保持语义完整性第三步是向量化用embedding模型把每个块转成向量同时把来源、页码、更新时间、所属部门、文档权限等元数据写进Payload第四步是入库批量Upsert到Qdrant第五步是召回用户问题先经过权限过滤和向量检索再从库里取回Top K个文档块拼进Prompt最后交给大模型生成回答。这套链路里每一步都有各自的坑。切块切太大一个块里混了多个主题召回时得分被稀释切块切太小语义不完整大模型拿到的上下文支离破碎。我在项目里通常让技术组和业务组一起定规则文档类型不同切块策略就不同制度类文档按条款切FAQ按一问一答切产品手册按章节切。6.2 权限过滤必须在服务端拼好知识库最怕的不是检索结果不准而是把不该看的文档内容通过大模型泄露出去。权限过滤这件事一定不能依赖客户端传过来的Payload条件。客户端请求到服务端之后服务端要根据当前登录用户、角色、来源IP等信息在后端代码里统一拼出Filter再把Filter传给Qdrant。这样即使客户端恶意构造请求也绕不过服务端的权限拼接逻辑。具体的实现上我会在每条文档入库时把允许访问它的角色列表或部门列表写进Payload比如allowed_roles字段存一个数组。查询时把当前用户拥有的所有角色组成一个MatchAny条件from qdrant_client.models import MatchAny query_filter Filter( must[ FieldCondition( keyallowed_roles, matchMatchAny(any[HR, admin, employee]), ) ] )这里用MatchAny而不是多个MatchValue能保证用户只要命中其中一个角色就允许访问。这个字段一定要建索引否则每次查询都全量扫一遍数据量上来后必炸。6.3 为什么我不建议用Chroma直接当生产知识库我看到很多工程团队会在原型阶段用Chroma这个选择没问题我自己也这么干过。但生产环境要接知识库时我会谨慎评估。Chroma本地默认会在文件存储里维护一堆内部表包括集合表、向量表、元数据关联表它们在本地看就是“添加集合后产生了很多表”对原型脚本来说无所谓但到线上就有几个现实问题一是内部结构不透明出了问题很难排查二是权限过滤的表达能力和查询性能有限三是多副本、快照、集群扩展这些生产必备能力比较弱。Qdrant把Collection、Point、Payload的模型摆在明面上每个概念都有对应的API和状态可查出问题容易定位备份恢复有正式的Snapshot机制数据量大可以用分片横向扩。对知识库这种“持续写入、持续检索、随时可能要恢复数据”的场景我会选模型更清晰、运维边界更可控的方案。当然如果你只是想在本地跑通一个RAG演示Chroma依然是效率很高的选择两者面向的生产阶段确实不同。7. 生产环境里踩过的坑存储、模型版本、过滤索引与备份7.1 删除后磁盘没降别急着报警我在项目里遇到过一件怪事明明调用了delete接口删掉了大量文档磁盘占用却一点没降。排查到最后发现Qdrant的删除不会立刻释放物理空间删除动作只是给旧segment里的数据打了删除标记真正的空间回收要等后台optimizer完成segment合并之后才发生。如果你刚删完数据看了一眼磁盘发现没变化这是正常的别急着以为删错了。更稳的做法是用Collection的状态来判断等到状态正常、后台任务跑完再去确认磁盘空间。在数据变更频繁的场景里我建议定期观察Collection的分段情况必要时可以手动触发一次优化。这个机制本身不是缺陷它是为了平衡写入性能和空间复用理解了这一点就不会被表象吓到。7.2 换Embedding模型重建集合比你想象的更必要有一阵子我觉得检索效果不够好想把原来的bge-small模型换成效果更好的中文模型。想得很简单写个脚本把旧向量读出来重新编码再upsert回去。结果发现这一步并不轻松因为嵌入模型变了整个向量空间的分布都变了旧数据和新数据混在同一个Collection里距离计算就没有可比性查询结果会变得很飘。这件事给我的教训是embedding模型和Collection是一一对应的不要试图在同一个Collection里混用不同模型产出的向量。如果确实要升级模型正确做法是新建一个wiki_v2的Collection新数据灌入新库线上做灰度切换确认效果后再把旧库下线。这种版本化管理看起来多了一步但能避免线上检索质量突然劣化。7.3 没建Payload索引查询从毫秒崩到秒级一次典型的性能事故是这样的数据量还没到百万之前所有查询都很快某天在Filter里加了一个新的字段条件查询延迟突然从几毫秒涨到了好几秒。一开始我以为是服务负载问题排查了很久才发现新加的字段没有建Payload索引Qdrant只能对这个字段做全量扫描数据量一大就扛不住。给字段建索引之后延迟立刻回到了毫秒级。这个坑主要在于“小数据量时看不出差距”。数据少的时候扫描全量也很快你会误以为不建索引没关系等数据涨上来才暴露问题。所以我的建议是但凡知道这个字段将来会出现在Filter里就在建Collection之后立刻建好Payload索引不要拖到线上报警再补。补充索引是可行的但线上环境临时补索引内存和CPU会有明显波动。7.4 Snapshot备份的正确姿势知识库存的是有价值的企业内容备份不能靠“拷贝数据目录”这种野路子。Qdrant官方推荐的是Snapshot机制创建快照的REST接口很简单curl -X POST http://localhost:6333/collections/wiki/snapshots快照文件会生成到Qdrant容器内对应的目录因为我启动时挂载了宿主机volume所以快照实际也会落到宿主机上。恢复时创建一个同名的空Collection然后调用恢复接口curl -X POST http://localhost:6333/collections/wiki/snapshots/recover \ -H Content-Type: application/json \ -d {location: file:///qdrant/snapshots/wiki-xxx.snapshot}注意恢复接口里的location是容器视角的路径不是宿主机路径这个我一开始搞反过折腾了好一阵。还有个小习惯每次恢复完我都会用一条已知数据去查询一遍确认快照内容完整再让线上流量切过去。备份这种事平时看着平淡真出事的时候就是救命稻草。最后再分享一个我个人的习惯每隔一段时间我会把生产环境的快照拉下来在本地起一个临时Qdrant实例做恢复演练。这个操作不复杂但能保证真正需要恢复时你手里那份快照是可用的。踩过几次坑之后我对这类基础设施工具的态度就是功能少一点没关系关键进程出问题的时候你要能快速、确定地把它救回来。Qdrant这套快照机制在这件事上给我的确定性是很高的。
返回列表