Chapter 01

四个设计面:demo 到生产多了什么

起点页给了概念地图和一句话本质——这一章把"四个面"钉成词汇表:每个面一句话定义、它对应深潜教程里的哪个 Milvus 机制、以及一个不设计它就会出的真实问题。后两章再逐面展开。

本章你将建立的 schema

  • demo RAG 的快乐路径,和它每一步藏着的 Milvus 默认值。
  • 四个设计面:① 租户隔离 ② 检索质量 ③ 新鲜度/一致性 ④ 元数据过滤——各是什么、为什么生产才需要。
  • 每个面对应深潜教程的哪个机制(这篇是把机制用到 RAG,不重讲机制)。
  • 每个面不设计时的失败模式——尤其那些"不报错、但答案错"的。

1.1从 demo 到生产:四个面的来源

demo RAG 是一条快乐路径:分块 → 嵌入 → 写入 → 检索 top-k;生产化就是把这条路径上每个被默认值带过去的决策,重新按场景拍一遍。

为什么 demo 能跑、生产会塌

demo 只有一个用户、一份不变的语料、一个本地查询。它把租户隔离(只有一个)、检索质量(dense 够用)、新鲜度(反正不更新)、过滤(没有元数据)全部跳过了。一旦上真实流量——多租户、语料持续变、要按权限/时间过滤——这四个被跳过的决策同时回来找你。

demo 快乐路径 分块 chunk 嵌入 embed 写入 insert 检索 search · top-k 生产要改的默认值 ↓ 粒度定召回 模型固定·换=新表 默认 Bounded·单表 默认 dense·无 filter
图 1.1demo 的四步流水线,每一步底下都压着一个 Milvus 默认值。注意:这些默认值 demo 阶段无所谓,生产阶段每个都要按场景重新决定——下面四个面,正是这四个默认值长出来的。

下面四节,每个面用同一套结构讲:一句话定义 → 为什么生产才需要 → 它对应深潜里的哪个 Milvus 机制 → 一个不设计它就会出的问题。机制本身不重讲,要补的话点链接回 深潜教程。

1.2面① 租户隔离:filter 是安全边界

租户隔离 = 决定每个租户的数据写到哪、查询能看到谁;在多租户 RAG 里,限定租户的那个 filter 不是优化项,是安全边界。

为什么生产才需要

demo 只有一个语料。生产里多个客户/部门/用户共用一套 Milvus,A 的检索绝不能召回 B 的文档——否则 B 的内容会进 A 的 LLM 上下文,变成一次跨租户数据泄漏。

对应的 Milvus 机制:这是 深潜 01 的数据模型用到 RAG——用 Collection、Partition、或 Partition Key 把租户切开(02 章详述四级选型)。Partition Key 方案下,查询带上 tenant_id == "A" 触发分区裁剪(深潜 04),只扫该租户的 segment。比文档深一层的关键:Milvus 的 RBAC 权限只到 collection / database 粒度,管不到一个 partition-key collection 内部的单个租户——所以在最常用的 partition-key 多租户里,租户隔离完全靠应用层那个 filter。漏写一次,就是漏洞。

search · 忘了 tenant filter 租户 A 发起的检索 租户 A 的 chunk 应当命中 ✓ 租户 B 的 chunk 不该命中 → 泄漏 召回 也召回了
图 1.2少了 tenant_id filter,检索把别的租户的 chunk 也召回、塞进同一个 LLM 上下文。注意:在 partition-key 多租户里 RBAC 管不到 collection 内部,这个 filter 是应用层唯一的安全边界——漏一次就是跨租户泄漏,不是性能问题。
想一想

团队用 partition key 按 tenant_id 隔离了几千个租户,并且给每个租户配了独立的 Milvus 角色(RBAC)。这样租户 A 的查询还会拿到租户 B 的数据吗?

展开答案(先停 10 秒)

可能。RBAC 只能限制"能不能访问这个 collection",管不到同一个 partition-key collection 内部的租户边界。只要应用层在某条查询路径上漏写了 tenant_id == "A" 这个 filter,B 的 chunk 就会被召回。隔离在这里是应用层的契约,不是数据库给你兜底的——所以它必须像鉴权一样被测试和强制。

与下一面的关系:租户隔离决定"查谁的数据";下一面检索质量决定"在这些数据里查得多准"。

1.3面② 检索质量:混合检索 + 两阶段 rerank

检索质量 = 让召回的 top-k 真的相关;生产做法是 dense(语义)+ sparse/BM25(精确词)双路召回,再用交叉编码器做第二阶段 rerank。

为什么生产才需要

demo 用纯 dense(语义嵌入)就够演示。但 dense 对精确关键词、产品型号、人名、代码标识符这类"字面匹配"很弱——它会把"BGE-M3"和"某个嵌入模型"混为相近,漏掉用户其实在精确找的那个词。生产的检索质量天花板,往往卡在这里。

对应的 Milvus 机制:这是 深潜 03 的索引与度量用到 RAG——dense 向量走 HNSW + COSINE,sparse/BM25 走 SPARSE_INVERTED_INDEX + IP,两路各自检索后融合。比文档深一层:rerank 分两类,别混淆——融合 rerank(RRF / Weighted)只是把两个召回列表合并排序,便宜;模型 rerank(交叉编码器,如 bge-reranker)是把 query 和每个候选成对重新打分,贵但精度高。生产的标准两阶段是:先 dense+sparse 召回 top-100(便宜、宽),再交叉编码器精排出 top-5~10 喂 LLM(贵、准)。具体 API 与取舍在 03 章。

洞察 · 两个 rerank 不是一回事

