向量数据库与 RAG 检索原理
向量数据库与 RAG 检索原理
本文讲清 RAG 为什么离不开向量检索:Embedding 如何把语义变成向量、ANN 索引如何在上亿向量里快速找近邻、RAG 的完整链路与常见坑。
为什么需要向量检索
传统检索靠关键词匹配,用户搜"怎么退订会员",文档里写的是"取消订阅流程",字面不重合就搜不到。向量检索先把文本编码成语义向量,再用向量距离衡量相似度,能跨越字面差异。
RAG(Retrieval-Augmented Generation)就是在这个基础上,把检索到的片段塞进大模型上下文,让回答有据可依,减少幻觉。
Embedding:文本怎么变成向量
Embedding 模型把一段文本映射成固定维度的浮点数组,语义相近的文本在向量空间里距离更近。常见维度是 768、1024、1536。
相似度一般用余弦相似度:
cos(a, b) = (a · b) / (|a| × |b|)
值越接近 1 越相似。工程上通常先把向量归一化,这样余弦相似度等价于内积,计算更快。
选型要点:中文场景优先选中文语料训练过的模型;同一批数据必须用同一个模型编码,换模型要全量重建索引。
向量数据库:存什么、怎么查
向量数据库要解决三件事:存向量和元数据、建 ANN 索引、支持过滤与更新。
精确最近邻(暴力算所有距离)在百万级还能扛,上亿就不现实。所以用近似最近邻(ANN),用少量召回损失换数量级的性能提升。
主流 ANN 索引
| 索引 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| HNSW | 多层可导航小世界图 | 查询快、召回高 | 内存占用大、构建慢 |
| IVF | 先聚类分桶,再查少数桶 | 内存友好、可扩展 | 需要训练、边界样本易漏 |
| IVF-PQ | IVF + 乘积量化压缩 | 亿级可用、省内存 | 有量化误差 |
| Flat | 暴力精确检索 | 最准 | 慢,只适合小数据 |
HNSW 是当前最常用的默认选择,参数 M(每层连接数)和 ef_construction 影响召回与构建成本,查询时 ef_search 影响召回与延迟。
常见产品
- Milvus:分布式、功能全,适合大规模。
- Qdrant:Rust 实现,过滤能力强,部署轻。
- Weaviate:自带混合检索与模块化。
- pgvector:PostgreSQL 扩展,已有 PG 时最省事。
- Redis / Elasticsearch:也提供向量字段,适合已有基础设施复用。
RAG 的完整链路
- 文档解析:PDF、HTML、Markdown 转成纯文本,保留标题层级。
- 切分(chunk):按语义或结构切成 300~800 字的片段,带少量重叠。
- 嵌入:用 Embedding 模型把每个 chunk 编码成向量。
- 入库:向量 + 原文 + 元数据(来源、章节、时间)写入向量库。
- 检索:用户问题编码后查 Top-K 近邻,可叠加元数据过滤。
- 重排(rerank):用交叉编码器对候选片段精排,提升相关性。
- 生成:把片段拼进 prompt,要求模型只依据上下文回答并给出引用。
切分策略
- 固定长度切分简单,但容易把一句话切断。
- 按标题/段落切分更符合语义,适合结构化文档。
- 父子块:用小块检索、用大块送给模型,兼顾召回与上下文完整。
- 重叠窗口:相邻块保留 10%~20% 重叠,避免边界信息丢失。
混合检索与重排
纯向量检索对专有名词、编号、代码不敏感,纯关键词检索又不懂语义。实践中常用 BM25 + 向量混合召回,再用 RRF(倒数排名融合)合并。
重排模型(如 bge-reranker)对「问题-片段」逐对打分,精度高于向量相似度,但成本高,一般只对 Top 20~50 做精排。
一个最小可用的 RAG 示例
from openai import OpenAI
client = OpenAI()
def embed(texts):
resp = client.embeddings.create(model="text-embedding-3-small", input=texts)
return [d.embedding for d in resp.data]
# 1. 切分并入库(伪代码,向量库可为 pgvector/Qdrant/Milvus)
chunks = split_documents(docs, chunk_size=500, overlap=80)
vectors = embed([c.text for c in chunks])
vector_store.upsert(chunks, vectors)
# 2. 检索
q_vec = embed([question])[0]
hits = vector_store.search(q_vec, top_k=20)
# 3. 重排
reranked = rerank(question, hits, top_n=5)
# 4. 生成
context = "\n\n".join(h.text for h in reranked)
prompt = f"仅根据以下资料回答,找不到就说不知道。\n\n{context}\n\n问题:{question}"
answer = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
).choices[0].message.content
常见坑
- 切分太碎:语义不完整,召回片段答非所问。先保证段落完整,再控制长度。
- 只做向量检索:编号、人名、代码搜不准,必须配关键词召回。
- 不做重排:Top-K 里混入弱相关片段,模型被误导。
- 元数据缺失:无法按时间、部门、权限过滤,召回一堆过期或越权内容。
- 换模型不重建索引:查询向量和库向量不在同一空间,结果完全错乱。
- 忽略评估:只看 demo 效果,不做召回率与忠实度评测。
评估怎么做
- 检索侧:Recall@K、MRR、命中率。
- 生成侧:忠实度(是否只依据上下文)、答案相关性、引用准确率。
- 端到端:人工抽样 + 固定问题集回归,防止调参把已有效果调坏。
最佳实践
- 先做小规模端到端跑通,再优化召回,不要一上来就调索引参数。
- 中文优先选中文 Embedding,且全链路统一模型版本。
- 结构化文档按标题切分,并保留来源元数据。
- 混合检索 + 重排是性价比最高的提升手段。
- 给模型明确的"不知道就说不知道"约束,并在回答里带引用。
- 建回归测试集,每次改切分、模型、参数都跑一遍。
总结
| 环节 | 关键点 |
|---|---|
| Embedding | 语义向量,模型全链路统一 |
| 索引 | HNSW 常用,IVF-PQ 省内存 |
| 切分 | 保语义、留重叠、带元数据 |
| 检索 | 向量 + 关键词混合,再重排 |
| 生成 | 限定上下文、要求引用 |
| 评估 | 召回率 + 忠实度 + 回归集 |
RAG 的效果上限由检索决定:检索不到,模型再强也只能编。把切分、混合召回和重排做扎实,比换更大的模型更划算。
- 点赞
- 收藏
- 关注作者
评论(0)