DLAI-人工智能高级搜索笔记-全-
DLAI 人工智能高级搜索笔记(全)
001:课程介绍 🎬
在本节课中,我们将对《AI高级检索:Chroma》课程进行概述,了解检索增强生成(RAG)的基本概念、当前简单检索方法的局限性,以及本课程将要探讨的先进技术。
检索增强生成(RAG)通过检索相关文档来为大型语言模型(LLM)提供上下文。这使得LLM在回答查询和执行任务时表现更佳。
目前,许多团队使用基于语义相似度或嵌入向量的简单检索技术。但在本课程中,你将学习更复杂的技术,这些技术能带来远优于简单检索的效果。
RAG的常见工作流程 🔄
上一段我们介绍了RAG的基本概念,本节中我们来看看一个典型的工作流程。
一个常见的RAG工作流程是:获取用户查询并将其转换为嵌入向量,然后在向量数据库中查找具有最相似嵌入向量的文档,这些文档即为提供的上下文。
简单检索的局限性 ⚠️
然而,上述方法存在一个问题:它倾向于找到与查询主题相似、但未必真正包含答案的文档。
先进的查询改进技术 🛠️
为了解决上述局限性,我们可以对初始用户查询进行改写,这被称为查询扩展。改写查询的目的是为了直接检索到更相关的文档。
以下是两种关键的查询扩展技术:
- 查询多路扩展:将原始查询通过不同方式的措辞或重写,扩展成多个查询。
- 假设文档生成:甚至可以先猜测一个假设性的答案雏形,然后尝试在文档集合中寻找与这个“答案”更相似的文本,而不仅仅是寻找与查询主题一般性相关的文档。
讲师介绍 👨🏫
本课程由Anton Troynikov担任讲师。Anton是推动AI应用检索技术前沿的创新者之一,也是流行开源向量数据库Chroma的联合创始人。如果你学习过由Harrison Chase讲授的LangChain短期课程,那么很可能已经使用过Chroma。
Anton表示,他非常高兴能共同讲授这门课程,并分享他在实际RAG部署中观察到的有效与无效经验。
课程内容大纲 📚
接下来,我们简要了解一下本课程的内容安排。
课程首先快速回顾RAG应用。接着,你将了解简单向量搜索在检索中表现不佳的一些常见缺陷。然后,你将学习多种改进检索结果的方法。

正如Andrew所提到的,第一种方法是使用LLM来改进查询本身。另一种方法借助交叉编码器来对查询结果进行重新排序,交叉编码器接收一对句子并生成相关性分数。
你还将学习如何根据用户反馈调整查询嵌入向量,以产生更相关的结果。
目前RAG领域创新不断。因此,在最后一课中,我们还将概述一些尚未成为主流、刚刚出现在研究中的前沿技术。我们认为这些技术很快就会变得更加普及。
致谢 🙏
我们感谢为这门课程做出贡献的Chroma团队成员,以及Chroma的开源开发者社区。同时也感谢深度学习团队的成员。

本节课中我们一起学习了RAG的基本概念、简单检索的局限性,以及本课程将涵盖的先进检索技术,如查询扩展和重新排序。第一课将从RAG概述开始。掌握这些技术后,即使是规模较小的团队也能构建出高效的AI系统。希望学完本课程后,你能运用这些曾被视作“非主流”的方法,构建出真正出色的应用。
002:基于嵌入的检索系统概述 🧠


