Python RAG 实战手册——嵌入

0 阅读30分钟

Embedding models 会将文本、图像和其他内容转换为 vectors,用这些向量捕捉语义含义。在 RAG 系统中,这些 vectors 让 retriever 能够在大型非结构化集合中,搜索与用户问题相关的内容。Retriever 会对 query 做 embedding,将其与 vector database 中存储的 vectors 进行比较,并按距离对候选项排序。距离越小,表示语义相似度越高,这也决定了哪些内容可以放入 LLM 有限的 context window 中。

一个典型的基于 embedding 的 retrieval flow 如下:

  1. 构建 vector database 时,将 documents 拆分成 chunks 并进行 embedding。
  2. 使用同一个模型对每个进入的 user query 做 embedding。
  3. 计算 query vector 与已存储 vectors 之间的距离。
  4. 检索最接近的 chunks,并将它们作为 context 传给 LLM。

本章展示如何使用来自 OpenAI、Google 和开源项目等 providers 的 embedding models。你将生成 embeddings、可视化语义关系、度量 vector distances,并将它们用于实际 RAG pipelines。Recipes 还会覆盖模型选择、多模态 embeddings,以及将 vectors 与关键词或 metadata filters 结合的 hybrid retrieval。

你可以在本书 GitHub repository 中找到本章所有代码示例。

5.1 将 Text Chunks 的语言含义映射为数值表示

Problem

你想将单词和句子的语义含义映射为数值表示。

Solution

使用 embedding model 将文本转换为 numerical vectors。下面的代码示例会使用 OpenAI 和开源模型进行演示。图 5-1 展示了生成的 vectors 如何让语义相似文本在 embedding space 中彼此更接近。

image.png

图 5-1:Vector space 中 text chunks 之间的 semantic similarity

Text chunk 的最大大小不能超过 embedding model 支持的最大 token length。表 5-1 展示了流行 embedding models 的最大 token window 和维度数量。

表 5-1:现代 embedding models

ModelCompanyPlatformMax tokensDimensions
text-embedding-3-smallOpenAIOpenAI API8,1911,536
voyage-large-2 instructVoyage AIVoyage API16,0001,536
text-embedding-005GoogleGemini API2,048768
all-MiniLM-L6-v2Open sourceHugging Face512384

这个 recipe 包含两个示例模型:

  • OpenAI 的一个强大 embedding model
  • 流行开源模型 all-MiniLM-L6-v2,准确率较低,但可以本地运行

要使用它们,需要安装 OpenAI SDK 和 Sentence Transformers library:

pip install openai sentence-transformers

然后使用 OpenAI SDK 和 OpenAI 的一个 text embedding model,为两个示例文本字符串生成 embedding vectors:

from openai import OpenAI

text_chunks = ["The sky is blue.", "The grass is green."]

client = OpenAI()  # Uses the environment variable OPENAI_API_KEY

embeddings_list = []

for text_chunk in text_chunks:
    response = client.embeddings.create(
        input=[text_chunk], model="text-embedding-3-small"
    )
    embedding = response.data[0].embedding
    embeddings_list.append(embedding)

OpenAI API 返回的 response object 包含一个 data attribute,它是 embedding objects 的列表。每个 embedding object 都有一个 embedding attribute,其中保存实际 vector,也就是一个由浮点数组成的 Python list。对于 text-embedding-3-small 模型,这个列表包含 1,536 个值。Embeddings 是 normalized 的,单个值通常在 -1 到 1 之间,尽管大多数值会落在更窄范围内。前面的代码通过 response.data[0].embedding 从 response 中提取第一个 embedding。

图 5-2 展示了 embedding vectors 列表的样子。在这里,生成的 embeddings 每个 vector 有 1,536 个维度。

image.png

图 5-2:1,536 维 embedding vectors 的示例输出

作为 OpenAI 的替代方案,你可以使用较小的开源模型。下面示例使用最流行的开源模型之一:all-MiniLM-L6-v2。虽然它的准确率和性能无法与 Google 或 OpenAI 的现代 transformer-based foundation embedding models 相比,但它仍然很受欢迎,因为它速度快、成本低,并且可以运行在你自己的基础设施上,因此可以部署在较小 edge devices 或本地笔记本上。

在这个示例中,你使用 Sentence Transformers library。它构建在 Hugging Face Transformers 之上,并专门用于生成 text embeddings:

from sentence_transformers import SentenceTransformer

# Load the model
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
text_chunks = ["The sky is blue.", "The grass is green."]

embeddings = model.encode(text_chunks)

encode 方法返回一个 NumPy array,其中每一行代表对应 text chunk 的 embedding vector。生成的 embeddings array shape 是 (2, 384),意思是两个 text chunks,每个有 384 维 vector。

Discussion

Embedding models 会将文本转换为能够捕捉语义含义的 numerical vectors。这是 RAG 系统能通过 vector space 中的 similarity 查找相关内容,而不是依赖 exact keyword matches 的核心机制。

现代 text embedding models 通过在大型文本数据集上训练,学习语义关系。理解它们与 one-hot encoding 等简单编码方法的差异,可以说明它们的能力。One-hot encoding 将类别视为相互独立、没有语义关系的 labels;而 embedding models 会学习有意义的 representations,让相似概念在 vector space 中位置接近。

