1. 为什么大模型项目突然离不开向量数据库
做了一段时间大模型应用开发的人,应该都有同感:模型本身的能力固然重要,但真正让应用落地、让回答靠谱的,往往是外围的工程设施。向量数据库就是其中最基础、也是最容易被低估的那一块。
我最初接触这三个名字——Chroma、Weaviate、Qdrant,是在做一个内部知识库问答系统的时候。当时的需求很朴素:把一堆产品文档灌进去,让大模型基于这些文档回答员工问题,而不是让它凭空发挥。如果直接把所有文档拼进Prompt,Token消耗大、上下文长度也不够,效果还很差。于是方案收敛到了RAG(检索增强生成):先把文档切块,转成向量存起来,用户在提问时检索相关片段,再交给大模型生成答案。
这么一搞,向量数据库就成了整个系统里绕不开的中间件。它要管的不只是"把向量存下来"这么简单,还涉及索引构建、相似度检索、元数据过滤、数据更新、持久化、部署方式、扩展能力。Chroma、Weaviate、Qdrant这三款是我实际落地过程中用到或深度调研过的方案,它们各自的定位差异很大,选错了后面重建索引、迁移数据都是代价。
这篇博文不打算做那种"三个数据库特性罗列"的说明书,而是从实际选型和使用角度,拆解它们各自能解决什么问题、适合什么场景、部署和代码层面有哪些坑,以及如果让你从头做一次RAG系统的存储层,应该怎么选、怎么搭、怎么排查问题。
先说结论:Chroma适合快速验证原型和轻量级场景,Weaviate适合需要可视化、多租户、模块化能力的中型项目,Qdrant在性能、过滤能力和部署灵活性上更稳,适合对检索质量和吞吐有要求的正经线上系统。但这个结论背后的细节,值得慢慢展开。
2. 大模型场景下的向量数据库到底解决什么问题
要想搞清楚工具选型,先得明白它要解决什么问题。
大模型本身是一个概率模型,训练数据里可能包含你的业务资料,也可能不包含。你问它"我们公司的报销流程是什么",它大概率会编一个听起来合理的流程,然后你拿去照做,就出事了。这就是所谓幻觉问题。RAG的解决思路,是让模型不去"背答案",而是先检索你准备好的资料片段,然后基于这些片段总结回答。这样回答不再是凭空生成的,而是有数据来源支撑的。
在这个链路里,向量数据库承担的核心职责有四个:
第一个是把非结构化数据变成可检索的索引。文档、PDF、表格、图片描述,经过Embedding模型转成几百上千维的浮点数组后,向量数据库负责把这些向量组织成高效的索引结构,让后续检索不是暴力全量遍历,而是近似最近邻(ANN)搜索。
第二个是支持语义相似度检索。传统的关键词搜索匹配的是字面,用户搜"电脑坏了怎么报修",关键词搜索可能因为"报修"两个字没对上而漏掉内容。向量检索匹配的是语义,即使表述不同,只要语义相近,也能召回相关片段。这是RAG优于传统搜索的关键原因。
第三个是元数据过滤。向量检索解决"找什么内容"的问题,但很多时候你还得限定范围。比如只查某个部门、某个时间段的文档,或者排除某些来源。这类过滤条件如果靠Embedding模型编码进向量里,既不精确也不可控。向量数据库需要提供向量相似度检索和结构化过滤的组合能力。
第四个是持久化和增量更新。数据不是一次性灌进去就完事,文档会更新、会删除、会新增。向量数据库要保证旧索引不失效、新数据能及时可见,同时查询性能不随数据量增加而断崖式下跌。
围绕这四点,Chroma、Weaviate、Qdrant给出了不同的设计取舍。把它们放在一起比,本质上是放在"轻量易用"和"高性能可扩展"这组矛盾里看,没有绝对的好坏,只有匹配不匹配。
3. 逐个拆解:Chroma、Weaviate、Qdrant的定位与核心特性
3.1 Chroma:本地优先的轻量级选手
Chroma是我最早用起来的那一个,原因很直接:它在Python生态里几乎是零成本启动。
安装就是一条pip命令,代码里直接嵌入使用,不需要单独起服务。这种模式和SQLite之于传统数据库非常像:你写程序的时候不要想着去运维一个数据库服务器,数据就是本地的文件,函数调用就是读写。
Chroma的核心设计面向的是"快速跑通"的场景。我当时的做法是:
import chromadb from chromadb.utils import embedding_functions client = chromadb.PersistentClient(path="./chroma_data") embedder = embedding_functions.DefaultEmbeddingFunction() collection = client.get_or_create_collection( name="product_docs", embedding_function=embedder, metadata={"hnsw:space": "cosine"} ) collection.upsert( documents=["文档内容切片1", "文档内容切片2"], ids=["doc_001", "doc_002"], metadatas=[{"source": "handbook", "dept": "IT"}, {"source": "handbook", "dept": "IT"}] ) results = collection.query( query_texts=["电脑坏了怎么办"], n_results=3, where={"dept": "IT"} )这段代码基本覆盖了Chroma最常见的用法:创建持久化客户端、自动Embedding、写入文档和元数据、带条件查询。从拿到数据到跑出检索结果,十几分钟就能完成。
Chroma的亮点在于快,但也意味着它在某些维度上做了取舍。默认的Embedding函数调用的是托管服务,数据量大了以后成本不可忽视,而且网络一抖就影响写入速度。自带的HNSW索引参数只能在collection创建时通过metadata配置,后期改动需要重建。分布式部署和水平扩展基本不在它的能力范围内,单机数据量上了几百万条,写入和查询性能就会有明显体感。
所以Chroma正确的使用边界是:原型验证、教学演示、个人项目、中小规模内部工具。如果你的系统还在探索阶段,不确定检索策略是不是合理,用Chroma跑通全流程是性价比最高的选择。但如果你明确知道数据量会到千万级别、吞吐有硬性要求,它就不该是你线上的第一选择。
3.2 Weaviate:模块化设计的中型方案
Weaviate是一款独立运行的向量数据库,用Go语言写的,以模块化架构和丰富的集成能力为主要卖点。
它和我前面说的"哪个快用哪个"思路完全不同。Weaviate上来就是一个完整的服务端程序,需要通过Docker或者官方部署脚本启动。它自带一个WebUI,可以直接在浏览器里查看和管理数据,这在调试和演示时非常有用。
Weaviate最大的特点是模块化。它允许你配置不同的向量化模块,包括外部的Embedding服务,也可以配置为不使用内置向量化、在写入时直接传入向量。这种灵活性在某些场景下是硬需求:比如你的生产环境里已经有了一套统一的Embedding服务,或者你用了私有化部署的开源Embedding模型,Weaviate可以做到"只管存储和检索,不参与向量化"。
另一个让它在中型项目中受欢迎的特性,是良好的多租户支持。多个业务线可以共用一套Weaviate服务,用Class作为逻辑隔离单元,各自管理自己的数据,互不干扰。这对团队的协作方式很友好,不需要为每个新项目单独部署一套数据库。
我用Weaviate做过的测试大致是这样:
import weaviate import weaviate.classes as wvc client = weaviate.connect_to_local() client.collections.create( name="Article", vectorizer_config=wvc.config.Configure.Vectorizer.text2vec_transformers(), properties=[ wvc.config.Property(name="title", data_type=wvc.config.DataType.TEXT), wvc.config.Property(name="content", data_type=wvc.config.DataType.TEXT), wvc.config.Property(name="category", data_type=wvc.config.DataType.TEXT), ] ) collection = client.collections.get("Article") collection.data.insert_many([ { "title": "报销流程更新说明", "content": "从本月起,报销审批统一走线上系统...", "category": "finance" } ]) response = collection.query.near_text( query="报销流程", limit=3, filters=wvc.query.Filter.by_property("category").equal("finance") )Weaviate的GraphQL查询方式在这几年变得越来越友好,Python客户端也封装得比较完善。不过它的默认向量化模块依赖外部的模型推理服务,比如text2vec-transformers需要在Docker里额外起一个模型容器,资源占用不小。在标注化模块依赖外部服务的场景下,部署复杂度会明显上升。如果你所在团队的运维能力一般,或者部署环境受限,这一点能提前劝退一批人。
3.3 Qdrant:高性能与强过滤的代表
Qdrant用Rust写的,命名来源于"quadrant"(象限、坐标区域),目标是打造一个可靠的嵌入式向量搜索引擎。它既支持独立服务模式,也支持嵌入式模式,语言生态覆盖得很全面。
我第一次接触Qdrant,是因为一个项目需要在大量带有复杂过滤条件的记录里做向量检索。比如按商品类目过滤后再做相似度搜索,或者按用户ID精确圈定数据范围再搜索。这类需求在Chroma里要么写法别扭,要么过滤性能堪忧;在Weaviate里能实现但部署偏重。Qdrant在过滤这块做得很扎实,它内部把向量索引和Payload(元数据)索引作为两套独立的索引结构,查询时先通过Payload索引圈定候选集,再在候选集内做向量匹配,避免了全量过滤再计算相似度的性能灾难。
Qdrant的另一个优势是API设计得非常干净。RESTful接口配合Python客户端,几乎没什么学习成本,文档也写得算同行里比较用心的。
from qdrant_client import QdrantClient, models client = QdrantClient(host="localhost", port=6333) client.create_collection( collection_name="products", vectors_config=models.VectorParams(size=768, distance=models.Distance.COSINE), ) client.upsert( collection_name="products", points=[ models.PointStruct( id=1, vector=[0.1] * 768, payload={"category": "electronics", "price": 299.0} ), models.PointStruct( id=2, vector=[0.2] * 768, payload={"category": "books", "price": 59.0} ) ] ) client.query_points( collection_name="products", query=[0.15] * 768, query_filter=models.Filter( must=[ models.FieldCondition( key="category", match=models.MatchValue(value="electronics") ) ] ), limit=5 ).pointsQdrant在构建索引时支持按需选择是否启用HNSW,也提供了内存映射模式,让大数据量下的内存占用更可控。它的数据持久化做得也比较踏实,写盘、WAL(Write-Ahead Log)机制都齐备,关键数据的安全保障能力比很多轻量方案高一档。
代价是Qdrant的中文社区资料相对少一些,遇到问题去官方仓库和文档里翻答案的路径比较长。它的某些高级能力,比如分片集群、多节点配置,需要一定的学习和试错成本。不过一旦配置好,它的稳定性给我留下的印象确实深。
4. 三者的横向对比与选型决策
很多人在选型时会陷入一个误区:试图找一个"综合最强"的向量数据库。实际上,这三款产品背后代表的三种设计哲学差异很大,选型的关键是匹配你的场景。
我从六个维度整理了一个对比表,方便直接对照:
| 维度 | Chroma | Weaviate | Qdrant |
|---|---|---|---|
| 部署方式 | 嵌入式,进程内使用 | 独立服务,Docker/Linux部署 | 独立服务或嵌入式,模式可切换 |
| 数据规模 | 小到中(百万级以下) | 中到大 | 中到大,支持分布式扩展 |
| 开发语言生态 | Python 为主 | Python/Go/Java/JS 等 | Python/Rust/Go/Java/JS 等 |
| 元数据过滤 | 支持,能力较弱 | 支持,查询语法灵活 | 支持,Payload 索引能力强 |
| 向量化集成 | 内置默认模型/自定义 | 模块化外接多个模型 | 自带无模型,仅靠外部向量 |
| 运维复杂程度 | 极低 | 中高 | 中 |
基于这个表,我能给出几条经过实践检验的选型建议:
第一,先看部署约束。如果你只有一个Python脚本,目标是最快速度验证RAG链路通不通,Chroma是无脑选择。不需要Docker,不需要维护端口,本地文件就是数据库。如果有人建议你用Qdrant做原型验证,虽然也能做,但那是在给自己增加不必要的运维工作量。
第二,看数据增长预期。假设你是一个团队内部的知识库,初期十万条文档切片,半年后会到三百万,这个增长曲线就应该让你从一开始就避开Chroma。偶尔迁移一次数据还能忍,反复因为性能重建索引就不值得了。Qdrant和Weaviate更合适,其中如果检索条件复杂,Qdrant的过滤优势会进一步体现。
第三,看团队技能栈。Chroma和Qdrant的Python客户端都足够顺手,Weaviate的GraphQL风格查询适合有API设计偏好的团队,但其部署复杂度需要团队成员多少懂一点Docker和容器编排,对纯业务出身的开发者来说,操作门槛会直观一些。
第四,看是否需要模块化集成。Weaviate能外接不同的Embedding服务和生成模块,在构建一个包含多模型能力的中台型产品时,这种设计可扩展性是有价值的。Qdrant则更纯粹,它只管向量存储和检索,向量怎么来是你的自由。Chroma虽说可以换Embedding函数,但整个架构没有围绕"模块化"来做深。
选型这件事,说到底是在给定约束下找一个"够用且不那么痛苦"的方案。如果你还拿不准,我个人建议是:原型期用Chroma,生产期如果是单机场景且过滤简单,Qdrant或者Weaviate都可以;如果明确有分布式需求,优先考虑Qdrant的集群能力。
5. 从零到一搭建一个可用的RAG检索实例
理论聊得再多,不如亲手跑一遍。下面我用一套模拟的数据,分别演示三款数据库在检索链路里的实际接入方式。目标是让大家看到,"在代码层面它们到底有什么差别"。
5.1 模拟数据与统一Embedding逻辑
假定我们要做一个企业内部规章制度问答系统,文档内容涉及考勤、报销、信息安全三个类别。不考虑真实数据的版权和隐私问题,我用三句模拟文本做演示:
- "考勤管理规定:员工每日上下班需打卡,迟到三次计一次旷工。"
- "报销流程指南:报销单需经部门负责人审批后提交财务。"
- "信息安全规范:员工电脑必须安装杀毒软件,禁止使用弱密码。"
这里选用一个固定的Embedding维度,比如768维,实际上会用模型把文本转成向量。为了演示流程简单,先固定为一个小型向量,重点是展示API调用方式,而不是不同模型的效果对比。
5.2 用Chroma跑通最小链路
import chromadb client = chromadb.PersistentClient(path="./rag_chroma") collection = client.get_or_create_collection("rules") # 写入模拟数据,向量需要自行计算后传入 collection.upsert( ids=["rule_001", "rule_002", "rule_003"], embeddings=[ [0.01] * 768, [0.02] * 768, [0.03] * 768 ], documents=[ "考勤管理规定:员工每日上下班需打卡,迟到三次计一次旷工。", "报销流程指南:报销单需经部门负责人审批后提交财务。", "信息安全规范:员工电脑必须安装杀毒软件,禁止使用弱密码。" ], metadatas=[ {"category": "attendance"}, {"category": "finance"}, {"category": "security"} ] ) res = collection.query( query_embeddings=[[0.015] * 768], n_results=2, where={"category": {"$in": ["attendance", "finance"]}} )在真实项目中,Embedding向量由模型生成,Chroma的默认函数会自动处理文本到向量的转换。我这里直接传入向量的写法,是为了让大家看清:Chroma的存储单元是(id, embedding, document, metadata)四元组,检索时既可以用文本,也可以用已经算好的向量。
提示:Chroma的
where过滤语法在版本演进中有过调整,老版本用{"category": "attendance"},新版本推荐写完整的操作符方式。遇到语法报错时,优先去官方文档确认当前版本的过滤表达式格式。
5.3 用Weaviate实现模块化检索
Weaviate的接入需要先把服务跑起来。以Docker为例子,默认的本地开发命令是:
docker run -d --name weaviate \ -p 8080:8080 \ -e QUERY_DEFAULTS_LIMIT=20 \ -e AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true \ -e PERSISTENCE_DATA_PATH='/var/lib/weaviate' \ -e DEFAULT_VECTORIZER_MODULE=none \ -e CLUSTER_HOSTNAME='node1' \ semitechnologies/weaviate这里把DEFAULT_VECTORIZER_MODULE设为none,意思是外部传入向量,不由Weaviate内置模块做向量化。这是生产环境更可控的一种方式。
import weaviate import weaviate.classes as wvc client = weaviate.connect_to_local() client.collections.create( name="Rule", vectorizer_config=wvc.config.Configure.Vectorizer.none(), properties=[ wvc.config.Property(name="content", data_type=wvc.config.DataType.TEXT), wvc.config.Property(name="category", data_type=wvc.config.DataType.TEXT), ] ) col = client.collections.get("Rule") col.data.insert( properties={"content": "考勤管理规定:员工每日上下班需打卡。", "category": "attendance"}, vector=[0.01] * 768 )Weaviate的写法里,向量和属性是分开的:properties里放的是元数据字段,vector参数单独传入。检索时如果选择了near_vector,就是纯向量检索;如果选了near_text,则是走文本向量化再检索。
Weaviate的"模块化"在这里体现得很明显:数据库本身不关心你的向量是谁生成的,它只负责管好这些向量和对应的属性。这意味着你可以把Embedding模型的升级、切换、A/B测试都放到外层来做,数据库层保持稳定。
5.4 用Qdrant实现精准过滤检索
Qdrant的代码风格在三者中最为"结构化"。创建集合时需要声明向量维度、距离度量方式,写入时用PointStruct包装,查询时用Filter构建过滤条件。
from qdrant_client import QdrantClient, models client = QdrantClient(host="localhost", port=6333) client.recreate_collection( collection_name="rules", vectors_config=models.VectorParams(size=768, distance=models.Distance.COSINE), optimizers_config=models.OptimizersConfigDiff(default_segment_number=2) ) client.upsert( collection_name="rules", points=[ models.PointStruct(id=1, vector=[0.01] * 768, payload={"content": "考勤管理规定:员工每日上下班需打卡。", "category": "attendance"}), models.PointStruct(id=2, vector=[0.02] * 768, payload={"content": "报销流程指南:报销单需经部门负责人审批后提交财务。", "category": "finance"}), models.PointStruct(id=3, vector=[0.03] * 768, payload={"content": "信息安全规范:员工电脑必须安装杀毒软件。", "category": "security"}) ] ) results = client.query_points( collection_name="rules", query=[0.015] * 768, query_filter=models.Filter( must=[ models.FieldCondition(key="category", match=models.MatchAny(any=["attendance", "finance"])) ] ), limit=2, with_payload=True ).pointsQdrant引人注意的细节是,它在创建集合时就允许你规划优化器参数,比如default_segment_number这个值。数据量中等时,默认的分段数量基本够用。如果写入的数据会频繁更新,可以适当调大段数量来提升写入并发能力,但如果查询远多于写入,段数量过多反而可能拖慢查询。这类参数要在理解原理之后再调,不建议照抄别人的配置。
这里我用Qdrant做了一次带category过滤的检索,实际返回结果会聚焦在考勤和报销两类上,不会因为语义相似把信息安全的文本也带出来。
6. 真实场景里踩过的坑和排查记录
实际动手以后,我发现很多问题不是文档里写清楚的,通常要等代码跑挂了才意识到。
6.1 维度不一致:Embedding模型升级导致的数据不兼容
这是最容易踩也是最伤的一个坑。
有一次我给一个项目升级Embedding模型,从A模型换到B模型,向量维度从384变成768。我忘了清理旧数据,直接往同一个collection里写入新向量,结果查询时数据库直接报错。因为一个集合里的向量维度必须是统一的,HNSW索引在建的时候已经按照固定维度构建了。
排查方式其实不难:如果你发现写入时报Vector dimension mismatch或类似错误,不要怀疑数据库坏了,先检查你当前生成向量的模型维度跟创建集合时声明的是否一致。更稳妥的方式是,在代码里做一个向量维度校验:
def check_dimension(client, collection_name, expected_dim): info = client.get_collection(collection_name) actual_dim = info.config.params.vectors.size assert actual_dim == expected_dim, f"维度不一致: {actual_dim} != {expected_dim}"6.2 距离度量的选择影响了检索结果
Chroma默认的集合距离度量是squared_l2,Qdrant推荐cosine,Weaviate内建多种。很多人不管这些细节,直接默认配置跑起来,最后发现检索结果跟预期差得远。
我自己的经验是,对于文本场景,cosine比L2更稳定。因为文本Embedding向量通常关注方向而非模长,而L2距离对向量的模长变化敏感。举个直观的例子:两句话语义完全相同,只是其中一句用词更丰富导致向量模长更大,L2距离可能把它们算得很远,cosine相似度则几乎不受影响。
如果你用的是OpenAI或者其他通用Embedding API,这个建议可以直接套用。但如果你用的是一个自制的高维向量,数据特性完全不同,那建议先用一小批样例分别测试cosine和dot,看哪个指标更符合业务判断。
6.3 HNSW索引参数没调,数据量上去后查询变慢
HNSW算法有一个ef_construct参数,控制建索引时考虑的候选数;还有一个搜索时的ef参数,控制查询的探索范围。
默认值通常偏保守,在数据量小的时候没问题。但当数据量到了几十万条,你会发现查一下的延迟从几毫秒涨到了几十毫秒。这时候不要急着遣责数据库,先确认索引参数是不是还停留在默认值。
以Qdrant为例,建集合时可以用hnsw_config设置:
models.HnswConfigDiff( m=16, ef_construct=100, full_scan_threshold=10000 )m控制每个节点的最大连接数,越大召回率越高但内存占用也越大;ef_construct控制索引构建质量,太大建索引时间剧增。建议从m=16, ef_construct=100起步,实测后再按数据分布调整。
6.4 持久化配置不当导致数据丢失
Chroma本地模式如果不指定PersistentClient,默认只是在内存里跑,程序一退出数据全没了。Weaviate不配置持久化路径也一样。这是一个看似基础但实际很容易犯的错,尤其在快速原型阶段。
我现在习惯在任何项目的第一天就明确数据持久化路径和备份策略。特别是Weaviate的PERSISTENCE_DATA_PATH、Chroma的path参数、Qdrant的storage目录,这三处配置里,谁先忽视了,谁的数据就有可能在一次重启后消失。
6.5 集合数量规划失当
有些团队习惯为每个业务线各建一个collection甚至一个数据库实例,这在数据量少时没什么问题,但到了几十个collection时,管理成本和内存开销都会上升。Qdrant每个collection都有独立的索引和段结构,collection越多,内存中的元数据越多。
合理的方式是评估一下业务是否需要物理隔离。如果只是逻辑隔离,没必要拆collection,用payload字段区分即可。只有涉及权限隔离、数据生命周期管理差异巨大的场景,才值得拆。
6.6 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 写入时维度报错 | Embedding模型切换导致向量维度变化 | 重建集合或迁移数据,统一维度 |
| 检索结果明显不合理 | 距离度量不匹配文本场景 | 切换为 cosine 或 dot |
| 数据量增大后查询变慢 | HNSW参数未调优 | 调ef_construct和m |
| 重启后数据消失 | 持久化路径未配置 | 配置PersistentClient/PERSISTENCE_DATA_PATH/storage 路径 |
| 过滤查询性能差 | Payload索引未启用或字段类型错误 | 检查索引配置,确认字段类型正确 |
| 多个业务线数据互相干扰 | 集合/租户隔离规划不清 | 评估是否拆 collection 或加租户字段 |
7. 从检索质量到成本:几个容易被忽略的工程视角
如果只关注功能跑通,这些数据库的差别不会太明显。但在生产环境里,还有几个工程视角值得展开讲讲。
第一个是Embedding成本。很多人在评估向量数据库时只盯着数据库本身的性能,却忽略了Embedding环节的消耗。以Chroma默认的托管Embedding服务为例,虽然调用简单,但百万级的文档片段转换下来,API费用是个不小的数字。更可控的做法是提前批量生成好Embedding,离线存好,写入向量数据库时直接传入向量。这也意味着数据库选型要优先考虑支持"外部向量直连"的模式,Qdrant和Weaviate都能满足,Chroma稍弱但也能自定义Embedding函数。
第二个是向量数据本身的存储开销。假设Embedding维度是768,每条向量至少占用768 * 4 字节 = 3KB(float32)。100万条原始数据,光裸向量就是3GB,加上HNSW索引的开销(通常比裸向量多出30%-50%),5GB左右是起步。这还不算元数据存储。如果你的服务器内存只有8GB,跑百万级数据会非常吃力。此时要么降维,比如用更小的Embedding模型;要么选择支持内存映射的数据库,用磁盘空间换内存占用。
第三个是数据更新的时效性。文档改了,旧切片要不要删?新切片什么时候可见?这涉及数据库的写入一致性和索引刷新策略。Qdrant默认采用写后异步构建索引的方式,新数据写入后有短暂的可见性延迟,但如果配置同步模式或强制刷新,延迟可以降到很低。Chroma的本地写入则几乎是实时的,但也因此在高并发写入时更容易阻塞。Weaviate通过定期合并Segment来维持性能,并发写入时建议开启批处理。
第四个是检索策略的演进空间。向量检索只是RAG链路里的一环,它前面可能还有关键词兜底、重排序、多路召回。向量数据库是否方便你配合这些策略,决定了后续优化是否顺畅。Qdrant支持用Query混合检索,可以在一次调用里做多个向量的对比和融合;Weaviate也有混合搜索能力。如果你的产品规划了从"纯向量"升级到"混合检索",这些特性能让你少改一层代码。
8. 最后补充一点个人体会
我做过的项目里,凡是稳定跑在线上、迭代顺畅的系统,都有一个共性:数据层不折腾。向量数据库这种基础设施,别看它只是存储和检索,一旦上线,迁移成本远高于初期选型的成本。所以我会在项目开始之前,花至少一个下午把数据规模、部署环境、检索过滤复杂度这三个问题彻底想清楚,再做出选择。
具体到这三个库,我现在的工作流基本固定下来了。探索新思路、写法、算法策略时,用Chroma快速跑实验;面对需要交付客户演示、支持多用户使用的项目时,直接用Weaviate或Qdrant做底层存储。如果让我在Weaviate和Qdrant之间再深选一层,我倾向把"过滤是否复杂、单一还是长期服务"作为决定性分叉点,过滤复杂且生命周期长的,选Qdrant;重视模块化集成和可视化界面,团队且有时间和意愿在中间层封装一层图式查询的,选Weaviate。毕竟这两个库都不差,真正拉开差距的是使用场景和操作习惯的匹配。
如果你正在为你的大模型项目选向量数据库,我的建议是不必被"哪个更流行"或"哪个更高级"裹挟。把上面那些问题对着自己的项目逐一回答一遍,自然就会有答案。这篇内容里讲的都是我用过的路径和踩过的坑,希望能帮你少走几步弯路。