Skip to content

07-RAG 系统(上):文档加载与切分 ​

RAG(Retrieval-Augmented Generation,检索增强生成)是 AI Agent 系统的核心技术之一。它通过将外部知识库与 LLM 结合,让模型能够回答训练数据之外的问题。本篇文章将深入讲解 RAG 架构、文档处理流程以及 Embedding 模型选型。


一、RAG 架构全景 ​

1.1 为什么需要 RAG? ​

大语言模型存在三个核心局限:

局限说明RAG 解决方案
知识截止模型知识有截止日期,不了解最新信息实时检索外部知识库
幻觉问题可能生成看似合理但错误的内容基于检索的事实约束
领域知识对专业领域理解有限注入领域专属文档

1.2 RAG 核心架构 ​

┌─────────────────────────────────────────────────────────────┐
│                         RAG 系统架构                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────┐ │
│  │  文档加载器  │───▶│  文本切分器  │───▶│  Embedding模型  │ │
│  │  (Loaders)  │    │ (Splitters) │    │                 │ │
│  └─────────────┘    └─────────────┘    └────────┬────────┘ │
│                                                  │          │
│  ┌───────────────────────────────────────────────┘          │
│  │                                                          │
│  ▼                                                          │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────┐ │
│  │  向量数据库  │◀───│  向量检索器  │◀───│    用户查询      │ │
│  │ (Vector DB) │    │  (Retriever)│    │                 │ │
│  └──────┬──────┘    └─────────────┘    └─────────────────┘ │
│         │                                                   │
│         ▼                                                   │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────┐ │
│  │  检索结果    │───▶│   Prompt    │───▶│      LLM        │ │
│  │  (Context)  │    │   构建      │    │                 │ │
│  └─────────────┘    └─────────────┘    └─────────────────┘ │
│                                                  │          │
│                                                  ▼          │
│                                         ┌─────────────────┐ │
│                                         │    最终回答      │ │
│                                         └─────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.3 RAG 工作流程 ​

索引阶段(离线):

  1. 加载原始文档(PDF、Markdown、网页等)
  2. 将文档切分为适当大小的文本块
  3. 使用 Embedding 模型将文本转换为向量
  4. 存储到向量数据库中

检索阶段(在线):

  1. 接收用户查询
  2. 将查询转换为向量
  3. 在向量数据库中检索相似文本块
  4. 将检索结果作为上下文输入 LLM
  5. 生成最终回答

二、文档加载器与切分策略 ​

2.1 文档加载器(Document Loaders) ​

LangChain 提供了丰富的文档加载器,支持多种数据源:

python
from langchain_community.document_loaders import (
    PyPDFLoader,           # PDF 文件
    UnstructuredMarkdownLoader,  # Markdown 文件
    TextLoader,            # 纯文本文件
    WebBaseLoader,         # 网页
    CSVLoader,             # CSV 文件
    JSONLoader,            # JSON 文件
    DirectoryLoader        # 整个目录
)

# 加载 PDF
pdf_loader = PyPDFLoader("document.pdf")
pdf_docs = pdf_loader.load()

# 加载 Markdown
md_loader = UnstructuredMarkdownLoader("readme.md")
md_docs = md_loader.load()

# 加载网页
web_loader = WebBaseLoader("https://docs.python.org/3/")
web_docs = web_loader.load()

# 批量加载目录
dir_loader = DirectoryLoader(
    "./docs",
    glob="**/*.md",
    loader_cls=UnstructuredMarkdownLoader
)
dir_docs = dir_loader.load()

2.2 文档切分策略 ​

切分是 RAG 中最关键的步骤之一,直接影响检索质量。

2.2.1 切分方式对比 ​

切分方式特点适用场景
字符切分按固定字符数切分简单文本
递归字符切分优先按段落、句子切分通用场景
Token 切分按 Token 数量切分控制上下文长度
语义切分按语义边界切分高质量需求
结构化切分按文档结构切分Markdown/HTML

2.2.2 核心参数说明 ​

python
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    # 每个文本块的目标大小(字符数)
    chunk_size=1000,
    
    # 相邻文本块之间的重叠字符数
    chunk_overlap=200,
    
    # 用于切分的分隔符列表,按优先级尝试
    separators=["\n\n", "\n", "。", ",", " ", ""],
    
    # 是否保留分隔符
    keep_separator=True,
    
    # 是否按 Token 长度计算(需要 tiktoken)
    length_function=len,
)

