第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 文件): 如何使用 TextLoaderPyPDFLoader 加载文档。

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-cpufaiss-gpu)。
    • ChromaDB: 开源的向量数据库,易于使用,支持本地持久化 (需要 chromadb)。
    • LanceDB: 另一个高性能的本地向量数据库选项。
  • 云/托管服务:
    • Pinecone, Weaviate, Qdrant, Milvus, Redis, PostgreSQL (with pgvector) 等等。

选择考虑:

  • 数据量: 内存存储适合中小型数据,大规模数据需要专用数据库。
  • 持久化: 是否需要将索引持久化存储。
  • 部署: 本地运行还是云服务。
  • 性能与扩展性: 查询速度、并发能力、水平扩展能力。
  • 特性: 是否支持元数据过滤、混合搜索等高级功能。

工作流程:

  1. 实例化: 创建一个 Vector Store 实例,通常需要传入一个 Embedding 函数。
  2. 添加文档: 调用 add_documents() 或类似方法,传入分割后的 Document 块列表。Vector Store 会自动调用 Embedding 函数将文本转换为向量并存储。
  3. 相似度搜索: 调用 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) 流程的核心步骤:

  1. 准备数据: 创建一个临时文本文件。
  2. 加载 (Load): 使用 TextLoader 加载文本文件。
  3. 分割 (Split): 使用 RecursiveCharacterTextSplitter 将文档分割成块。
  4. 嵌入 (Embed): 使用 OpenAIEmbeddings 为文本块创建向量嵌入。
  5. 存储 (Store): 使用 Chroma (内存向量数据库) 存储文本块和嵌入。
  6. 检索 (Retrieve): 使用 VectorStore 创建 Retriever 并执行相似度搜索。

运行前准备:

  1. 确保已安装 langchain, langchain-openai, python-dotenv, chromadb, tiktoken: pip install langchain langchain-openai python-dotenv chromadb tiktoken (tiktoken 通常是 openai 库的依赖,但显式安装更保险)

  2. 确保项目根目录下或当前目录有 .env 文件,并包含 OPENAI_API_KEY

  3. 激活 Python 虚拟环境。

  4. 准备嵌入模型:当前已经将嵌入模型bge-large-zh-v1.5存放在项目根目录下的models目录中。

  5. 准备加载文件:在项目统计目录创建一个存放需要加载文件的目录,当前案例的目录名为temp_data_for_indexing,里面创建一个需要加载的txt文件,sample_document.txt。(文件已经存xiaozhou-ai\源码:示例与项目\LangChain目录中)

  6. 运行: 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', '未知')}")