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: "孙悟空施展火焰技能" ❌ 不包含关键词,匹配度低

特点

  • ✅ 精确匹配关键词
  • ✅ 速度快
  • ❌ 无法理解同义词

原理:基于语义相似度,使用 embedding 模型。

示例

查询: "烈焰拳"
文档1: "猢狲使用烈焰拳击退妖怪" ✅ 包含关键词,相似度高
文档2: "孙悟空施展火焰技能" ✅ 语义相似,相似度也高

特点

  • ✅ 理解语义
  • ✅ 支持同义词
  • ❌ 速度较慢

原理:结合 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:文档长度归一化参数(高级,一般不用改)

工作原理:

  1. 分词:把文档和查询分成词
  2. 计算词频:统计每个词出现的次数
  3. 计算 IDF:词的重要性(越少见的词越重要)
  4. 打分:综合词频和 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)

每个词都有一个独立的向量,最精确但最慢。
你可以把“多向量嵌入”想象成一个为文档的每个词都配备了独立“小磁铁”的精确搜索系统

通俗解释:

  • 普通搜索(单向量):把一整段话变成一个“大毛线团”来代表。比较两个“毛线团”时,只能看整体像不像,比较粗糙。
  • 多向量搜索:把一段话的每个词都变成一块独立的“小磁铁”。搜索时,把你的问题也拆成“小磁铁”,然后去文档里一块一块地对吸。只要有一块能对上,就能找到相关信息。

举个例子:

  • 文档:“这只棕色的狐狸敏捷地跳过了那只懒惰的狗。”
  • 你的问题:“关于那只狗的句子。”

过程如下:

  1. 拆成“小磁铁”
    • 文档被拆成:[这, 只, 棕色, 的, 狐狸, 敏捷, 地, 跳过, 了, 那, 只, 懒惰, 的, 狗],每个词变成一个向量(小磁铁)。
    • 你的问题被拆成:[关于, 那, 只, 狗],每个词也变成一个向量。
  2. 精细匹配:系统会用你问题里的每个“小磁铁”,去文档里寻找能“吸住”(即相似)的磁铁。
    • 问题中的 会强烈匹配文档中的
    • 问题中的 也可能匹配到文档中“那只狗”前面的
  3. 得出结果:由于“狗”这个词的磁铁完美匹配上了,系统就能精准地找到包含“狗”的这句话,并返回给你。

总结它的特点:

  • 为什么最精确:因为它进行的是“词对词”的精细对比,能捕捉到具体的术语和表述,即使整体意思不完全一样。
  • 为什么最慢:想象一下,一段话有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}")

混合策略说明:

  1. 简单合并(上面的示例)

    • 取两种检索结果的并集
    • 去重
    • 简单但有效
  2. 加权融合(高级)

    # 计算加权分数
    final_score = 0.7 * bm25_score + 0.3 * vector_score
    
  3. 重排序(最优)

    # 先用 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)

学习路径

  1. 简易RAG 学习
  2. LCEL 语法学习
  3. LangChain 读取数据
    1. LangChain 读取文本数据
    2. LangChain 读取图片数据
    3. LangChain 读取 PDF 数据
    4. LangChain 读取表格数据
  4. 文本切块
  5. 向量嵌入与检索
  6. 向量存储
  7. 检索前处理
  8. 索引优化
  9. 检索后处理
  10. 响应生成
  11. 系统评估

项目地址

本文所有代码示例都在 GitHub 开源:

https://github.com/zonezoen/refine-rag

欢迎 Star 和 Fork,一起学习 RAG 技术!

posted @ 2026-03-17 15:04  zone7  阅读(75)  评论(0)    收藏  举报