图 5-3 展示了 one-hot encoding,它将 “Bachelor’s” 表示为 [0, 1, 0],将 “Master’s” 表示为 [1, 0, 0],将 “PhD” 表示为 [0, 0, 1]。这些随机分配的 vectors 丢失了所有语义信息。它们无法表达 PhD 学生通常先有 bachelor’s degree,也无法表达这些学位代表不同教育层级。

image.png

图 5-3:Categorical data 的 label encoding 与 one-hot encoding

NOTE

Vector 是一组数字,用结构化方式表示某个对象,类似地图上的坐标,例如 [5, 3] 表示“向北五个街区、向东三个街区”。Vectors 让我们可以把抽象概念,例如词义,表示为计算机可以处理并进行数学比较的数字。

RAG 系统依赖能够捕捉语义含义的 embedding models,因为它们支持基于概念关系的 retrieval,而不只是关键词匹配。Embedding models 不是创建随机数字,而是创建 vectors,并赋予每个维度清晰含义,将特征映射到 vector space 中。

在图 5-4 的例子中,模型使用三个有意义维度来映射 queen、king、princess 和 prince 这些词:age,国王通常比王子更年长;gender,queen 和 princess 是女性;royal status,王室中的等级层次。Vector 中的每个数字都表示三个特征之一。

因此,king 会得到 vector [0.8, 0, 1],表示年长男性且地位高。Princess 被表示为 [0.3, 1, 0.5],表示更年轻、女性、地位较低。

你甚至可以对这些 vectors 做算术运算。当你加减 vectors 时,就是对每个对应数字做加减:

king - man + woman = queen
[0.8, 0, 1] - [0.7, 0, 0.6] + [0.6, 1, 0.5]
= [0.8 - 0.7 + 0.6, 0 - 0 + 1, 1 - 0.6 + 0.5]
= [0.7, 1, 0.9]

这是因为减去 man、加上 woman 会改变 gender 维度,同时保留 age 和 status,最终得到一个描述 queen 的 vector。

图 5-4 还展示了第二个例子,即首都和国家:France – Germany + Berlin = Paris

image.png

图 5-4:带有意义 embedding 维度的 vector arithmetic

Embedding models 是在大型文本数据集上训练的 neural networks。数据集中的每个唯一单词都会被分配给输入层中的一个 neuron。图 5-5 展示了一个简单例子,其中 hidden layer 只有两个 neurons。训练期间,neural network 会根据每个输入词预测下一个词。例如,当 Google 的输入 neuron 被激活时,网络会被训练去预测单词 is。当模型完全训练后,hidden-layer neurons 的 weights 就成为 embedding model。这些 learned weights 会将每个单词或句子映射到一个空间中,并决定每个词的位置。

image.png

图 5-5:用于生成 text embeddings 的 neural network architecture

任何需要基于语义含义而非 exact keyword matches 查找相关内容的 RAG 系统,都可以使用 embedding models。这包括 question answering、document retrieval 和 content recommendation systems。

当你需要 exact string matching 时,不要使用 embeddings,而应使用 full-text search;当处理高度结构化数据,例如 database queries 时,应使用 SQL;当 interpretability 至关重要时,也要避免 embeddings,因为 embeddings 是不透明盒子。主要取舍是成本与准确率:OpenAI 的 text-embedding-3-small 等 cloud-based models 准确率更高,但会产生 API 成本;而 all-MiniLM-L6-v2 等开源模型可以本地运行,但 retrieval quality 较低。

虽然 embedding models 的演进不像 LLMs 那样剧烈,但它们仍然是 RAG 系统的基础。大多数当前模型,即使是几年前的模型,也能提供强表现,因此选择更多取决于基础设施约束,例如 cloud 还是 local,而不是准确率差异。

See Also

Joshua Starmer 的 Word Embedding and Word2Vec playlist 提供了 embedding models 工作原理的可视化解释。

Hugging Face embeddings guide 覆盖实现细节和模型架构。

5.2 通过降维技术可视化 Text Chunks 之间的语义关系

Problem

你想在二维中可视化高维 embedding vectors,以验证语义相似文本是否聚集在一起。

Solution

图 5-6 展示了 dimensionality reduction process。使用 principal component analysis(PCA)将 embedding dimensions 从 1,536 降到 2,并用 Matplotlib 绘制。这可以通过展示哪些 text chunks 按含义聚集在一起,揭示语义模式。

image.png

图 5-6:使用 PCA 的 dimensionality reduction process

代码使用 scikit-learn 中的 PCA。Scikit-learn 是一个流行机器学习库,提供 dimensionality reduction 和 data analysis 工具。Matplotlib 用于创建可视化,pandas 帮助以表格形式管理 embedding data。

要运行代码,安装以下 dependencies:

pip install openai scikit-learn matplotlib pandas

然后为六个示例句子生成 embeddings,并将它们与 text chunks 一起存入 DataFrame:

import os
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from openai import OpenAI

# Define text chunks
text_chunks = [
    "The sky is blue.",
    "The sun is shining.",
    "I love chocolate.",
    "Ice cream is delicious.",
    "Roses are red.",
    "Violets are blue.",
]

# Initialize OpenAI client
# Assumes the OPENAI_API_KEY environment variable is set
client = OpenAI()
embeddings = []

