RAG从入门到精通-文档切分策略

目标

了解并掌握文档切分策略。

RAG的核心需求

  • 精准回答:用户提问时,系统能快速找到最相关的内容。
  • 完整回答:对于复杂问题,答案能覆盖多个维度或段落,而不是零碎的片段。
  • 理解上下文:系统能理解跨越句子、段落甚至章节的复杂逻辑和指代关系。

遇到的问题和挑战

  • 信息完整性(上下文窗口)vs. 检索精度(噪声干扰)的权衡

    • 块过大:检索时容易引入不相关信息(噪声),淹没核心答案,同时可能超出LLM的上下文窗口,导致关键信息被截断。
    • 块过小:丢失上下文信息。例如,将一个论点与它的论据分开,或者将一个指代(如"这个方法")与它的前文(指代的是什么方法)割裂,导致LLM无法理解。
  • 语义边界问题

    • 如何让"块"成为一个独立的、语义完整的单元?例如,是按段落分,还是按小节分?强行在段落中间切断会破坏语义。
  • 多粒度问题

    • 用户的问题可能是细粒度的("某个函数的参数是什么?"),也可能是粗粒度的("请总结这篇文章的核心思想")。单一的切分粒度难以应对所有问题。

问题示例

python 复制代码
# 原始文档内容(连续的技术说明)
"""
Transformer模型的核心是自注意力机制。该机制允许模型在处理每个词时关注输入序列中的所有其他词。

自注意力的计算公式为:Attention(Q, K, V) = softmax(QK^T/√d_k)V

其中Q是查询矩阵,K是键矩阵,V是值矩阵。这种机制使模型能够捕获长距离依赖关系。
"""

# 不合理的切分结果:
chunk1 = "Transformer模型的核心是自注意力机制。该机制允许模型在处理每个词时关注输入序列中的所有其他词。"

chunk2 = "自注意力的计算公式为:Attention(Q, K, V) = softmax(QK^T/√d_k)V"

chunk3 = "其中Q是查询矩阵,K是键矩阵,V是值矩阵。这种机制使模型能够捕获长距离依赖关系。"

问题 :公式被从解释中分离,导致"上下文碎片化",LLM理解困难。
如何解决:不同的文档类型和内容形式,利用不同的解析、切分和检索策略。

RAG文档切分策略

1. 固定大小切分

  • 方法:简单地按字符数或Token数进行切分(如每512个字符一块)。
  • 优点:实现简单,计算高效。
  • 缺点:非常容易切断完整的句子和段落,破坏语义完整性。是"最懒惰"的策略。
  • 适用场景:
    • 结构简单的文档(如纯文本、日志文件)
    • 对检索速度要求高的场景
    • 初步原型开发
python 复制代码
from langchain.text_splitter import CharacterTextSplitter

def fixed_size_split(text, chunk_size=500, chunk_overlap=50):
    splitter = CharacterTextSplitter(
        separator="\n",
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap
    )
    return splitter.split_text(text)

# 或者使用Token长度切分
from langchain.text_splitter import TokenTextSplitter

token_splitter = TokenTextSplitter(
    chunk_size=512
)

2. 重叠切分策略

  • 方法:在切分时,让相邻的块保留一部分重叠的内容(如重叠100个字符)。
  • 优点:有效缓解了"上下文割裂"问题,确保关键信息在跨越边界时不会完全丢失。
  • 缺点:重叠部分可能引起错误理解,存储量增加。
  • 适用场景:
    • 需要保持上下文连贯性的文档
    • 技术文档、法律文件
    • 长篇小说、剧本等连续性文本
python 复制代码
def sliding_window_split(text, window_size=512, overlap=50):
    """滑动窗口切分"""
    chunks = []
    start = 0
    
    while start < len(text):
        end = start + window_size
        chunk = text[start:end]
        chunks.append(chunk)
        start += (window_size - overlap)  # 滑动,保留重叠部分
    
    return chunks