在本节课中,我们将学习基于嵌入的检索系统的基本构成,以及它如何与大型语言模型结合,形成一个完整的检索增强生成工作流。
系统总览
上一节我们介绍了课程目标,本节中我们来看看整个系统是如何运作的。检索增强生成的工作流程如下:用户输入一个查询,系统会从预先嵌入并存储在检索系统中的文档集合中寻找相关信息。在本例中,我们使用Chroma作为检索系统。
以下是其核心步骤:
- 用户查询进入系统。
- 查询通过一个嵌入模型进行处理,该模型与用于嵌入文档的模型相同,从而生成查询的嵌入向量。
- 检索系统根据查询的嵌入向量,通过寻找最邻近的文档嵌入向量,来查找最相关的文档。
- 系统将查询和检索到的相关文档一并提供给LLM。
- LLM综合检索到的文档信息,生成最终答案。
实践:处理文档
现在,让我们看看如何在实践中实现上述流程。首先,我们需要导入一些辅助函数。
# 这是一个基础的文本换行函数,用于美观地打印文档内容。
def wrap_text(text):
# 实现文本换行逻辑
pass
我们将以一个PDF文件为例进行演示。这里使用一个简单的开源Python包来读取PDF文件。
from pdf_reader import PDFReader
# 读取微软2022年年度报告
reader = PDFReader('microsoft_annual_report_2022.pdf')
pdf_texts = []
for page in reader.pages:
text = page.extract_text().strip()
if text: # 过滤掉空字符串,确保不向检索系统发送空页面
pdf_texts.append(text)
# 打印第一页提取的文本作为示例
print(pdf_texts[0])
文本分块策略
处理完文档后,下一步是对文本进行分块。我们首先按字符进行分块。
我们将使用LangChain中的文本分割工具,具体是RecursiveCharacterTextSplitter。这个分割器会递归地根据特定分隔符(如双换行符)来分割文本。如果分割后的块仍然大于目标大小(这里设为1000个字符),它会继续使用下一个分隔符进行分割,直到按字符边界分割。
from langchain.text_splitter import RecursiveCharacterTextSplitter
char_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=0, # 块之间不重叠,这是一个可以调整的超参数
separators=["\n\n", "\n", " ", ""]
)
char_chunks = char_splitter.split_text("\n".join(pdf_texts))
print(f"字符分割器共生成 {len(char_chunks)} 个块。")
print("第10个文本块示例:", char_chunks[9])
然而,仅按字符分割还不够。因为我们使用的嵌入模型(Sentence Transformers)有有限的上下文窗口长度(256个标记)。如果文本块超过这个长度,模型会直接截断超出的部分,导致信息丢失。
因此,我们还需要按标记数量进行分块。这里使用SentenceTransformersTokenTextSplitter。
from langchain.text_splitter import SentenceTransformersTokenTextSplitter
token_splitter = SentenceTransformersTokenTextSplitter(
chunk_size=256, # 与嵌入模型的上下文窗口长度一致
chunk_overlap=0
)
# 对字符分割后的块进行再次分割
final_chunks = []
for chunk in char_chunks:
final_chunks.extend(token_splitter.split_text(chunk))
print(f"标记分割器共生成 {len(final_chunks)} 个块。")
print("第10个文本块示例:", final_chunks[9])
嵌入模型与向量化
文本分块完成后,下一步是将这些块加载到检索系统中。我们使用Chroma,并需要为其配置一个嵌入模型。
我们使用Sentence Transformers作为嵌入模型。它是BERT架构的扩展,能够将整个句子或小文档(如我们的文本块)编码成一个单一的密集向量,而不是为每个标记单独生成向量。

import chromadb
from chromadb.utils import embedding_functions
# 创建Sentence Transformers嵌入函数
sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")



# 演示嵌入函数如何工作:将第10个文本块转换为向量
sample_embedding = sentence_transformer_ef([final_chunks[9]])
print(f"嵌入向量维度:{len(sample_embedding[0])}")
print("向量示例(前10个值):", sample_embedding[0][:10])
这个向量有384个维度,它密集地表示了文本块的语义信息。
构建检索系统
现在,我们使用Chroma来建立检索系统。
# 创建Chroma客户端和集合
chroma_client = chromadb.Client()
collection = chroma_client.create_collection(
name="microsoft_annual_report_2022",
embedding_function=sentence_transformer_ef
)
# 为每个文本块创建ID(这里用其序号)
ids = [str(i) for i in range(len(final_chunks))]
# 将文档添加到集合中
collection.add(
documents=final_chunks,
ids=ids
)
print(f"集合中文档数量:{collection.count()}")


执行查询与RAG流程

系统搭建好后,让我们连接一个LLM,构建完整的RAG系统来回答查询。
首先,我们提出一个简单的问题:“What was the total revenue for this year?”,并从Chroma中检索相关文档。
query = "What was the total revenue for this year?"
results = collection.query(
query_texts=[query],
n_results=5 # 返回最相关的5个文档
)
retrieved_docs = results['documents'][0] # 获取第一个查询的检索结果
print("检索到的相关文档:")
for i, doc in enumerate(retrieved_docs):
print(f"\n--- 文档 {i+1} ---")
print(doc[:500]) # 打印前500个字符
接下来,我们将检索到的文档和原始查询一起发送给LLM(这里使用GPT-3.5-turbo),让它生成答案。
import openai
import os
# 设置OpenAI客户端
openai.api_key = os.getenv("OPENAI_API_KEY")
def rag_answer(query, retrieved_docs):
# 将检索到的文档合并为一个字符串
information = "\n\n".join(retrieved_docs)
# 构建消息
messages = [
{
"role": "system",
"content": "你是一个专业的财务研究助手。用户正在询问一份年度报告中的信息。你将看到用户的问题和来自年度报告的相关信息。请仅使用这些信息来回答用户的问题。"
},
{
"role": "user",
"content": f"问题:{query}\n\n请根据以下信息回答:\n{information}"
}
]
# 调用OpenAI API
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=messages,
temperature=0
)
return response.choices[0].message.content
# 获取最终答案
answer = rag_answer(query, retrieved_docs)
print("LLM生成的答案:")
print(answer)
运行后,我们可能会得到类似“截至2022年6月30日财年,微软的总收入为1982.7亿美元”的答案。
总结与探索
本节课中,我们一起学习了基于嵌入的检索增强生成系统的基本构建流程。我们从处理PDF文档开始,经历了文本分块、嵌入向量化、构建Chroma检索库,到最终结合LLM生成答案的全过程。