# Generate embeddings for text chunks
for text_chunk in text_chunks:
    response = client.embeddings.create(
        input=[text_chunk], model="text-embedding-3-small"
    )
    embeddings.append(response.data[0].embedding)

# Convert embeddings to a DataFrame
# Each row represents one text chunk, each column one embedding dimension
embeddings_df = pd.DataFrame(
    embeddings, columns=[f"dim_{i}" for i in range(len(embeddings[0]))]
)

接下来,scikit-learn 的 PCA 函数会将 embedding vectors 转换到新的坐标系统中。每个 principal component 都是一个新轴,尽可能捕捉数据中的变化。因此,最前面的 components 包含大部分信息。只可视化前两个 components,通常可以识别语义模式:

# Perform PCA with 2 components
pca = PCA(n_components=2)
df_reduced = pca.fit_transform(embeddings_df)

# Create a new DataFrame with reduced dimensions
df_reduced = pd.DataFrame(df_reduced, columns=["PC1", "PC2"])
df_reduced["text"] = text_chunks  # Add original text chunks for labeling

# Create a scatter plot
plt.figure(figsize=(8, 6))
plt.scatter(df_reduced["PC1"], df_reduced["PC2"])

# Add labels to each point
for i, label in enumerate(df_reduced["text"]):
    plt.text(df_reduced["PC1"][i], df_reduced["PC2"][i], label, fontsize=9)

plt.xlabel("Principal Component 1")
plt.ylabel("Principal Component 2")
plt.title("PCA Scatter Plot")

# Save the plot
plt.savefig("../principal_component_plot.svg", format="svg")

图 5-7 展示了结果。在这个例子中,与食物和饮品相关的句子聚集在右侧,与天气相关的句子位于左下角,与花相关的句子位于左上角。

image.png

图 5-7:PCA 可视化展示二维语义 clusters

可视化结果确认,语义相关的 text chunks 会在降维后的空间中聚集在一起。这验证了 embedding model 能捕捉有意义的语义关系,即使从 1,536 维大幅降到 2 维,相似主题仍然自然分组。

Discussion

Dimensionality reduction 的工作方式,是将高维 vectors 投影到更低维空间,通常是二维(2D)或三维(3D),同时保留 points 之间的相对距离。PCA 会找到数据中最大方差方向,并将它们作为新轴。这意味着在 1,536 维空间中接近的语义相似 text chunks,在 2D 中通常仍然接近。

你可以在开发阶段使用可视化,验证 embedding model 是否将语义相似内容聚集在一起。这可以在部署前验证 “legal documents” 是否与 “marketing materials” 分开聚类。可视化也能在受监管行业中建立 stakeholder trust,因为它能在 demos 中让 retrieval behavior 更具体可见。

不要在生产中使用 2D plots 做 diagnostics。它们把 1,536 个维度简化成 2 个维度,因此无法展示所有关系。开发阶段可以用它验证 clustering behavior,但生产中应跟踪 precision@k 等 retrieval metrics。

主要取舍是简单性与准确性。Uniform Manifold Approximation and Projection(UMAP)和 t-SNE(t-Distributed Stochastic Neighbor Embedding)比 PCA 更好保留 local structure,但计算更慢,而且如果参数调得不好,可能产生误导性模式。PCA 更快且 deterministic,但可能不如前两者捕捉非线性关系。

See Also

Scikit-learn 中的 PCA documentation 覆盖 dimensionality reduction 的实现和参数。

Scikit-learn 的 manifold learning guide 解释了 t-SNE、UMAP 和其他 dimensionality reduction 技术。

5.3 计算 Embeddings 之间的距离

Problem

你想通过计算两段文本 embedding vectors 之间的距离,衡量它们的相似度。

Solution

使用 NumPy 或 Sentence Transformers library 计算 embedding vectors 之间的 cosine similarity。

这个 recipe 使用 OpenAI text embedding models 生成 embeddings,并使用 NumPy 计算 cosine similarity。按如下方式安装:

pip install openai numpy sentence-transformers pandas

然后为三个历史陈述和一个用户问题生成 embeddings。目标是找到与用户问题最匹配的历史事实,用户问题要求提供关于历史上疾病的信息:

import numpy as np
from numpy.linalg import norm
import pandas as pd
import os
from openai import OpenAI

text_chunks = [
    "The Great Fire of London in 1666 destroyed over 13,000 houses.",
    "Julius Caesar was assassinated on the Ides of March (March 15) in 44 BCE.",
    "The Black Death is estimated to have killed nearly one-third of the "
    "European population.",
]

users_question = "Tell me something interesting about diseases in history"

embeddings_df = pd.DataFrame(text_chunks, columns=["text_chunk"])

client = OpenAI()
embeddings = []

def create_embeddings(text_chunk, client):
    return client.embeddings.create(
        input=[text_chunk],
        model="text-embedding-3-small"
    ).data[0].embedding

# Apply function create_embeddings to the correct column
embeddings_df["embedding"] = embeddings_df["text_chunk"].apply(
    create_embeddings, client=client
)

users_question_embedding = create_embeddings(
    text_chunk=users_question, client=client
)

接下来,使用 NumPy 计算 cosine similarity:

# Create a list to store the calculated cosine similarity
cos_sim = []