# 使用LangChain实现
from langchain.text_splitter import CharacterTextSplitter

overlap_splitter = CharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,  # 20%的重叠
    separator="\n"
)

3. 语义边界切分

  • 方法:利用文本自身的结构进行切分。
    • 按段落切分:以换行符或空行为界。
    • 按标题/小节切分:利用Markdown的#、##或HTML的<h1><h2>等标签。
    • 按句子切分:使用NLP句子分割器。
  • 优点:能较好地保持语义单元的完整性。
  • 缺点:对于长度差异大的文档,块的大小会非常不均匀。
  • 适用场景:
    • 结构化文档(Markdown、HTML、PDF)
    • 需要保持逻辑完整性的场景
    • 技术文档、学术论文

递归切分

python 复制代码
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 递归切分(推荐)
recursive_splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "。", "!", "?", "\.", "!", "\?", " ", ""],
    chunk_size=800
)

markdown

python 复制代码
from langchain.text_splitter import MarkdownHeaderTextSplitter

# 定义要分割的标题层级
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on
)

# 示例Markdown文档
markdown_text = """
# 第一章 介绍
本章介绍基本概念。

## 1.1 核心原理
Transformer的核心是注意力机制。

### 1.1.1 注意力公式
Attention(Q,K,V) = softmax(QK^T/√d_k)V
"""

docs = markdown_splitter.split_text(markdown_text)

基于句子切分

python 复制代码
import nltk

# 基于句子的切分
def sentence_aware_split(text, max_sentences=5, overlap_sentences=1):
    sentences = nltk.tokenize.sent_tokenize(text)
    chunks = []
    
    for i in range(0, len(sentences), max_sentences - overlap_sentences):
        chunk = ' '.join(sentences[i:i + max_sentences])
        chunks.append(chunk)
    
    return chunks

4. 高级语义切分

  • 方法:使用专门的NLP模型(如TextSplitter)或小型的LLM来识别语义边界,甚至可以进行主题分割。
  • 优点:最智能,能识别出潜在的、非显式的语义边界。
  • 缺点:计算成本高,实现复杂。
  • 适用场景:
    • 高质量知识库系统
    • 复杂逻辑文档
    • 对回答质量要求极高的场景

主题感知切分

python 复制代码
import numpy as np
import nltk
from sentence_transformers import SentenceTransformer

# 主题感知切分(简化版)
chunks = []
"""基于主题变化的切分(需要嵌入模型)"""
model = SentenceTransformer('all-MiniLM-L6-v2')
sentences = nltk.tokenize.sent_tokenize(text)
if len(sentences) > 1
    embeddings = model.encode(sentences)
    current_chunk = [sentences[0]]
    for i in range(1, len(sentences)):
        # 计算相邻句子的相似度
        similarity = np.dot(embeddings[i-1], embeddings[i])
        
        if similarity < 0.8:
            # 主题变化,开始新块
            chunks.append(' '.join(current_chunk))
            current_chunk = [sentences[i]]
        else:
            current_chunk.append(sentences[i])

    if current_chunk:
        chunks.append(' '.join(current_chunk))

完整的多策略切分管道