核心在于,RAG将LLM从依赖记忆事实的模型,转变为一个能够处理并综合外部信息的处理器。在进入下一节深入分析系统细节之前,建议你尝试提出自己的问题,与这个检索系统互动,直观感受模型和检索器协同工作的能力与局限。
003:检索的陷阱 - 当简单的向量搜索失效!🔍

在本节课中,我们将学习向量检索的一些陷阱。我们将看到一些案例,在这些案例中,简单的向量搜索不足以让检索为你的AI应用良好工作。仅仅因为内容在特定嵌入模型下的向量语义上接近,并不总是意味着你能直接获得理想的结果。
环境设置与数据加载
首先,我们需要设置环境并加载数据。我们将使用Chroma和相应的嵌入函数。
# 导入必要的库并设置嵌入函数
from chromadb.utils import embedding_functions
import chromadb
# 创建嵌入函数
sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
# 使用辅助函数加载Chroma集合
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_collection(name="my_collection", embedding_function=sentence_transformer_ef)
# 输出集合中的向量数量以确认
print(f"集合中的向量数量:{collection.count()}")
运行后,确认有349个文本块被嵌入到Chroma中。
可视化嵌入空间
为了理解检索结果,将高维嵌入向量可视化非常有帮助。我们将使用UMAP技术将向量投影到二维空间。
UMAP(Uniform Manifold Approximation and Projection)是一个开源库,用于将高维数据降维到二维或三维以便可视化。与PCA不同,UMAP会尽可能保留数据点之间的距离结构。
以下是投影步骤:
import umap
import numpy as np
from tqdm import tqdm
# 从集合中获取所有嵌入向量
all_embeddings = collection.get(include=['embeddings'])['embeddings']
all_embeddings_array = np.array(all_embeddings)
# 拟合UMAP变换模型
umap_transform = umap.UMAP(random_state=0)
umap_transform.fit(all_embeddings_array)
# 定义一个函数来投影嵌入向量
def project_embeddings(embeddings, umap_transform):
projected = np.empty((len(embeddings), 2))
for i, emb in enumerate(tqdm(embeddings)):
projected[i] = umap_transform.transform([emb])
return projected
# 投影所有数据嵌入
projected_data = project_embeddings(all_embeddings_array, umap_transform)





现在,我们可以使用Matplotlib来可视化投影后的数据点。
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 8))
plt.scatter(projected_data[:, 0], projected_data[:, 1], s=10, alpha=0.6)
plt.title("数据嵌入的二维投影")
plt.xlabel("UMAP 维度 1")
plt.ylabel("UMAP 维度 2")
plt.show()
这个散点图展示了我们数据在二维空间中的分布。语义相似的内容通常会聚集在一起,尽管二维投影无法完全保留高维空间的所有结构,但它有助于我们形成几何直觉。



分析查询与检索结果
上一节我们可视化了整个数据集。本节中,我们来看看具体的查询及其检索结果,以理解简单向量搜索的局限性。
我们将使用之前的查询“total revenue”进行检索。
# 定义查询
query = "total revenue"
# 使用集合进行查询
results = collection.query(
query_texts=[query],
n_results=5,
include=['documents', 'embeddings']
)
# 提取并打印检索到的文档
retrieved_docs = results['documents'][0]
for i, doc in enumerate(retrieved_docs):
print(f"结果 {i+1}: {doc[:150]}...") # 打印前150个字符
检索结果可能包含与“收入”相关的文档,但也可能混入一些关于“成本”或其他财务主题的文档,这些是干扰项。
接下来,我们将查询和检索到的结果一起可视化,观察它们在嵌入空间中的位置关系。
# 获取查询的嵌入向量
query_embedding = sentence_transformer_ef([query])
# 获取检索结果的嵌入向量
retrieved_embeddings = results['embeddings'][0]


# 投影查询和检索结果的嵌入向量
projected_query = project_embeddings(query_embedding, umap_transform)
projected_retrieved = project_embeddings(retrieved_embeddings, umap_transform)