def calculate_cosine_similarity(text_chunk_embedding, users_question_embedding):
    A = text_chunk_embedding
    B = users_question_embedding

    # Calculate the cosine similarity
    cosine = np.dot(A, B) / (norm(A) * norm(B))
    return cosine

# Apply function calculate_cosine_similarity to the correct column
embeddings_df["similarity"] = embeddings_df["embedding"].apply(
    calculate_cosine_similarity, users_question_embedding=users_question_embedding
)

图 5-8 展示了输出。包含 embeddings 的 DataFrame 现在增加了一个 similarity 列,显示每个句子与用户问题的 cosine similarity。在这个基础例子中,关于 Black Death 的句子相似度最高。

image.png

图 5-8:历史文本查询的 cosine similarity scores

图 5-9 展示了 cosine similarity 公式。使用 Recipe 5.2 中的 PCA,右侧还将 embedding vectors 绘制为 2D 可视化。

Cosine similarity 是 cosine distance 的反向概念,因此 Black Death 这条记录的 cosine similarity 为 0.31,是最接近的匹配。请记住,cosine similarity 测量的是高维空间中 vectors 之间的角度,而不是绝对大小。2D 图只是展示概念,并不反映原始高维空间中的真实距离。

image.png

图 5-9:Cosine similarity 公式及几何表示

Discussion

Cosine similarity 衡量两个 vectors 之间的角度,而不是它们的绝对距离。对于 RAG 系统,这是首选 distance metric,因为它关注语义方向而不是向量大小。像 “reset password” 这样的短 query,与一段解释密码重置流程的长段落,会指向相同语义方向,因此获得高 similarity score,即使它们的 vector magnitudes 差异很大。

图 5-10 展示了 embeddings 常用的三种 distance metrics:cosine similarity、Euclidean distance 和 Manhattan distance。

image.png

图 5-10:用于衡量 vector similarity 的常见 distance metrics

这种对长度差异的稳健性很重要,因为用户 queries 通常比 retrieved documents 短得多。如果没有 normalization,较长文档会仅仅因为 magnitude 而主导排序,而不是因为相关性。

对于使用现代 transformer-based embeddings 的标准 RAG retrieval,应使用 cosine similarity。它能很好处理不同文本长度,并有效捕捉语义含义。当 vector magnitude 本身携带语义含义时,可以使用 Euclidean distance,但这在现代 embeddings 中很少见,只会出现在 embedding norms 编码额外信息的专门领域。生产系统中可以使用 dot product 提升速度,前提是你在 indexing 阶段对 vectors 做一次 normalization 并复用,避免重复 normalization overhead。

除非你处理的是高维稀疏 vectors,并且 Manhattan distance 有计算优势,否则应避免使用 Manhattan distance。实践中,对于使用 transformer-based embeddings 的 RAG 系统,cosine similarity 仍然是超过 95% 场景的默认选择。

See Also

NumPy linear algebra documentation 覆盖 normdot 和其他 vector calculations 操作。

Sentence Transformers util module 提供 cosine similarity、dot product 和 pairwise distances 的函数。

scipy.spatial.distance documentation 覆盖 cosine、Euclidean 和 Manhattan distance metrics 的实现。

5.4 选择合适的 Embedding Model

Problem

你需要为项目选择合适的 embedding model。

Solution

按照以下步骤为 RAG 系统选择合适的 embedding model:

  1. Check benchmarks:查看 Hugging Face 上的 Massive Text Embedding Benchmark(MTEB)leaderboard,了解模型在不同任务上的表现。对于多语言系统,查看 MIRACL(Multilingual Information Retrieval Across a Continuum of Languages)benchmark scores。
  2. Identify deployment constraints:确定你是否可以使用商业云 APIs,例如 OpenAI、Google、Cohere,还是因为数据敏感或离线需求而必须本地部署。
  3. Establish baselines:使用商业 providers 时,从 OpenAI 的 text-embedding-3-small 等成熟模型开始;本地部署时,从 all-MiniLM-L6-v2 开始。
  4. Test with real data:使用实际 use case 中 50–100 个代表性 queries 做端到端测试。通过检查正确 documents 是否出现在 top results 中来衡量 retrieval accuracy。
  5. Compare alternatives:如果 baseline accuracy 不足,用相同 query set 测试更大模型,例如 text-embedding-3-large,以量化改进。
  6. Measure performance:使用 vector database 的 query endpoint 测量平均 retrieval latency。确保它满足应用需求,例如 interactive apps 小于 100ms。
  7. Calculate costs:对于 cloud APIs,将预期月度 query volume 乘以 per-token pricing。将结果与 baseline 比较,以验证预算适配性。

主要决策是:你是否可以使用商业云模型,还是必须使用开源模型进行本地部署。对于 RAG 应用,text-embedding-3-small 或 Google 的 text-embedding-005 等商业模型适合大多数 use cases。

Discussion

表 5-2 提供了基于 use case 选择模型的指南。

表 5-2:Embedding model selection criteria

Use caseRecommended approach
大型数据集,超过 1M documents,或高 query volume,每天超过 1,000 次用较小模型优先考虑速度和成本。
对准确率要求关键,例如法律、医疗、金融,且少于 100K documents即使成本更高,也使用更大模型。
实时面向用户的应用偏好 latency 小于 50ms 的模型。
Batch processing 或 offline indexing准确率可以优先于速度。

