跳到正文

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 万条以上,或多租户 / 高 QPSMilvus
只需要缓存命中,不需要持久召回Redis

边界 ​

  • RAG 治不了「知识库本身是错的」:文档质量、权限、时效是上游问题。
  • 切分决定上限:块切错了,换再贵的向量库也救不回来。
  • 向量库不是数据库:事务、join、强约束仍在业务库;Milvus 存的是可重建的检索副本。
  • 评测集会有分布偏差:线上真实问题要持续回流到评测集,否则指标好看但体验不变。
  • 厂商 benchmark 不可直接采信:数据集、维度、召回率、并发条件不同,结论可能相反。

讲清楚 ​

RAG 不是「接个向量库就完了」。离线要把文档切好、用固定版本的模型转向量、连同元数据写进去;在线要把查询改写、多路召回、融合、重排串起来,最后只把最相关的几条交给模型。任何一环出问题,最终回答都会错,而且下游补不回来。

选型先看约束:已经有 PostgreSQL 且数据不到千万,pgvector 最省事;已有 ES 且以混合检索为主,就用 ES;数据上亿、多租户、高 QPS,才值得上 Milvus 这样的专用集群。Redis 适合做缓存和状态,不适合当知识库的唯一存储。

最值得投入的不是换库,而是评测集和重排。有评测集才能判断改动是否有效,重排往往比换向量库带来更大的效果提升。

把一门技术讲透到既懂也会用