# 可视化
plt.figure(figsize=(10, 8))
# 绘制所有数据点(灰色)
plt.scatter(projected_data[:, 0], projected_data[:, 1], s=10, alpha=0.1, c='gray', label='全部数据')
# 绘制查询点(红色X)
plt.scatter(projected_query[:, 0], projected_query[:, 1], s=200, marker='x', c='red', label='查询')
# 绘制检索结果点(绿色圆圈)
plt.scatter(projected_retrieved[:, 0], projected_retrieved[:, 1], s=100, facecolors='none', edgecolors='green', linewidths=2, label='检索结果')
plt.title(f"查询与检索结果可视化: '{query}'")
plt.xlabel("UMAP 维度 1")
plt.ylabel("UMAP 维度 2")
plt.legend()
plt.show()
在图中,红色X代表查询,绿色圆圈代表检索到的文档。你会发现,有些结果点离查询点较远,它们是潜在的干扰项。这是因为嵌入模型是基于通用语义训练的,并不了解我们当前的具体任务(例如,精确查找“总收入”)。
探索更多查询案例
为了进一步说明问题,让我们分析其他几个查询。
以下是关于“人工智能战略”的查询示例:



query_ai = "what's the strategy around artificial intelligence that is AI"
# ...(重复上述查询和可视化步骤)
结果可能包含与AI相关的文档,但也可能包含关于数据库或元宇宙的文档,这些内容只是与“技术投资”间接相关。
以下是关于“研发投资”的查询示例:

query_rd = "What has been the investment in research and development"
# ...(重复上述查询和可视化步骤)
这个通用查询的检索结果可能会更加分散。当一个查询落在数据“云团”之外时,它找到的最近邻可能来自云团的不同部分,因此结果会显得分散,包含更多不相关的干扰项。

处理不相关查询
最后,我们探讨一个完全不相关的查询会带来什么问题。
考虑查询“Michael Jordan近期为微软做了什么”:
query_irrelevant = "what has Michael Jordan done for us lately"
# ...(重复上述查询和可视化步骤)
不出所料,检索结果与迈克尔·乔丹毫无关系。但在RAG(检索增强生成)流程中,这些不相关的结果(全部是干扰项)会被提供给大语言模型,导致模型产生混乱或错误的输出,这种现象很难诊断和调试。


可视化结果显示,这些结果点在空间中非常分散,印证了查询与数据集内容完全不匹配。


总结与下节预告
本节课中,我们一起学习了基于简单向量嵌入的检索系统可能存在的陷阱。我们了解到,即使对于简单的查询,系统也可能返回干扰项或不相关的结果,这主要是因为通用嵌入模型缺乏对特定任务的理解。
我们通过UMAP将高维嵌入空间投影到二维进行可视化,这有助于我们直观地理解查询与数据点的几何关系,以及干扰项产生的原因。

要改善检索质量,我们需要更智能的技术。在下节课中,我们将介绍一种称为查询扩展的技术,利用大语言模型来改进查询本身,从而获得更相关、更精确的检索结果。
004:Lab 3 - 查询扩展 🧠

在本节课中,我们将学习如何利用大型语言模型来增强查询,从而提升基于向量检索系统的结果相关性。我们将重点介绍两种查询扩展技术:基于生成答案的扩展和基于多查询的扩展。
信息检索作为自然语言处理的一个子领域已存在多年,有许多方法可用于提升查询结果的相关性。新的变化在于,我们现在拥有了强大的大型语言模型,可以利用它们来增强发送给基于向量检索系统的查询,从而获得更好的结果。
准备工作 ⚙️
上一节我们介绍了基础的检索流程,本节中我们来看看如何利用LLM来优化查询。首先,我们需要设置环境并加载必要的工具。
以下是初始化步骤的代码:
# 导入必要的库并设置环境
import chromadb
from chromadb.utils import embedding_functions
import openai
import umap
import matplotlib.pyplot as plt
# 创建Chroma客户端和嵌入函数
client = chromadb.Client()
embedding_func = embedding_functions.OpenAIEmbeddingFunction()
# 设置OpenAI客户端
openai.api_key = "your-api-key"
# 使用UMAP准备数据降维以进行可视化
reducer = umap.UMAP()
基于生成答案的查询扩展 🤖
这种扩展方法的核心思想是:让LLM为原始查询生成一个假设性的答案,然后将原始查询与这个生成的答案拼接起来,形成一个新的、更丰富的查询,再将其送入检索系统。
以下是实现此功能的核心函数:




def augment_query_generated(query, model="gpt-3.5-turbo"):
# 系统提示词,指导模型生成假设性答案
system_prompt = "你是一个有用的专家金融研究系统。请针对给定的问题,提供一个可能在年度报告等文档中找到的示例答案。"
# 用户提示词即为原始查询
user_prompt = query
response = openai.ChatCompletion.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
hypothetical_answer = response.choices[0].message.content
# 拼接原始查询和生成的答案
joint_query = query + " " + hypothetical_answer
return joint_query


