首先给出示例代码:
python">#### INDEXING ##### Load blog
import bs4
from langchain_community.document_loaders import WebBaseLoader# 设置一个常见的浏览器 User-Agent 字符串
os.environ["USER_AGENT"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"loader = WebBaseLoader(web_paths=("https://www.yixue.com/%E6%80%8E%E6%A0%B7%E7%9C%8B%E8%A1%80%E5%B8%B8%E8%A7%84%E5%8C%96%E9%AA%8C%E5%8D%95",),bs_kwargs=dict(parse_only=bs4.SoupStrainer(class_=("mw-body-content"))),
)blog_docs = loader.load()# Split
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(chunk_size=300, chunk_overlap=50)# Make splits
splits = text_splitter.split_documents(blog_docs)
# print(splits)# Index
from langchain_community.embeddings import HuggingFaceBgeEmbeddingsmodel_name = "BAAI/bge-small-zh-v1.5"
model_kwargs = {"device": "cpu"}
encode_kwargs = {"normalize_embeddings": True}
hf_embeddings = HuggingFaceBgeEmbeddings(model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs
)from langchain_community.vectorstores import Chroma
vectorstore = Chroma.from_documents(documents=splits, embedding=hf_embeddings)retriever = vectorstore.as_retriever()dense_retriever = retrieverfrom langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retrieverbm25_retriever = BM25Retriever.from_documents(splits)ensemble_retriever = EnsembleRetriever(retrievers=[bm25_retriever, dense_retriever], weights=[0.5, 0.5]
)question = "血红蛋白(Hb)测定人体正常范围是多少?"
docs = ensemble_retriever.invoke(question)
docs
下面我们一步步解释这段代码的作用和含义。
1. 加载网页内容
python">import bs4
from langchain_community.document_loaders import WebBaseLoaderos.environ["USER_AGENT"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"loader = WebBaseLoader(web_paths=("https://www.yixue.com/%E6%80%8E%E6%A0%B7%E7%9C%8B%E8%A1%80%E5%B8%B8%E8%A7%84%E5%8C%96%E9%AA%8C%E5%8D%95",),bs_kwargs=dict(parse_only=bs4.SoupStrainer(class_=("mw-body-content"))),
)blog_docs = loader.load()
解释:
- 设置 User-Agent: 为了防止网站屏蔽爬虫,这里设置了一个常见浏览器的 User-Agent 字符串。
- WebBaseLoader: 利用
WebBaseLoader
加载指定 URL(这里是一个医学科普博客页面),并只解析 HTML 中 CSS 类名为mw-body-content
的部分(通常是页面的主要内容),这样可以过滤掉导航栏、广告等无关信息。 - 加载文档:
loader.load()
将网页内容提取出来,存储在变量blog_docs
中。
举例说明:
假如你想获取一本在线教程中的正文内容而不包含广告和侧边栏,通过这种方法就能只提取核心内容。
2. 文本分块
python">from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(chunk_size=300, chunk_overlap=50)splits = text_splitter.split_documents(blog_docs)
解释:
- 文本切分器: 这里使用了
RecursiveCharacterTextSplitter
将加载的长文本分割成更小的块,每块最多 300 个字符,且相邻块有 50 个字符的重叠。 - 目的: 切分后的文本块更容易生成向量表示,方便后续的检索和匹配,同时重叠部分可以避免信息断裂,保证上下文连续性。
举例说明:
类似于将一本书拆成多个小段,每段之间有部分重叠,这样在搜索时,即使某一段只有部分信息,也能通过重叠部分关联到完整内容。
3. 构建向量索引
python">from langchain_community.embeddings import HuggingFaceBgeEmbeddingsmodel_name = "BAAI/bge-small-zh-v1.5"
model_kwargs = {"device": "cpu"}
encode_kwargs = {"normalize_embeddings": True}
hf_embeddings = HuggingFaceBgeEmbeddings(model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs
)
解释:
- 生成嵌入: 使用 HuggingFace 上的中文模型 “BAAI/bge-small-zh-v1.5” 将每个文本块转换为数值向量(嵌入)。
- 归一化: 归一化后的向量在计算相似度时更稳定,适合后续比较。
举例说明:
就像给每一段文字都制作一个数字指纹,这样当你输入查询问题时,可以计算数字指纹之间的相似度,找到最匹配的文本段。
4. 构建向量库并设置检索器
python">from langchain_community.vectorstores import Chroma
vectorstore = Chroma.from_documents(documents=splits, embedding=hf_embeddings)retriever = vectorstore.as_retriever()dense_retriever = retriever
解释:
- 向量库: 使用
Chroma
将所有文本块及其嵌入向量存入一个向量数据库。 - 检索器: 通过
as_retriever()
方法,将向量库包装成一个检索器,可以根据查询计算相似度并返回最相关的文本块。 - dense_retriever: 这里将基于向量相似度的检索器赋值给变量
dense_retriever
,表示这是一种基于“密集向量”(dense vector)的检索方法。
举例说明:
类似于在图书馆中给每本书都贴上了一个数字标签,当你搜索某个主题时,可以快速比对这些数字标签,找出最相关的书。
5. 构造 BM25 检索器及集成检索器(Ensemble Retriever)
python">from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retrieverbm25_retriever = BM25Retriever.from_documents(splits)ensemble_retriever = EnsembleRetriever(retrievers=[bm25_retriever, dense_retriever], weights=[0.5, 0.5]
)
解释:
- BM25Retriever: 这是一个基于 BM25 算法的传统检索器,它利用关键词、词频等统计信息来衡量查询与文档之间的匹配。
- 集成检索器 EnsembleRetriever: 将两种检索方法组合起来,通过设置相同的权重(各 50%)对查询结果进行综合排序。
- 目的: 利用传统关键词匹配(BM25)和基于语义的向量匹配(dense_retriever)的优势,互补不足,从而得到更全面、更准确的检索结果。
举例说明:
假设你搜索“血红蛋白正常值”,传统 BM25 检索器会根据关键词“血红蛋白”、“正常值”来查找完全匹配的段落,而 dense_retriever 则能发现语义上相关但词汇不同的内容。将两者融合后,你既不会错过精确匹配的内容,也能捕捉到隐含语义相似的答案。
6. 检索问答示例
python">question = "血红蛋白(Hb)测定人体正常范围是多少?"
docs = ensemble_retriever.invoke(question)
docs
解释:
- 输入问题: 设置查询问题:“血红蛋白(Hb)测定人体正常范围是多少?”
- 调用检索器: 通过
ensemble_retriever.invoke(question)
进行查询,综合 BM25 和 dense 检索器的结果,返回与问题最相关的文本块。 - 输出结果: 最终返回的
docs
变量中包含了经过排序后认为最能回答这个问题的文本内容。
举例说明:
假设页面中有一段文字写道:“一般来说,男性血红蛋白正常值在13.5至17.5 g/dL之间,女性在12.0至15.5 g/dL之间。”经过检索器处理后,这段内容就可能被排在最前面,作为回答问题的依据。
总结
整段代码的整体流程如下:
- 加载网页内容 —— 从指定博客页面中提取核心内容;
- 文本分块 —— 将长文档切分成小块,保证后续处理更加高效;
- 生成向量嵌入 —— 利用预训练模型将每个文本块转换为数值向量;
- 构建向量库 —— 存储所有嵌入并包装成检索器;
- 构造两个检索器并集成 —— 使用传统关键词匹配(BM25)和语义匹配(dense 检索器)各自查找相关文档,然后综合排序;
- 执行检索问答 —— 输入查询问题,得到与问题最相关的文本块作为答案依据。
通过这种方法,即使面对不同表达方式的查询,也能综合多种检索策略,给出更加准确和全面的答案。
这段代码中用到两个检索器是为了利用各自的优势,从而提高检索结果的全面性和准确性。
-
BM25Retriever:
这是一个基于传统信息检索算法 BM25 的检索器。BM25 算法利用文档中的关键词、词频和逆文档频率(IDF)等统计信息,来衡量查询和文档之间的匹配程度。它在捕捉精确词汇匹配方面表现很好,适合那些查询中包含明确关键词的场景。 -
dense_retriever:
这是基于大语言模型生成的文本嵌入(dense embeddings)来计算相似度的检索器。它通过将查询和文档都转换为向量,然后计算它们之间的相似性(例如使用余弦相似度),可以捕捉到更深层次的语义信息。这样,即使查询和文档之间没有完全相同的关键词,也能发现它们在语义上的相似性。 -
两者的区别和联系:
- 区别: BM25Retriever 依赖于传统的关键词统计和匹配,注重词汇层面的精确匹配;而 dense_retriever 则依赖于预训练语言模型的语义理解能力,更擅长捕捉隐含语义信息。
- 联系: 两者都旨在评估查询和文档之间的相关性,但侧重点不同。通过组合它们(例如各占 50% 权重),可以使得检索系统既能利用关键词匹配的精确性,又能发挥语义匹配的广泛覆盖能力,从而提升整体检索效果。
这种集成方法通常能弥补单一方法的不足,使得最终的检索结果既精准又全面。