当你可以容忍 API latency 和成本,并以此换取 state-of-the-art accuracy 时,使用 cloud-based models,例如 OpenAI 的 text-embedding-3-small 或 Google 的 text-embedding-005。这些模型擅长理解细微 queries,并且无需 retraining 就能很好支持多语言。

当数据由于合规要求不能离开基础设施、API 成本在你的 query volume 下超出预算,或需要离线运行时,使用 open source models,例如 all-MiniLM-L6-v2BGE-small-en-v1.5。准确率差距已经大幅缩小,现代开源模型在许多 retrieval tasks 中接近商业模型表现。

避免过度使用大 embedding model。如果你用 text-embedding-3-small 做 baseline tests 时 retrieval accuracy 已达到 85%,升级到 text-embedding-3-large 后达到 87%,那么这 2% 的提升对大多数应用来说,通常不值得付出双倍成本和延迟。此时应把优化重点放在 chunking strategy 和 retrieval configuration 上。

主要风险是在没有基于真实数据验证的情况下承诺使用某个模型。MTEB 等 benchmarks 衡量的是跨多种任务的一般表现,但你的具体领域,例如法律合同、医疗记录、代码文档,可能表现不同。生产部署前,一定要使用来自实际 use case 的 50–100 个真实 queries 测试。

NOTE

近年来,embedding models 并没有像 LLMs 那样快速进步。许多几年前构建的 RAG 系统仍然使用 OpenAI 的 text-embedding-ada-002,因为其准确率对 retrieval tasks 仍然足够。这种稳定性意味着,模型选择通常是一次性决策,很少需要重新审视。

See Also

Hugging Face 上的 MTEB leaderboard 提供模型 benchmarks 和 details,包括 context window sizes。

OpenAI embeddings guide 覆盖 API 使用、model selection 和 best practices。

5.5 使用 CLIP 为图像和文本生成 Embeddings

Problem

你想使用 embeddings 对图像进行分类,或在图像和文本之间执行 cross-modal matching。

Solution

使用 Contrastive Language–Image Pre-training(CLIP)为图像和文本生成 embeddings,以支持 cross-modal matching 和 classification。

图 5-11 展示了如何将 CLIP 用作 retrieval system 的一部分:

  1. 为所有希望加入 vector store 的 images 和 text 生成 embeddings。
  2. 当用户提问时,使用 CLIP 为 query 生成 embedding,并搜索 vector store。
  3. 将新生成的 embedding vector 与 vector store 中存储的 embedding vectors 进行比较。
  4. 将检索到的 images 和 text 发送给能够同时解释二者的 multimodal model,并回答用户问题。

image.png

图 5-11:基于 CLIP 的 multimodal retrieval architecture

这种方法允许你构建大型图像数据库,并通过提问来查询它,因为基于 CLIP 的 retriever 可以双向连接 text 和 images。

这个 recipe 演示使用 CLIP 进行 classification。虽然前面的图展示了 CLIP 如何在 RAG 系统中支持 multimodal retrieval,但现在我们聚焦 classification,因为它清楚展示了 CLIP 的核心能力:将 images 和 text 映射到共享 embedding space。Image classifier 的工作方式与 RAG retriever 类似,二者都是比较 embeddings 来找到最接近匹配。

只需几行代码,你就可以构建一个猫狗图片分类器。图 5-12 展示了 CLIP 的工作方式。你为 images 生成 embeddings,也为 “a photo of a cat” 和 “a photo of a dog” 这样的 descriptive text 生成 embeddings。当分类一张新图像时,将它的 embedding 与 text embeddings 进行比较。最接近的匹配决定预测 label。

image.png

图 5-12:使用 CLIP embeddings 的 cross-modal classification

CLIP 是开源的,这意味着你可以用 PyTorch 加载它,并在本地机器上运行。为了预处理 images,可以使用 Pillow library。按如下方式安装二者:

pip install transformers torch pillow

然后加载 CLIP model,为 images 和 text chunks 创建 embeddings。CLIP processor 会 tokenize 文本并预处理图像。接下来,CLIP 生成 embeddings,你计算每个 image-text pair 的 similarity scores。输出包含每个 image embedding 与每个 text embedding 之间的 similarity scores。Probabilities 通过 softmax function 从这些 similarity scores 派生出来,以确保它们总和为 1:

import torch
from PIL import Image
from transformers import CLIPProcessor, CLIPModel

# Load the model and processor
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

# Text and image inputs
descriptions = ["A photo of a cat", "A photo of a dog"]
images = [
    Image.open("../datasets/images/cat.jpg"),
    Image.open("../datasets/images/dog.jpg"),
]

# Disable gradient tracking since this code only runs inference
with torch.no_grad():
    inputs = processor(
        text=descriptions,
        images=images,
        return_tensors="pt",
        padding=True,
        truncation=True,
    )
    outputs = model(**inputs)

dot_products_per_text = outputs.logits_per_text

# Calculate probabilities
probabilities = dot_products_per_text.softmax(dim=1)

最终输出是一个 probability matrix。第一列显示每张图像包含猫的可能性,第二列显示每张图像包含狗的可能性。换句话说,classification 方法仍然会将每张图像与文本描述的相似度进行分配。