让我们看一个实践例子。假设原始查询是:“高管团队是否有重大人员变动?”
使用上述函数,我们可能会得到一个如下的联合查询:“高管团队是否有重大人员变动? 在过去财年,高管团队没有发生重大人员变动。核心管理层成员保持稳定...”
将这个联合查询发送给Chroma向量数据库进行检索,我们可以获得与领导层稳定性、董事会构成等更相关的文档。
为了直观展示效果,我们可以比较原始查询嵌入向量和联合查询嵌入向量在空间中的位置。可视化结果显示,联合查询的嵌入向量(橙色X)与原始查询(红色X)位于不同区域,并成功召回了更相关的文档簇(绿色圆点)。

基于多查询的扩展 🔄
上一节我们介绍了通过生成答案来扩展单条查询,本节中我们来看看另一种更强大的方法:让LLM生成多个相关的子查询。这种方法特别适用于复杂的原始查询,它可以帮助我们从不同角度获取信息。
以下是生成多个相关查询的函数:
def augment_query_multiple(query, model="gpt-3.5-turbo"):
system_prompt = """你是一个有用的专家金融研究助手。用户正在询问关于年度报告的问题。
请针对提供的问题,建议最多五个额外的相关问题,以帮助他们找到所需信息。
建议只提供简短的疑问句,不要使用复合句。
建议涵盖该主题不同方面的多种问题。
确保输出的是完整的、与原始问题相关的问题。"""
response = openai.ChatCompletion.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
]
)
# 解析响应,获取生成的查询列表
augmented_queries = parse_response_to_list(response)
return augmented_queries
理解这些将LLM引入检索循环的技术时,提示工程变得非常重要。建议学生在实验中尝试修改这些提示词,观察它们如何影响生成的查询类型。
让我们实践一下。假设原始查询是:“导致收入增长的最重要因素是什么?”
使用上述函数,我们可能会得到以下一组扩展查询:
- 导致收入下降的最重要因素是什么?
- 收入来源有哪些?
- 销售和收入在不同产品线之间是如何分布的?
- 定价策略是否有变化?
- 公司是否获得了新客户?
可以看到,这些都是与原始查询相关但侧重点不同的问题,这能有效拓宽检索范围。
以下是执行多查询检索的步骤:
# 1. 构建查询数组:原始查询 + 生成的扩展查询
all_queries = [original_query] + augmented_queries

# 2. Chroma支持批量查询,获取所有查询的结果
results = collection.query(
query_texts=all_queries,
n_results=5
)



# 3. 由于不同查询可能返回相同文档,需要进行去重
unique_documents = remove_duplicates(results['documents'])
通过这种方法,我们能够检索到关于收入增长、Windows收入增加、销售营销费用、税收优惠等不同方面的文档。每个扩展查询都为我们提供了略有不同的结果集。
可视化显示,原始查询(红色X)和多个扩展查询(橙色点)在嵌入空间中指向不同区域,并召回了更广泛的相关信息(绿色圆点)。这大大增加了我们为复杂问题找到所有相关信息的几率。

总结与展望 📝





本节课中我们一起学习了两种利用LLM进行查询扩展的技术:
- 基于生成答案的扩展:通过让LLM“幻想”一个答案来丰富查询语义。
- 基于多查询的扩展:通过生成多个相关子查询来从不同角度覆盖主题。
这两种技术的核心公式可以概括为:
- 扩展查询 = LLM(原始查询) + 原始查询
- 检索结果 = Retrieve(扩展查询)
它们的优点是能显著提升复杂查询的召回率。然而,一个明显的缺点是可能会返回大量结果,其中部分可能与原始查询的相关性不高。
在下一个实验中,我们将介绍交叉编码器重排序技术。该技术可以对所有返回结果的相关性进行评分,并只筛选出与原始查询最匹配的部分,从而解决结果过多和噪音问题。

我建议你尝试修改本实验中的查询扩展提示词,针对微软年度报告提出不同类型的问题,观察所得到的结果有何变化。
005:Lab 4 - 交叉编码器重排序 📊

在本节课中,我们将学习一种名为“交叉编码器重排序”的技术,用于评估检索结果与查询之间的相关性,并对结果进行重新排序,以提升最终答案的质量。