chunks = text_splitter.split_documents(docs)

2.2.3 切分策略详解 ​

递归字符切分(推荐):

python
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 默认配置,适合大多数场景
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", "。", ",", " ", ""]
)

# 代码专用配置
code_splitter = RecursiveCharacterTextSplitter.from_language(
    language="python",
    chunk_size=1000,
    chunk_overlap=200
)

Markdown 专用切分:

python
from langchain.text_splitter import MarkdownHeaderTextSplitter

# 按 Markdown 标题层级切分
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on
)

md_chunks = markdown_splitter.split_text(markdown_content)

Token 切分:

python
from langchain.text_splitter import TokenTextSplitter

# 按 Token 切分,适合精确控制上下文长度
token_splitter = TokenTextSplitter(
    chunk_size=512,      # 每个块 512 tokens
    chunk_overlap=50     # 重叠 50 tokens
)

token_chunks = token_splitter.split_documents(docs)

2.3 切分策略选择指南 ​

┌─────────────────────────────────────────────────────────────┐
│                    切分策略选择决策树                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  文档类型是什么?                                            │
│       │                                                     │
│       ├─► Markdown/HTML ──▶ MarkdownHeaderTextSplitter      │
│       │                                                     │
│       ├─► 代码文件 ────────▶ Language-aware Splitter        │
│       │                                                     │
│       └─► 普通文本 ────────▶ RecursiveCharacterTextSplitter │
│                              │                              │
│                              ▼                              │
│                    需要语义连贯性?                          │
│                         │                                   │
│                    是 ──┼──► 增大 chunk_overlap (20-30%)    │
│                         │                                   │
│                    否 ──┴──► 标准 overlap (10-15%)          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

chunk_size 选择建议:

场景chunk_sizechunk_overlap说明
问答系统200-50050精确检索短答案
文档摘要1000-2000200保留完整上下文
代码检索500-1000100保留函数完整性
长文档分析2000-4000400减少切分数量

三、Embedding 模型选型 ​

3.1 Embedding 模型原理 ​

Embedding 模型将文本转换为高维向量,使得语义相似的文本在向量空间中距离相近。

文本 ──▶ Embedding 模型 ──▶ 向量 (如 768 维或 1536 维)

"苹果是一种水果"  ──▶  [0.12, -0.34, 0.56, ...]
"iPhone 是手机"   ──▶  [0.45, 0.12, -0.78, ...]
"香蕉很好吃"      ──▶  [0.15, -0.30, 0.52, ...]  ← 与第一句距离近

3.2 主流 Embedding 模型对比 ​

模型维度语言支持特点适用场景
text-embedding-3-small1536多语言OpenAI 出品,性价比高通用英文场景
text-embedding-3-large3072多语言性能更强,价格更高高质量需求
text-embedding-ada-0021536多语言上一代模型兼容性需求
BGE-M31024中英为主开源,中文效果好中文场景首选
BGE-large-zh1024中文开源中文 SOTA纯中文场景
GTE-large1024多语言阿里开源,效果优秀中英文混合
M3E-base768中文开源,轻量资源受限场景

3.3 模型选型代码示例 ​

OpenAI Embedding:

python
from langchain_openai import OpenAIEmbeddings

# text-embedding-3-small(推荐)
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    dimensions=1536  # 可指定输出维度
)

# text-embedding-3-large
embeddings_large = OpenAIEmbeddings(
    model="text-embedding-3-large"
)

开源 Embedding(HuggingFace):

python
from langchain_huggingface import HuggingFaceEmbeddings

# BGE-M3(中文推荐)
bge_embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-m3",
    model_kwargs={'device': 'cuda'},
    encode_kwargs={'normalize_embeddings': True}
)

# BGE-large-zh
bge_zh_embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-large-zh-v1.5"
)

# GTE-large
gte_embeddings = HuggingFaceEmbeddings(
    model_name="thenlper/gte-large"
)

Ollama 本地 Embedding:

python
from langchain_ollama import OllamaEmbeddings

# 使用 Ollama 本地运行 embedding 模型
ollama_embeddings = OllamaEmbeddings(
    model="nomic-embed-text",  # 或 "mxbai-embed-large"
    base_url="http://localhost:11434"
)