图 5-13 展示了结果。在这个例子中,第一张图显示猫的概率是 0.9999,这意味着它显示狗的概率极低。请记住,所有类别的概率总和永远为 1。因此,如果图像是大象,但 elephant 不在类别中,classification 方法仍然会给 cat 或 dog 中的某一个分配较高概率。

image.png

图 5-13:猫狗图像的 CLIP classification probabilities

Discussion

CLIP 通过在互联网上数百万 image-text pairs 上训练,学习将两种 modalities 映射到共享 vector space。关键洞察是,语义相似的内容无论属于哪种 modality,最终都会在这个空间中彼此接近。一张狗的图像会产生接近文本 “a photo of a dog” 的 vector,因为模型在训练中学到了这种关联。

这个模型不需要 task-specific training,你只需要提供 candidate text descriptions,CLIP 就会按与图像的 similarity 对它们排序。

当你有很多可能类别,并希望避免训练传统 classifier 时,可以使用 CLIP 做 image classification。添加新类别只需要写新的 text descriptions,不需要重新训练模型。这让 CLIP 非常适合快速变化的 taxonomies,或 labeled training data 稀缺的场景。

当图像和文本紧密耦合时,也可以使用 CLIP 做 cross-modal retrieval,例如产品目录中,用户可能通过描述或图像搜索;或科学论文中,figures 与文本解释紧密相关。

不过,对于大多数 RAG 应用,一种更简单的方法效果更好:使用 multimodal models,例如 GPT-5.2、Claude Sonnet 4.5,为图像生成文本描述,然后用标准 text embedding models 对这些描述做 embedding。将文本描述存入 vector database;当该 chunk 被检索到时,只将原始图像传给 LLM。这种方法利用了针对 semantic text matching 优化过的 text embedding models,而 CLIP 的直接 image-text mapping 会增加复杂度。

WARNING

CLIP 的主要限制是,它无法可靠处理图像中的文本。一张 “No Parking” 路牌照片,如果模型将输入视为普通标志图像,可能不会 embed 到接近文本 “no parking” 的位置。单独提取并 embedding 图像中的文字,可以避免这个问题。

See Also

OpenAI 的 CLIP research page 解释了模型架构和训练方法。

Hugging Face 上的 CLIP model documentation 覆盖实现细节和使用示例。

LlamaIndex multimodal retrieval example 演示了如何用 CLIP 构建 multimodal RAG 系统。

5.6 使用 Embeddings 执行文本分类

Problem

你想通过在 text embeddings 上训练 classifier,将用户问题路由到特定数据子集,例如按 topic、department 或 document type。

Solution

在 text embeddings 上训练 classifier,将用户问题路由到 vector store 中的特定数据子集。这个 prefiltering step 会在 semantic retrieval 运行前缩小搜索空间。

图 5-14 展示了两步流程:先通过 chunk documents 并生成 embeddings 准备数据集,然后使用这些 embedding vectors 作为 features,训练 classifier 来预测 document categories。

image.png

图 5-14:使用 text embeddings 训练 random forest classifier

这个 recipe 使用两个 PDF 文件演示流程。首先安装所需 dependencies:

pip install openai scikit-learn pandas PyPDF2 langchain-text-splitters

NOTE

下面的代码会从本书 GitHub repository 中的 datasets/pdf_files/ 加载示例 PDF 文件。请下载这些文件,或将路径调整为你自己的文档。

接下来,加载两个示例文本文件,一个关于 Premier League 的历史,另一个关于 deep learning 的历史,然后应用 recursive chunking 创建 text chunks:

from langchain_text_splitters import RecursiveCharacterTextSplitter
import PyPDF2
import pandas as pd

pdf_files = [
    {
        "file_path": "../datasets/pdf_files/history_of_deep_learning.pdf",
        "label": "Deep_Learning",
    },
    {
        "file_path": "../datasets/pdf_files/premier_league_history.pdf",
        "label": "Premier_League",
    },
]

chunks_dict_list = []

# Split both documents into chunks and append to the same list of dicts
for pdf_file in pdf_files:
    with open(pdf_file["file_path"], "rb") as file:
        reader = PyPDF2.PdfReader(file)

        text = ""
        for page in reader.pages:
            text += page.extract_text()

    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=200,
        chunk_overlap=0,
        length_function=len,
        is_separator_regex=False,
    )

    chunks = text_splitter.split_text(text)

    for chunk in chunks:
        chunks_dict_list.append({"text": chunk, "label": pdf_file["label"]})

chunks_df = pd.DataFrame(chunks_dict_list)

现在遍历刚创建的数据集,并为每个 text chunk 生成一个 embedding vector:

client = OpenAI()

def create_embeddings(text_chunk, client):
    return client.embeddings.create(
        input=[text_chunk],
        model="text-embedding-3-small"
    ).data[0].embedding


chunks_df["embedding"] = chunks_df["text"].apply(
    create_embeddings,
    client=client
)

生成的 DataFrame 是训练 classifier 的基础。

你将 labels 设置为目标向量 y。Feature matrix X 由 embeddings 定义。使用 lambda function,将 embedding vector 中的每个维度映射为输入矩阵 X 中的一列。因此,生成的输入矩阵有 1,536 列,对应 embedding vector 的每个维度。

最后,使用 scikit-learn 训练 random forest classifier:

