大模型很聪明,但它不知道你公司内部的文档写了什么,也不知道昨天刚发布的公告。RAG(Retrieval-Augmented Generation,检索增强生成)就是为解决这个问题而生的工程范式。

这篇文章不做概念科普式的泛泛而谈,而是按工程落地的顺序讲四件事:RAG 为什么存在它在生产环境里会怎么坏以及怎么修如何量化地知道它到底好不好作为其基座的 Milvus 该怎么选索引

一、RAG 的技术背景

1.1 大模型的四个先天缺陷

缺陷 具体表现 典型翻车场景
知识截止 训练数据有截止日期,之后的事一无所知 问「上个月发布的新版本有什么变化」→ 编造或拒答
幻觉 对不知道的事倾向于「合理编造」而非承认不知道 编出不存在的 API、法条、论文引用
私有知识缺失 训练语料不含企业内部文档、代码库、工单 问内部系统怎么申请权限 → 一本正经胡说
上下文成本与容量 窗口有限;且长上下文存在「迷失在中间」效应 把 100 页手册全塞进去,准确率反而下降

最后一条值得展开:Liu et al. 的 Lost in the Middle 实验表明,当关键信息位于长上下文的中部时,模型召回它的准确率显著低于位于开头或结尾时——也就是说,「把相关文档全塞进 prompt」并不是一个可靠的方案,位置和噪声都会伤害效果。

1.2 RAG 与微调、长上下文的取舍

这三者不是互相替代,而是各有边界。工程上要先判断「知识问题」还是「能力问题」:

维度 RAG 微调(SFT / LoRA) 长上下文硬塞
知识更新速度 改知识库即生效,分钟级 需重新训练,天级 改 prompt 即生效
成本结构 存储 + 检索算力,低 GPU 训练 + 标注数据,高 token 费用随长度线性增长
可溯源 强,可精确给出引用段落 弱,知识混在参数里
幻觉抑制 中~强(有据可依) 弱(仍可能编造)
适合场景 知识密集、频繁更新的问答 固定风格 / 输出格式 / 领域语感 单篇长文档的一次性分析

一句话结论:微调改变的是「怎么说」,RAG 补充的是「说什么」。 绝大多数企业知识问答场景,RAG 的性价比远高于微调。

1.3 RAG 的原理

RAG 最早由 Lewis et al. 在 2020 年提出,本质是把生成建模为对隐变量(检索到的文档)的边缘化:

1
2
p(y | x) = Σ_z  p(z | x) · p(y | x, z)
└─检索器─┘ └──生成器──┘
  • x 是问题,z 是检索到的文档片段,y 是答案
  • 检索器 p(z|x) 负责从海量知识中挑出可能相关的片段
  • 生成器 p(y|x,z) 负责基于这些片段组织答案

工程上我们并不真的做这个求和,而是用 Top-K 检索近似它——这也解释了为什么 RAG 的效果对「检索质量」极度敏感:检索错了,生成器再强也只能基于错误材料作答。

1.4 三代演进

阶段 核心思路 典型手段 局限
Naive RAG 索引 → 检索 → 生成,一条直线 固定长度切块、单路向量检索、Top-K 拼 prompt 切块割裂语义、召回不准、无优化手段
Advanced RAG 在检索前后加优化环节 查询改写 / 扩展、混合检索、Rerank、上下文压缩 链路变长,调参成本高
Modular RAG 模块化可编排,按问题类型路由 路由、记忆、多跳检索、自我反思、工具调用 架构复杂,需要评测体系支撑

1.5 典型架构

各环节的职责与常见坑:

环节 职责 常见失败模式
解析 把异构文档变成纯文本 表格/公式被拍平、扫描件无 OCR
分块 切成语义完整的检索单元 切在句子中间、块过大稀释语义
向量化 把文本映射到语义空间 模型与语种/领域不匹配
索引存储 支持高效相似度检索 索引选型不当导致慢或不准
检索 召回相关片段 只做单路向量检索,专有名词召回差
重排 精排候选,提升精度 缺失,导致噪声挤占上下文
生成 基于证据组织答案 无视证据自由发挥、引用错位

二、RAG 常见问题与解决方案

2.1 问题全景

按链路把问题分成四段,先建立一张「排障地图」:

