第04节:索引
大语言模型本身通常不具备关于你私有数据(如公司内部文档、项目代码、个人笔记等)的知识。要让 LLM 能够基于这些外部数据回答问题或执行任务,我们就需要一种方法将相关的上下文信息提供给模型。这就是检索增强生成 (Retrieval Augmented Generation, RAG) 技术发挥作用的地方,而索引 (Indexing) 和检索 (Retrieval) 正是 RAG 的核心组成部分。
简单来说,RAG 的工作流程是:当用户提出问题时,系统首先从外部知识库中检索出与问题最相关的文档片段,然后将这些片段与原始问题一起作为提示词 (Prompt) 输入给大语言模型,由模型生成最终的答案。
为了让 RAG 系统能够高效地检索到相关的文档片段,我们就需要对外部数据进行预处理和组织,这个过程就是索引。LangChain 的索引模块提供了一整套工具链,用于加载、处理外部数据,并将其转换为 LLM 易于查询和检索的格式。核心流程通常是:
加载 -> 分割 -> 存储 -> 检索
1. 文档加载器
第一步是从各种来源加载数据。LangChain 提供了丰富的 Document Loaders
来处理不同格式和来源的数据。
什么是 Document
对象?
在 LangChain 中,加载的数据通常被表示为 Document
对象。它包含两个主要部分:
page_content
(str): 文档的主要文本内容。metadata
(dict): 关于文档的元数据,例如来源(文件名、URL)、页码、章节等。元数据在后续的过滤和检索中非常有用。
常用 Document Loaders (需要安装相应依赖):
TextLoader
: 加载纯文本文件 (.txt
)。PyPDFLoader
: 加载 PDF 文件 (需要pypdf
)。CSVLoader
: 加载 CSV 文件 (需要csv
)。DirectoryLoader
: 递归加载目录下的文件(可以指定加载器类型)。WebBaseLoader
: 从 URL 加载网页内容 (需要beautifulsoup4
)。UnstructuredFileLoader
: 使用unstructured
库加载多种格式文件(Word, PowerPoint, HTML, 图片等,功能强大但依赖较重)。- 还有很多针对特定数据库、API、平台(如 Notion, Slack, GitHub)的加载器。
示例 (代码见 .py
文件): 如何使用 TextLoader
和 PyPDFLoader
加载文档。
2. 文本分割器
LLM 通常有输入长度限制(Context Window)。直接将一篇长文档或整个数据库喂给模型是不可行的。因此,我们需要将加载的 Document
分割成更小的、语义相关的块(Chunks)。这就是 Text Splitters
的作用。
分割策略:
- 基于字符分割: 按固定字符数分割,简单但可能破坏语义。
- 递归字符分割 (
RecursiveCharacterTextSplitter
): 推荐使用。它会尝试按一系列分隔符(如\n\n
,\n
,)进行分割,并尽可能保持段落、句子完整。
- 基于 Token 分割 (
TokenTextSplitter
): 根据 LLM 的 tokenizer 计算 token 数量进行分割,更精确地控制块大小以适应模型限制 (需要tiktoken
)。 - Markdown / LaTeX 分割器: 针对特定格式优化。
关键参数:
chunk_size
: 每个块的目标大小(字符数或 token 数)。chunk_overlap
: 相邻块之间的重叠大小。设置重叠有助于保持块之间的上下文连续性。
示例 (代码见 .py
文件): 如何使用 RecursiveCharacterTextSplitter
分割文档。
3. 文本嵌入模型
为了让计算机能够理解文本的语义并进行相似度比较,我们需要将文本块转换为向量 (Vectors),这个过程称为嵌入 (Embedding)。
嵌入模型会将文本映射到一个高维向量空间,语义相近的文本在向量空间中的距离也更近。
常用 Embedding Models:
OpenAIEmbeddings
: 使用 OpenAI 的嵌入模型 (如text-embedding-ada-002
,text-embedding-3-small
等,需要 API Key 且收费)。HuggingFaceEmbeddings
: 加载 Hugging Face Hub 上的各种开源嵌入模型(如sentence-transformers
系列),可以在本地运行。OllamaEmbeddings
: 使用本地运行的 Ollama 服务的嵌入模型。- 还有许多针对特定服务(如 Cohere, Google PaLM/Vertex AI)的嵌入模型。
选择考虑:
- 性能: 不同模型的嵌入效果(语义捕捉能力)不同。
- 成本: OpenAI 等 API 服务收费。
- 速度: 本地模型可能较慢,API 服务有网络延迟。
- 隐私: 使用本地模型可以避免数据传输。
- 维度: 向量维度影响存储和计算开销。
安装嵌入模型
在配置嵌入模型的时候,需要提前需要安装Microsoft C++ Build Tools https://visualstudio.microsoft.com/visual-cpp-build-tools/
并安装依赖
pip install langchain-community chromadb --force-reinstall
同时还需要下载嵌入模型 https://hf-mirror.com/BAAI/bge-large-zh-v1.5/tree/main
使用Git下载嵌入模型
git clone https://hf-mirror.com/BAAI/bge-large-zh-v1.5
并将嵌入模型放在根目录下的models
目录下,才能正常启动项目。
4. 向量存储
生成文本块的向量后,我们需要一个地方来存储这些向量及其对应的原始文本块,并能高效地执行相似度搜索。这就是向量存储 (Vector Stores) 或 向量数据库 (Vector Databases) 的作用。
核心功能:
- 存储大量的向量和关联的文档块。
- 给定一个查询向量 (Query Vector),能够快速找到与其最相似的 N 个向量(及其对应的文档块)。
常用 Vector Stores:
- 内存存储:
FAISS
: Facebook AI Similarity Search 库,非常高效的本地相似度搜索库 (需要faiss-cpu
或faiss-gpu
)。ChromaDB
: 开源的向量数据库,易于使用,支持本地持久化 (需要chromadb
)。LanceDB
: 另一个高性能的本地向量数据库选项。
- 云/托管服务:
- Pinecone, Weaviate, Qdrant, Milvus, Redis, PostgreSQL (with pgvector) 等等。
选择考虑:
- 数据量: 内存存储适合中小型数据,大规模数据需要专用数据库。
- 持久化: 是否需要将索引持久化存储。
- 部署: 本地运行还是云服务。
- 性能与扩展性: 查询速度、并发能力、水平扩展能力。
- 特性: 是否支持元数据过滤、混合搜索等高级功能。
工作流程:
- 实例化: 创建一个 Vector Store 实例,通常需要传入一个
Embedding
函数。 - 添加文档: 调用
add_documents()
或类似方法,传入分割后的Document
块列表。Vector Store 会自动调用 Embedding 函数将文本转换为向量并存储。 - 相似度搜索: 调用
similarity_search(query)
或similarity_search_by_vector(embedding)
来查找与查询最相关的文档块。
5. 检索器
向量存储本身提供了相似度搜索的功能。但 LangChain 进一步抽象出了 检索器 (Retriever) 的概念。Retriever 是一个更通用的接口,负责根据用户的查询字符串返回相关的 Document
列表。
- 最常见的 Retriever 就是基于 Vector Store 的相似度搜索,可以通过
vectorstore.as_retriever()
方法轻松创建。 - Retriever 接口允许实现更复杂的检索逻辑,例如:
- 元数据过滤: 在相似度搜索的基础上,根据元数据进一步筛选结果 (
self_query
Retriever)。 - 多查询检索 (
MultiQueryRetriever
): 使用 LLM 生成多个不同角度的查询,合并检索结果。 - 上下文压缩 (
ContextualCompressionRetriever
): 在返回结果前,使用 LLM 进一步筛选或总结检索到的文档,减少无关信息。
- 元数据过滤: 在相似度搜索的基础上,根据元数据进一步筛选结果 (
as_retriever()
的常用参数:
search_type
: 搜索类型,如similarity
(默认),mmr
(Maximal Marginal Relevance, 旨在提高结果多样性),similarity_score_threshold
(基于相似度得分阈值过滤)。search_kwargs
: 传递给底层向量存储搜索方法的参数,最常用的是k
,指定返回多少个结果 (默认为 4)。
本示例将演示构建一个简单 RAG (Retrieval-Augmented Generation) 流程的核心步骤:
- 准备数据: 创建一个临时文本文件。
- 加载 (Load): 使用 TextLoader 加载文本文件。
- 分割 (Split): 使用 RecursiveCharacterTextSplitter 将文档分割成块。
- 嵌入 (Embed): 使用 OpenAIEmbeddings 为文本块创建向量嵌入。
- 存储 (Store): 使用 Chroma (内存向量数据库) 存储文本块和嵌入。
- 检索 (Retrieve): 使用 VectorStore 创建 Retriever 并执行相似度搜索。
运行前准备:
确保已安装 langchain, langchain-openai, python-dotenv, chromadb, tiktoken:
pip install langchain langchain-openai python-dotenv chromadb tiktoken
(tiktoken 通常是 openai 库的依赖,但显式安装更保险)确保项目根目录下或当前目录有
.env
文件,并包含OPENAI_API_KEY
。激活 Python 虚拟环境。
准备嵌入模型:当前已经将嵌入模型
bge-large-zh-v1.5
存放在项目根目录下的models
目录中。准备加载文件:在项目统计目录创建一个存放需要加载文件的目录,当前案例的目录名为temp_data_for_indexing,里面创建一个需要加载的txt文件,sample_document.txt。(文件已经存
xiaozhou-ai\源码:示例与项目\LangChain
目录中)运行:
python demo04.py
import os
import shutil
from dotenv import load_dotenv
## 1. 加载器
from langchain_community.document_loaders import TextLoader
## 2. 分割器
from langchain_text_splitters import RecursiveCharacterTextSplitter
## 3. 嵌入模型
from langchain_huggingface import HuggingFaceEmbeddings
## 4. 向量存储
from langchain_community.vectorstores import Chroma
## 加载环境变量
load_dotenv()
## --- 1. 加载文档 ---
DEMO_DIR = "./temp_data_for_indexing"
DEMO_FILE = os.path.join(DEMO_DIR, "sample_document.txt")
loader = TextLoader(DEMO_FILE, encoding="utf-8")
docs = loader.load()
print(f"文档加载完成。共加载 {len(docs)} 个文档。")
print(f"第一个文档内容预览 (前 50 字符): {docs[0].page_content[:50]}...")
print(f"第一个文档元数据: {docs[0].metadata}")
## --- 2. 分割文档 ---
print("\n--- 3. 分割文档 --- ")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100, # 每个块的目标大小 (字符数)
chunk_overlap=20, # 相邻块的重叠大小
length_function=len,
is_separator_regex=False,
)
splitted_docs = text_splitter.split_documents(docs)
print(f"文档分割完成。共分割成 {len(splitted_docs)} 个文本块。")
print("前三个文本块内容预览:")
for i, doc in enumerate(splitted_docs[:3]):
print(f" 块 {i+1}: {doc.page_content}")
## --- 3. 初始化嵌入模型 ---
print("\n--- 4. 初始化嵌入模型 --- ")
## 使用 OpenAI 的嵌入模型
## 请确保 OPENAI_API_KEY 已在 .env 或环境变量中设置
## 也可以选择本地模型,如 HuggingFaceEmbeddings
## embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 注释掉 OpenAI 初始化
## 使用从镜像站下载到本地的 HuggingFace 模型 BAAI/bge-large-zh-v1.5
## 请确保下面的路径指向你实际存放模型文件的文件夹
local_model_path = "./models/bge-large-zh-v1.5" # <--- 修改这里!指向包含模型文件的本地路径
model_kwargs = {'device': 'cpu'} # 如果有 CUDA 环境且安装了 torch GPU 版本, 可以改为 'cuda'
encode_kwargs = {'normalize_embeddings': True} # BGE 模型通常推荐归一化
embeddings = HuggingFaceEmbeddings(
model_name=local_model_path, # <--- 使用本地路径加载模型
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs,
# query_instruction="为这个句子生成表示以用于检索相关文章:" # BGE v1.5 版本对于 query 默认有指令,可以不用显式指定
)
print(f"嵌入模型 ({local_model_path}) 初始化成功。") # <--- 更新打印信息
## --- 4. 创建并填充向量存储 ---
print("\n--- 5. 创建并填充向量存储 (Chroma) --- ")
## 使用 Chroma 作为内存向量数据库
## 它会自动处理文本块的嵌入过程
## persist_directory 参数可以指定持久化路径,如果省略则只在内存中
vectorstore = Chroma.from_documents(
documents=splitted_docs, # 传入分割后的文档块
embedding=embeddings, # 传入 HuggingFaceEmbeddings 实例
# persist_directory="./chroma_db" # 可选:指定持久化目录
)
print("向量存储创建并填充完成 (使用 Chroma)。")
print(f"向量库中的文档数量: {vectorstore._collection.count()}") # 访问底层 collection 确认
## --- 5. 从向量存储创建检索器并检索 ---
print("\n--- 6. 创建检索器并进行检索 --- ")
## 从 VectorStore 创建 Retriever
## k=2 表示返回最相似的 2 个结果
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
print("检索器创建成功。")
query = "LangChain 有哪些核心组件?"
print(f"\n使用查询进行检索: '{query}'")
## 使用 Retriever 进行检索
retrieved_docs = retriever.invoke(query)
print(f"检索到 {len(retrieved_docs)} 个相关文档块:")
for i, doc in enumerate(retrieved_docs):
print(f"\n--- 相关文档 {i+1} ---")
print(f"内容: {doc.page_content}")
print(f"来源: {doc.metadata.get('source', '未知')}")