3.4 Embedding 模型评估 ​

python
# 简单的相似度测试
test_queries = [
    ("机器学习是什么", "什么是机器学习", True),   # 应该相似
    ("机器学习是什么", "深度学习原理", True),     # 应该较相似
    ("机器学习是什么", "今天天气很好", False),    # 应该不相似
]

for q1, q2, should_similar in test_queries:
    vec1 = embeddings.embed_query(q1)
    vec2 = embeddings.embed_query(q2)
    
    # 计算余弦相似度
    similarity = cosine_similarity([vec1], [vec2])[0][0]
    print(f"'{q1}' vs '{q2}': {similarity:.3f}")

四、实战:加载技术文档并切分 ​

4.1 完整代码示例 ​

python
import os
from langchain_community.document_loaders import (
    PyPDFLoader,
    UnstructuredMarkdownLoader,
    DirectoryLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma

# ========== 1. 加载文档 ==========

def load_documents(docs_path):
    """加载多种格式的文档"""
    documents = []
    
    # 加载 PDF
    if os.path.exists(os.path.join(docs_path, "pdfs")):
        for pdf_file in os.listdir(os.path.join(docs_path, "pdfs")):
            if pdf_file.endswith(".pdf"):
                loader = PyPDFLoader(
                    os.path.join(docs_path, "pdfs", pdf_file)
                )
                documents.extend(loader.load())
    
    # 加载 Markdown
    md_loader = DirectoryLoader(
        docs_path,
        glob="**/*.md",
        loader_cls=UnstructuredMarkdownLoader,
        show_progress=True
    )
    documents.extend(md_loader.load())
    
    print(f"共加载 {len(documents)} 个文档片段")
    return documents

# ========== 2. 切分文档 ==========

def split_documents(documents):
    """递归切分文档"""
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,      # 每个块约 1000 字符
        chunk_overlap=200,    # 重叠 200 字符,保持上下文连贯
        separators=["\n\n", "\n", "。", ",", " ", ""],
        length_function=len,
        is_separator_regex=False
    )
    
    chunks = text_splitter.split_documents(documents)
    
    # 添加元数据
    for i, chunk in enumerate(chunks):
        chunk.metadata["chunk_id"] = i
        chunk.metadata["chunk_size"] = len(chunk.page_content)
    
    print(f"切分为 {len(chunks)} 个文本块")
    print(f"平均块大小: {sum(len(c.page_content) for c in chunks) / len(chunks):.0f} 字符")
    
    return chunks

# ========== 3. 创建向量数据库 ==========

def create_vectorstore(chunks, persist_dir="./chroma_db"):
    """创建并持久化向量数据库"""
    
    # 初始化 Embedding 模型
    embeddings = HuggingFaceEmbeddings(
        model_name="BAAI/bge-m3",
        model_kwargs={'device': 'cuda'},  # 使用 GPU
        encode_kwargs={
            'normalize_embeddings': True,  # 归一化向量
            'batch_size': 32
        }
    )
    
    # 创建向量数据库
    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=persist_dir,
        collection_name="tech_docs"
    )
    
    # 持久化
    vectorstore.persist()
    
    print(f"向量数据库已保存至: {persist_dir}")
    print(f"集合中的文档数: {vectorstore._collection.count()}")
    
    return vectorstore

# ========== 4. 主流程 ==========

def main():
    docs_path = "./technical_docs"
    
    # 1. 加载
    documents = load_documents(docs_path)
    
    # 2. 切分
    chunks = split_documents(documents)
    
    # 3. 存储
    vectorstore = create_vectorstore(chunks)
    
    # 4. 测试检索
    query = "什么是 RAG 系统?"
    results = vectorstore.similarity_search(query, k=3)
    
    print(f"\n查询: {query}")
    print("=" * 50)
    for i, doc in enumerate(results, 1):
        print(f"\n结果 {i}:")
        print(f"来源: {doc.metadata.get('source', 'unknown')}")
        print(f"内容: {doc.page_content[:200]}...")

if __name__ == "__main__":
    main()

4.2 运行结果示例 ​

共加载 15 个文档片段
切分为 127 个文本块
平均块大小: 892 字符
向量数据库已保存至: ./chroma_db
集合中的文档数: 127

