向量检索:从 Embedding 到向量数据库
上一篇文章讲了文档切分——RAG 管线的第一步。切分好的文本块需要被转成数字向量,才能被检索系统使用。这篇文章讲第二步和第三步:向量化和向量存储/检索。
一、Embedding:把文本变成向量
1.1 什么是 Embedding
Embedding 模型把文本映射到一个高维向量空间中。语义相近的文本,其向量在空间中也相近。
"猫" → [0.23, -0.15, 0.87, ..., 0.42] (1536维)
"狗" → [0.21, -0.13, 0.85, ..., 0.40] (1536维) ← 和"猫"很接近
"量子计算" → [-0.56, 0.72, 0.03, ..., -0.91] (1536维) ← 和"猫"很远
向量的维度通常是 384、768、1024 或 1536。维度越高,能表达的语义信息越丰富,但计算和存储成本也越高。
两个向量之间的距离(通常是余弦相似度)就代表了对应文本之间的语义相似度。余弦相似度越接近 1,语义越相似;越接近 0,越不相关。
1.2 主流 Embedding 模型对比
| 模型 | 维度 | 参数量 | 特点 | 价格 |
|---|---|---|---|---|
| text-embedding-3-large | 3072 | - | OpenAI 最强,MTEB 基准领先 | $0.13/1M tokens |
| text-embedding-3-small | 1536 | - | OpenAI 性价比款,效果不错 | $0.02/1M tokens |
| embed-v3 | 1024 | - | Cohere,支持压缩维度 | $0.10/1M tokens |
| BGE-large | 1024 | 326M | 开源首选,中英文效果好 | 免费 |
| GTE-large | 1024 | 326M | 阿里开源,中文场景优秀 | 免费 |
| E5-large | 1024 | 326M | Meta 开源,多语言支持好 | 免费 |
1.3 怎么选 Embedding 模型
选择 Embedding 模型需要考虑三个因素:
效果: 在 MTEB(Massive Text Embedding Benchmark)上的分数。这个基准覆盖了 58 个数据集,包括分类、聚类、检索、语义相似度等任务。分数越高,模型在各种任务上的平均表现越好。
速度: 生成一个向量需要多长时间。本地模型取决于你的 GPU,API 模型取决于服务商的响应速度。
成本: API 模型按 token 计费,本地模型需要 GPU 资源。
选择建议:
- 快速原型:用 text-embedding-3-small,API 调用,零运维
- 中文场景:BGE-large 或 GTE-large,开源免费,中文效果好
- 多语言场景:E5-large,多语言支持好
- 追求极致效果:text-embedding-3-large,但贵
- 资源有限:BGE-small 或 GTE-small,参数量小,速度快
1.4 Embedding 的使用方式
# 方式 1:OpenAI API
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vector = embeddings.embed_query("什么是检索增强生成?")
vectors = embeddings.embed_documents(["文档1", "文档2", "文档3"])
# 方式 2:本地模型(Sentence Transformers)
from langchain_community.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-large-en-v1.5")
vector = embeddings.embed_query("什么是检索增强生成?")
# 方式 3:Cohere API
from langchain_cohere import CohereEmbeddings
embeddings = CohereEmbeddings(model="embed-english-v3.0")
vector = embeddings.embed_query("什么是检索增强生成?")
二、向量数据库
有了向量,需要一个地方存储和检索它们。向量数据库就是干这个的。
2.1 主流向量数据库对比
| 数据库 | 类型 | 最大规模 | 特点 | 运维成本 |
|---|---|---|---|---|
| Chroma | 嵌入式 | 百万级 | 零配置,Python 原生,适合原型 | 极低 |
| Milvus | 分布式 | 十亿级 | 高性能,支持大规模数据 | 高 |
| Pinecone | 云服务 | 十亿级 | 全托管,无需运维 | 低(但按量付费) |
| Weaviate | 开源 | 亿级 | 混合检索,GraphQL 接口 | 中 |
| Qdrant | 开源 | 十亿级 | 高性能,Rust 实现 | 中 |
| pgvector | PG 扩展 | 亿级 | 复用 PG 基础设施 | 低(已有 PG 的话) |
2.2 怎么选向量数据库
原型阶段:Chroma。零配置,pip install 就能用,数据存在本地磁盘。
from langchain_community.vectorstores import Chroma
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
中小规模生产:Qdrant 或 Weaviate。开源免费,性能优秀,支持混合检索。
大规模生产:Milvus 或 Pinecone。Milvus 适合自托管的大规模场景,Pinecone 适合不想运维的团队。
已有 PostgreSQL 基础设施:pgvector。不需要额外部署,直接在 PG 里加向量搜索能力。
# pgvector 示例
from langchain_community.vectorstores import PGVector
vectorstore = PGVector.from_documents(
documents=chunks,
embedding=embeddings,
connection_string="postgresql://user:pass@localhost:5432/rag_db"
)
2.3 索引策略
向量数据库的检索速度取决于索引类型。常见的索引:
| 索引类型 | 原理 | 速度 | 精度 | 适用场景 |
|---|---|---|---|---|
| Flat | 暴力搜索,计算每个向量的距离 | 慢 | 100% | 小数据集(<10 万) |
| IVF | 先聚类,搜索最近的簇 | 快 | 95%+ | 中等规模 |
| HNSW | 层级可导航小世界图 | 很快 | 99%+ | 大规模,首选 |
| PQ | 乘积量化,压缩向量 | 很快 | 90%+ | 内存受限场景 |
选择建议:大多数场景用 HNSW。它在速度和精度之间取得了最好的平衡。Chroma 默认用 HNSW,Milvus 和 Qdrant 也推荐 HNSW。
三、混合检索
纯向量检索有盲区。考虑以下场景:
- "LangChain 的 LCEL 是什么?"——用户明确提到了技术术语 "LCEL",但向量检索可能返回关于 LangChain 其他方面的内容。
- "2026 年 3 月的产品发布会"——包含精确日期,向量检索很难精确匹配日期。
混合检索结合向量检索和关键词检索(BM25),互相补充。
3.1 为什么需要混合检索
| 维度 | 向量检索 | 关键词检索(BM25) |
|---|---|---|
| 优势 | 理解语义,"汽车"能匹配到"轿车" | 精确匹配,"LangChain"只匹配 "LangChain" |
| 劣势 | 不擅长精确匹配技术术语、日期、专有名词 | 不理解语义,"汽车"和"轿车"互不认识 |
| 擅长场景 | 模糊查询、语义查询 | 精确查询、技术术语查询 |
3.2 混合检索的实现
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# 向量检索器
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# BM25 关键词检索器
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
# 混合检索器
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.6, 0.4] # 向量检索权重 0.6,BM25 权重 0.4
)
results = ensemble_retriever.invoke("LangChain 的 LCEL 是什么?")
3.3 融合策略
多路召回的结果需要融合。常见的融合方法:
加权融合:给不同检索器的结果加权求和。简单直接,但权重需要调参。
RRF(Reciprocal Rank Fusion):基于排名的融合。每个检索器的结果按排名赋予分数(1/rank),最后按总分排序。不需要调参,效果通常很好。
from langchain.retrievers import EnsembleRetriever
# RRF 融合(默认)
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.5, 0.5],
c=60 # RRF 常数,通常为 60
)
我的建议:先用 RRF,效果不好再尝试加权融合。RRF 不需要调参,通常效果已经很好了。
四、Reranking:检索效果提升的利器
4.1 为什么需要 Reranking
向量检索的结果是按相似度排序的,但不一定是最相关的。特别是当查询比较复杂、chunk 比较多时,初步检索的 top-k 结果可能包含一些"看起来相似但实际不相关"的 chunk。
Reranking 用一个更精准的模型对初步检索结果重新排序,把真正相关的 chunk 排到前面。
4.2 交叉编码器(Cross-Encoder)
Reranking 通常用交叉编码器。和双编码器(Bi-Encoder,即 Embedding 模型)不同:
| 维度 | 双编码器(Embedding) | 交叉编码器(Reranker) |
|---|---|---|
| 输入 | 单个文本 | 文本对(query + document) |
| 输出 | 向量 | 相关性分数 |
| 速度 | 快(可以预计算) | 慢(每次都要推理) |
| 精度 | 较低 | 较高 |
| 用途 | 初步检索 | 重排序 |
交叉编码器能同时看到 query 和 document,判断它们之间的相关性。这比分别编码再算相似度要精准得多。
4.3 Reranking 的实现
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from sentence_transformers import CrossEncoder
# 基础检索器(向量检索)
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20}) # 先召回更多
# Reranker
cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
reranker = CrossEncoderReranker(model=cross_encoder, top_n=5)
# 压缩检索器(先检索 20 个,再 rerank 取前 5 个)
compression_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever
)
results = compression_retriever.invoke("LangChain 的 LCEL 是什么?")
4.4 Reranking 的效果
Reranking 通常能显著提升检索效果。根据我的经验:
- 纯向量检索的 top-5 准确率假设是 70%
- 加上 reranking 后,top-5 准确率通常能提升到 85-90%
- 代价是增加 100-500ms 的延迟(取决于文档数量)
建议:如果对检索精度要求高,reranking 是值得的投入。如果只是内部工具,纯向量检索可能就够了。
五、从项目看向量检索:Claude Code MagmaAdapter
回到 Claude Code 的记忆系统。它的向量检索实现比较基础——纯向量检索,没有混合检索,没有 reranking。
// MagmaAdapter 的核心检索逻辑
async readLanceDB(query: string, limit: number = 10, layer?: string) {
const queryVector = await this.embedQuery(query, lancedb)
for (const tableName of await db.tableNames()) {
const table = await db.openTable(tableName)
const results = await table
.vectorSearch(queryVector)
.limit(limit)
.distanceType('COSINE')
.toArray()
results.push({
key, value,
score: 1 - entry._distance // 余弦距离转相似度
})
}
return results.sort((a, b) => b.score - a.score).slice(0, limit)
}
这个实现的特点:
- 纯向量检索:只用 LanceDB 的 vectorSearch
- COSINE 距离:标准的余弦相似度
- 按分数排序:最简单的排序方式
- 没有混合检索:没有 BM25
- 没有 reranking:没有交叉编码器
为什么这么基础?因为记忆系统的数据量不大(几十到几百条),而且每条记忆都是人工撰写的、格式规范的文本。在这种场景下,基础向量检索已经够用。
但对于大规模的 RAG 系统(几千到几百万个 chunk),只做纯向量检索是不够的。你需要混合检索来提高召回率,需要 reranking 来提高精度。
六、向量检索的常见坑
坑 1:Embedding 模型不匹配
症状:检索结果和问题的语义相关性不高。
原因:Embedding 模型没有正确理解你的数据类型。比如用英文模型处理中文文档,或者用通用模型处理专业领域文档。
解决:换一个更适合的 Embedding 模型。
坑 2:chunk 太大导致向量质量下降
症状:检索结果包含大量不相关内容。
原因:chunk 太大,一个 chunk 里包含多个主题,向量被"稀释"。
解决:减小 chunk_size,或换用更精准的切分策略。
坑 3:只用向量检索
症状:精确关键词匹配效果不好。
原因:向量检索不擅长匹配技术术语、日期、专有名词。
解决:加入 BM25 关键词检索,做混合检索。
坑 4:检索结果不够精准
症状:top-k 结果里有一些不太相关的 chunk。
原因:初步检索的精度不够。
解决:加上 reranking。先召回更多结果(top-20 或 top-50),再用交叉编码器重排序取 top-5。
坑 5:向量数据库性能不够
症状:检索延迟太高。
原因:数据量太大,或者索引策略不对。
解决:
- 检查索引类型(推荐 HNSW)
- 调整索引参数(HNSW 的
M和efConstruction) - 考虑分片或分布式部署
七、总结
向量检索是 RAG 管线的核心环节。
核心要点:
- Embedding 模型要根据数据类型选择,中文用 BGE/GTE,多语言用 E5
- 向量数据库要根据规模选择,原型用 Chroma,生产用 Qdrant/Milvus/Pinecone
- 混合检索是生产标配,向量 + BM25,RRF 融合
- Reranking 是效果提升的利器,先召回更多,再重排序
下一篇文章讲检索后优化——拿到检索结果后,怎么进一步提升 RAG 的效果。
系列: