Files
2026-09-22 15:42:25 +08:00

374 lines
24 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段四 · 小点 2:向量数据库与记忆存储
> 所属:阶段四 工具生态与外围组件集成
> 定位:向量库是 Agent 的「长期记忆 + 知识库」的地基。这一讲从「什么叫向量」讲起,把 Embedding 选型、ANN 索引、混合检索、Rerank 一次讲清,再给你一份「Redis + 向量库 + PostgreSQL」的生产级记忆存储分工方案。
## 精简大纲
1. 向量基础:Embedding、相似度、检索原理
2. 轻量方案:Chroma / FAISS
3. 生产级向量库:Milvus / Qdrant / Pinecone
4. 检索进阶:元数据过滤、混合检索、Rerank
5. 记忆存储组合方案(生产架构)
6. 总结:向量库怎么选、怎么配
## 学习内容详情
### 1. 向量基础
#### 1.1 从「词 → 向量」看 Embedding
```mermaid
graph LR
A["放假通知"] -->|Embedding<br/>编码模型| B[[ 0.12, 0.87, -0.34, ... 0.05 ]<br/>384维向量]]
C["年假安排"] -->|Embedding| D[[ 0.11, 0.85, -0.30, ... 0.04 ]<br/>384维向量]]
E["红烧肉做法"] -->|Embedding| F[[ 0.6, -0.1, 0.9, ... 0.8 ]]
B -.语义相近.-> D
```
- **Embedding**:把一段文字变成一串数字(向量)。语义相近的文字,向量方向也相近。
- **Embedding 维度**:向量长度(384 / 512 / 1536 维),维度越高表达力越强但越贵、越慢、占内存越大。
- **选型三问**:
1. **中文效果**——BGE 系列对中文远好于英文模型,中文语料优先 BGE;
2. **本地部署(开源)还是 API(闭源)**——离线/隐私要求选开源本地,图省事选 API;
3. **精度够不够**——先小模型跑通流程,不够再升维 / 换更强模型。
#### 1.2 余弦相似度
- 衡量两个向量「方向」是否接近,`cos = (A·B) / (|A||B|)`,取值 `[-1, 1]`,越接近 1 表示越相似(同向)。
- 向量检索默认用余弦相似度(只关心方向、不关心长度),这也是为什么它能忽略文本长短差异。
```python
import numpy as np
def cos_sim(a, b):
"""余弦相似度: 1=完全同向, 0=正交, -1=反向"""
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
a = np.array([0.12, 0.87, -0.34]) # "放假通知"
b = np.array([0.11, 0.85, -0.30]) # "年假安排"
c = np.array([0.60, -0.10, 0.90]) # "红烧肉"
print(f"放假 vs 年假: {cos_sim(a, b):.3f}") # 高, 语义相近
print(f"放假 vs 红烧: {cos_sim(a, c):.3f}") # 低, 语义无关
```
> 🔬 **深度理解 · 向量嵌入与相似度检索(维度 & 度量)**:
> 1. **本质**:Embedding 是把一段文本映射到高维连续向量空间里的一个点,**“语义相近”≈“空间距离近”**。它做了词法表达("年假"vs"休假")到语义表达的一次升维抽象,让“检索”从“字面匹配嘴上”变成“几何上找邻近点”。
> 2. **机制**:维度就是语义能“记住”的自由度的数量——384/768/1536 维,每一维可粗略理解为在某个隐含语义轴上的强度。相似度度量选的是**在该向量空间里怎么量“近”**:余弦看方向(`(A·B)/(|A||B|)`,忽略长度,文本检索默认)、点积看方向+长度(未归一化时受向量模长干扰)、L2 欧氏看绝对距离(对向量长度/尺度敏感)。多数 embedding 模型输出已归一化时三者排序往往等价,但**选错度量(尤其对非归一化向量)会导致排序失真**。库与模型必须“维度一致 + 度量一致”,换模型=维度变=全量重嵌重插。
> 3. **为什么重要**:它是整套 RAG/记忆的根基——召回质量上限由“向量空间里语义是否真的邻近”决定,之后的分块、检索、Rerank 都只能在这个基础上修修补补;维度与度量是选型时最容易埋错的两个“隐含合同”。
> 4. **易错点/常见误解**:误解一「维度越高越好」——表达力强但更贵更慢更占内存,够用即止;误解二「所有模型都能互相混用」——不同 embedding 模型的空间不同,混用即维度或度量不匹配,直接报错或召回失真;误解三「余弦 = 万金油」——对带长度信号的语义(如词频加权)可能丢弃信息,得看模型与场景。
#### 1.3 ANN 近似检索(HNSW / IVF)
```mermaid
graph TD
A[百万级向量不可能全扫] --> B[暴力全扫: O(N) 太慢]
A --> C[ANN 近似检索: 建索引]
C --> D[HNSW 分层跳表式图索引 ★默认]
C --> E[IVF 先聚类再就近找桶]
D --> F[近似但极快<br/>海量数据毫秒返回]
```
- **ANN(Approximate Nearest Neighbor)**:不保证 100% 精确,但换来指数级提速,是海量向量检索的标配。
- **HNSW**:当前事实上的默认索引(分层导航图,跳着找,速度快精度高)。
- **IVF**:先聚类成桶,检索只扫最近的几个桶,更省内存。
- **HNSW 关键参数(生产调优就这三个)**:
- `ef_construction`:建索引的精度/速度权衡,128-256 常用(建索引时定死,改了要重建索引);
- `ef_search`:查询的精度/延迟权衡,**运行时可调**、无需重建索引,召回不够先加它;
- `M`:图连接度(每个节点的邻居数),越大召回越稳、内存占用越高。
- **生产先评估再调参**:用固定查询集跑 **recall@k** 基线,缺精度再动参数,别拍脑袋。
> ⏳ **短期可不深究**:HNSW 的 `ef_construction / ef_search / M` 三个参数具体怎么配、IVF 的桶数选多少,**第一遍知道“ANN 是近似检索、有速度-精度权衡”即可**,不用背数值——等真正压到百万级向量、跑 recall 基线时再细调。先记住「全扫 O(N) 太慢 → 建索引提速、要精度就调索引参数」。
### 2. 轻量 vs 生产方案
```mermaid
graph LR
A[向量库选型] --> B[轻量<br/>演示/本地调试]
A --> C[生产<br/>海量/并发]
B --> B1[Chroma 嵌入式<br/>pip装完即用]
B --> B2[FAISS 纯内存<br/>快但不持久化]
C --> C1[Qdrant Rust<br/>示例首选]
C --> C2[Milvus 分布式]
C --> C3[Pinecone SaaS]
```
| 方案 | 定位 | 亮点 | 短板 |
|-|-|-|-|
| **Chroma** | 嵌入式 | pip 装完即用,本地调试首选 | 单机,数据不大 |
| FAISS | Meta 纯内存库 | 极快 | 不持久化,只适合实验 |
| Milvus | 国产分布式 | 大规模、云原生 | 组件多、部署重 |
| **Qdrant** | Rust 写的 | 单机/分布式皆宜,原生支持混合检索与元数据过滤 | 需自行部署 |
| Pinecone | 全托管 SaaS | 免运维 | 数据出境,成本高 |
| **pgvector** | 已有 PostgreSQL 技术栈的团队首选(向量与业务数据同库、事务一致) | 中小规模(百万级以内向量)够用,不新增存储组件 | 大规模 ANN 性能与内存管理弱于专用库 |
#### 2.1 Chroma 五件套:增删改查 + 元数据过滤
```python
# pip install chromadb
import chromadb
client = chromadb.PersistentClient(path="./chroma_db") # 持久化落盘
col = client.get_or_create_collection("docs") # 建集合
def embed_bge(text: str) -> list:
"""演示用简易哈希向量(真实项目换成中文BGE模型/API)。
Chroma 默认用英文小模型, 中文语料务必显式指定中文Embedding。"""
import hashlib
vec = []
for i in range(len(text)):
vec.append(int(hashlib.md5(f"{text}{i}".encode()).hexdigest(), 16) / 1e38)
return vec or [0.0]
# 增
col.upsert(
ids=["doc1", "doc2"],
embeddings=[embed_bge("年假最长15天"), embed_bge("报销需附发票")],
documents=["年假规定", "报销规定"],
metadatas=[{"dept": "HR", "date": 2025}, {"dept": "Finance", "date": 2024}],
)
# 查(带元数据过滤: 只要HR部门的, 再取top1)
res = col.query(
query_embeddings=[embed_bge("我能休几天假?")],
where={"dept": "HR"}, # 先按结构化条件过滤
n_results=1,
)
print(res["documents"])
# 改
col.update(ids=["doc1"], documents=["年假最长15天, 需提前申请"])
# 删
col.delete(ids=["doc2"])
```
> ⚠️ **注意(演示局限)**:上面的 `embed_bge` 是按字符数生成的可变长哈希向量,**维度随文本长度变化**。Chroma 要求同一 collection 内所有向量**等维**,query 与入库文本长度不同会报「维度不匹配」。真实项目务必用**固定维度**的中文 Embedding 模型(如 BGE / sentence-transformers);本示例仅演示 API 流程。
#### 2.2 Qdrant 生产对照(示例首选)
五件套流程与 Chroma 一一对应(建集合 → 写入 → 过滤检索 → 删除),但 API 更贴生产:显式声明向量配置、payload 结构化过滤、原生混合检索。
```python
# pip install qdrant-client
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams, Distance, PointStruct, Filter, FieldCondition, MatchValue, Query,
)
# ① 连接: 本地调试用内存模式零部署; 生产换成 QdrantClient(url="http://localhost:6333")
client = QdrantClient(":memory:")
# ② 建集合: 显式声明向量维度与距离(必须与 embedding 模型一致)
client.create_collection(
collection_name="docs",
vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)
# ③ 写入: PointStruct(id, vector, payload), payload 即元数据
client.upsert(collection_name="docs", points=[
PointStruct(id=1, vector=embed_bge("年假最长15天"),
payload={"dept": "HR", "tenant": "t1"}),
PointStruct(id=2, vector=embed_bge("报销需附发票"),
payload={"dept": "Finance", "tenant": "t1"}),
])
# ④ 检索: query_points(旧版叫 search) + Filter, must/should/must_not 三种关系
hits = client.query_points(
collection_name="docs",
query=embed_bge("我能休几天假?"),
query_filter=Filter(
must=[FieldCondition(key="dept", match=MatchValue(value="HR"))], # 必须满足(AND)
# should=[FieldCondition(key="tenant", match=MatchValue(value="t1"))], # 满足其一(OR)
# must_not=[FieldCondition(key="dept", match=MatchValue(value="Finance"))], # 必须不满足
),
limit=3,
).points
print([(h.id, h.payload) for h in hits])
# ⑤ 删除
client.delete(collection_name="docs", points_selector=[1])
```
- **混合检索(简述)**:Qdrant 原生支持在 `query_points` 里配 `prefetch`——先并行发两路子查询(稠密向量 + 稀疏向量),再用 `fusion=Query.FUSION_RRF` 融合排序一次返回。对照上节「稠密 + 稀疏双路召回 + RRF 融合」的思路,只是由库在一次请求里完成。
#### 2.3 pgvector 最小用法
已有 PostgreSQL 的团队不必新上向量库——`pgvector` 扩展让 PG 直接当向量库用,向量与业务数据同库、同一事务。
```sql
-- ① 启用向量扩展(库管理员执行一次)
CREATE EXTENSION vector;
-- ② 建表: embedding 列的维度必须与 embedding 模型输出一致(这里以 768 维为例)
CREATE TABLE kb_chunks (
id bigserial PRIMARY KEY,
content text,
embedding vector(768)
);
-- ③ 写入: 向量以 '[0.1, 0.2, ...]' 文本形式直接插入
INSERT INTO kb_chunks (content, embedding)
VALUES ('年假最长15天', '[0.12, 0.87, ...]');
-- ④ 相似检索: <=> 是余弦距离(越小越近), 取最相近的 5 条
SELECT content
FROM kb_chunks
ORDER BY embedding <=> $1::vector
LIMIT 5;
```
> ⚠️ **向量维度必须与 embedding 模型输出一致**:建列时定死 `vector(768)`,入库向量就必须是 768 维;**换模型 = 维度变 = 全量重嵌重插**,没有「部分迁移」这回事(Qdrant 的 `VectorParams(size=...)` 同理)。
> ⏳ **短期可不深究**:pgvector 的具体建表 SQL(`CREATE EXTENSION vector`、`embedding vector(768)`、`<=>` 运算符)第一遍知道“PG 加上扩展就能当向量库”即可,不必现在就背语法——等确定要用“PG 现有技术栈不引组件”这条路线时再抄。真正要紧的是「维度必须与模型一致」这条隐含合同。
### 3. 检索进阶三件套 ★生产标配
#### 3.1 元数据过滤
- **思路**:先按结构化条件(`dept=HR`、`date>2025`)缩小范围,再在子集内做向量检索——精度大幅提升。
- **多租户隔离也靠它**:每个租户的文档打上 `tenant_id` 标签,检索强制带该过滤条件,天然隔离数据。
#### 3.2 混合检索(稠密 + 稀疏双路召回)
```mermaid
graph LR
A[查询] --> B[稠密向量<br/>语义匹配 放假≈年假]
A --> C[稀疏BM25<br/>精确词 QX-300 人名 数字]
B --> E[RRF 融合排序]
C --> E
E --> F[最终结果<br/>两路盲区互补]
```
- **稠密向量(Dense)**:捕捉语义,但「放假」匹配「年假」这种同义替换容易漏精确型号。
- **稀疏 BM25(Sparse)**:精确词匹配(型号 QX-300、人名、数字),但不懂语义。
- 单路都有盲区,**生产标配是双路召回再融合(RRF)**。
```python
def reciprocal_rank_fusion(dense_hits: list, sparse_hits: list, k: int = 60) -> list:
"""
RRF 融合: 两路排名按 1/(k+rank) 打分再求和, 取总分最高的。
- dense_hits 稠密召回结果(每项带 id)
- sparse_hits 稀疏BM25召回结果
"""
score = {}
for rank, hit in enumerate(dense_hits, start=1):
score[hit["id"]] = score.get(hit["id"], 0) + 1 / (k + rank) # 稠密路贡献分
for rank, hit in enumerate(sparse_hits, start=1):
score[hit["id"]] = score.get(hit["id"], 0) + 1 / (k + rank) # 稀疏路贡献分
return sorted(score.items(), key=lambda x: x[1], reverse=True)
```
> 🔬 **深度理解 · 混合检索(稠密 + 稀疏 + RRF 融合)**:
> 1. **本质**:混合检索是在**“语义检索”和“词语检索”两个互补空间上各跑一路,再把排名融合**。稠密向量擅长“放假≈年假”这种换词同义,稀疏(BM25)/关键词擅长“QX-300、人名、精确编号”这种必须字面命中、向量语义抓不住的场景——两路各自的盲区恰好是对方的强项。
> 2. **机制**:① **稠密路**把 query 和 doc 都过 embedding,在连续向量空间中找语义邻近;② **稀疏路**基于词频统计(BM25 = TF 词的文档频率加权 · IDF 倒排频率)做精确词匹配,能命中新词、编号、专有名词;③ **RRF(Reciprocal Rank Fusion)** 不比较原始分数(两路分数尺度不可比),而是只看**各自名次**:每个候选按 `1/(k+rank)` 计分、两路求和,`k` 通常取 60 用于平滑——名次越靠前贡献越大,被两路同时命中即“双倍加分”。这样绕开了“稠密分数 0.8 和 BM25 分数 12 怎么直接相加”的度量难题。
> 3. **为什么重要**:单一检索总有盲区,混合检索把召回率整体抬高、让后续检索更可靠;它不替代 Rerank,而是为 Rerank 提供“更全、更不偏科”的候选集。
> 4. **易错点/常见误解**:误解一「把两路的原始分数直接相加」——尺度不同会失衡,RRF 的意义正是用名次替代分数;误解二「RRF 求的是并集」——同一候选在两路都出现是被合并加权,而不是重复;误解三「混合 = 精排」——它只解决“召得全”,排序的最终质量还要靠后面的 Cross-Encoder 精排。
#### 3.3 Rerank 重排序(Cross-Encoder)
```mermaid
graph LR
A[便宜召回 top-20<br/>Bi-Encoder 快但粗] --> B[Cross-Encoder 精排<br/>查询+文档拼一起细算分]
B --> C[取 top-3<br/>准但慢 只对top做]
```
- **Bi-Encoder**:查询和文档各自编码再比相似度,快但粗,适合第一轮海量召回。
- **Cross-Encoder**:把「查询+文档」拼成一个序列一起送给模型精细打分,准但慢,只对候选 top-20 做。
- **两段式**:便宜召回 top-20 → Rerank 精排取 top-3,兼顾质量与速度。
```python
# 概念示例: 两段式检索的调用顺序
def two_stage_retrieve(q: str, vector_col, reranker, top_candidates=20, top_final=3):
# 第一段: Bi-Encoder 快速从10万条里召回 top-20 (便宜)
candidates = vector_col.query(query_embeddings=[embed_bge(q)], n_results=top_candidates)
# 第二段: Cross-Encoder 精排 top-20 取 top-3 (准)
reranked = reranker.rerank(
query=q,
documents=candidates["documents"],
top_n=top_final,
)
return reranked # 返回精排后的最终 3 条
```
> 🔬 **深度理解 · Rerank 精排(Bi-Encoder vs Cross-Encoder)**:
> 1. **本质**:Rerank 是**“先用便宜模型海选、再用贵模型精排”的两段式召回管线**。第一段追求召回率和速度(宁可多召回但别漏),第二段只对候选小集合做精细相关度打分,追求序的准确性。
> 2. **机制**:① **Bi-Encoder(一次召回)**:query 和 doc **分别**过同一个 encoder 得两个向量,再算相似度——在每个 doc 只与 query 比较,可离线预计算 doc 向量、超高吞吐,但**糟糕在它把“文档相关性”压成一个单点向量夹角**,细粒度交互信息(某个词到底重不重要)没法保留,所以“快但粗”;② **Cross-Encoder(精排)**:把「query 与 doc」**拼接成一个输入序列**一起送模型,让两者在 attention 层面充分交互,直接输出“这对是否相关”的精细打分——准,但无法预计算,每个候选都要单独跑一次模型,所以“准但慢”,只适合对 top-20 这类小集合做。**Bi-Encoder 开销约 O(N),Cross-Encoder 开销约 O(候选数)**,这正是“两段式”能兼顾的原因。
> 3. **为什么重要**:向量召回只保证“语义上像”,不代表“能直接回答这个问题”;Rerank 把“凭切面感觉排的候选”重排成“真正贴合 query 意图的答案”,是 RAG 回答质量显著拉高的关键一步。
> 4. **易错点/常见误解**:误解一「拿 Cross-Encoder 对全库做」——会慢到不可用,必须两段式;误解二「Rerank 重新算一遍向量」——Cross-Encoder 走的是交互打分而非向量相似度,别再调余弦;误解三「Rerank 能兜住召回没捞到的东西」——**它是“排序”不是“召回”**,top-20 里压根没有的正确答案,精排也救不回来,所以召回阶段必须放宽 top-K。
### 4. 记忆存储组合方案(生产架构)
#### 4.1 三种存储的职责分工
```mermaid
graph LR
A[记忆存储三件套] --> B[Redis<br/>短期记忆/会话状态]
A --> C[向量库 Qdrant<br/>长期记忆/语义召回]
A --> D[PostgreSQL<br/>结构化元数据/审计]
B --> B1[TTL 到期自动清理<br/>读写毫秒级]
C --> C1[语义检索 + 元数据过滤]
D --> D1[记忆归属/重要度/审计日志]
```
| 存储 | 管什么 | 为什么用它 |
|-|-|-|
| **Redis** | 会话状态 / 短期记忆 | TTL 到期自动清理,读写毫秒级 |
| **向量库(Qdrant 等)** | 长期记忆 | 语义召回 + 元数据过滤 |
| **PostgreSQL** | 结构化元数据 | 记忆归属 / 重要度 / 审计日志 |
- **核心思想**:三者各司其职,不要用一个存储硬扛所有场景。短期存 Redis、语义存向量库、结构化存 PG。
- 例:一条用户偏好记忆 → 文本进向量库(可语义召回)、`user_id + importance + 时间戳` 进 PG(可过滤审计)、最近对话进 Redis(TTL 短)。
### 5. 生产提示
- 文档入库前先完成**清洗与分块**(对照 LlamaIndex 链路,阶段三讲过)——脏数据进库,检索全是噪声。
- **Chroma 默认用英文小模型做 Embedding,中文语料建议显式指定中文 BGE 模型**(如 `BAAI/bge-small-zh-v1.5`)。
### 6. 总结:向量库怎么选
```mermaid
graph TD
A{你的场景?} -->|<1万条 本地调试| B[Chroma]
A -->|海量/并发/分布式| C[Milvus]
A -->|示例/平衡/混合检索| D[Qdrant ★]
A -->|免运维/数据可出境| E[Pinecone]
A -->|纯实验 不持久| F[FAISS]
```
## 本节自检
- [ ] 能用 Chroma 完成增删改查 + 元数据过滤
- [ ] 能用 Qdrant 实现混合检索 + Rerank,并说清 Redis / 向量库 / PG 的分工
- [ ] 能说清 HNSW 索引为什么比全扫快
- [ ] 能讲清「稠密 BM25 双路召回 + RRF 融合 + Cross-Encoder 精排」的完整链路
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:向量检索为什么能“语义相同”命中不同说法?维度与度量各指什么、选错会怎样?
- **面试官想考察**:是否理解了 embedding 从“字面”到“几何”的抽象,还是只背了“用余弦算相似度”;能否讲清维度/度量这两个隐含合同。
- **专业作答(含深度)**:
1. **为什么会“语义命中”**:embedding 把词/句映射到高维空间,语义相近的文本落在邻近区域——“年假”与“休假”的词法不同,但编码后的点相互靠近,检索=在该空间找最近邻,于是不同说法也能命中。
2. **维度是表达力预算**:384/768/1536 等代表语义“记住”多少个自由轴;维度高表达力强但更贵、更慢、更占内存,够用即止。
3. **度量是“怎么算近”**:余弦看方向(忽略长度、文本默认)、点积看方向+长度、L2 看绝对距离;面向非归一化向量时选错度量会让排序失真。
4. **两个合同**:库与模型必须维度一致 + 度量一致;换模型=维度变=全量重嵌重插,没有部分迁移。
- **加分亮点 / 深度追问**:可主动提“多数开源 embedding 输出已归一化时,余弦/点积排序往往等价,所以关键是别混用不同模型”;被追问“高维反正规问题”时可答“维度升高后几何直觉失效(n 维球体≈表面),这也是为何要借助 ANN 而非迷信维度”。
### 面试题 2:HNSW 为什么比全库扫描快?它和精确检索的取舍点在哪?
- **面试官想考察**:是否真懂 ANN 的“近似换速度”,能否讲清 HNSW 的分层导航思路以及速度-精度来自哪个旋钮。
- **专业作答(含深度)**:
1. **全扫的痛**:精确检索对每条算相似度是 O(N),百万级向量不可接受。ANN 用索引把“全扫”换成“跳邻居”来找近似近邻。
2. **HNSW 思路**:用跳表思想搭“分层导航图”——上层稀疏连接(长距离跳跃快速逼近粗区域),下层稠密连接(局部精确定位近邻),查询从上往下逐层降入,所以“跳着找”而非“挨个比”。
3. **速度-精度平衡旋钮**:`M`(图连接度,越大召回越稳、内存越高)、`ef_construction`(建索引精度/速度,定死要重建)、`ef_search`(查询精度/延迟,**运行时**可调、召回不够先加它)。判断缺不缺精度用固定查询集跑 recall@k 基线,不拍脑袋。
- **加分亮点 / 深度追问**:可并列提 IVF“先聚类再扫桶”作对照(省内存,精度略低于 HNSW);被追问“为什么 HNSW 是事实默认”时答“精度-延迟-内存三项平衡最好,且 `ef_search` 可在查询时不重建索引地动态调”。
### 面试题 3:为什么生产队偏爱「稠密+稀疏双路召回 + RRF 融合 + Cross-Encoder 精排」这一整套?
- **面试官想考察**:能否把召回(recall 阶段的目标)和精排(precision 阶段)的分工讲透,而不是把一堆名词罗列出来。
- **专业作答(含深度)**:
1. **先分层**:召回阶段目标“召得全、不偏科”,排序阶段目标“序得准、贴合意图”,两段目标不同所以要两套方案。
2. **为什么双路**:稠密向量能抓语义换词,扔了“QX-300/人名/编号”这种字面精确信息;稀疏 BM25 能抓精确词,却不懂语义。双方盲区互斥,混合让“候选集更全更有代表性”。
3. **为什么用 RRF 而不是加分数**:两路原始分数尺度不可比(0.8 vs 12),RRF 只看名次、按 `1/(k+rank)` 融合,`k=60` 平滑,被两路共同命中者权重叠加。
4. **为什么还要 Rerank**:召回的密集相似≠真的贴题;Cross-Encoder 把 query+doc 拼成一个输入做细粒度交互打分,只对 top-20 精排取 top-3,用“O(候选数)”的昂贵代价换取最终序的质量。
- **加分亮点 / 深度追问**:可强调“Rerank 是排序不是召回,top-K 放宽才扛得住漏召回”;被追问“这套在什么时候可以简化”时答“语料干净、主题单一的轻量场景,一路稠密检索+可选 Rerank 即可,混合检索在精确词场景才显出增益”。