007:RAG 入门-向量嵌入与检索
本文是 refine-rag 系列教程的第七篇,我们来学习一下什么是向量嵌入?有哪些检索方法?
本文所有代码都在:https://github.com/zonezoen/refine-rag
目录
- 前言
- 什么是向量嵌入?为什么需要它?
- 检索方法对比
- BM25 检索(关键词匹配)
- BGE-M3(多功能嵌入)
- 多模态嵌入(图文检索)
- 混合检索策略
- 方案对比与选择
- 向量维度越高越好吗?
- 学习路径
前言
前面我们学习了如何读取数据和切块,现在到了 RAG 的核心环节:向量嵌入与检索。
这一步决定了你的 RAG 系统能不能找到正确的知识点。就像图书馆的索引系统,索引做得好,找书就快;索引做得差,找半天也找不到。
什么是向量嵌入?为什么需要它?
简单来说,向量嵌入就是把文字(或图片)转成一串数字。
比如:
文本: "孙悟空使用金箍棒"
向量: [0.12, -0.34, 0.56, ..., 0.78] # 1024 个数字
为什么要转成数字?因为计算机只认识数字,不认识文字。把文字转成向量后,就可以:
- 计算相似度(两段文字有多像)
- 快速检索(从海量文档中找到相关内容)
- 聚类分析(把相似的内容归类)
向量的神奇之处:
语义相似的文本,向量也相似:
"孙悟空使用金箍棒" → [0.12, -0.34, 0.56, ...]
"悟空拿着金箍棒" → [0.15, -0.30, 0.52, ...] # 对比第一句向量很接近
"一只猫在睡觉" → [0.89, 0.23, -0.67, ...] # 对比第一句向量差异大
检索方法对比
目前主流的检索方法有三种:
1. 关键词检索(BM25)
原理:基于词频统计,不需要 embedding 模型。
示例:
查询: "烈焰拳"
文档1: "猢狲使用烈焰拳击退妖怪" ✅ 包含关键词,匹配度高
文档2: "孙悟空施展火焰技能" ❌ 不包含关键词,匹配度低
特点:
- ✅ 精确匹配关键词
- ✅ 速度快
- ❌ 无法理解同义词
2. 向量检索(Semantic Search)
原理:基于语义相似度,使用 embedding 模型。
示例:
查询: "烈焰拳"
文档1: "猢狲使用烈焰拳击退妖怪" ✅ 包含关键词,相似度高
文档2: "孙悟空施展火焰技能" ✅ 语义相似,相似度也高
特点:
- ✅ 理解语义
- ✅ 支持同义词
- ❌ 速度较慢
3. 混合检索(Hybrid Search)
原理:结合 BM25 和向量检索的优势。
示例:
查询: "烈焰拳"
BM25 分数: [0.8, 0.1, 0.6]
向量分数: [0.9, 0.7, 0.5]
混合分数: 0.7 * BM25 + 0.3 * 向量 = [0.83, 0.28, 0.57]
特点:
- ✅ 结合两者优势
- ✅ 检索质量最高
- ❌ 实现稍复杂
对比总结
| 方法 | 速度 | 精度 | 关键词匹配 | 语义理解 | 推荐度 |
|---|---|---|---|---|---|
| BM25 | ⚡⚡⚡ | ⭐⭐⭐ | ✅ 强 | ❌ 弱 | ⭐⭐⭐ |
| 向量检索 | ⚡⚡ | ⭐⭐⭐⭐ | ❌ 弱 | ✅ 强 | ⭐⭐⭐⭐ |
| 混合检索 | ⚡⚡ | ⭐⭐⭐⭐⭐ | ✅ 强 | ✅ 强 | ⭐⭐⭐⭐⭐ |
BM25 检索(关键词匹配)
最经典的检索算法,基于词频统计,不需要 embedding 模型。
文件名: 01-BM25检索-修复版.py
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
# 1. 准备测试数据
docs = [
Document(page_content="猢狲在无回谷遭遇了妖怪,妖怪开始攻击,猢狲使用铜云棒抵挡。"),
Document(page_content="妖怪使用寒冰箭攻击猢狲但被烈焰拳反击击溃。"),
Document(page_content="猢狲施展烈焰拳击退妖怪随后开启金刚体抵挡神兵攻击。"),
Document(page_content="猢狲召唤烈焰拳与毁灭咆哮击败妖怪随后收集妖怪精华。"),
Document(page_content="在战斗中猢狲使用了多种技能包括烈焰拳金刚体和铜云棒。"),
]
print("文档数量:", len(docs))
print("\n文档内容:")
for i, doc in enumerate(docs, 1):
print(f"{i}. {doc.page_content}")
# 2. 创建 BM25 检索器
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 3 # 返回前3个最相关的文档
# 3. 测试检索
queries = [
"烈焰拳",
"妖怪攻击",
"铜云棒",
"战斗技能"
]
for query in queries:
print(f"\n查询: {query}")
results = bm25_retriever.invoke(query)
print(f"检索到 {len(results)} 个相关文档:")
for i, doc in enumerate(results, 1):
print(f" {i}. {doc.page_content}")
print("-" * 50)
参数说明:
k=3:返回前 3 个最相关的文档k1=1.5:词频饱和度参数(高级,一般不用改)b=0.75:文档长度归一化参数(高级,一般不用改)
工作原理:
- 分词:把文档和查询分成词
- 计算词频:统计每个词出现的次数
- 计算 IDF:词的重要性(越少见的词越重要)
- 打分:综合词频和 IDF 计算相关度
优点:
- 速度快,无需模型
- 精确匹配关键词
- 适合专业术语搜索
缺点:
- 无法理解同义词
- 无法理解语义
适用场景: 代码搜索、专业术语搜索、关键词精确匹配
BGE-M3(多功能嵌入)
BGE-M3 是目前强大的开源嵌入模型,支持三种嵌入方式。
什么是 BGE-M3?
M3 代表:
- Multi-Functionality(多功能):支持三种嵌入方式
- Multi-Linguality(多语言):支持 100+ 种语言
- Multi-Granularity(多粒度):支持不同长度的文本
三种嵌入方式
1. 密集嵌入(Dense Embedding)
把整个文本压缩成一个向量,适合语义搜索。
文件名: 04-BGE-M3.py
from FlagEmbedding import BGEM3FlagModel
# 1. 加载模型
print("正在加载 BGE-M3 模型...")
model = BGEM3FlagModel("BAAI/bge-m3", use_fp16=False)
print("模型加载完成!\n")
# 2. 准备文本
passage = ["猢狲施展烈焰拳,击退妖怪;随后开启金刚体,抵挡神兵攻击。"]
print(f"原始文本: {passage[0]}\n")
# 3. 生成密集嵌入
passage_embeddings = model.encode(
passage,
return_dense=True # 只返回密集嵌入
)
dense_vecs = passage_embeddings["dense_vecs"]
print("【1. 密集嵌入 (Dense Embedding)】")
print(f"维度: {dense_vecs[0].shape}")
print(f"说明: 整个文本被压缩成一个 {dense_vecs[0].shape[0]} 维的向量")
print(f"用途: 语义搜索、相似度计算")
print(f"前10维示例: {dense_vecs[0][:10]}")
特点:
- 整个文本一个向量
- 理解语义相似度
- 适合问答系统
2. 稀疏嵌入(Sparse Embedding)
类似 BM25,只存储重要的词及其权重。
# 生成稀疏嵌入
passage_embeddings = model.encode(
passage,
return_sparse=True # 只返回稀疏嵌入
)
sparse_vecs = passage_embeddings["lexical_weights"]
print("【2. 稀疏嵌入 (Sparse Embedding)】")
print(f"非零元素数量: {len(sparse_vecs[0])}")
print(f"说明: 只存储重要的 token 及其权重(类似 BM25)")
print(f"用途: 关键词匹配、精确检索")
print(f"前10个非零值示例:")
for token_id, weight in list(sparse_vecs[0].items())[:10]:
print(f" Token ID {token_id}: 权重 {weight:.4f}")
特点:
- 只存储重要的词
- 精确匹配关键词
- 类似 BM25 效果
3. 多向量嵌入(ColBERT Multi-Vector)
每个词都有一个独立的向量,最精确但最慢。
你可以把“多向量嵌入”想象成一个为文档的每个词都配备了独立“小磁铁”的精确搜索系统。
通俗解释:
- 普通搜索(单向量):把一整段话变成一个“大毛线团”来代表。比较两个“毛线团”时,只能看整体像不像,比较粗糙。
- 多向量搜索:把一段话的每个词都变成一块独立的“小磁铁”。搜索时,把你的问题也拆成“小磁铁”,然后去文档里一块一块地对吸。只要有一块能对上,就能找到相关信息。
举个例子:
- 文档:“这只棕色的狐狸敏捷地跳过了那只懒惰的狗。”
- 你的问题:“关于那只狗的句子。”
过程如下:
- 拆成“小磁铁”:
- 文档被拆成:
[这, 只, 棕色, 的, 狐狸, 敏捷, 地, 跳过, 了, 那, 只, 懒惰, 的, 狗],每个词变成一个向量(小磁铁)。 - 你的问题被拆成:
[关于, 那, 只, 狗],每个词也变成一个向量。
- 文档被拆成:
- 精细匹配:系统会用你问题里的每个“小磁铁”,去文档里寻找能“吸住”(即相似)的磁铁。
- 问题中的
狗会强烈匹配文档中的狗。 - 问题中的
那和只也可能匹配到文档中“那只狗”前面的那和只。
- 问题中的
- 得出结果:由于“狗”这个词的磁铁完美匹配上了,系统就能精准地找到包含“狗”的这句话,并返回给你。
总结它的特点:
- 为什么最精确:因为它进行的是“词对词”的精细对比,能捕捉到具体的术语和表述,即使整体意思不完全一样。
- 为什么最慢:想象一下,一段话有20个词,问题有5个词,那就需要比较 20 x 5 = 100 次。如果文档库很大,这个计算量是非常惊人的。
如果你在做对准确率要求极高的搜索(比如法律条文查询、精密技术文档检索),哪怕多花点钱、慢一点,也要用 ColBERT;如果只是普通的网页搜索或聊天机器人,普通向量就够用了。
# 生成多向量嵌入
passage_embeddings = model.encode(
passage,
return_colbert_vecs=True # 返回多向量嵌入
)
colbert_vecs = passage_embeddings["colbert_vecs"]
print("【3. 多向量嵌入 (ColBERT Multi-Vector)】")
print(f"维度: {colbert_vecs[0].shape}")
print(f"说明: 文本被分成 {colbert_vecs[0].shape[0]} 个 token")
print(f" 每个 token 有一个 {colbert_vecs[0].shape[1]} 维向量")
print(f"用途: 精确匹配、细粒度检索")
特点:
- 每个词一个向量
- 最精确的匹配
- 计算成本最高
三种嵌入对比
| 嵌入类型 | 维度 | 速度 | 精度 | 适用场景 |
|---|---|---|---|---|
| 密集嵌入 | (1024,) | ⚡⚡⚡ | ⭐⭐⭐⭐ | 语义搜索、问答系统 |
| 稀疏嵌入 | 字典 | ⚡⚡⚡ | ⭐⭐⭐ | 关键词搜索、精确匹配 |
| 多向量嵌入 | (tokens, 1024) | ⚡ | ⭐⭐⭐⭐⭐ | 高精度检索、学术研究 |
多模态嵌入(图文检索)
多模态嵌入可以将图片和文本映射到同一个向量空间,实现图文检索。
什么是多模态嵌入?
简单来说,就是让图片和文字"说同一种语言"。
示例:
图片: [一张悟空战斗的图片]
图片向量: [0.12, -0.34, 0.56, ..., 0.78]
文本: "悟空在战斗"
文本向量: [0.15, -0.30, 0.52, ..., 0.75]
相似度: 0.95 ✅ 很相似!
3.1 本地 CLIP 模型(推荐)
CLIP 是 OpenAI 开发的多模态模型,可以理解图片和文本的关系。
文件名: 05-多模态嵌入-CLIP版本.py,代码就不详细展示了,可以看看:https://github.com/zonezoen/refine-rag
优点:
- 完全免费,无限制
- 支持图文检索
- 模型成熟稳定
- 可以实现以图搜图
缺点:
- 需要下载模型(约 600MB)
- 需要本地计算资源
适用场景: 图文检索、以图搜图、零样本图像分类
3.2 Jina AI API(真正的多模态)
如果不想下载模型,可以使用 Jina AI 的多模态 embedding API。
文件名: 07-真正的多模态嵌入-JinaAI.py
import os
import requests
import base64
from io import BytesIO
from PIL import Image
from dotenv import load_dotenv
import numpy as np
load_dotenv()
class JinaMultimodalEmbedding:
"""Jina AI 多模态嵌入客户端"""
def __init__(self, api_key=None):
self.api_key = api_key or os.getenv("JINA_API_KEY")
self.api_url = "https://api.jina.ai/v1/embeddings"
self.model = "jina-clip-v1"
def image_to_base64(self, image_path):
"""将图片转为 base64 编码"""
with Image.open(image_path) as img:
img.thumbnail((512, 512))
if img.mode == 'RGBA':
img = img.convert('RGB')
buffered = BytesIO()
img.save(buffered, format="JPEG")
img_str = base64.b64encode(buffered.getvalue()).decode()
return f"data:image/jpeg;base64,{img_str}"
def encode_text(self, text):
"""编码文本"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
data = {
"model": self.model,
"input": [{"text": t} for t in (text if isinstance(text, list) else [text])]
}
response = requests.post(self.api_url, headers=headers, json=data)
result = response.json()
embeddings = [item["embedding"] for item in result["data"]]
return np.array(embeddings)
def encode_image(self, image_path):
"""编码图片"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
image_base64 = self.image_to_base64(image_path)
data = {
"model": self.model,
"input": [{"image": image_base64}]
}
response = requests.post(self.api_url, headers=headers, json=data)
result = response.json()
embedding = result["data"][0]["embedding"]
return np.array(embedding)
# 使用示例
client = JinaMultimodalEmbedding()
# 编码图片和文本
image_vec = client.encode_image("image.jpg")
text_vec = client.encode_text("悟空在战斗")
# 计算相似度
similarity = np.dot(image_vec, text_vec[0])
print(f"相似度: {similarity:.4f}")
优点:
- API 调用,无需下载模型
- 真正的多模态 embedding
- 国内可访问
- 有免费额度(100万 tokens/月)
缺点:
- 需要注册账号
- 超出免费额度需付费
适用场景: 不想下载模型、需要真正的多模态 embedding
混合检索策略
实际项目中,混合检索往往效果最好。通常很多面试官问你的问题,都会涉及到混合检索,或者说也是你的回答要点之一。
文件名: 03-LangChain-BM25-OpenSource.py
from langchain_community.retrievers import BM25Retriever
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.documents import Document
# 1. 准备数据
battle_logs = [
"猢狲身披锁子甲。",
"猢狲在无回谷遭遇了妖怪,妖怪开始攻击,猢狲使用铜云棒抵挡。",
"猢狲施展烈焰拳击退妖怪随后开启金刚体抵挡神兵攻击。",
"妖怪使用寒冰箭攻击猢狲但被烈焰拳反击击溃。",
"猢狲召唤烈焰拳与毁灭咆哮击败妖怪随后收集妖怪精华。"
]
docs = [Document(page_content=log) for log in battle_logs]
# 2. 创建 BM25 检索器
bm25_retriever = BM25Retriever.from_texts(battle_logs)
bm25_retriever.k = 3
# 3. 创建向量检索器
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
vector_store = InMemoryVectorStore(embeddings)
vector_store.add_documents(docs)
vector_retriever = vector_store.as_retriever()
# 4. 查询
query = "猢狲有什么装备和招数?"
# BM25 检索
bm25_results = bm25_retriever.invoke(query)
print("BM25 检索结果:")
for doc in bm25_results:
print(f" - {doc.page_content}")
# 向量检索
vector_results = vector_retriever.invoke(query)
print("\n向量检索结果:")
for doc in vector_results:
print(f" - {doc.page_content}")
# 混合检索(去重)
hybrid_results = list({doc.page_content for doc in bm25_results + vector_results})
print("\n混合检索结果:")
for content in hybrid_results:
print(f" - {content}")
混合策略说明:
-
简单合并(上面的示例)
- 取两种检索结果的并集
- 去重
- 简单但有效
-
加权融合(高级)
# 计算加权分数 final_score = 0.7 * bm25_score + 0.3 * vector_score -
重排序(最优)
# 先用 BM25 快速筛选 # 再用向量模型重排序 candidates = bm25_retriever.invoke(query, k=20) final_results = rerank_with_vector(candidates, query, k=5)
方案对比与选择
文本检索方案对比
| 方案 | 速度 | 精度 | 成本 | 适用场景 | 推荐度 |
|---|---|---|---|---|---|
| BM25 | ⚡⚡⚡ | ⭐⭐⭐ | 免费 | 关键词搜索、代码搜索 | ⭐⭐⭐ |
| BGE-M3 密集 | ⚡⚡ | ⭐⭐⭐⭐ | 免费 | 语义搜索、问答系统 | ⭐⭐⭐⭐ |
| BGE-M3 混合 | ⚡⚡ | ⭐⭐⭐⭐⭐ | 免费 | 高质量检索 | ⭐⭐⭐⭐⭐ |
| 混合检索 | ⚡⚡ | ⭐⭐⭐⭐⭐ | 免费 | 通用场景 | ⭐⭐⭐⭐⭐ |
多模态方案对比
| 方案 | 类型 | 成本 | 国内访问 | 推荐度 |
|---|---|---|---|---|
| 本地 CLIP | 真多模态 | 免费 | ✅ | ⭐⭐⭐⭐⭐ |
| Jina AI | 真多模态 | 有免费额度 | ✅ | ⭐⭐⭐⭐⭐ |
| 千问+BGE | 伪多模态 | ¥0.008/千tokens | ✅ | ⭐⭐⭐⭐ |
向量维度越高越好吗?
答:不一定。
- 维度高:表达能力强,但计算慢,存储大
- 维度低:速度快,存储小,但精度稍低
常见维度:
- 384 维:轻量级,适合移动端
- 768 维:平衡,最常用
- 1024 维:高精度,适合服务器
推荐:
- 一般应用:768 维(如 BGE-base)
- 高精度:1024 维(如 BGE-M3)
- 移动端:384 维(如 BGE-small)
学习路径
- 简易RAG 学习
- LCEL 语法学习
- LangChain 读取数据
- LangChain 读取文本数据
- LangChain 读取图片数据
- LangChain 读取 PDF 数据
- LangChain 读取表格数据
- 文本切块
- 向量嵌入与检索
- 向量存储
- 检索前处理
- 索引优化
- 检索后处理
- 响应生成
- 系统评估
项目地址
本文所有代码示例都在 GitHub 开源:
https://github.com/zonezoen/refine-rag
欢迎 Star 和 Fork,一起学习 RAG 技术!

浙公网安备 33010602011771号