from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification

y = chunks_df["label"]
X = chunks_df["embedding"].apply(
    lambda x: pd.Series(eval(x)) if isinstance(x, str) else pd.Series(x)
)

# Train a random forest classifier
clf = RandomForestClassifier()
clf.fit(X, y)

接下来,你用一个明显关于 football,也就是 soccer,的问题测试模型。Random forest 会预测每个 class 的 probability。在这个例子中,模型有 94% 信心认为该问题与 Premier League 内容相关:

test_embedding = create_embeddings(
    text_chunk="What is the name of the top football league in England?",
    client=client,
)

X_test = [test_embedding]

# Predict the most likely class
predicted_classes = clf.predict(X_test)  # e.g., ['Premier_League']
probabilities = clf.predict_proba(X_test)  # e.g., array([[0.06, 0.94]])

训练好的模型现在可以作为 RAG workflow 的一部分使用。

Discussion

Classification routing 的工作方式,是训练一个模型将 embedding vectors 映射到预定义 categories。Random forest classifier 会学习 1,536 维 embedding space 中区分 topics 的模式。当新的 query 到来时,其 embedding 会输入 classifier,classifier 会以 probability score 预测最可能的 category。

这个 prefiltering 会在 semantic retrieval 运行前缩小搜索空间。你不再搜索 vector store 中全部 100,000 个 documents,而是让 classifier 将 sports queries 路由到 20,000 个 sports documents 中,从而降低 retrieval latency 并提升 precision。

当 vector store 中混合了截然不同的 content types,并且 semantic similarity 可能造成误导时,可以使用 classification routing。例如,关于 “market performance” 的问题可能检索到关于球队表现的 sports articles,而不是 financial data,因为 embedding models 认为二者语义相似。Classification 会先将 query 路由到正确 domain,从而防止这种情况。

当你有清晰、稳定的 categories,例如 sports、politics、finance,并且每个 category 有足够训练样本,至少 100–200 个 labeled chunks 时,可以使用 classification routing。Classifier 需要足够样本来学习 embedding space 中的判别模式。

当 categories 明显重叠或 domain boundaries 模糊时,应避免 classification routing。关于 “sports betting regulations” 的问题同时跨 sports 和 legal domains,强制分到单一 category 会丢失相关信息。对于重叠 domains,metadata filtering 效果更好:在 ingestion 阶段分配 category metadata fields,然后在 semantic search 前使用 category IN ['sports', 'legal'] 过滤。这可以保留多个相关 categories 中的 documents,而不需要 classifier。

当 document collection 很小,小于 10,000 documents,或高度同质时,也应避免 classification routing。当 semantic search 本身效果很好时,训练和维护 classifier 的开销大于收益。先从 semantic search 开始。只有当你在 retrieval logs 中观察到跨领域混淆时,再添加 classification。

主要取舍是 precision 与 maintenance burden。Classification 可以提升 distinct domains 的 routing precision,但需要 labeled training data、随着内容演化定期 retraining,以及额外 inference latency,通常 10–20ms。Classifier 也可能犯错——misrouting queries 会完全丢失相关 documents,不像 semantic search 可能返回次优但仍相关的结果。

See Also

Scikit-learn 的 random forest documentation 覆盖 classifier 实现和参数。

Scikit-learn 的 model evaluation guide 解释了评估 classification performance 的 metrics。

Scikit-llm library documentation 展示了如何将 transformer models 与 scikit-learn workflows 集成。

5.7 使用 Hybrid Search 方法提升搜索结果

Problem

你希望通过组合多种搜索技术,而不是依赖单一方法,来提升 retrieval quality。

Solution

将基于 BM25 的 keyword search 与 vector similarity search 结合起来,然后使用 reciprocal rank fusion(RRF)合并它们的 rankings。

图 5-15 分步骤展示了该过程:

  1. Keyword search 使用 BM25 基于 exact term matches 对 documents 排名。
  2. Embedding model 为用户 query 生成 embedding。
  3. Semantic search 基于 vector similarity 对 documents 排名。
  4. 将两种搜索的 rankings 合并为单一 ranking。

image.png

图 5-15:使用 reciprocal rank fusion 组合 keyword search 和 semantic search

要实现 hybrid search,需要三个组件:

  • BM25,用于 keyword-based ranking,特别适合 product names、IDs、codes 和 domain-specific terms。
  • Text embedding model,用于 semantic similarity search。
  • Rank fusion method,用于将两种 rankings 合并为一个结果列表。

按如下方式安装这些组件:

pip install openai rank-bm25 sentence-transformers pandas

这个示例使用三个短 text chunks。首先运行 BM25 keyword search:

from rank_bm25 import BM25Okapi
import pandas as pd

text_chunks = [
    "The Great Fire of London in 1666 destroyed over 13,000 houses.",
    "Julius Caesar was assassinated on the Ides of March (March 15) in 44 BCE.",
    "The Black Death is estimated to have killed nearly one-third of the "
    "European population.",
]

# Tokenize text into words
tokenized_chunks = [chunk.split(" ") for chunk in text_chunks]

bm25 = BM25Okapi(tokenized_chunks)

user_query = "Tell me something interesting about diseases in history"
tokenized_query = user_query.split(" ")

# BM25 scores for each document
bm25_scores = bm25.get_scores(tokenized_query)

