主题
RAG 落地与向量库选型
一句话:RAG 的效果上限由「切分、embedding、召回、重排」共同决定,向量库只是其中一环;选型的核心不是谁更强,而是数据规模、过滤方式、团队运维能力三者的匹配。
适用版本:Milvus 2.6.x;对比项的结论标注了前提,跨版本可能变化。
为什么需要
大模型不知道你的私有文档,直接问会幻觉;把整篇文档塞进上下文,成本和长度都撑不住。
RAG(Retrieval-Augmented Generation)的做法是:先从知识库召回最相关的片段,再让模型基于片段回答。它把「记住所有知识」变成「按需查资料」。
核心机制
一条完整的链路
每一环都会决定最终效果,而且上游的错误下游补不回来。
切分:最容易被低估的一环
- 按结构切:标题、段落、表格、代码块,比固定字符数切更保语义。
- 块大小:中文常用 300–800 字,英文 256–1024 token;太小丢上下文,太大稀释相似度。
- 重叠:10%–15%,防止答案正好被切断。
- 父子块:用小块检索(相似度高),命中后取父块(上下文完整)喂给模型。
- 元数据:来源、章节、时间、权限,过滤和引用都靠它。
embedding:必须成对且版本一致
- 写入和查询必须用同一个模型、同一版本、同一归一化方式。
- 换模型等于换向量空间,旧向量全部失效,必须全量重建。
- 归一化后余弦与内积等价;不归一化时两者排序可能不同,metric 要和模型用法一致。
- 多语言语料要确认模型跨语言对齐能力,否则中文查询召不回英文文档。
召回与重排
- 每路先取 30–50 条,融合后再取 20–50 条,重排后留 5–10 条给模型。
- 重排(rerank)通常比换向量库更能提升效果:它是 query-document 交叉编码,精度高但慢,只跑在小候选集上。
- topK 不是越大越好:噪声片段会干扰生成,还会挤占上下文预算。
增量同步与重建
- 业务库变更通过 CDC / outbox 推到向量库,用主键做幂等 upsert。
- 删除要显式传播;Milvus 的删除是标记删除,长期要 compaction。
- 重建用别名切换:新 collection 灌完、评测通过后,把 alias 指过去,旧 collection 再释放。
python
client.create_alias(collection_name="kb_v2", alias="kb")
client.alter_alias(collection_name="kb_v2", alias="kb") # 原子切换评测:没有评测就没有优化
建一个 100–300 条的评测集,每条包含问题、标准答案、应命中的文档 ID。
| 指标 | 看什么 |
|---|---|
| Recall@k | 正确文档有没有进候选 |
| MRR / nDCG@10 | 正确文档排得够不够前 |
| 忠实度 | 回答是否只依据召回内容 |
| 拒答率 | 无答案时是否会编 |
每次改切分、embedding、索引参数,都跑同一套评测,否则无法判断是变好还是变坏。
最小上手
python
from openai import OpenAI
from pymilvus import MilvusClient, DataType
oa = OpenAI(base_url="https://api.openai.com/v1", api_key="...")
def embed(texts):
r = oa.embeddings.create(model="text-embedding-3-small", input=texts)
return [d.embedding for d in r.data]
client = MilvusClient(uri="http://localhost:19530")
schema = client.create_schema(auto_id=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("text", DataType.VARCHAR, max_length=4096)
schema.add_field("vec", DataType.FLOAT_VECTOR, dim=1536)
idx = client.prepare_index_params()
idx.add_index(field_name="vec", index_type="HNSW", metric_type="COSINE",
params={"M": 16, "efConstruction": 200})
client.create_collection("kb", schema=schema, index_params=idx)
chunks = ["退款需在签收后七天内申请。", "物流一般三到五天送达。"]
client.insert("kb", [{"id": i, "text": t, "vec": v}
for i, (t, v) in enumerate(zip(chunks, embed(chunks)))])
hits = client.search("kb", data=embed(["多久能退款"]), limit=2,
output_fields=["text"],
search_params={"metric_type": "COSINE",
"params": {"ef": 64}})
context = "\n".join(h["entity"]["text"] for h in hits[0])
print(context) # 把 context 拼进 prompt生产动作
- 先做评测集,再调参:没有 gold set,所有优化都是感觉。
- 固定 embedding 版本:把模型名和版本写进配置与 collection 元数据,升级走重建流程。
- 写入用批量 + 背压:embedding 服务有限流,向量库有 WAL 容量,两侧都要限速。
- 监控召回质量而不只是延迟:抽样统计空结果率、低分率、用户点踩率。
- 配额与成本:embedding 按 token 计费,重建一次全量知识库的成本要提前算;向量库成本主要是内存。
- 多租户:租户多时用 partition key,查询入口统一注入租户过滤,见 03 篇。
选型
| 方案 | 优势 | 更适合 | 主要代价 |
|---|---|---|---|
| Milvus | 索引类型全、十亿级、多租户、GPU | 数据量大、QPS 高、需要多种索引与隔离 | 组件多,运维重 |
| pgvector | 与业务数据同库、事务一致、运维简单 | 已有 PostgreSQL、千万级以内、要 join 和事务 | 规模上限受单库限制,索引调优空间小 |
| Elasticsearch | 全文 + 向量 + 成熟过滤聚合 | 已有 ES、混合检索为主、日志/搜索团队 | 向量规模大时内存与成本高 |
| Redis | 亚毫秒延迟、天然缓存 | 语义缓存、会话状态、小规模热数据 | 不是知识库主存储,容量与持久化受限 |
| MongoDB Atlas | 文档与向量同库、开发快 | 数据已在 Atlas、团队不想加组件 | 向量检索与 Atlas 绑定,规模与调优受限 |
| Qdrant / Weaviate | 部署轻、过滤能力强 | 中等规模、想自建但不想上重型集群 | 超大规模与生态不如 Milvus |
规模分界线只是经验值,以压测为准:
| 条件 | 选择 |
|---|---|
| 500 万条以内,且已有 PostgreSQL | 先用 pgvector |
| 已有 ES,且混合检索是主诉求 | 先用 ES |
| 1000 万条以上,或多租户 / 高 QPS | Milvus |
| 只需要缓存命中,不需要持久召回 | Redis |
边界
- RAG 治不了「知识库本身是错的」:文档质量、权限、时效是上游问题。
- 切分决定上限:块切错了,换再贵的向量库也救不回来。
- 向量库不是数据库:事务、join、强约束仍在业务库;Milvus 存的是可重建的检索副本。
- 评测集会有分布偏差:线上真实问题要持续回流到评测集,否则指标好看但体验不变。
- 厂商 benchmark 不可直接采信:数据集、维度、召回率、并发条件不同,结论可能相反。
讲清楚
RAG 不是「接个向量库就完了」。离线要把文档切好、用固定版本的模型转向量、连同元数据写进去;在线要把查询改写、多路召回、融合、重排串起来,最后只把最相关的几条交给模型。任何一环出问题,最终回答都会错,而且下游补不回来。
选型先看约束:已经有 PostgreSQL 且数据不到千万,pgvector 最省事;已有 ES 且以混合检索为主,就用 ES;数据上亿、多租户、高 QPS,才值得上 Milvus 这样的专用集群。Redis 适合做缓存和状态,不适合当知识库的唯一存储。
最值得投入的不是换库,而是评测集和重排。有评测集才能判断改动是否有效,重排往往比换向量库带来更大的效果提升。