目标
了解并掌握文档切分策略。
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系统的检索精度和生成质量。
思考
文档切分的策略是随着文档的类型、内容等等来决定的,如何决定用哪种策略进行切分,有哪几种方式来验证切分质量、评估检索效果呢。