阶段 典型问题 症状 首选手段
检索前 分块不合理 答案被切断、检索不到 语义/递归分块 + 重叠 + 小块检索大块生成
元数据缺失 无法按部门/时间过滤 入库时补齐标量字段
Embedding 不匹配 语义相近却检索不到 换多语/领域模型,或走混合检索
检索中 召回不足 正确答案不在 Top-K 混合检索、多路查询、提高召回数
专有名词失效 产品名/编号搜不到 BM25 关键词通道 + 同义词词典
排序不准 相关文档排在很后面 Cross-Encoder Rerank
检索后 上下文过载 噪声挤掉关键信息 上下文压缩、去重、重排后截断
位置偏差 关键信息在中部被忽略 把最相关的放首尾
生成 幻觉 答案无据可依 强制引用、无依据则拒答
不忠实 与原文矛盾 降低温度、few-shot 约束、事实性校验

2.2 分块:最容易被低估的一步

分块策略直接决定检索上限——块切错了,后面所有优化都是在错误的候选集上做精排

策略 做法 适用 代价
固定长度 按 token/字符数硬切 快速验证 语义割裂严重
递归切分 按 段落→句子→词 逐级降级切分 通用文档(推荐默认) 需调 chunk_size
语义切分 用相邻句向量相似度找断点 长论述、技术文档 建库慢
结构化切分 按 Markdown 标题 / HTML 层级 结构化文档(最优) 依赖文档规范
父子块 小块检索、大块(或父段)喂给 LLM 几乎所有场景 需额外存储映射

实践参数建议(以中文技术文档为例):

  • chunk_size300~600 tokenoverlap10%~15%
  • 每个块必须携带元数据:来源文件、章节标题、更新时间、权限标签
  • 在块前拼接「文档标题 + 章节名」再向量化,可显著提升短块的语义完整性

一个常被忽略的技巧是父子块(Small-to-Big):用 128~256 token 的小块做检索以保证精度,命中后返回其所属的父段落(或前后各扩一块)给 LLM 提供完整上下文。这样同时缓解了「小块语义不全」和「大块检索不准」的矛盾。

2.3 检索:从单路向量到混合检索

纯向量检索的软肋是精确匹配:产品型号 Milvus 2.4.9、错误码 ERR_0x5A、人名缩写,向量模型往往抓不住。而 BM25 恰好擅长这些。

混合检索(Hybrid Search) 把两路结果融合,主流融合算法:

融合方式 公式 / 做法 特点
RRF(倒数排名融合) score = Σ 1 / (k + rank_i),k 常取 60 无需分数归一化,鲁棒,推荐默认
加权求和 score = α·vec + (1-α)·bm25 需先归一化,α 要调
级联 先向量召回,再 BM25 过滤 简单,但会丢掉单路独有的结果

RRF 之所以好用,是因为它只看排名不看分数,从而绕开了「向量相似度」和「BM25 分数」量纲不可比的问题。

2.4 查询优化:性价比最高的一环

在检索结果不理想时,优化查询往往比换更大的模型更有效。 常见手段按复杂度递增:

手段 做什么 什么时候用 成本
查询改写 补全指代、去口语、纠错 多轮对话(”它”、”这个”指什么) 1 次小模型调用
同义词扩展 加入领域同义词/缩写 用户用词与文档不一致 词典维护
HyDE 先让 LLM 生成假想答案,用答案去检索 问题太短、与文档表述差异大 1 次 LLM 调用
Multi-Query 生成 N 个不同角度的查询,多路召回后融合 单查询召回不足 N 次检索
查询分解 把多跳问题拆成子问题依次求解 “A 和 B 相比,谁在 C 上更强” 多次 LLM + 检索
Step-back 先问一个更抽象的元问题,再回到具体问题 细节问题缺乏背景知识 1 次 LLM 调用
意图路由 先判断问题类型,走不同链路 知识库混合了 FAQ/文档/表格 1 次分类

关键实现细节

HyDE 的核心洞察是「假想答案」比「原始问题」更接近文档的表述分布——因为文档里写的是答案,不是问题。所以让模型先写一段可能的答案,再用它去检索,召回率常有明显提升。代价是增加一次 LLM 调用,且假想答案跑偏时会引入偏差。

