向量检索:从 Embedding 到向量数据库

向量检索:从 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 的 MefConstruction
  • 考虑分片或分布式部署

七、总结

向量检索是 RAG 管线的核心环节。

核心要点:

  • Embedding 模型要根据数据类型选择,中文用 BGE/GTE,多语言用 E5
  • 向量数据库要根据规模选择,原型用 Chroma,生产用 Qdrant/Milvus/Pinecone
  • 混合检索是生产标配,向量 + BM25,RRF 融合
  • Reranking 是效果提升的利器,先召回更多,再重排序

下一篇文章讲检索后优化——拿到检索结果后,怎么进一步提升 RAG 的效果。


系列