AI 工程基础体系 · 第 18/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
AI 向量检索基础:Embedding、切块、ANN、过滤和召回评测
1. 向量检索在 RAG 中解决什么问题
向量检索(vector retrieval)是把查询和文档转换为向量,再按照向量之间的相似度找到相关内容的检索方法。它通常是 RAG(Retrieval-Augmented Generation,检索增强生成)流水线中的“证据获取”阶段:系统先从外部知识库检索候选片段,再把候选片段放入生成模型的上下文中。
RAG 的基本因果链是:
flowchart LR
A[原始文件] --> B[解析与规范化]
B --> C[切块]
C --> D[Embedding]
D --> E[向量索引]
Q[用户问题] --> F[查询 Embedding]
F --> G[ANN 候选检索]
G --> H[权限与元数据过滤]
H --> I[精排/重排]
I --> J[上下文拼接]
J --> K[生成与引用]
离线摄取流程和在线查询流程并不对称:
- 离线阶段处理文件、解析结构、切块、生成文档向量并建立索引。
- 在线阶段处理查询、生成查询向量、检索候选、执行过滤和重排。
- 任何一个阶段的错误都会影响最终回答。例如,生成模型回答错误不一定是模型推理失败,也可能是切块时丢失了表头,或者权限过滤后没有留下真正相关的片段。
原始 RAG 论文将外部非参数记忆与生成模型结合起来;工程上常见的向量检索、元数据过滤和重排,都是为了让“送给生成模型的上下文”更相关、更安全、更可控。可以参见 Retrieval-Augmented Generation 和 OpenAI Retrieval Guide。
2. Embedding:把文本映射到语义空间
2.1 定义与基本形式
Embedding 是一个把离散对象映射到连续向量空间的函数。对文本来说,可以写成:
其中:
- 是文本集合;
- 是向量维度;
- 是文本 的 embedding 向量。
例如:
“退款多久到账” -> [0.12, -0.04, ..., 0.31]
“退款处理时间” -> [0.10, -0.03, ..., 0.29]
这两个向量通常应当比较接近,因为它们表达的意图相似。这里的“接近”不是字面相似,而是由 embedding 模型在训练数据中学习到的语义关系。
向量空间不是人工预先定义的“词义坐标系”。第 17 维不一定代表“时间”,第 42 维也不一定代表“退款”。单个维度通常难以解释,工程上关注的是整体向量之间的相对几何关系。
2.2 Tokenization、Transformer 和池化
文本并不会直接进入 embedding 模型,而是先经过 Tokenization。Tokenizer 把文本转换为 token ID:
“退款多久到账”
-> ["退", "款", "多久", "到", "账"] # 仅为示意
-> [t1, t2, t3, t4, t5]
实际 token 不一定对应汉字或完整单词,也可能是子词、字节片段或其他词表单元。不同模型的 tokenizer、词表和最大上下文长度可能不同,因此“字符数”和“token 数”不能互换。
典型的 Transformer 编码过程可以抽象为:
其中:
- 是 个 token 的初始表示;
- 是位置编码;
- 是隐藏层维度;
- 是每个 token 经过多层注意力和前馈网络后的表示。
要得到一个固定长度的句向量,需要进行池化(pooling)。常见方式包括:
- CLS 池化:取特殊
[CLS]token 的隐藏状态; - Mean pooling:对有效 token 的隐藏状态求平均;
- 加权平均:对不同 token 赋予不同权重;
- 模型专用池化头:由模型训练时定义的投影层输出最终 embedding。
Mean pooling 的形式为:
其中:
- 是第 个 token 的隐藏状态;
- 是 attention mask;
- padding token 的 。
如果错误地把 padding 也纳入平均,短文本的向量会被大量无意义的零填充或特殊表示污染。更重要的是,不能把“任意语言模型的隐藏状态取平均”直接当成质量可靠的 embedding;embedding 模型通常针对语义相似度、检索或对比学习目标进行过专门训练。
2.3 语义空间为何能支持检索
Embedding 模型通常通过对比学习、双塔训练或相似度学习,使相关的查询—文档对靠近,不相关的对远离。一个简化的对比损失可以写为:
其中:
- 是查询;
- 是正样本文档;
- 是一个 batch 中的候选文档;
- 是相似度函数;
- 是 batch 大小;
- 是温度参数。
训练目标不是让所有语义相近的文本拥有相同向量,而是让正样本相对于负样本具有更高相似度。因此 embedding 质量依赖于:
- 训练语料是否覆盖目标领域;
- 正负样本是否能反映真实检索任务;
- 查询和文档是否使用正确的输入格式;
- 语言、术语、代码、表格和长文本是否在模型能力范围内。
2.4 相似度:余弦、点积与欧氏距离
最常见的余弦相似度为:
它只关注方向,不关注向量长度。将向量归一化为:
之后:
此时,最大化余弦相似度等价于最大化归一化向量的点积;同时还有:
所以在向量已归一化时,余弦、点积和欧氏距离可以通过单调变换互相转换。
但这不表示三种度量永远等价。若向量没有归一化:
- 点积同时受方向和长度影响;
- 余弦只受方向影响;
- 欧氏距离受绝对位置和长度影响。
因此,索引配置中的 metric 必须与 embedding 模型的训练方式、归一化方式保持一致。错误示例是:模型和离线评测使用 cosine,生产索引却使用未归一化向量的 inner product,导致排序分布发生变化。
2.5 文档向量与查询向量必须匹配
一个检索系统至少包含两个输入集合:
- 文档 chunk:被写入索引;
- 用户 query:在线检索时输入。
最安全的原则是使用同一个 embedding 模型、同一版本、同一输入规范和同一相似度定义。若模型提供 query/document 不同的编码模式,则必须按文档说明分别编码;不能把查询模式当作文档模式,也不能混用不同版本后直接比较。
Embedding 版本需要进入索引元数据。例如:
{
"embedding_model": "example-embed-v3",
"embedding_dimension": 1536,
"metric": "cosine",
"normalized": true,
"created_at": "2025-01-15T10:00:00Z"
}
更换模型、维度或归一化策略后,通常需要重建索引。直接把新旧向量放入同一个空间会产生不可解释的相似度。
3. 切块:决定检索单元的粒度
3.1 为什么不能直接把整篇文档做成一个向量
假设一份 80 页的手册包含安装、权限、计费和故障排查。把整篇手册编码成一个向量,会产生两个问题:
- 查询“如何重置 API 密钥”时,向量表示被其他主题稀释;
- 检索到整篇文档后,上下文过大,生成模型需要从大量无关内容中寻找答案。
因此,RAG 通常先把文档切成 chunk,再为每个 chunk 建立向量。
切块的目标不是简单地“每 N 个字符截断”,而是让每个检索单元同时满足:
- 包含足够上下文,使其独立表达一个事实;
- 尽可能聚焦一个主题;
- 长度适合 embedding 模型和生成模型;
- 能在检索后被准确引用和定位。
3.2 结构化切块优先于固定长度切块
固定长度切块可以定义为:
其中:
- 是 chunk 长度;
- 是相邻 chunk 的重叠长度;
- 步长是 。
例如,以 100 个 token 为窗口、20 个 token 为重叠:
chunk 1: token 0 - 99
chunk 2: token 80 - 179
chunk 3: token 160 - 259
重叠可以减少边界截断造成的信息损失,但会带来更多 chunk、更多 embedding 成本和更多重复上下文。
更稳妥的顺序通常是:
- 先识别文档结构:标题、章节、段落、列表、表格、代码块;
- 以语义单元为基本边界;
- 若单元过长,再按句子或 token 窗口递归拆分;
- 对相邻片段保留适量上下文,而不是无条件复制大量文本。
例如,下面的内容不应简单按字符截断:
## 退款条件
退款仅适用于购买后 30 天内的订单。
| 状态 | 是否可退款 |
|---|---|
| 未使用 | 是 |
| 已使用 | 否 |
如果表头被切到另一个 chunk,后续片段中的“是”和“否”将失去列语义。表格通常应被序列化成带表头的文本,或作为完整结构单元处理:
退款条件:
购买后 30 天内的订单可以申请退款。
状态“未使用”:可退款。
状态“已使用”:不可退款。
代码、配置和法律条款也有类似问题:一个函数签名、一个 YAML 层级或一个条款编号被拆开后,局部文本可能产生错误含义。
3.3 Chunk 的语义独立性与父子关系
一个 chunk 可能需要继承父级标题:
产品手册 > API > 身份认证 > 刷新令牌
刷新令牌需要在 access token 过期前调用……
如果只保留正文,“刷新令牌”在向量空间中可能仍有一定语义,但在生成阶段缺少产品和章节范围,引用也不完整。常见做法是把路径标题拼接到 chunk 前面,同时保留原文偏移:
{
"chunk_id": "doc-17#chunk-04",
"text": "产品手册 > API > 身份认证 > 刷新令牌\n刷新令牌需要……",
"source_document": "doc-17",
"section_path": ["API", "身份认证", "刷新令牌"],
"char_start": 4210,
"char_end": 4478
}
对于长文档,可以采用父子检索:
- 子 chunk 用于精确召回;
- 父 chunk 或完整段落用于向生成模型提供上下文。
这样可以减少向量检索单元的长度,又避免把生成上下文压缩到一句孤立的文本。
3.4 切块的完整算例
原文:
## 密钥轮换
管理员可以在控制台创建新密钥。创建新密钥后,旧密钥仍可使用 24 小时。
完成客户端配置更新后,管理员应撤销旧密钥。
撤销操作不可逆,撤销前必须确认所有客户端已经完成切换。
若按句子切成三个 chunk:
C1: 管理员可以在控制台创建新密钥。
C2: 创建新密钥后,旧密钥仍可使用 24 小时。
C3: 完成客户端配置更新后,管理员应撤销旧密钥。
C4: 撤销操作不可逆,撤销前必须确认所有客户端已经完成切换。
查询“创建新密钥后旧密钥还能用多久”时,C2 足够回答。查询“什么时候可以撤销旧密钥”时,C3 和 C4 都重要。如果只取 top-1,C3 可能被召回,但“撤销前必须确认所有客户端”这一安全条件会丢失。
因此,切块评测不能只问“是否召回了某个 chunk”,还要标注答案所需的最小证据集合。例如该问题的相关集合是:
只有同时召回 C3 和 C4,生成模型才有机会得到完整答案。
4. 索引前的元数据与数据状态
向量不是检索记录的全部。生产中的一个 chunk 至少应包含:
{
"chunk_id": "doc-17#chunk-04",
"document_id": "doc-17",
"tenant_id": "tenant-a",
"text": "……",
"embedding": [0.01, -0.02],
"embedding_model": "example-embed-v3",
"source_uri": "s3://bucket/manual.pdf",
"section_path": ["API", "身份认证"],
"language": "zh",
"acl": ["group:admins", "user:u123"],
"version": 7,
"content_hash": "sha256:...",
"valid_from": "2025-01-01T00:00:00Z",
"valid_to": null
}
这些字段承担不同职责:
document_id和chunk_id用于去重、引用和删除;tenant_id用于租户隔离;acl或等价权限字段用于授权过滤;version和content_hash用于增量更新与幂等;embedding_model用于防止向量空间混用;source_uri和偏移量用于生成引用;- 有效时间字段用于处理版本和时态知识。
摄取过程应有明确状态,而不是“写入向量数据库即完成”:
DISCOVERED
-> PARSED
-> CHUNKED
-> EMBEDDED
-> INDEXED
-> ACTIVE
失败路径也必须可恢复:
EMBEDDED -> INDEX_FAILED -> RETRYING -> INDEXED
PARSED -> CHUNK_FAILED -> DEAD_LETTER
ACTIVE -> DELETED -> TOMBSTONED
常见的生产错误是文档删除成功,但旧 chunk 仍留在索引中;或者新版本写入后旧版本仍被检索。这些问题不是相似度算法能解决的,需要通过版本字段、软删除、物理删除和查询时过滤共同处理。
5. ANN:近似最近邻检索
5.1 精确最近邻是什么
给定查询向量 和向量集合:
精确 top- 检索会计算 与所有 的相似度,再排序取前 个。复杂度大致是:
其中 是向量数量, 是维度。数据量较大时,逐个比较会增加延迟和计算成本。
精确搜索仍然非常重要,因为它是 ANN 的评测基线。没有精确结果,就无法知道近似索引漏掉了多少真正的邻居。
5.2 ANN 的基本目标
ANN(Approximate Nearest Neighbor,近似最近邻)不保证总能返回数学意义上的真实 top-,而是在可接受的延迟和内存下,尽量返回真实近邻。
它优化的是三者之间的平衡:
- 召回率:真实 top- 中有多少被找回;
- 延迟:一次查询需要多久;
- 资源:索引占用多少内存、构建需要多少 CPU 和时间。
ANN 不是一种单一算法,而是一类算法和索引结构。
5.3 HNSW:图上的逐步搜索
HNSW(Hierarchical Navigable Small World)将向量组织成多层近邻图。高层节点较少,用于快速跨区域跳转;低层节点较多,用于局部精细搜索。
查询过程可以抽象为:
- 从最高层入口点开始;
- 比较当前节点和其邻居;
- 若某个邻居更接近查询,则移动到该邻居;
- 在当前层无法继续改进时,下降到下一层;
- 在底层保留一组候选节点,扩展并取 top-。
关键参数通常包括:
M:每个节点的最大连接数,影响图的连通性、内存和构建成本;ef_construction:建图时搜索候选规模,较大通常有利于图质量,但构建更慢;ef_search:查询时搜索候选规模,较大通常提高召回,但增加延迟。
HNSW 的失败表现包括:
ef_search太小,召回率低但延迟很好看;- 图构建质量不足,某些区域难以到达;
- 过滤条件很严格时,候选图节点大多不符合条件;
- 删除大量节点后,逻辑状态与物理图结构不一致。
HNSW 通常适合需要低延迟、内存允许、更新相对频繁的场景,但具体表现取决于数据分布、维度、过滤方式和实现。
5.4 IVF:先分桶,再搜索部分桶
IVF(Inverted File)先通过聚类把向量分成若干个桶(list):
查询时先找距离查询最近的若干个聚类中心,再只搜索这些桶。nprobe 表示查询时探测的桶数量。
当 nprobe=1 时,查询只搜索最接近的一个桶,速度较快但可能漏掉边界附近的真实邻居;增加 nprobe 会搜索更多桶,通常提高召回并增加延迟。
IVF 的两个阶段分别有风险:
- 聚类中心不合理,相关向量被分散到不易访问的桶;
nprobe太小,正确结果位于未探测的桶;- 数据分布变化后,旧聚类不再适合新数据;
- 过滤条件集中在少数桶时,探测策略可能不够。
5.5 PQ:用压缩向量降低成本
PQ(Product Quantization)把向量分成多个子空间,并为每个子空间学习有限个码字。原向量不再完整保存,而是保存每个子空间对应的码字编号。
假设一个 的向量被分成 4 个二维子空间,每个子空间有 256 个码字,则每个向量只需保存 4 个字节编号,而不是 8 个浮点数。实际系统中的参数和压缩比例由实现决定。
PQ 降低内存和带宽,但会引入量化误差:
检索使用近似向量 后,排序可能改变。常见补偿方式是:
- 用压缩向量做粗排;
- 对候选的原始向量进行精确重算;
- 调大候选集,再交给重排模型。
PQ 适合数据量大、内存成本敏感的场景,但不能只看索引大小,还要测量压缩后 Recall@k 的变化。
6. 一个可运行的精确检索基线
在调 ANN 之前,先建立一个小规模精确搜索程序。下面的代码只依赖 NumPy,可直接运行。
import numpy as np
documents = [
("c1", "退款申请需要在购买后 30 天内提交。"),
("c2", "退款通常会在审核通过后的 3 到 5 个工作日到账。"),
("c3", "API 密钥可以在控制台的安全设置中轮换。"),
("c4", "轮换密钥后,旧密钥仍可使用 24 小时。"),
]
# 仅用于演示。真实系统中的向量应由 embedding 模型生成。
doc_vectors = np.array([
[0.90, 0.10, 0.00],
[0.82, 0.18, 0.02],
[0.05, 0.10, 0.99],
[0.10, 0.20, 0.95],
], dtype=np.float32)
query_vector = np.array([0.85, 0.15, 0.01], dtype=np.float32)
def normalize(x):
norm = np.linalg.norm(x, axis=-1, keepdims=True)
return x / np.maximum(norm, 1e-12)
doc_vectors = normalize(doc_vectors)
query_vector = normalize(query_vector)
scores = doc_vectors @ query_vector
order = np.argsort(-scores)
for rank, index in enumerate(order[:3], start=1):
doc_id, text = documents[index]
print(f"{rank}. {doc_id} score={scores[index]:.4f} {text}")
在这个例子中,查询向量被人为设置为接近退款相关向量,因此预期前两项是 c1 和 c2。这个结果不能证明向量模型有效,因为向量是手工构造的;它只验证了余弦相似度实现、排序方向和 top- 逻辑。
这段代码还说明了一个重要边界:精确搜索返回的是“按照当前向量和 metric 定义的最近邻”,并不保证它们就是人类认为的相关文档。若 c3 排名很高,问题可能在 embedding 模型、数据表达或查询改写,而不是索引。
7. 过滤:相关性之外的约束
7.1 过滤的定义
过滤(filtering)是根据结构化条件限制可检索文档集合。例如:
tenant_id = "tenant-a"
AND language = "zh"
AND status = "published"
AND user_id ∈ allowed_users
过滤与向量相似度解决的是不同问题:
- 向量相似度回答“哪些内容语义上接近查询”;
- 过滤回答“哪些内容允许进入本次候选集合”。
权限过滤不能被向量相似度替代。把“机密”作为文本写进 chunk,并期待 embedding 自动把它排到后面,既不可靠也不安全。
7.2 预过滤、后过滤和过滤后的 top-k
设全集为 ,过滤后的集合为:
严格的过滤检索应当计算:
而不是先在全集中取 个,再删除不符合条件的结果。后者的流程是:
两者不等价。
完整算例:
候选按相似度排序:
A: 0.99,允许
B: 0.98,不允许
C: 0.97,不允许
D: 0.96,允许
E: 0.95,允许
当 时:
- 先取 top-3 再过滤:只剩 A;
- 先过滤再取 top-3:得到 A、D、E。
如果后过滤后不足 ,生成模型可能拿到很少的证据,或者系统错误地认为“没有更多相关内容”。
后过滤有时作为工程折中,例如 ANN 索引不支持高效过滤。但对于 ACL、租户隔离、删除状态等安全条件,必须确保未授权内容不会暴露给应用层或生成模型。仅靠“返回后再过滤”还可能造成侧信道:返回数量、分数分布和延迟可能泄露受保护数据的存在。
7.3 过滤选择性与候选集大小
定义过滤选择性:
当 较低时,普通 ANN 的 top- 候选中可能大部分都被过滤掉。一个常见补救方式是先取更大的候选数 ,再过滤:
ANN top-100
-> ACL 过滤
-> 保留 8 个
-> 重排后取 top-5
但增大 不是严格保证。若真正相关且有权限的向量未进入 ANN 候选集,后续过滤无法找回它。
过滤性能还取决于实现方式:
- 预过滤:搜索时只遍历符合条件的向量;
- 分区或分片:按 tenant、时间或语言拆分索引;
- 位图/倒排结构:先计算结构化条件对应的候选集合;
- 后过滤:先做向量检索,再在应用层筛选。
按 tenant 分片可以降低跨租户搜索风险,但租户数量很多且数据量不均衡时,会增加索引管理和热点问题。将 ACL 作为数组存储便于表达权限,但高基数用户权限可能造成过滤索引膨胀。具体取舍依赖数据库实现,不能仅凭“支持 metadata filter”判断其性能或安全语义。
8. ANN 召回与检索召回不是同一个概念
“召回率”在向量检索中至少有两层含义。
8.1 ANN 召回率
给定精确检索结果集合 ,ANN 返回结果集合 ,ANN Recall@k 为:
例如精确 top-5 是:
G5 = {a, b, c, d, e}
ANN 返回:
A5 = {a, b, c, x, y}
则:
这只衡量 ANN 是否接近精确向量搜索,不判断文档是否真正回答了问题。
8.2 任务相关召回率
对一个查询 ,人工或业务标注得到相关文档集合 。检索结果为 ,则:
若问题需要 C3 和 C4 两个证据,标注集合为:
R(q) = {C3, C4}
检索 top-5 只返回 C3,则任务 Recall@5 是:
即使 ANN Recall@5 达到 1,也可能因为 embedding 模型把 C4 排在后面而导致任务召回不足。
这两个指标的诊断意义不同:
- ANN Recall 低:索引参数、压缩、过滤执行或 ANN 算法有问题;
- ANN Recall 高但任务 Recall 低:embedding、切块、查询表达或标注有问题。
9. 检索评测:从集合指标到排序指标
9.1 Precision@k、Recall@k 和 F1
设 top- 结果中相关文档数量为 ,真实相关文档总数为 :
Precision@k 关注返回结果是否“干净”,Recall@k 关注相关证据是否“找全”。
例如:
真实相关集合:{A, B, C, D}
top-3:{A, X, B}
则:
Precision@3 = 2/3
Recall@3 = 2/4
二者不能互相替代。RAG 常常更怕遗漏关键事实,因此需要较高的候选召回;但候选过多又会增加上下文成本和干扰,最终还需要重排和截断。
9.2 MRR:第一个正确结果有多早
MRR(Mean Reciprocal Rank)适合“只要找到一个能回答问题的结果”的场景:
其中 是第一个相关结果的排名;若没有相关结果,贡献为 0。
如果三个查询的第一个相关结果分别位于第 1、2、4 位,则:
MRR 不关心第二个、第三个相关结果,因此不适合需要多个证据共同完成答案的问题。
9.3 nDCG:考虑等级和位置
当相关性不只是“相关/不相关”,而是有 0、1、2、3 等等级时,可以使用 nDCG。
其中 是第 位结果的相关性等级。再用理想排序的 DCG 归一化:
完整算例:三个结果的相关等级按排序为:
[3, 0, 2]
则:
理想排序为 [3, 2, 0]:
所以:
nDCG 能表达“高度相关的结果应该排在前面”,比单纯集合召回更适合评估排序质量。
9.4 端到端 RAG 还需要评估什么
检索指标无法直接证明最终答案正确。至少还要分开记录:
- 检索相关性:相关证据是否进入候选;
- 上下文精度:送入模型的内容中有多少真正有用;
- 上下文完整性:回答所需证据是否齐全;
- 答案正确性:生成内容是否符合证据;
- 引用正确性:引用是否确实支持对应陈述;
- 拒答正确性:无证据或无权限时是否拒绝编造。
一个典型的错误链是:
Recall@20 很高
-> 重排丢掉关键的第二个证据
-> 生成模型只看到局部条件
-> 答案看似流畅但结论不完整
因此,检索评测应保存每个查询的中间结果:原始 ANN top-N、过滤后的结果、重排分数、最终上下文、引用和生成答案,而不是只保存一个最终准确率。
10. 一个简单的评测程序
下面的代码计算二值相关性下的 Recall@k、Precision@k 和 MRR。它不依赖向量数据库,适合先验证评测定义。
def recall_at_k(retrieved, relevant, k):
retrieved_k = retrieved[:k]
return len(set(retrieved_k) & set(relevant)) / max(len(relevant), 1)
def precision_at_k(retrieved, relevant, k):
retrieved_k = retrieved[:k]
return len(set(retrieved_k) & set(relevant)) / max(k, 1)
def reciprocal_rank(retrieved, relevant):
relevant = set(relevant)
for rank, doc_id in enumerate(retrieved, start=1):
if doc_id in relevant:
return 1.0 / rank
return 0.0
retrieved = ["A", "X", "B", "Y", "C"]
relevant = ["A", "B", "C"]
print("Recall@3:", recall_at_k(retrieved, relevant, 3))
print("Precision@3:", precision_at_k(retrieved, relevant, 3))
print("RR:", reciprocal_rank(retrieved, relevant))
预期输出:
Recall@3: 0.6666666666666666
Precision@3: 0.6666666666666666
RR: 1.0
MRR 为 1,是因为第一个结果 A 就是相关结果;但 Recall@3 只有 ,因为 C 还没有出现。这个例子说明“第一个结果正确”和“证据已经找全”是不同目标。
评测集至少应包含:
- 真实用户查询;
- 查询所属租户和权限身份;
- 相关文档或 chunk;
- 相关性等级;
- 需要多个证据的组合问题;
- 无答案问题;
- 同义词、缩写、拼写错误和中英文混合查询;
- 时效性、版本和权限边界案例。
如果所有测试问题都是从文档标题直接改写出来,指标会过于乐观,不能代表真实用户查询。
11. 查询流程中的候选、过滤与重排
生产查询通常不会“向量 top-k 后直接交给生成模型”,而是分成多个阶段:
query
-> 查询规范化/改写
-> query embedding
-> ANN top-K
-> 过滤 tenant、ACL、版本、时间和状态
-> 去重与相邻 chunk 合并
-> 可选 BM25/关键词混合检索
-> cross-encoder 或其他重排
-> 截断到上下文预算
-> 生成与引用
查询改写可能把用户问题转换为更适合检索的形式,但也可能改变原意。尤其在权限和实体名称场景,不应让模型自由改写出新的租户、项目或用户范围。
混合检索把稀疏检索和向量检索结合起来:
- 向量检索擅长同义表达和语义匹配;
- BM25、倒排索引或关键词检索擅长精确匹配产品名、错误码、版本号和 API 参数。
一个常见的融合方式是 Reciprocal Rank Fusion:
其中:
- 是不同检索列表;
- 是文档 在列表 中的名次;
- 是平滑常数。
重排模型通常读取 query 和候选文本的交互,而不是分别编码后只比较两个向量。因此它可能比双塔 embedding 更准确,但计算成本更高,适合对较小候选集进行精排。
12. 切块、Embedding 和 ANN 的故障诊断
12.1 关键词命中但向量检索找不到
可能原因包括:
- 查询中的错误码或专有名词被 embedding 弱化;
- chunk 被切断,术语与解释分离;
- 查询和文档编码模式不一致;
- 文档语言或领域超出模型能力;
- ANN 参数导致近邻漏召回。
诊断顺序应先用精确搜索验证:
- 关键词能否在原始文本中找到;
- 目标 chunk 是否正确生成;
- query 和目标 chunk 的相似度是多少;
- 精确 top-k 是否包含目标;
- ANN top-k 是否丢失目标;
- 过滤前后目标在哪一步消失。
若精确搜索也找不到,问题不在 ANN;若精确搜索找到而 ANN 找不到,才应调索引参数或更换索引策略。
12.2 检索结果相关但答案缺少关键条件
常见原因是:
- chunk 太短,条件位于邻近 chunk;
- top-k 太小;
- 去重逻辑只保留同一文档的一个 chunk;
- 重排偏好主题相似的片段,却丢掉补充限制;
- 上下文预算截断了后半段。
这类问题不能只通过“提高 embedding 维度”解决。应检查问题对应的最小证据集合,并在评测中明确要求多证据完整性。
12.3 结果很多但内容重复
chunk 重叠过大、同一文档多个版本并存,或相邻 chunk 都进入 top-k,都会造成重复。可以在重排前后做:
- 按
document_id或section_path的多样性约束; - 相邻 chunk 合并;
- MMR(Maximal Marginal Relevance)平衡相关性和新颖性;
- 只保留最新有效版本。
但去重不能简单按文本字符串完全相等,因为同一事实可能在不同章节以不同形式出现。
12.4 权限过滤后的答案不稳定或不安全
典型错误包括:
- 先把跨租户结果交给应用层,再尝试删除;
- 过滤字段缺失时默认允许;
- 文档更新后旧 chunk 的 ACL 未同步;
- 缓存 key 没有包含 tenant 或用户权限;
- 生成上下文缓存被不同用户复用。
权限判断应采用默认拒绝原则:缺失 ACL、租户不匹配、版本失效或状态不明确时,不应进入候选上下文。缓存也必须把权限范围、租户和数据版本纳入键或做等价隔离。
13. 成本、延迟和一致性
13.1 Embedding 成本
离线 embedding 成本大致与输入 token 数成正比:
切块重叠会增加总 token 数。若 chunk 长度为 ,重叠为 ,文档总长度为 ,粗略 chunk 数为:
在 时,步长为 400;若把重叠提高到 250,步长变成 250,chunk 数和 embedding 成本会显著增加。
成本还包括:
- 解析和 OCR;
- 向量数据库存储;
- ANN 索引构建与重建;
- 查询时 ANN、过滤和重排 CPU;
- 生成模型输入上下文 token;
- 评测和日志存储。
增大 top-K 往往同时增加过滤、重排和生成上下文成本,因此不能单独追求 Recall@100。
13.2 延迟预算
在线延迟可分解为:
各项不一定全部存在,但必须分别观测。平均延迟可能掩盖尾延迟问题,因此应记录 P50、P95 和 P99。
常见因果关系是:
- 增大 HNSW
ef_search:ANN 延迟上升,ANN 召回可能提升; - 增大 IVF
nprobe:访问桶更多,延迟上升,召回可能提升; - 增大候选 K:过滤和重排成本上升;
- 增加重排模型:相关性可能提升,但产生额外模型调用;
- 增加上下文长度:生成延迟和 token 成本上升,且可能引入噪声。
13.3 更新一致性
文档更新时,至少存在三个状态:
- 原始文档状态;
- chunk 和 embedding 状态;
- ANN 索引可见状态。
如果文档已经在主数据库中更新,但新向量尚未完成索引,查询可能暂时读到旧内容。系统应明确采用哪种一致性模型:
- 最终一致:接受短暂旧数据,并记录索引滞后;
- 版本一致:查询只允许返回指定版本;
- 强一致切换:新索引构建完成后原子切换 alias。
删除尤其需要谨慎。逻辑删除可以快速隐藏数据,但会暂时占用索引空间;物理删除释放空间,却可能需要重建图或压缩索引。对于法律删除、租户注销和权限撤销,应验证删除不仅影响主表,也影响缓存、备份、索引和异步任务。
14. ANN 参数调优的正确方法
不要直接根据经验设置 HNSW 或 IVF 参数,而应使用固定评测集进行网格实验。
例如对每个参数配置记录:
配置 A: ef_search=32
配置 B: ef_search=64
配置 C: ef_search=128
对同一批查询分别计算:
ANN Recall@10
任务 Recall@10
nDCG@10
P50/P95 延迟
CPU、内存和索引大小
调优流程是:
- 用精确搜索生成 ground truth;
- 固定 embedding、切块、过滤和查询集;
- 只改变一个 ANN 参数;
- 观察召回—延迟曲线;
- 再测试过滤选择性、数据规模和分布变化;
- 选择满足业务下限的最低成本配置。
如果没有 ground truth,单看“搜索返回了很多看起来相关的内容”无法判断 ANN 是否漏掉了更相关的结果。若 embedding 模型或切块策略发生变化,应重新生成 ground truth,因为真实 top-k 已经改变。
15. 真实边界与常见误解
误解一:向量相似就等于事实正确
相似度只表示模型认为两个输入在语义空间中接近,不表示文档内容真实、最新或适用于当前用户。需要结合来源可信度、更新时间、版本和权限。
误解二:chunk 越小,检索越精确
chunk 太小会失去条件、指代和标题上下文;chunk 太大则会稀释主题并增加上下文成本。合理粒度取决于文档结构、问题类型和下游上下文预算,不能用一个固定字符数覆盖所有数据。
误解三:top-k 越大,答案一定越好
更大的候选集可能提高召回,但也会带来更多重复和噪声。若重排模型、上下文截断或生成模型无法处理额外内容,最终答案质量可能下降。
误解四:ANN 召回率高,RAG 就一定好
ANN 只衡量近似索引是否接近精确向量搜索。若 embedding 没有表达领域术语,精确搜索本身也会返回错误内容。必须同时评估索引层和任务层。
误解五:metadata filter 是权限系统
元数据过滤可以参与授权执行,但它不是完整的身份认证、授权策略、审计和数据生命周期系统。权限信息的来源、更新、缓存隔离、默认拒绝和删除验证都需要由整个系统保证。
误解六:更换 embedding 模型只需重新写入新向量
不同模型的向量空间通常不可直接比较。更换模型后应重新编码所有有效 chunk,重建对应索引,更新评测基线,并在切换期间处理新旧索引的一致性。
16. 生产系统的最小闭环
一个可验证的向量检索系统至少应形成以下闭环:
文档采集
-> 解析结果可追踪
-> 结构化切块
-> 记录模型版本和内容哈希
-> 生成文档向量
-> 建立 ANN 索引
-> 在线执行权限过滤
-> 保存候选和重排结果
-> 通过标注集计算召回
-> 根据失败样本修改切块、模型或索引参数
对于每个查询,应能回答:
- 使用了哪个 query embedding 模型;
- 查询向量是否归一化;
- ANN 返回了哪些候选;
- 精确搜索是否能找到更好的结果;
- 哪些候选被 ACL、租户、版本或时间过滤掉;
- 重排前后排名如何变化;
- 最终上下文来自哪些 chunk;
- 生成答案中的每个引用对应哪段原文。
当这些中间状态可观测时,Embedding、切块、ANN 和过滤就不再是一个黑盒。可以先用精确搜索建立正确性基线,再分别验证切块和 embedding 的语义质量,最后调节 ANN 的速度—召回折中;同时把权限、版本、成本和延迟作为同一生产系统的约束,而不是在检索完成后才补上的附加功能。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:多模态 AI 工程:图片、音频、视频、文档输入和结果验证
- 下一篇:RAG 完整流水线:摄取、解析、切块、检索、重排、上下文和引用
- 延伸:Tokenization 与 Embedding:词表、子词、位置、池化和语义空间
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论