概述
上一节我们介绍了如何通过大语言模型来增强查询,以改善检索结果。本节中,我们将探讨另一种技术:交叉编码器重排序。这种方法能为检索到的文档进行相关性评分,并据此重新排序,确保最相关的结果排在最前面。
什么是重排序?🤔
重排序是一种根据结果与特定查询的相关性,对结果进行排序和评分的方法。
其工作原理如下:在为一个特定查询检索到结果后,你需要将这些结果连同原始查询一起,输入到一个重排序模型中。这个模型会为每个结果输出一个相关性分数,分数最高的结果被认为最相关。然后,你可以选择排名最高的结果作为最相关的答案。
另一种理解方式是:重排序模型会基于查询,为每个结果计算一个条件分数。
实践操作:基础重排序
让我们看看如何在实践中应用这一技术。
首先,像之前一样导入辅助函数,并将数据加载到Chroma数据库中。
重排序的一个用途是从查询结果的“长尾”部分挖掘更多有用信息。我们来看一个之前使用过的查询:“what has been the investment in research and development”。
通常我们要求返回5个结果,但这次我们将要求返回10个。这意味着我们将获得一个可能包含更多有用信息的更长结果列表。我们同样会包含文档和嵌入向量。
检索文档后,我们查看结果。前五个结果与之前相同,因为检索是确定性的。但我们还获得了五个新的结果,它们可能包含与问题相关的信息。
关键在于,如何判断哪些结果真正与我们的特定查询相关,而不仅仅是嵌入空间中的最近邻。我们通过交叉编码器重排序来实现这一点。
交叉编码器模型 🧠
我们将使用sentence-transformers库中的交叉编码器,并用一个特定模型来实例化它。
以下是sentence-transformers中的两种主要模型:
- 双编码器:将查询和文档分别编码,然后通过计算余弦相似度来寻找最近邻。
- 交叉编码器:将查询和文档一起输入到一个分类器中,直接输出一个相关性分数。
我们将使用交叉编码器来为检索结果评分。具体做法是:将原始查询与每一个检索到的文档配对,输入交叉编码器,得到的分数即为该文档的相关性排名分数。
我们实例化了交叉编码器。接下来要做的第一件事是创建配对列表。
以下是创建配对和评分的步骤:
- 创建配对:每个配对包含原始查询和一个检索到的文档。
- 使用交叉编码器为每个配对评分。
让我们打印出这些分数。可以看到,前两个文档的分数很高。值得注意的是,第二个检索到的文档分数远高于第一个。此外,一些来自“长尾”部分的文档,其分数甚至高于前五个结果中的某些文档。
如果我们根据分数重新排序文档,会看到第二个文档现在排名第一,第一个文档排名第二。一些来自“长尾”的文档进入了前五名。实际上,重排序后的前五名包含了原本排名第六和第七的结果,而原本的第四、第五名结果排名则很低。
通过这种方式,我们利用交叉编码器生成的分数对结果进行了重排序。现在,如果我们只取前五个结果,它们应该比之前的结果相关性更高,因为我们从“长尾”中挖掘出了更多与问题真正相关的信息。
结合查询扩展进行重排序 🔄
你可能已经想到了,我们可以将重排序技术与上一节的查询扩展结合使用。
考虑到查询扩展会生成多个查询,每个生成的查询都针对复杂问题的不同部分,我们可以使用交叉编码器重排序技术,从所有扩展查询的检索结果中,筛选出对原始查询最有利的结果,而不是简单地将所有结果都发送给大语言模型。
具体操作如下(沿用上一节的代码):
- 定义原始查询和生成的扩展查询。
- 将所有查询(原始+生成的)拼接在一起,进行检索。
- 对检索结果进行去重。
- 创建配对:将原始查询与每一个去重后的检索文档配对。
- 使用交叉编码器为这些配对评分。这样,我们就可以评估扩展查询的检索结果对于原始查询的相关性。
- 根据评分重新排序,并选择评分最高的五个结果传递给大语言模型。
使用此类交叉编码器模型的一个巨大优势是,它非常轻量级,并且完全在本地运行。
通过这种方式,我们能够从查询扩展产生的众多结果“长尾”中,筛选出与原始查询最相关的信息,并将其传递给大语言模型。
总结
在本实验中,我们学习了如何使用交叉编码器作为重排序模型。我们看到了如何应用重排序技术,既可以从单个查询的“长尾”中获取更多信息,也可以过滤扩展查询的结果,只保留与原始查询本身最相关的内容。
这是一个非常强大的技术,值得进一步实验。理解并直观感受重排序分数如何随查询变化(即使检索系统返回的结果相同)是很有益的。这是因为交叉编码器重排序器可能强调查询中与嵌入模型不同的部分,因此它提供的排名更依赖于特定查询本身,而不仅仅是检索系统返回的原始结果。