查询: 什么是 RAG 系统?
==================================================

结果 1:
来源: ./technical_docs/rag_intro.md
内容: RAG(Retrieval-Augmented Generation)是一种将检索与生成相结合的AI架构。它通过从外部知识库检索相关信息...

结果 2:
来源: ./technical_docs/ai_architecture.md
内容: 检索增强生成系统由三个核心组件构成:文档检索器、向量数据库和大语言模型...

结果 3:
来源: ./technical_docs/rag_intro.md
内容: 在RAG系统中,当用户提出问题时,系统首先将问题转换为向量表示,然后在向量数据库中查找最相似的文档片段...

五、面试考点 ​

5.1 RAG 核心流程 ​

Q: 请描述 RAG 的完整工作流程?

答:RAG 分为两个阶段:

索引阶段:

  1. 使用 Document Loader 加载原始文档
  2. 通过 Text Splitter 将文档切分为适当大小的 chunks
  3. 使用 Embedding 模型将文本转换为向量
  4. 存储到向量数据库(如 Chroma、Milvus、Pinecone)

检索生成阶段:

  1. 接收用户查询,转换为向量
  2. 在向量数据库中检索 Top-K 相似文本块
  3. 将检索结果作为上下文,构建 Prompt
  4. 调用 LLM 生成最终回答

5.2 切分策略选择 ​

Q: 如何选择合适的 chunk_size 和 chunk_overlap?

答:

  • chunk_size:取决于任务类型和 LLM 上下文窗口。问答任务建议 200-500,摘要任务建议 1000-2000。需考虑 Embedding 模型的最大输入长度。
  • chunk_overlap:通常为 chunk_size 的 10-20%。重叠可保持上下文连贯性,但过大会增加冗余和存储成本。
  • 选择原则:在保证语义完整性的前提下,尽量增大 chunk_size 以减少向量数量,提高检索效率。

Q: 递归字符切分和普通字符切分有什么区别?

答:

  • 普通字符切分:按固定长度切分,可能切断句子或段落,破坏语义完整性。
  • 递归字符切分:按优先级尝试多个分隔符(如段落、句子、标点),优先在语义边界处切分,能更好地保持文本连贯性。

5.3 Embedding 模型选型 ​

Q: 中文场景应该选择哪个 Embedding 模型?

答:

  • BGE-M3:目前中文开源模型的首选,支持多语言,效果优秀
  • BGE-large-zh:纯中文场景效果最佳
  • GTE-large:阿里开源,中英文混合场景表现好
  • OpenAI text-embedding-3:如果预算允许,API 调用方便
  • M3E-base:资源受限场景,模型较小

Q: 如何评估 Embedding 模型的效果?

答:

  1. 内在评估:计算标准数据集(如 C-MTEB)上的指标
  2. 外在评估:在下游任务(检索、聚类)中测试实际效果
  3. 人工评估:抽样检查相似度计算结果是否符合直觉
  4. 业务指标:端到端测试 RAG 系统的回答质量

5.4 常见问题 ​

Q: RAG 和 Fine-tuning 有什么区别?

维度RAGFine-tuning
知识更新实时更新知识库需要重新训练
幻觉控制基于检索事实,可控依赖模型自身
成本检索成本低训练成本高
适用场景频繁更新的知识固定领域知识

Q: 向量检索有哪些优化方法?

答:

  1. 索引优化:使用 HNSW、IVF 等近似最近邻算法
  2. 重排序(Rerank):使用 Cross-Encoder 对初筛结果精排
  3. 查询重写:扩展或改写用户查询以提高召回
  4. 混合检索:结合向量检索和关键词检索(BM25)
  5. 元数据过滤:先按标签过滤再向量检索

六、小结 ​

本文详细介绍了 RAG 系统的核心组件:

  1. RAG 架构:理解索引和检索两个阶段的工作流程
  2. 文档加载:掌握多种 Loader 的使用方法
  3. 切分策略:根据场景选择合适的切分方式和参数
  4. Embedding 选型:中文场景推荐 BGE 系列模型
  5. 实战演练:完整代码演示从加载到存储的全流程

在下一篇文章中,我们将深入讲解向量数据库选型、检索优化策略以及完整的 RAG 应用开发。


参考资源 ​

根据 MIT 许可证发布