「混合检索的 rerank」(RRF/Weighted) 解决的是"两个分数体系怎么合";「精排的 rerank」(交叉编码器) 解决的是"ANN 近似召回的粗排不够准"。生产里两者常常同时存在:先融合、再精排。把它们当成一件事,是检索质量上不去的常见原因。

与下一面的关系:检索质量假设数据已经在库里且可见;下一面新鲜度,管的正是"刚写的数据,多久能被这套检索看到"。

1.4面③ 新鲜度与一致性:默认 Bounded 不是 read-your-writes

新鲜度 = 刚写入或更新的文档,多久能被检索到;它由 Milvus 的一致性级别和 segment 状态共同决定,默认的 Bounded 不保证"写完立刻可见"。

为什么生产才需要

demo 的语料灌完就不动了。生产里文档持续新增、修改、删除:用户刚上传一份文档,下一句提问就期待能检索到它;一条被删的内容不能再出现在答案里。这要求你显式决定"写入到可检索"的延迟。

对应的 Milvus 机制:这是 深潜 04 的一致性级别 / TSO,叠加 深潜 01 的 segment 状态。默认是 Bounded(有界滞后,读到的是几秒前的视图)——从关系库直觉来的人会以为是 read-your-writes,其实不是。要"同一会话写完立刻能查到自己写的",用 Session;要全局最新用 Strong(最贵)。比文档深一层:刚写的数据先进 growing segment(暴力扫、还没建索引),所以"能不能看到"(一致性级别)和"看到了快不快"(segment 是否 sealed+indexed)是两个独立的问题——生产里常被合并误判成"Milvus 写入有延迟"。

想一想

测试脚本里:插入一条文档,紧接着用它的内容检索——结果经常是空的。是 Milvus 写丢了吗?

展开答案(先停 10 秒)

没丢。默认 Bounded 一致性下,检索读的是略旧的视图,刚插入的数据还不在可见窗口里;而且它还在 growing segment 里、未必已建索引。两个修法:检索时用 consistency_level="Session"(同 client read-your-writes),或在测试里写入后显式 flush。这是 RAG 灌库后立刻自测最常见的"假 bug"。

与下一面的关系:新鲜度管"数据何时可见";最后一面元数据过滤,管"可见的数据里,怎么按条件只取该取的子集"。

1.5面④ 元数据过滤:过滤越严,越伤图索引召回

元数据过滤 = 检索时按 tenant、时间、来源、权限标签等标量条件,只在符合条件的 chunk 里做向量检索;它的反直觉之处是:过滤越严,反而越容易拉低召回。

为什么生产才需要

demo 直接全量向量检索。生产里几乎每条查询都带条件:只查这个租户、最近 90 天、某来源、当前用户有权限看的文档。这些条件要靠标量字段 + 标量索引表达,且和向量检索同时生效。

对应的 Milvus 机制:这是 深潜 04 的过滤与分区裁剪,加 深潜 03 的过滤比↔索引。比文档深一层:两个反直觉点要记住——其一,过滤字段没建标量索引,filter 就退化成全表扫描,把向量索引的加速全抵消掉;其二,过滤比越高(通过的行越少),越伤 HNSW 的图遍历——图在一堆被过滤掉的点之间走不动,找不到足够的连通候选,召回反而塌。所以"加个 filter 让它更快更准"是错觉,filter 设计要连带考虑索引类型。

反直觉 · 记住它

关系库里 where 条件越严、扫的行越少、越快。向量检索相反:一个极严的 filter 会饿死 HNSW 的图遍历,严重时召回从 95% 掉到个位数。高选择性过滤要么配可迭代过滤、要么换 IVF 类索引——这条在 02、03 章会反复用到。

§本章 self-check

先合上教程,把答案写下来再展开对照。

  1. demo RAG 跳过了哪四个决策?用一句话说清每个在生产里变成哪个"面"。
  2. partition-key 多租户里,为什么 RBAC 不足以保证租户隔离?真正的边界在哪一层?
  3. 「融合 rerank(RRF/Weighted)」和「模型 rerank(交叉编码器)」各解决什么问题?生产里它们是二选一吗?
  4. 插入文档后立刻检索为空,最常见的两个原因是什么?分别怎么修?
答案(先做完再展开)
  1. 租户隔离、检索质量、新鲜度/一致性、元数据过滤。demo 因为单租户/dense 够用/语料不变/无元数据,把四者全跳过了。
  2. RBAC 只到 collection/database 粒度,管不到 partition-key collection 内部的单租户;真正的边界是应用层那个 tenant_id filter,必须像鉴权一样测试和强制。
  3. 融合 rerank 把 dense 与 sparse 两个分数体系合并成一个排序;模型 rerank 用交叉编码器对 query×候选成对精排,纠正 ANN 粗排。不是二选一,生产常先融合再精排。
  4. (1) 默认 Bounded 一致性,刚写的数据不在可见窗口 → 用 Session 或 flush;(2) 数据还在 growing segment、未建索引 → 等 sealed+indexed 或接受暴力扫。
进阶挑战 · 刚好够不着

把四个面叠到一条真实查询上

一个多租户客服 RAG:用户(属于某租户)问"上周提的工单进展如何"。请按这条查询,说出四个面各自要做什么决策:① 隔离到哪个租户、靠什么强制;② 用 dense 还是混合检索("工单号"这种精确词意味着什么);③ 一致性选哪档("上周"+ 内容或有更新);④ 要加哪些 filter、其中哪个会伤召回。

提示(卡住再展开)

"工单号"是精确词 → 偏向混合检索;"上周"是时间过滤 → 需要 timestamp 标量字段+索引;"刚更新的进展" → 偏 Session;"这个租户+最近7天"两个 filter 叠起来,选择性可能很高 → 注意 §1.5 的召回塌陷。四个面在这一条查询里全部出现,且相互牵制——这正是 04 章辨析题的形态。