第二章:构建知识库与向量化
2.1 引言
在第一章中,我们介绍了 RAG 的基本概念、核心组件以及开发环境的配置。作为 RAG 系统的第一步,构建一个高效的知识库并对其内容进行向量化是至关重要的。知识库是 RAG 系统中存储所有可检索信息的基础,而向量化则是实现语义检索的关键。
本章的目标是帮助 Java 程序员理解和实现以下内容:
- 准备和管理知识库,包括处理多种格式的文档(如文本、PDF 和网页内容)。
- 使用嵌入模型将文档内容转换为向量表示。
- 将向量存储到向量数据库(如 FAISS)中,并实现基本的检索功能。
- 解决知识库构建和向量化中的常见问题。
我们将通过详细的代码示例和步骤,带你完成从文档收集到向量存储的完整流程。本章假设你已经按照第一章的说明配置好了 Python 环境和必要的库(如 sentence-transformers
和 faiss-cpu
)。
2.2 知识库的准备
2.2.1 知识库的定义
知识库是 RAG 系统中存储所有可检索信息的集合,可以包含以下类型的数据:
- 纯文本文件:如
.txt
文件,适合存储简单的文档或 FAQ。 - 结构化文档:如 PDF、Word 或 Markdown 文件,常用于企业手册或技术文档。
- 数据库内容:如 MySQL 或 MongoDB 中的记录,适合存储动态数据。
- 网页内容:通过爬虫或 API 获取的在线文章或知识库页面。
- 企业数据:如内部 Wiki、CRM 系统中的客户记录或产品描述。
在本教程中,我们将以纯文本文件和 PDF 文档为例,展示如何构建知识库。后续你可以根据实际需求扩展到其他数据源。
2.2.2 知识库的设计原则
为了确保知识库在 RAG 系统中高效工作,需要遵循以下设计原则:
- 内容相关性:知识库应包含与目标应用场景高度相关的内容。例如,智能客服系统需要产品手册和 FAQ,而法律咨询系统需要法规和案例。
- 内容结构化:文档内容应尽可能结构化(如分段、分标题),以便于检索和生成。
- 内容更新性:知识库应支持动态更新,以反映最新的信息。
- 内容粒度:文档应按适当的粒度分割(如按段落或句子),以提高检索的精确度。
2.2.3 收集示例数据
为了便于讲解,我们将使用一个简单的示例知识库,模拟一个“企业产品知识库”,包含以下内容:
- 产品手册:描述公司产品的功能、规格和使用方法(PDF 格式)。
- FAQ 文件:常见问题和答案(纯文本格式)。
- 技术文档:产品安装和维护指南(Markdown 格式)。
你可以从以下方式获取示例数据:
- 手动创建:编写一些简单的文本文件或 PDF。
- 公开数据集:使用开源文档或维基百科页面。
- 企业数据:如果你有真实的业务数据,可以直接使用。
为了简化,我们将创建一个包含以下内容的目录结构:
rag_knowledge_base/
├── manuals/
│ ├── product_manual.pdf
│ ├── installation_guide.md
├── faqs/
│ ├── faq.txt
以下是示例文件的内容:
faq.txt
:
Q: 产品支持哪些操作系统?
A: 我们的产品支持 Windows 10/11、macOS 12+ 和 Ubuntu 20.04+。
Q: 如何联系技术支持?
A: 您可以通过邮箱 support@company.com 或电话 123-456-7890 联系我们。
product_manual.pdf
(假设内容为):
# 产品手册
## 产品概述
本产品是一款高性能的智能设备,支持多种功能,包括实时数据处理和远程控制。
## 功能列表
- 数据分析:支持多种数据格式。
- 远程控制:通过移动应用操作。
installation_guide.md
:
# 安装指南
## 环境要求
- 操作系统:Windows 10+ 或 Linux
- 内存:至少 8GB
## 安装步骤
1. 下载安装包。
2. 运行安装程序并按照提示操作。
2.2.4 预处理文档
在将文档存储到知识库之前,需要进行预处理,包括:
- 格式转换:将不同格式的文档(PDF、Markdown 等)转换为纯文本。
- 文本分割:将长文档分割成较小的片段(如按段落或句子),以便于向量化。
- 清理数据:移除无关内容(如页眉、页脚或特殊字符)。
我们将使用 Python 的以下库来处理文档:
PyPDF2
:解析 PDF 文件。markdown
:解析 Markdown 文件。nltk
:进行文本分割和清理。
安装依赖:
pip install PyPDF2 markdown nltk
预处理代码:
以下是一个完整的预处理脚本,用于加载和分割文档:
import os
import PyPDF2
import markdown
import nltk
from nltk.tokenize import sent_tokenize
# 下载 NLTK 数据
nltk.download('punkt')
def read_txt_file(file_path):
"""读取纯文本文件"""
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def read_pdf_file(file_path):
"""读取 PDF 文件"""
text = ""
with open(file_path, 'rb') as f:
pdf = PyPDF2.PdfReader(f)
for page in pdf.pages:
text += page.extract_text() + "\n"
return text
def read_md_file(file_path):
"""读取 Markdown 文件并转换为纯文本"""
with open(file_path, 'r', encoding='utf-8') as f:
md_text = f.read()
html = markdown.markdown(md_text)
# 简单清理 HTML 标签(实际项目中可能需要更复杂的处理)
return html.replace('<p>', '').replace('</p>', '\n').strip()
def split_text(text, max_length=500):
"""将文本分割成小片段"""
sentences = sent_tokenize(text)
chunks = []
current_chunk = ""
for sentence in sentences:
if len(current_chunk) + len(sentence) < max_length:
current_chunk += sentence + " "
else:
chunks.append(current_chunk.strip())
current_chunk = sentence + " "
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
def process_knowledge_base(directory):
"""处理知识库中的所有文件"""
documents = []
for root, _, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
if file.endswith('.txt'):
content = read_txt_file(file_path)
elif file.endswith('.pdf'):
content = read_pdf_file(file_path)
elif file.endswith('.md'):
content = read_md_file(file_path)
else:
continue
# 分割文本
chunks = split_text(content)
for i, chunk in enumerate(chunks):
documents.append({
'file': file,
'chunk_id': f"{file}_{i}",
'content': chunk
})
return documents
# 处理知识库
knowledge_base_dir = "rag_knowledge_base"
documents = process_knowledge_base(knowledge_base_dir)
# 打印结果
for doc in documents:
print(f"File: {doc['file']}, Chunk ID: {doc['chunk_id']}")
print(f"Content: {doc['content'][:100]}...")
print("-" * 50)
代码说明:
-
文件读取:
read_txt_file
:直接读取纯文本文件。read_pdf_file
:使用PyPDF2
提取 PDF 的文本内容。read_md_file
:使用markdown
库将 Markdown 转换为 HTML,然后简单清理为纯文本。
-
文本分割:
split_text
:使用 NLTK 的sent_tokenize
将文本按句子分割,并按最大长度(500 字符)组合成片段。- 每个片段是一个独立的检索单位,长度需要平衡检索精度和上下文完整性。
-
知识库处理:
process_knowledge_base
:遍历知识库目录,处理所有支持的文件格式。- 每个文档片段存储为一个字典,包含文件名、片段 ID 和内容。
运行以上代码后,你将得到一个 documents
列表,每个元素是一个文档片段,准备好进行向量化。
2.3 文档向量化
2.3.1 嵌入模型简介
向量化是将文本转换为高维向量表示的过程,这些向量捕捉了文本的语义信息。在 RAG 中,我们使用嵌入模型(Embedding Model)来完成这一任务。嵌入模型通常是基于 Transformer 的神经网络,训练后可以将相似的文本映射到向量空间中的靠近位置。
常用的嵌入模型包括:
- Sentence-BERT:专门为句子级语义嵌入优化,适合 RAG。
- Hugging Face 模型:如
all-MiniLM-L6-v2
,轻量且高效。 - OpenAI 嵌入:如
text-embedding-ada-002
,需要 API 密钥。
在本教程中,我们将使用 Hugging Face 的 all-MiniLM-L6-v2
模型,因为它:
- 体积小(约 80MB),适合本地部署。
- 性能优秀,支持多语言。
- 开源免费,无需 API 费用。
2.3.2 安装和配置嵌入模型
确保已安装 sentence-transformers
:
pip install sentence-transformers
以下是使用 sentence-transformers
加载嵌入模型的代码:
from sentence_transformers import SentenceTransformer
# 加载嵌入模型
embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
# 示例:嵌入单个句子
sentence = "This is a test sentence."
embedding = embedding_model.encode(sentence)
print(f"Embedding shape: {embedding.shape}")
print(f"Embedding sample: {embedding[:5]}...")
输出:
Embedding shape: (384,)
Embedding sample: [0.123, -0.456, 0.789, -0.234, 0.567]...
说明:
all-MiniLM-L6-v2
模型将文本转换为 384 维向量。- 每个向量表示文本的语义特征,可用于相似度计算。
2.3.3 向量化知识库
现在,我们将对知识库中的所有文档片段进行向量化,并将结果存储到一个列表中。以下是完整代码:
def vectorize_documents(documents, embedding_model):
"""向量化文档"""
embeddings = []
for doc in documents:
embedding = embedding_model.encode(doc['content'])
embeddings.append({
'file': doc['file'],
'chunk_id': doc['chunk_id'],
'content': doc['content'],
'embedding': embedding
})
return embeddings
# 向量化知识库
vectorized_documents = vectorize_documents(documents, embedding_model)
# 打印结果
for vec_doc in vectorized_documents[:2]:
print(f"File: {vec_doc['file']}, Chunk ID: {vec_doc['chunk_id']}")
print(f"Content: {vec_doc['content'][:100]}...")
print(f"Embedding shape: {vec_doc['embedding'].shape}")
print("-" * 50)
代码说明:
vectorize_documents
:遍历文档列表,使用嵌入模型为每个片段生成向量。- 每个向量化后的文档包含原始信息(文件名、片段 ID、内容)和嵌入向量。
性能优化提示:
-
如果知识库很大,可以使用批量编码(
embedding_model.encode
支持传入句子列表)来加速:contents = [doc['content'] for doc in documents] embeddings = embedding_model.encode(contents, batch_size=32)
2.4 存储到向量数据库
2.4.1 FAISS 简介
FAISS(Facebook AI Similarity Search)是一个高效的向量搜索库,广泛用于 RAG 系统。它支持快速的向量相似度搜索,适合存储和查询文档嵌入。FAISS 的主要特点包括:
- 高效性:支持大规模向量集的快速搜索。
- 灵活性:提供多种索引类型(如 Flat、IVF、HNSW)。
- 易用性:与 Python 集成良好。
在本教程中,我们将使用 FAISS 的 IndexFlatL2
索引,它基于欧几里得距离(L2 距离)进行精确搜索,适合小型知识库。
2.4.2 初始化 FAISS 索引
以下是使用 FAISS 存储向量化的文档的代码:
import faiss
import numpy as np
def create_faiss_index(embeddings):
"""创建 FAISS 索引"""
# 获取嵌入向量的维度
dimension = embeddings[0]['embedding'].shape[0]
# 创建 FAISS 索引(基于 L2 距离)
index = faiss.IndexFlatL2(dimension)
# 提取所有嵌入向量
embedding_vectors = np.array([doc['embedding'] for doc in embeddings]).astype('float32')
# 添加到索引
index.add(embedding_vectors)
return index, embeddings
# 创建 FAISS 索引
faiss_index, vectorized_documents = create_faiss_index(vectorized_documents)
# 打印索引信息
print(f"Total vectors in index: {faiss_index.ntotal}")
代码说明:
create_faiss_index
:初始化一个IndexFlatL2
索引,并将所有嵌入向量添加到索引中。- FAISS 要求输入向量为
float32
类型,因此使用np.array
进行转换。 faiss_index.ntotal
返回索引中的向量总数。
2.4.3 保存 FAISS 索引
为了在后续使用中加载索引,我们需要将其保存到磁盘:
def save_faiss_index(index, file_path):
"""保存 FAISS 索引"""
faiss.write_index(index, file_path)
# 保存索引
save_faiss_index(faiss_index, "rag_knowledge_base_index.faiss")
加载索引(示例):
def load_faiss_index(file_path):
"""加载 FAISS 索引"""
return faiss.read_index(file_path)
# 加载索引
loaded_index = load_faiss_index("rag_knowledge_base_index.faiss")
2.5 实现基本检索功能
2.5.1 检索流程
现在,我们已经将知识库向量化并存储到 FAISS 中,可以实现基本的检索功能。检索的步骤如下:
- 将用户查询(query)向量化。
- 使用 FAISS 索引搜索与查询向量最相似的文档向量。
- 返回对应的文档内容。
以下是实现检索功能的代码:
def retrieve_documents(query, embedding_model, faiss_index, vectorized_documents, top_k=3):
"""检索相关文档"""
# 将查询向量化
query_embedding = embedding_model.encode(query).astype('float32').reshape(1, -1)
# 搜索最相似的 top_k 个文档
distances, indices = faiss_index.search(query_embedding, top_k)
# 获取相关文档
results = []
for idx, distance in zip(indices[0], distances[0]):
doc = vectorized_documents[idx]
results.append({
'file': doc['file'],
'chunk_id': doc['chunk_id'],
'content': doc['content'],
'distance': float(distance)
})
return results
# 示例查询
query = "Which operating systems are supported by the product?"
results = retrieve_documents(query, embedding_model, faiss_index, vectorized_documents)
# 打印结果
for result in results:
print(f"File: {result['file']}")
print(f"Chunk ID: {result['chunk_id']}")
print(f"Content: {result['content'][:100]}...")
print(f"Distance: {result['distance']:.4f}")
print("-" * 50)
代码说明:
retrieve_documents
:将查询向量化,使用 FAISS 的search
方法查找最相似的文档。top_k
:控制返回的文档数量(默认为 3)。distances
:表示查询向量与文档向量之间的 L2 距离(越小越相似)。
示例输出:
File: faq.txt
Chunk ID: faq.txt_0
Content: Q: 产品支持哪些操作系统? A: 我们的产品支持 Windows 10/11、macOS 12+ 和 Ubuntu 20.04+。...
Distance: 0.1234
--------------------------------------------------
File: product_manual.pdf
Chunk ID: product_manual.pdf_1
Content: 本产品是一款高性能的智能设备,支持多种功能,包括实时数据处理和远程控制。...
Distance: 0.5678
--------------------------------------------------
...
2.5.2 检索结果分析
从输出中可以看到,查询“Which operating systems are supported by the product?”返回了 FAQ 文件中的相关片段,因为它直接包含了操作系统信息。其他返回的文档可能语义相关但不直接回答问题,这表明检索器能够捕捉语义相似性。
优化提示:
- 如果返回的文档不够精确,可以调整
top_k
或尝试其他嵌入模型。 - 对于大规模知识库,可以使用 FAISS 的
IndexIVFFlat
或IndexHNSW
索引来提高搜索效率。
2.6 常见问题与解决方案
在构建知识库和向量化过程中,Java 程序员可能遇到以下问题:
-
PDF 解析不准确:
-
问题:
PyPDF2
可能无法正确提取复杂 PDF 的文本(如包含表格或图像)。 -
解决方案:使用更强大的库,如
pdfplumber
或pymupdf
。 -
示例:
pip install pdfplumber
import pdfplumber def read_pdf_file(file_path): text = "" with pdfplumber.open(file_path) as pdf: for page in pdf.pages: text += page.extract_text() + "\n" return text
-
-
嵌入模型性能不足:
- 问题:
all-MiniLM-L6-v2
在某些领域(如法律或医疗)可能效果不佳。 - 解决方案:尝试领域特定的嵌入模型(如
legal-bert
)或更大的模型(如all-mpnet-base-v2
)。
- 问题:
-
FAISS 索引过大:
- 问题:知识库过大时,FAISS 索引可能占用大量内存。
- 解决方案:使用压缩索引(如
IndexIVFPQ
)或分布式向量数据库(如 Milvus)。
-
文本分割粒度不合适:
- 问题:片段过长可能导致检索不精确,过短可能丢失上下文。
- 解决方案:调整
max_length
参数,或使用语义分割算法(如基于主题的分割)。
2.7 整合代码
为了方便使用,我们将所有功能整合到一个完整的脚本中,包含文档处理、向量化、存储和检索:
import os
import PyPDF2
import markdown
import nltk
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
from nltk.tokenize import sent_tokenize
nltk.download('punkt')
class RAGKnowledgeBase:
def __init__(self, knowledge_base_dir, embedding_model_name='all-MiniLM-L6-v2'):
self.knowledge_base_dir = knowledge_base_dir
self.embedding_model = SentenceTransformer(embedding_model_name)
self.documents = []
self.vectorized_documents = []
self.faiss_index = None
def read_txt_file(self, file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def read_pdf_file(self, file_path):
text = ""
with open(file_path, 'rb') as f:
pdf = PyPDF2.PdfReader(f)
for page in pdf.pages:
text += page.extract_text() + "\n"
return text
def read_md_file(self, file_path):
with open(file_path, 'r', encoding='utf-8') as f:
md_text = f.read()
html = markdown.markdown(md_text)
return html.replace('<p>', '').replace('</p>', '\n').strip()
def split_text(self, text, max_length=500):
sentences = sent_tokenize(text)
chunks = []
current_chunk = ""
for sentence in sentences:
if len(current_chunk) + len(sentence) < max_length:
current_chunk += sentence + " "
else:
chunks.append(current_chunk.strip())
current_chunk = sentence + " "
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
def process_knowledge_base(self):
for root, _, files in os.walk(self.knowledge_base_dir):
for file in files:
file_path = os.path.join(root, file)
if file.endswith('.txt'):
content = self.read_txt_file(file_path)
elif file.endswith('.pdf'):
content = self.read_pdf_file(file_path)
elif file.endswith('.md'):
content = self.read_md_file(file_path)
else:
continue
chunks = self.split_text(content)
for i, chunk in enumerate(chunks):
self.documents.append({
'file': file,
'chunk_id': f"{file}_{i}",
'content': chunk
})
def vectorize_documents(self):
for doc in self.documents:
embedding = self.embedding_model.encode(doc['content'])
self.vectorized_documents.append({
'file': doc['file'],
'chunk_id': doc['chunk_id'],
'content': doc['content'],
'embedding': embedding
})
def create_faiss_index(self):
dimension = self.vectorized_documents[0]['embedding'].shape[0]
self.faiss_index = faiss.IndexFlatL2(dimension)
embedding_vectors = np.array([doc['embedding'] for doc in self.vectorized_documents]).astype('float32')
self.faiss_index.add(embedding_vectors)
def save_faiss_index(self, file_path):
faiss.write_index(self.faiss_index, file_path)
def retrieve_documents(self, query, top_k=3):
query_embedding = self.embedding_model.encode(query).astype('float32').reshape(1, -1)
distances, indices = self.faiss_index.search(query_embedding, top_k)
results = []
for idx, distance in zip(indices[0], distances[0]):
doc = self.vectorized_documents[idx]
results.append({
'file': doc['file'],
'chunk_id': doc['chunk_id'],
'content': doc['content'],
'distance': float(distance)
})
return results
# 使用示例
if __name__ == "__main__":
kb = RAGKnowledgeBase("rag_knowledge_base")
kb.process_knowledge_base()
kb.vectorize_documents()
kb.create_faiss_index()
kb.save_faiss_index("rag_knowledge_base_index.faiss")
query = "Which operating systems are supported by the product?"
results = kb.retrieve_documents(query)
for result in results:
print(f"File: {result['file']}")
print(f"Chunk ID: {result['chunk_id']}")
print(f"Content: {result['content'][:100]}...")
print(f"Distance: {result['distance']:.4f}")
print("-" * 50)
代码说明:
RAGKnowledgeBase
类封装了所有功能,包括文档处理、向量化、索引创建和检索。- 通过面向对象的方式组织代码,便于后续扩展和维护。
- 主程序展示了从知识库处理到检索的完整流程。
2.8 本章总结
本章详细讲解了如何构建知识库并实现文档向量化,为 RAG 系统的检索功能奠定了基础。你现在应该掌握了以下内容:
- 知识库的准备,包括处理多种格式的文档(文本、PDF、Markdown)。
- 使用
sentence-transformers
将文档向量化,生成语义嵌入。 - 使用 FAISS 存储和查询向量,实现基本的检索功能。
- 常见问题的解决方案,如 PDF 解析、嵌入模型选择和索引优化。
在下一章,我们将深入实现 RAG 的核心逻辑,包括查询处理、上下文增强和生成。我们将使用 langchain
框架整合嵌入模型、向量数据库和生成模型,构建一个完整的 RAG 流程。