# Document IDs ordered by keyword relevance (best first)
keyword_ranking_doc_ids = (
    pd.DataFrame(bm25_scores, columns=["score"])
    .sort_values(by="score", ascending=False)
    .index
    .to_list()
)

接下来,使用 vector embeddings 运行 semantic search。Semantic search 在 query 表达意图和上下文时表现很强,但可能错过 exact identifiers 或稀有关键词。代码如下:

from sentence_transformers.util import cos_sim
from openai import OpenAI

client = OpenAI()

def create_embedding(text):
    return (
        client.embeddings.create(
            input=[text], model="text-embedding-3-small"
        )
        .data[0]
        .embedding
    )

embeddings_df = pd.DataFrame(text_chunks, columns=["text_chunk"])
embeddings_df["embedding"] = embeddings_df["text_chunk"].apply(create_embedding)

query_embedding = create_embedding(user_query)

# Compute cosine similarity
embeddings_df["similarity"] = cos_sim(
    embeddings_df["embedding"], query_embedding
)

# Document IDs ordered by semantic similarity (best first)
semantic_ranking_doc_ids = (
    embeddings_df
    .sort_values(by="similarity", ascending=False)
    .index
    .to_list()
)

此时,你有两个 document IDs 的 ranked lists。最后一步是使用 RRF 合并这些 rankings。RRF 会给同时出现在多个 ranking 靠前位置的 documents 更高分:

# Reciprocal Rank Fusion parameter
# Higher k reduces the influence of very top ranks
k = 60

# Build a table with one row per document
rrf_df = pd.DataFrame({"doc_id": range(len(text_chunks))})

# Map document IDs to their rank positions
keyword_rank_map = {
    doc_id: rank for rank, doc_id in enumerate(keyword_ranking_doc_ids, start=1)
}
semantic_rank_map = {
    doc_id: rank for rank, doc_id in enumerate(semantic_ranking_doc_ids, start=1)
}

rrf_df["keyword_rank"] = rrf_df["doc_id"].map(keyword_rank_map)
rrf_df["semantic_rank"] = rrf_df["doc_id"].map(semantic_rank_map)

# Reciprocal Rank Fusion score
rrf_df["rrf_score"] = (
    1 / (k + rrf_df["keyword_rank"]) +
    1 / (k + rrf_df["semantic_rank"])
)

# Final hybrid ranking
rrf_df = rrf_df.sort_values("rrf_score", ascending=False)

final_ranking_doc_ids = rrf_df["doc_id"].to_list()

表中每一行代表一个 document。在 keyword search 和 semantic search 中都排名靠前的 documents,会获得最高 combined scores。

Discussion

Hybrid search 组合了两种独立 ranking signals,它们捕捉相关性的不同方面。Vector similarity search 衡量 embedding space 中的语义接近程度。Keyword search 捕捉 exact lexical matches。RRF 会通过给多个列表顶部结果更高分来合并 rankings。

这种机制有效,是因为 keyword 和 semantic signals 互补。Keyword matching 擅长 exact terms,例如 product IDs、medical codes、legal citations,这些场景中字符级精确性很重要。Semantic matching 则捕捉 intent,并处理 synonyms、paraphrasing 和 keyword search 错过的 conceptual similarity。

当语料库包含 embeddings 无法可靠捕捉的 identifiers 或 specialized terminology 时,应使用 hybrid search。例子包括 product SKUs,例如 “SKU-4792” 必须精确匹配;法律条文编号,其中 “article 230” 与 “section 230” 不同;medical diagnosis codes,例如 ICD-10 codes;以及 technical identifiers,例如 API endpoint names、database table names。Keyword matching 在这些场景中提供关键 ranking signal。

当用户 query 混合 natural language 和 precise terms 时,也应使用 hybrid search。像 “How do I configure SSL for endpoint /api/v2/users?” 这样的问题,既受益于对 “configure SSL” 的语义理解,也受益于 endpoint path 的 exact matching。

对于纯 knowledge retrieval,如果用户以自然语言提问且不包含技术 identifiers,应避免 hybrid search。像 “How do I reset my password?” 这样的问题不需要 keyword matching,semantic search 已经可以很好捕捉意图。加入 BM25 会增加复杂性,却不提升结果。

当 semantic search 本身已经满足质量要求时,也应避免 hybrid search。Hybrid search 会增加计算 overhead,因为你要运行两个 retrievers,再进行 rank fusion,并且提高系统复杂性。先从 semantic search 开始。只有当你在 retrieval logs 或用户反馈中观察到 exact-match failures 时,再添加 hybrid search。

主要取舍是 recall 与 complexity。Hybrid search 可以提升 identifier-heavy queries 的 recall,但需要维护两个 retrieval systems、调优 rank fusion 参数 k,并处理两个 rankers 意见不一致的 edge cases。参数 k 控制对 top-ranked results 的偏好程度:较高的 k,例如 60–100,会给较低排名更多权重;较低的 k,例如 10–20,则强烈偏好在两个 retrievers 中都排名靠前的 items。

See Also

Pinecone 的 hybrid search introduction 解释了如何组合 dense 和 sparse retrieval methods。

LlamaIndex BM25 retriever example 演示了如何在 semantic search 旁边实现 keyword-based search。