Multi-Query + RRF 是工程上最稳的组合:并行发起 3~5 个改写查询,每路各召回 20~50 条,用 RRF 融合后取 Top-20 交给 Rerank。

查询分解处理多跳问题的思路是「自问自答」:先检索并回答子问题 1,把答案作为子问题 2 的上下文,逐步逼近最终答案。

2.5 重排与上下文压缩

Rerank(重排) 用 Cross-Encoder 模型对 (query, doc) 成对打分,精度远高于向量内积,但无法预先建索引,因此只能作用于小候选集:

1
向量/混合检索召回 Top-50  →  Rerank 精排  →  取 Top-5 喂给 LLM

这是投入产出比最高的一步。中文场景常用 BGE-Reranker 系列,英文可用 Cohere Rerank 等。

上下文压缩则是在有限的上下文预算里塞进更多有效信息:抽取式做法只保留与问题相关的句子,或直接让小模型摘要每个片段。注意压缩会带来信息损失,务必纳入评测。

2.6 完整的查询优化链路

2.7 生成阶段:抑制幻觉

手段 做法 效果
强制引用 要求每个论断标注来源块编号 可溯源,便于人工核验
无据拒答 prompt 明确「材料中没有就说不知道」 显著降低编造
降低温度 temperature 设 0~0.3 减少发散
Few-shot 约束 给 2~3 个「有据/无据」的示例 稳定输出格式
后置校验 用小模型判断答案是否被原文支持 可拦截大部分不忠实输出

一个可直接使用的生成 Prompt 骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
你是严谨的技术助理。请仅依据下面的【参考资料】回答问题。

规则:
1. 每个结论后用 [编号] 标注来源,例如 [1][3]。
2. 若参考资料不足以回答,直接回复「根据已有资料无法回答」,不要推测。
3. 不要引入参考资料之外的任何事实。

【参考资料】
[1] 来源:xxx.md / 第 3 节
...内容...
[2] 来源:yyy.pdf / 第 1 页
...内容...

【问题】{question}

三、RAG 测评方法与实例

「感觉回答变好了」不是工程结论。RAG 评测的关键是分层归因——先确认是检索错了还是生成错了,否则优化方向全靠猜。

3.1 三层评测框架

归因逻辑Recall@K 低 → 检索问题,先别动 prompt;Recall@K 高但 Faithfulness 低 → 生成问题,改 prompt 或换模型。

3.2 第一层:检索指标

设对某个问题,标准答案片段集合为 G(gold),系统返回的 Top-K 列表为 R