在下一节中,我们将讨论查询适配器。查询适配器是一种利用用户反馈或其他类型数据,直接修改或增强查询嵌入向量本身,以获得更好查询结果的方法。
006:Lab 5 - 嵌入适配器 🧩
概述
在本节课中,我们将学习一种名为“嵌入适配器”的技术。该技术利用用户对检索结果相关性的反馈,自动优化检索系统的性能。我们将了解其工作原理,并通过代码实践,学习如何训练一个嵌入适配器模型来改进查询嵌入,从而获得更精准的检索结果。

嵌入适配器简介
在前几节课中,我们探讨了如何使用查询增强和交叉编码器重排序来改善检索结果。本节中,我们将学习如何利用用户对检索结果相关性的反馈,通过一种名为“嵌入适配器”的技术,自动提升检索系统的性能。
嵌入适配器是一种直接修改查询嵌入向量的方法,旨在产生更好的检索结果。实际上,我们在检索系统中插入了一个额外的阶段——嵌入适配器。它位于嵌入模型之后,但在检索最相关结果之前。我们通过一组查询的检索结果相关性用户反馈,来训练这个嵌入适配器。


准备工作
首先,我们需要像往常一样设置环境并加载数据。这里有一个特殊之处:我们需要使用 torch 库,因为我们将训练一个非常轻量级的模型。
import torch
# 创建嵌入函数并加载数据到Chroma
# ... (初始化代码)


生成模拟查询数据集
为了训练适配器,我们需要一个数据集。由于我们的RAG应用尚未有真实用户,我们可以使用大语言模型来生成一个模拟数据集。这需要精心设计提示词。
以下是生成查询的提示词示例:
你是一位乐于助人的金融研究助手。请提出10到15个在分析年报时重要的简短问题,并遵循给定的输出格式。
运行此提示词后,我们得到了一系列关于公司财务报表的合理问题。
评估检索结果相关性
接下来,我们从Chroma中检索与这些查询相关的文档。在真实的RAG系统中,可以请用户对输出结果给出“赞”或“踩”的反馈,并将其与检索结果关联,从而获得相关性信号。在本实验中,我们使用大语言模型来评估每个查询的检索结果相关性。
我们再次设计提示词,要求模型判断给定陈述是否与查询相关,并只输出“是”或“否”。然后,我们将“是”转换为标签+1,“否”转换为标签-1。这样做的原因将在稍后解释。
构建训练数据集
现在,我们将获取查询嵌入、文档嵌入以及从评估模型得到的标签,开始构建训练嵌入适配器的数据集。
数据集将包含以下三个部分:
- 适配器查询嵌入:原始查询的嵌入向量。
- 适配器文档嵌入:检索到的文档的嵌入向量。
- 适配器标签:根据相关性评估得到的
+1(相关)或-1(不相关)标签。
我们通过循环遍历所有查询和结果,创建这些三元组数据。标签设为+1和-1并非偶然,因为在训练嵌入适配器模型时,我们将把这些值用作余弦距离损失函数的目标。当两个向量相同时,它们的余弦相似度为1;当它们相反时,余弦相似度为-1。换句话说,我们希望相关结果的向量与查询向量方向相同,而不相关结果的向量方向相反。这正是我们要训练的模型的目标。
检查数据长度,例如得到150条,这代表15个查询,每个查询有10个结果,每个结果都有相关性标签。
转换为PyTorch数据集
由于我们使用PyTorch训练嵌入适配器,需要将数据转换为PyTorch张量数据集。
# 将数据转换为PyTorch张量类型
# ... (数据转换代码)
# 打包成PyTorch数据集
dataset = torch.utils.data.TensorDataset(query_embeddings_tensor, doc_embeddings_tensor, labels_tensor)
定义嵌入适配器模型与损失函数
接下来,我们设置嵌入适配器模型。模型本身相当简单:
- 输入:查询嵌入、文档嵌入和一个适配器矩阵。
- 计算更新后的查询嵌入:
updated_query_embedding = original_query_embedding @ adapter_matrix - 计算更新后的查询嵌入与文档嵌入之间的余弦相似度。
然后,我们定义损失函数。损失函数接收查询嵌入、文档嵌入、适配器矩阵和标签,运行模型计算余弦相似度,并计算余弦相似度与标签之间的均方误差。+1标签意味着余弦相似度应表明向量方向相同,-1标签则意味着方向应相反。通过这种方式,我们训练适配器矩阵,使查询向量与相关文档方向一致,与不相关文档方向相反。
我们初始化适配器矩阵进行训练。您可能会发现,这非常类似于传统神经网络中的线性层。
训练循环
设置训练循环,我们将训练100个周期。在每次迭代中,我们计算损失。如果当前损失优于之前记录的最佳损失,我们就更新记录的最佳矩阵。然后执行反向传播以更新矩阵。