python 复制代码
class SmartDocumentSplitter:
    def __init__(self):
        self.recursive_splitter = RecursiveCharacterTextSplitter(
            chunk_size=1000,
            chunk_overlap=100,
            length_function=len,
        )

    def split_document(self, document, doc_type="general"):
        """根据文档类型选择合适的切分策略"""

        if doc_type == "markdown":
            return self._split_markdown(document)
        elif doc_type == "technical":
            return self._split_technical(document)
        elif doc_type == "legal":
            return self._split_legal(document)
        elif doc_type == "academic":
            return self._split_academic(document)
        else:
            return self.recursive_splitter.split_text(document)

    def _split_markdown(self, document):
        """Markdown文档切分"""
        # 按标题切分
        headers_to_split_on = [
            ("#", "Header 1"),
            ("##", "Header 2"),
            ("###", "Header 3"),
        ]

        markdown_splitter = MarkdownHeaderTextSplitter(
            headers_to_split_on=headers_to_split_on
        )


        return markdown_splitter.split_text(document)

    def _split_technical(self, document):
        """技术文档切分 - 更细的粒度"""
        python_splitter = RecursiveCharacterTextSplitter.from_language(
            language=Language.PYTHON,
            chunk_size=1000,
            chunk_overlap=200
        )
        return python_splitter.split_text(document)

    def _split_legal(self, document):
        """法律文档切分 - 保持条款完整性"""
        # 法律文档通常需要保持条款完整
        legal_splitter = RecursiveCharacterTextSplitter(
            chunk_size=1200,  # 更大的块保持上下文
            chunk_overlap=150,
            separators=["\n\n第", "\n\nArticle", "\n\n", "\n", "。"]
        )
        return legal_splitter.split_text(document)

    def _split_academic(self, document):
        """学术论文切分"""
        # 按章节切分
        sections = {
            "abstract": [],
            "introduction": [],
            "methodology": [],
            "results": [],
            "discussion": [],
            "conclusion": []
        }

        # 简化的学术论文结构识别
        lines = document.split('\n')
        current_section = "abstract"

        for line in lines:
            line_lower = line.lower()
            if "abstract" in line_lower:
                current_section = "abstract"
            elif "introduction" in line_lower:
                current_section = "introduction"
            elif "method" in line_lower:
                current_section = "methodology"
            elif "result" in line_lower:
                current_section = "results"
            elif "discussion" in line_lower:
                current_section = "discussion"
            elif "conclusion" in line_lower:
                current_section = "conclusion"
            else:
                sections[current_section].append(line)

        # 对每个章节进行切分
        all_chunks = []
        for section_name, section_lines in sections.items():
            if section_lines:
                section_text = '\n'.join(section_lines)
                section_chunks = self.recursive_splitter.split_text(section_text)
                # 添加章节信息到元数据
                for chunk in section_chunks:
                    all_chunks.append({
                        "content": chunk,
                        "metadata": {"section": section_name}
                    })

        return all_chunks


# 使用示例
splitter = SmartDocumentSplitter()

# 处理不同类型的文档
markdown_chunks = splitter.split_document(markdown_text, "markdown")
technical_chunks = splitter.split_document(tech_doc, "technical")
legal_chunks = splitter.split_document(legal_doc, "legal")

总结

文档切分是RAG系统的基石,通过选择合适的切分策略并正确实现,可以显著提升RAG系统的检索精度和生成质量。

思考

文档切分的策略是随着文档的类型、内容等等来决定的,如何决定用哪种策略进行切分,有哪几种方式来验证切分质量、评估检索效果呢。

相关推荐
云起SAAS4 小时前
ai周公解梦抖音快手微信小程序看广告流量主开源
微信小程序·小程序·ai编程·看广告变现轻·ai周公解梦
Tang10247 小时前
Cursor AI 编程工具指南
ai编程·cursor
用户4099322502128 小时前
复杂查询总拖后腿?PostgreSQL多列索引+覆盖索引的神仙技巧你get没?
后端·ai编程·trae
麦麦麦造8 小时前
有了 MCP,为什么Claude 还要推出 Skills?
人工智能·aigc·ai编程
AI分享猿8 小时前
MonkeyCode:开源AI编程助手的技术实践与应用价值
开源·ai编程
RainbowSea9 小时前
11. Spring AI + ELT
java·spring·ai编程
RainbowSea9 小时前
12. 模型RAG评测
java·spring·ai编程
飞哥数智坊11 小时前
“说完就走,结果自达”:这才是 AI 协同该有的样子
人工智能·ai编程
Java樱木13 小时前
AI 编程 Trae ,有重大更新!用 Trae 做了个图书借阅网站!
人工智能·ai编程