指标 公式 含义 适用
Recall@K ` R ∩ G /
Precision@K ` R ∩ G / K`
Hit Rate@K 1 if R ∩ G ≠ ∅ 至少命中一个的比例 快速健康度检查
MRR 1 / rank_first_hit 第一个正确答案排多前 关注首条质量
NDCG@K DCG@K / IDCG@K 考虑排序位置的增益 多级相关性

其中 DCG@K = Σ (2^rel_i - 1) / log2(i + 1)

3.3 第二层:生成指标(RAGAS)

RAGAS 是当前使用最广的 RAG 自动评测框架,核心指标:

指标 衡量什么 怎么算(思路) 低分说明
Faithfulness 答案是否忠于检索到的上下文 把答案拆成若干论断,逐条判断能否由 context 推出 模型在编造
Answer Relevancy 答案是否切题 用答案反推可能的问题,与原问题比相似度 答非所问、绕圈子
Context Precision 检索到的上下文里,有用的是否排在前面 判断每个 context 是否支撑答案,加权平均 噪声排在前面
Context Recall 标准答案所需信息是否都被检索到 用标准答案的论断去核对每个 context 检索漏了
Answer Correctness 与标准答案的事实一致性 事实重叠 + 语义相似度 综合偏低

前四个指标不需要人工标注答案也能算(需要问题 + 答案 + 上下文),这让它在没有标注团队时也能跑起来;Answer CorrectnessContext Recall 则需要标准答案。

3.4 完整实例:从建评测集到定位问题

第一步:构造评测集

不需要几千条,50~200 条覆盖核心场景的问题就能发现大部分问题。关键是每条要有 question + ground_truth + gold_chunk_ids

示例(节选自一个技术文档问答场景):

id question ground_truth gold_chunk_ids
1 Milvus 中 HNSW 的 M 参数控制什么? 控制图中每个节点的最大出边数,越大召回越高、内存越大 c_1042
2 如何把 IVF_FLAT 的召回率调高? 增大 nprobe c_2210
3 混合检索的 RRF 公式是什么? score = Σ 1/(k + rank),k 通常取 60 c_3301
4 数据量超过内存时推荐哪种索引? DiskANN,索引主要落盘 c_4102, c_4103
5 父子块检索是什么? 小块检索、返回父块给 LLM c_1180

构造原则:每条问题必须是知识库里真实有答案的,并且标注出「答案应该来自哪个块」——这是计算 Recall@K 的前提。可以从线上真实提问里采样,再人工补 gold 标注。

第二步:跑检索层评测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pymilvus import MilvusClient

client = MilvusClient(uri="http://localhost:19530")
COLLECTION = "tech_docs"

def retrieve(question: str, top_k: int = 10):
"""返回检索到的 chunk id 列表(按相关性降序)"""
res = client.search(
collection_name=COLLECTION,
data=[embed(question)], # embed() 为你的向量化函数
limit=top_k,
output_fields=["chunk_id"],
)
return [hit["entity"]["chunk_id"] for hit in res[0]]

def recall_at_k(retrieved, gold, k):
return len(set(retrieved[:k]) & set(gold)) / len(gold)

def hit_rate_at_k(retrieved, gold, k):
return 1.0 if set(retrieved[:k]) & set(gold) else 0.0

def mrr(retrieved, gold):
for i, cid in enumerate(retrieved, start=1):
if cid in gold:
return 1.0 / i
return 0.0

# eval_set: [{"question": ..., "gold_chunk_ids": [...]}, ...]
for k in (1, 3, 5, 10):
r = sum(recall_at_k(retrieve(q["question"], 10), q["gold_chunk_ids"], k) for q in eval_set) / len(eval_set)
h = sum(hit_rate_at_k(retrieve(q["question"], 10), q["gold_chunk_ids"], k) for q in eval_set) / len(eval_set)
print(f"Recall@{k} = {r:.3f} HitRate@{k} = {h:.3f}")

m = sum(mrr(retrieve(q["question"], 10), q["gold_chunk_ids"]) for q in eval_set) / len(eval_set)
print(f"MRR = {m:.3f}")

第三步:跑生成层评测(RAGAS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (
faithfulness, answer_relevancy,
context_precision, context_recall,
)

# 先跑一遍完整 RAG 链路,收集 问题 / 答案 / 检索上下文 / 标准答案
rows = []
for q in eval_set:
contexts = retrieve_texts(q["question"], top_k=5) # 返回文本列表
answer = rag_answer(q["question"], contexts) # 你的生成函数
rows.append({
"question": q["question"],
"answer": answer,
"contexts": contexts,
"ground_truth": q["ground_truth"],
})

ds = Dataset.from_list(rows)
result = evaluate(
ds,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(result)
print(result.to_pandas().head())

第四步:读结果、做归因

假设第一轮跑出这样一张表(示例数据):

实验 配置 Recall@5 Context Precision Faithfulness Answer Relevancy
基线 单路向量检索 + Top-5 0.68 0.61 0.94 0.88
实验 A + 混合检索(RRF) 0.81 0.66 0.95 0.89
实验 B A + BGE-Reranker 0.83 0.88 0.96 0.90
实验 C B + Multi-Query 改写 0.91 0.87 0.96 0.92

怎么读这张表

  1. 基线 的 Faithfulness 已经 0.94,说明模型没有乱编——问题不在生成端,不要急着换模型或改 prompt。
  2. **瓶颈在 Recall@5 = 0.68**:有三成问题,正确答案压根没被检索出来。此时优化 prompt 是无效的,因为材料都没给到。
  3. 实验 A 提升 +0.13:说明相当一部分问题是「专有名词精确匹配」失败,BM25 通道补上了。这一步验证了「混合检索」这个假设。
  4. 实验 B 的 Context Precision 从 0.66 跳到 0.88,但 Recall 只涨 0.02:Rerank 不增加召回(候选集没变),它只是把有用的排到前面、把噪声挤出去——数据完全符合预期,说明 Rerank 正确生效了。
  5. 实验 C 让 Recall 从 0.83 → 0.91:Multi-Query 从不同角度发问,覆盖了原本漏掉的表述方式。这才是「提召回」的正确手段。

这张表最有价值的地方,是每一步验证了一个独立假设,而不是把所有优化一起上然后看总分涨没涨——那样一旦变差,你根本不知道该回退哪一步。

第五步:人工抽查不可省

自动指标会骗人。必须每周固定抽 20 条 badcase 人工看,重点关注三类:

  • 指标高但体验差Faithfulness 很高,但答案是「正确的废话」,没有解决问题——说明指标选错了
  • 检索到了但没用上:gold chunk 在 Top-5 里,答案却没用它——生成端问题
  • 答案对但引用错:内容正确,标注的来源编号对不上——Faithfulness 会漏掉这类错误

3.5 评测落地建议

建议 说明
先建 50 条金标集 不要等大而全,先有基线才能谈优化
固定评测集,只改一个变量 每次实验只动一个环节,否则无法归因
上线前回归 把评测集接入 CI,改动 prompt/模型/索引后自动跑
线上埋点闭环 记录用户追问率、点踩率、引用点击率,回流成新的评测样本
区分「检索失败」与「生成失败」 这是整个评测体系最重要的分流

四、Milvus 向量数据库选型与索引结构

4.1 为什么需要向量数据库

理论上用 FAISS + 自己写个服务也能跑。但当数据量上去、要求变多时,会陆续需要:增删改查、标量过滤、多租户隔离、水平扩展、高可用、一致性控制——这些正是一个数据库该干的事。

选型时按这几个维度评估:

维度 关注点
性能 召回率、QPS、延迟 P99、建索引耗时
容量与成本 内存占用、是否支持磁盘索引、能否用对象存储
功能 标量过滤、混合检索、多向量、分区、一致性级别
运维 部署复杂度、水平扩展、备份恢复、监控
生态 SDK、与 LangChain / LlamaIndex 的集成、社区活跃度

4.2 主流方案对比

方案 定位 优势 局限
Milvus 分布式专用向量数据库 索引类型最全、存算分离、支持十亿级、GPU 索引 组件多,部署较重(已有 Lite 版缓解)
Qdrant Rust 实现的向量库 单机性能好、过滤强、部署简单 超大规模生态不如 Milvus
Weaviate 带模块化的向量库 内置向量化模块、GraphQL 资源占用偏高
pgvector PostgreSQL 扩展 复用现有 PG、事务与 SQL 完整 数据量大后性能衰减明显
FAISS 向量检索库(非数据库) 极致的检索性能、算法全 无持久化/增删改/过滤,需自建服务
Chroma 轻量嵌入式 上手极快,适合原型 生产级能力有限
Elasticsearch 搜索引擎 + 向量 全文检索成熟、混合检索自然 向量性能与成本不如专用库

选择建议:原型验证用 Chroma / Milvus Lite;已有 PG 且数据量在千万级以内用 pgvector;千万级以上或需要混合检索、多租户、GPU 加速,选 Milvus

4.3 Milvus 架构

组件 职责
接入层 Proxy 对外统一入口,负责鉴权、路由、结果归并
协调层 四类 Coordinator 管理元数据、调度、负载均衡、DDL
执行层 Query / Data / Index Node 无状态,可水平扩展;分别负责查询、写入、建索引
存储层 etcd / 对象存储 / WAL 元数据、数据文件、预写日志三分离

存算分离 + 日志即数据(Log-as-Data)是 Milvus 的关键设计:数据先写 WAL 保证持久性,再异步落对象存储;查询节点无状态,因此扩容和故障恢复都很快。另外还有部署形态的选择:

形态 适用 说明
Milvus Lite 原型、单机、百万级以内 pip install pymilvus 即用,进程内嵌
Standalone 中小规模生产 单机 Docker Compose 部署
Distributed 大规模生产 K8s 集群,组件独立扩缩容

4.4 索引的三要素

Milvus 中每种索引都可以拆成三个部分,理解这个模型比死记索引名有用得多:

组成部分 作用 常见实现
数据结构 粗过滤,快速缩小候选范围 倒排文件 IVF、图结构 HNSW / Vamana
量化器(可选) 压缩向量,降低内存与计算量 SQ8(标量量化)、PQ(乘积量化)
精化器 Refiner(可选) 用高精度向量重算距离,找回精度 FP32 精化
  • IVF 系列:用聚类把向量分到 nlist 个桶,查询时只扫描质心最近的 nprobe 个桶。桶数越多越省时间,但召回会降。
  • 图结构(HNSW):多层导航小世界图,从稀疏的上层逐层下降定位近邻,查询复杂度接近对数级,延迟低、召回高,代价是内存占用大。
  • DiskANN:基于 Vamana 图 + PQ 压缩,把索引放在磁盘上,用少量内存支撑十亿级数据,代价是可能受 IOPS 限制。

量化的取舍很直观:

量化方式 压缩率 内存(128 维) 特点
不量化(FP32) 512 B 精度最高
SQ8 128 B 每维 8 位,速度快,精度损失小
PQ(8 子量化器) 32× 16 B 压缩比高,同压缩率下召回优于 SQ,但计算更慢

4.5 索引类型速查

索引 类型 内存 建索引 召回 典型场景
FLAT 暴力检索 100% 小数据、要求绝对准确
IVF_FLAT IVF 通用大规模,可调 nprobe
IVF_SQ8 IVF+SQ 低(1/4) 中高 内存受限,精度可让步
IVF_PQ IVF+PQ 极低 内存极度受限
HNSW 很高 低延迟、高召回的生产主力
HNSW_SQ / HNSW_PQ 图+量化 中/低 想省内存又要图索引
DiskANN 磁盘图 十亿级、内存装不下
SCANN IVF+各向异性量化 与 HNSW 类似的替代
GPU_CAGRA GPU 图 显存 高吞吐批量检索
SPARSE_INVERTED_INDEX 稀疏倒排 稀疏向量 / BM25 混合检索
BIN_FLAT / BIN_IVF_FLAT 二值 极低 二值向量(汉明距离)
AUTOINDEX 自动 自动 自动 自动 不想调参(Zilliz Cloud)

度量类型要和索引匹配

度量 适用 说明
L2 浮点向量 欧氏距离,越小越近
IP 浮点向量 内积,必须先归一化才能当余弦用
COSINE 浮点向量 余弦相似度,文本检索最常用(Milvus 内部会归一化)
JACCARD / HAMMING 二值向量 集合相似度 / 汉明距离

常见坑:向量已经归一化时用 IPCOSINE 等价;但未归一化就用 IP,会得到「模长大的向量分数虚高」的错误排序。

4.6 关键参数与内存估算

索引 参数 取值范围 调优方向
IVF_* nlist 1~65536 常用 4×sqrt(N);越大越快越不准
nprobe 1~nlist 在线可调,调大提召回、降 QPS
HNSW M 4~64 每个节点最大出边数;越大越准越占内存
efConstruction 8~512 建索引时的候选队列;越大索引质量越好、建得越慢
ef top_k~32768 查询时参数,越大越准越慢,需 ≥ top_k
DiskANN max_degree 1~512 图度数
search_list top_k~65535 查询候选数,越大越准越慢

内存估算实例(100 万条 128 维向量,来源:Zilliz 官方索引选型):

索引 组成 内存
IVF_FLAT 原始向量 1M × 128 × 4B 512 MB
IVF_SQ8 SQ8 压缩 1M × 128 × 1B + 质心 ≈ 1 MB ≈ 129 MB
IVF_PQ PQ 压缩 1M × 16 × 1B + 质心 ≈ 9 MB
HNSW 图结构 1M × 32 × 4B + 原始向量 512 MB ≈ 640 MB
HNSW + PQ 图结构 128 MB + PQ 压缩 8 MB ≈ 136 MB

这解释了为什么「数据量上不去」几乎总是内存问题:HNSW 的图结构本身就要额外占用一半以上的空间,而量化能把内存压下一个数量级。

4.7 选型决策

结合官方经验规律,给出一张可直接查的表:

条件 推荐索引 理由
数据量 < 100 万,追求简单 HNSWFLAT 内存充足,直接上最稳的
100 万 ~ 1 亿,低延迟 HNSW QPS 与召回最优
一百万级但内存紧张 HNSW_SQ / IVF_SQ8 牺牲少量召回换 4 倍内存
内存极度受限 IVF_PQ 压缩 32 倍
十亿级 / 内存装不下 DiskANN 索引落盘,内存只需 1/4 数据量级
top-K 很大(> 2000) IVF_* 图索引在大 top-K 下优势衰减
过滤比例 < 85% 图索引 过滤后候选仍多,图遍历更优
过滤比例 85%~95% IVF_* 倒排桶更适应高过滤
过滤比例 > 98% FLAT 候选已经很少,暴力最准
需要 BM25 / 稀疏检索 SPARSE_INVERTED_INDEX 与稠密向量做混合检索

以上是经验起点,最终必须以你自己的数据做召回率-延迟曲线来定

4.8 工程实践要点

① 建表时就把标量字段设计好。 检索往往要带过滤条件(只看某部门、某时间之后、有权限的文档):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pymilvus import MilvusClient, DataType

client = MilvusClient(uri="http://localhost:19530")

schema = client.create_schema(auto_id=False, enable_dynamic_field=False)
schema.add_field("chunk_id", DataType.VARCHAR, max_length=64, is_primary=True)
schema.add_field("vector", DataType.FLOAT_VECTOR, dim=1024)
schema.add_field("text", DataType.VARCHAR, max_length=8192)
schema.add_field("doc_id", DataType.VARCHAR, max_length=64)
schema.add_field("dept", DataType.VARCHAR, max_length=32) # 用于权限过滤
schema.add_field("updated_at", DataType.INT64) # 用于时效过滤

index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 200},
)

client.create_collection("tech_docs", schema=schema, index_params=index_params)

② 用 ef / nprobe 在线调「召回-延迟」平衡,不用重建索引:

1
2
3
4
5
6
7
8
res = client.search(
collection_name="tech_docs",
data=[query_vec],
limit=10,
search_params={"metric_type": "COSINE", "params": {"ef": 128}},
filter='dept == "backend" and updated_at > 1750000000',
output_fields=["chunk_id", "text", "doc_id"],
)

③ 混合检索用内置的 RRF,避免自己写融合逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pymilvus import AnnSearchRequest, RRFRanker

req_dense = AnnSearchRequest(data=[dense_vec], anns_field="vector",
param={"metric_type": "COSINE", "params": {"ef": 128}}, limit=50)
req_sparse = AnnSearchRequest(data=[sparse_vec], anns_field="sparse",
param={"metric_type": "IP"}, limit=50)

res = client.hybrid_search(
collection_name="tech_docs",
reqs=[req_dense, req_sparse],
ranker=RRFRanker(k=60),
limit=20,
output_fields=["chunk_id", "text"],
)

④ 一致性级别按场景选,别一律用默认值:

级别 语义 适用
Strong 读到最新写入 写入后立即要查(如去重校验)
Bounded 有界滞后(默认) 大多数生产场景
Eventually 最终一致 高吞吐、可容忍延迟
Session 保证读到本会话写入 用户写入后自己查看

⑤ 分区(Partition)做租户隔离。 多租户场景按租户分区,检索时指定分区,可显著减少扫描量;但分区数不宜过多(建议不超过数百个),否则元数据开销上升。


五、总结

把全文压缩成几条可以立刻用的结论:

  1. RAG 的本质是「用检索近似边缘化」,它的上限由检索决定——检索漏了,生成端再强也救不回来。
  2. 排障先分层归因Recall@K 低就修检索(混合检索、多路查询、调分块),Faithfulness 低才修生成(强制引用、无据拒答)。
  3. 性价比最高的三个动作:加 Rerank、加混合检索(BM25 + 向量 + RRF)、优化分块(父子块)。
  4. 评测必须分层且单变量:50 条金标集起步,每次只改一个环节,让每一步验证一个独立假设。
  5. Milvus 选索引先问三件事:数据量多大、内存有多少、要多大 top-K 和多高召回。默认 HNSW,内存不够降级到 HNSW_SQ / IVF_PQ,十亿级上 DiskANN
  6. 索引不是一劳永逸的ef / nprobe 是查询期参数,可以随时在线调整来平衡召回与延迟。

RAG 是一个系统工程,不是调一个模型就完事。真正拉开差距的,往往是分块策略、混合检索和评测闭环这些「不性感」的工程细节。


参考