best_loss = float(‘inf’)
best_matrix = None
for epoch in range(100):
for q_emb, d_emb, label in dataset:
loss = compute_loss(q_emb, d_emb, adapter_matrix, label)
if loss < best_loss:
best_loss = loss
best_matrix = adapter_matrix.clone()
# 反向传播和优化器步骤
# ...
运行训练循环。训练速度非常快,因为这本质上等同于训练传统神经网络中的单个线性层。
查看得到的最佳损失值,例如0.5,这表示我们从起点取得了相当不错的改进。
分析适配器矩阵的影响
为了理解适配器矩阵如何影响查询向量,我们可以构造一个全为1的测试向量,并将其乘以我们的最佳矩阵。这将告诉我们向量的各个维度被缩放的程度。


您可以将嵌入适配器视为对空间进行拉伸和挤压:它放大与特定查询最相关的维度,同时缩小不相关的维度。您还会注意到,它甚至可以反转某些维度的方向。
绘制结果图,您可以看到测试向量(全为1)的每个维度是如何被拉伸和挤压的。有些维度被显著拉长,而有些则几乎变为零。这意味着我们的嵌入适配器基本上判断出:这些维度更相关,那些较不相关,这些实际上与我们想找的内容相反,而那些则更相关。

可视化适配效果
最后,让我们看看这实际上对查询产生了什么效果。我们像之前一样,获取生成的查询并嵌入它们,同时计算适配后的查询嵌入,然后将它们投影出来并绘图。
与原始查询(在数据集中较为分散)相比,新的查询(经过嵌入适配器转换后)集中在了数据集中与我们的查询最相关的特定区域。您可以看到红色的原始查询如何通过嵌入适配器转变为绿色的查询,并将它们推向了空间的特定部分。
总结与展望
本节课中,我们一起学习了嵌入适配器。这是一种简单而强大的技术,用于根据您的特定应用定制查询嵌入,以提高检索相关性。
为了使这项技术有效,您需要收集一个数据集,可以是像我们这里生成的合成数据集,也可以是基于真实用户行为的数据集。用户数据通常效果最好,因为它真实反映了人们使用检索系统完成特定任务的情况。
由于这种方法涉及提示词工程和大语言模型的使用,值得尝试不同的提示词。同时,也值得尝试不同的适配器矩阵初始化方式,甚至可以考虑使用一个完整的轻量级神经网络来代替简单的矩阵。您可能还想调整嵌入适配器训练过程的超参数,或者收集更具体的数据,针对特定的应用场景(而不是我们这里“理解财务报表”这样非常通用的目标)进行尝试。

在下一课中,我们将介绍一些其他新兴的研究技术,以进一步改进基于嵌入的检索系统。
007:L6_其他检索技术 🧠
在本节课中,我们将探讨基于嵌入的检索领域中的其他前沿技术与研究方向。基于嵌入的检索仍然是一个非常活跃的研究领域,存在许多值得了解的技术。
上一节我们介绍了嵌入适配器的训练与应用,本节中我们来看看该领域内其他值得关注的技术方向。
其他技术概览
以下是几种可以进一步提升检索效果的技术路径:
- 微调嵌入模型:你可以直接使用与我们在“嵌入适配器实验”中相同类型的数据,来微调嵌入模型本身。
- 微调大语言模型:最近的研究表明,直接微调LLM本身,使其能够预期检索结果并对其进行推理,取得了非常好的效果。你可以参考这里高亮的一些论文。
- 使用更复杂的嵌入适配器模型:你可以尝试使用更复杂的模型架构,例如一个完整的神经网络,甚至是一个Transformer层,来构建嵌入适配器。
- 使用更复杂的相关性建模模型:除了我们在实验中描述的交叉编码器,你还可以尝试使用更复杂的模型来评估检索结果的相关性。
- 优化数据分块策略:一个常被忽视的环节是,检索结果的质量通常取决于数据存入检索系统之前的分块方式。目前有很多研究正在探索使用包括Transformer在内的深度模型,来实现最优和智能的数据分块。
课程总结 🎯
本节课中我们一起学习了基于嵌入空间的检索增强生成技术的基础知识。

我们探讨了如何利用大语言模型来增强和优化查询,以获得更好的检索结果。
我们学习了如何使用交叉编码器模型进行重排序,为检索结果的相关性进行评分。
我们还了解了如何利用来自人类的相关性反馈数据来训练嵌入适配器,以改进查询结果。
最后,我们介绍了一些当前研究文献中关于改进AI应用检索的最令人兴奋的工作。

感谢你参与本课程,我们非常期待看到你构建的作品。

浙公网安备 33010602011771号