
RAG调优进阶:21种切块策略,不光有代码,更有超详细场景、优缺点分析! 原创 精华
在深入每一种切块策略之前,需要先把基础工具和模拟数据准备好。
import re
import nltk
from nltk.tokenize import sent_tokenize, word_tokenize
from transformers import AutoTokenizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
# 确保已经下载了nltk的punkt分词器
try:
nltk.data.find('tokenizers/punkt')
except nltk.downloader.DownloadError:
nltk.download('punkt')
# 模拟一些文本数据
sample_text_long = """
RAG(Retrieval-Augmented Generation)是一种结合了检索和生成能力的AI技术。它的核心思想是,当大语言模型(LLM)需要回答问题时,它不再是凭空生成答案,而是首先从一个庞大的知识库中检索出最相关的上下文信息。
这些检索到的信息随后会被作为输入的一部分,提供给LLM,LLM再基于这些“额外”的知识来生成最终的回答。这种方式能够显著减少模型“胡说八道”(幻觉)的现象,并使得模型能够回答特定领域的问题,因为它的知识不再仅仅局限于训练时的数据。
在实际应用中,RAG系统涉及到多个关键组件。首先是**数据摄取和预处理**,这通常包括将原始文档(如PDF、网页、数据库记录等)清洗、解析,并转换为适合存储和检索的格式。接着是**切块(Chunking)**,这是RAG流程中至关重要的一步。切块的目的是将长文本分割成较小的、易于管理和检索的片段。
切块策略的选择直接影响检索的质量。如果切块过大,可能包含太多无关信息,增加LLM处理的负担;如果切块过小,则可能丢失上下文,导致LLM无法理解完整语义。
然后是**嵌入(Embedding)**,将文本切块转换为高维向量,这些向量能够捕捉文本的语义信息。接着是**向量数据库(Vector Database)**,用于高效存储和检索这些嵌入向量。当用户提出问题时,**检索器(Retriever)**会根据用户问题的嵌入向量,在向量数据库中查找最相似的文本切块。
最后,**生成器(Generator)**,也就是LLM,会结合用户的原始问题和检索到的相关上下文信息,生成最终的答案。整个RAG流程的优化是一个迭代的过程,需要不断地调整各个环节,才能达到最佳效果。
"""
sample_text_structured = """
# 第一章 引言
## 1.1 RAG的魅力
RAG技术有效解决了大模型幻觉问题。
## 1.2 本文目的
本文将深入探讨RAG中的切块策略。
# 第二章 切块基础
## 2.1 朴素切块
这是一种简单的切块方法。
## 2.2 固定大小切块
我们将会看到固定大小切块的实现。
---
**Note:** 本文档旨在提供RAG切块的全面指南。
"""
sample_text_with_tables = """
这是一段关于公司业绩的文本。
| Month | Sales | Profit |
|---|---|---|
| Jan | 100 | 20 |
| Feb | 120 | 25 |
| Mar | 110 | 22 |
以上是第一季度的财务数据。
下面是团队成员信息:
| Name | Role |
|---|---|
| Alice | Engineer |
| Bob | Designer |
"""
sample_text_mixed_format = """
这是一个段落。它包含一些重要的信息,需要被完整保留。
1. 这是一个列表项。
2. 这是另一个列表项。
* 子项1
* 子项2
## 重要提示
请注意以下表格数据:
| Item | Quantity | Price |
|---|---|---|
| Apple | 10 | 1.0 |
| Banana | 5 | 0.5 |
"""
一、基础篇:简单粗暴,但有时特好用!
这些方法操作起来贼简单,对付特定数据效果还真不赖。
1. 朴素切块(Naive Chunking):
- 场景使用:当你的文本天然就以行为单位组织,并且每一行都承载一个相对完整、独立的想法时,这种方法特别高效。比如,会议纪要、聊天记录、产品FAQ列表、项目待办事项、带有明确换行符的笔记等等。
- 优点:
a.实现简单: 几乎是所有切块方法中最容易实现的,一行代码搞定。
b.速度快: 处理大量文本时效率极高,不会引入复杂计算。
c.语义清晰(特定场景下): 如果文本就是按行划分语义,那么切出来的块语义很纯粹。
- 缺点:
a.上下文丢失风险: 如果一句话或一个完整想法跨越多行,这种方法会直接将其“腰斩”,导致重要的上下文信息丢失。
b.块大小不均: 每行长度不一,导致切出来的块大小差异大,可能超出LLM的token限制或过短导致信息不足。
c.不适用于连续文本: 对于小说、论文、博客等段落式的连续文本,效果通常很差。
def naive_chunking(text):
"""
按行分割文本。
"""
return text.split('\n')
print("--- 1. 朴素切块 ---")
chunks_naive = naive_chunking(sample_text_long)
for i, chunk in enumerate(chunks_naive[:5]): # 只打印前5个示例
print(f"Chunk {i+1}: '{chunk.strip()}'")
print("...")
2. 固定大小切块(Fixed-size/Fixed Window Chunking):
- 场景使用:面对“一锅粥”式的原始、混乱文本数据时,比如从PDF中OCR(光学字符识别)出来的、没有标点符号或格式的文本,或者大型的日志文件、数据流。当你对文本结构一无所知,又需要快速将数据拆分成固定大小的片段以适应模型输入时,这是最直接的选择。
- 优点:
a.简单且快速: 实现起来也很简单,切割效率高。
b.易于管理: 每个块的大小固定,便于批量处理和模型输入管理,尤其是在LLM有严格token限制时。
c.兜底策略: 在其他结构化切块方法都失效时,可以作为一种普适的兜底方案。
- 缺点:
a.上下文被截断: 最大的缺点是它会毫不留情地在任何位置切开文本,常常会把句子、段落甚至完整的想法截断,导致语义不完整或上下文信息流失严重。
b.信息冗余: 在处理结构化文本时,一个块可能包含多余的信息,或与下一个块的内容高度重叠。
def fixed_size_chunking(text, chunk_size, overlap=0):
"""
按固定大小切块,可选择重叠。
chunk_size: 每个切块的字符数
overlap: 重叠的字符数
"""
chunks = []
text_len = len(text)
# 注意:这里 i 的步长是 chunk_size - overlap,确保重叠
for i in range(0, text_len, chunk_size - overlap):
chunk = text[i:i + chunk_size]
chunks.append(chunk)
if i + chunk_size >= text_len: # 防止最后一个切块不完整,但又跳过重叠部分
break
return chunks
print("\n--- 2. 固定大小切块 ---")
chunks_fixed = fixed_size_chunking(sample_text_long, chunk_size=200, overlap=0)
for i, chunk in enumerate(chunks_fixed[:3]):
print(f"Chunk {i+1}: '{chunk.strip()}'")
print("...")
3. 滑动窗口切块(Sliding Window Chunking):
- 场景使用:当你的文本内容上下文关联紧密、信息连续性强时,比如小说、叙事性报告、详细的技术文档、自由流动的随笔等。它能有效缓解固定大小切块中上下文被截断的问题,尤其适用于LLM需要更广阔语境才能准确理解和生成回答的场景。
- 优点:
a.保持上下文: 通过块之间的重叠,能有效保留跨块的上下文信息,降低LLM理解时出现“断层”的风险。
b.提高检索精度: 检索时,即使查询命中一个重叠部分,也能带回包含更完整上下文的块。
c.适用于无结构文本: 对没有明确标题、段落分隔的文本也能较好地处理。
- 缺点:
a.冗余度增加: 重叠部分会增加存储和处理的冗余,导致向量数据库更大,嵌入和检索成本增加。
b.计算开销: 更多的块意味着更多的嵌入计算和检索操作。
c.参数调优:chunk_size
和 overlap
的比例需要根据实际数据和LLM的特性进行仔细调优,否则可能效果不佳。
def sliding_window_chunking(text, chunk_size, overlap):
"""
滑动窗口切块,每个切块与前一个重叠。
chunk_size: 每个切块的字符数
overlap: 重叠的字符数
"""
chunks = []
text_len = len(text)
start = 0
while start < text_len:
end = min(start + chunk_size, text_len)
chunk = text[start:end]
chunks.append(chunk)
if end == text_len:
break
start += (chunk_size - overlap)
return chunks
print("\n--- 3. 滑动窗口切块 ---")
chunks_sliding = sliding_window_chunking(sample_text_long, chunk_size=200, overlap=50)
for i, chunk in enumerate(chunks_sliding[:3]):
print(f"Chunk {i+1}: '{chunk.strip()}'")
print("...")
4. 基于句子切块(Sentence-based Chunking):
- 场景使用:最适合语法结构完整、句子独立承载完整语义的文本,如新闻报道、博客文章、产品说明书、法律条文、论文摘要、结构化的文档或纯文本数据。它可以作为更复杂切块策略的“第一步”,得到粒度最小的语义单元。
- 优点:
a.语义完整性高: 每个切块都是一个完整的句子,通常能保证最小的语义单元不被破坏。
b.粒度精细: 提供了最细粒度的信息,便于后续的重排、过滤或更复杂的组合操作。
c.易于理解: LLM处理完整句子时,理解成本更低。
- 缺点:
a.上下文不足: 单个句子可能缺乏足够的上下文来完全理解其含义,尤其是在上下文分散于多个句子的复杂概念中。
b.数量庞大: 对于长文档,句子切块会生成大量小块,增加存储和检索的负担。
c.标点依赖: 严重依赖文本中的标点符号来识别句子边界,如果文本质量差(如OCR错误、缺乏标点),效果会大打折扣。
def sentence_based_chunking(text):
"""
使用nltk进行句子级别的切块。
"""
return sent_tokenize(text)
print("\n--- 4. 基于句子切块 ---")
chunks_sentence = sentence_based_chunking(sample_text_long)
for i, chunk in enumerate(chunks_sentence[:5]):
print(f"Chunk {i+1}: '{chunk.strip()}'")
print("...")
5. 基于段落切块(Paragraph-based Chunking):
- 场景使用:适用于传统意义上以段落为单位组织内容的文档,如博客文章、报告、论文、书籍章节等。当你想在保持一定上下文量的同时,又能根据文章的自然逻辑进行分割时,这是非常好的选择。
- 优点:
a.上下文适中: 比句子切块能提供更多的上下文,通常一个段落能表达一个完整的想法或论点。
b.结构清晰: 尊重文档原有的段落结构,切块结果更符合人类阅读习惯。
c.实现简单: 通常通过双换行符(\n\n
)或简单的文本解析就能实现。
- 缺点:
a.段落长度不一: 不同段落的长度差异可能很大,有些段落可能过长超出LLM的token限制,有些可能过短信息量不足。
b.上下文跨段落: 如果一个逻辑概念跨越多个段落,可能会被切断。
c.格式依赖: 依赖于文本中正确的段落分隔符,如果原始文本格式混乱,效果会受影响。
def paragraph_based_chunking(text):
"""
按双换行符分割文本(通常表示段落)。
"""
# 使用正则表达式匹配一个或多个换行符,并移除空字符串
paragraphs = [p.strip() for p in re.split(r'\n{2,}', text) if p.strip()]
return paragraphs
print("\n--- 5. 基于段落切块 ---")
chunks_paragraph = paragraph_based_chunking(sample_text_long)
for i, chunk in enumerate(chunks_paragraph[:3]):
print(f"Chunk {i+1}: '{chunk.strip()}'")
print("...")
6. 基于页面切块(Page-based Chunking):
- 场景使用:主要针对具有明确分页结构的文档,如PDF文档、扫描的纸质文档、幻灯片(PPT)、书籍等。当你的检索结果需要引用到页码,或者文档的布局(如图片、表格分布)与页面的逻辑高度相关时,这种方法就显得尤为重要。
- 优点:
a.保留原始布局信息: 每个切块对应一个物理页面,能完整保留该页面的所有信息和布局,方便在原始文档中定位。
b.易于引用: 直接关联页码,便于用户或LLM引用原始出处。
c.简化处理: 对于已分页的文档,省去了复杂的语义分析。
- 缺点:
a.上下文被截断: 如果一个概念、句子或表格跨越页面,则会被无情截断,导致语义不完整。
b.块大小差异大: 不同页面的内容量差异可能很大,导致切块大小不均,影响LLM处理效率。
c.依赖于文档格式: 必须是已经分页的文档才能使用,对于纯文本或无分页概念的文档不适用。
def page_based_chunking(text, page_delimiter="---PAGE_BREAK---"):
"""
模拟页面切块,假设文本中存在页面分隔符。
实际应用中需要从PDF等文件读取。
"""
# 模拟一个多页文本
multi_page_text = (
"这是第一页的内容。\n一些重要的信息在这里。\n" +
page_delimiter + "\n" +
"这是第二页的内容。\n继续重要的讨论。\n" +
page_delimiter + "\n" +
"第三页是总结。\n全文到此结束。"
)
return [p.strip() for p in multi_page_text.split(page_delimiter) if p.strip()]
print("\n--- 6. 基于页面切块 ---")
chunks_page = page_based_chunking(sample_text_long) # 使用模拟文本
for i, chunk in enumerate(chunks_page):
print(f"Page {i+1}:\n'{chunk}'")
7. 结构化切块(Structured Chunking):
- 场景使用:当你处理的是具有明确内部结构的数据时,如日志文件(按日志条目)、JSON数据(按字段)、XML/HTML文档(按标签)、Markdown文档(按标题或特定元素)、CSV文件(按行或特定列)。这种方法能确保切块结果严格遵循数据本身的逻辑和层级。
- 优点:
a.语义完整性强: 每个切块都对应数据中的一个逻辑单元,语义上高度完整和聚焦。
b.准确性高: 不依赖模糊的文本特征,而是基于确定的结构规则,切块准确率高。
c.便于信息抽取: 切块后可以直接提取结构化信息,方便后续的知识图谱构建或特定字段检索。
- 缺点:
a.依赖于结构: 如果数据结构不一致或有错误,切块会失败。
b.解析复杂性: 需要针对不同结构编写特定的解析逻辑,增加了实现的复杂性。
c.通用性差: 每种结构需要一套独立的切块规则,无法通用。
def structured_chunking(text):
"""
根据Markdown标题结构进行切块。
"""
chunks = []
# 匹配Markdown标题,同时捕获标题和其后的内容
# 注意:这里会把每个标题下的内容切成一个块
sections = re.split(r'^(#+ .*)$', text, flags=re.MULTILINE)
current_heading = ""
current_content = []
# 第一个元素通常是空字符串或标题之前的内容
if sections[0].strip():
chunks.append(sections[0].strip())
for i in range(1, len(sections)):
part = sections[i].strip()
if part.startswith('#'): # 这是一个标题
if current_content: # 如果有之前收集的内容,先作为一个块
chunks.append("\n".join(current_content).strip())
current_content = [] # 重置
current_heading = part
current_content.append(current_heading) # 将标题也包含在块内
else: # 这是一个标题下的内容
current_content.append(part)
if current_content: # 添加最后一个块
chunks.append("\n".join(current_content).strip())
return [chunk for chunk in chunks if chunk] # 过滤空块
print("\n--- 7. 结构化切块 ---")
chunks_structured = structured_chunking(sample_text_structured)
for i, chunk in enumerate(chunks_structured):
print(f"Chunk {i+1}:\n'{chunk}'")
8. 基于文档结构切块(Document-Based Chunking):
- 场景使用:适用于具有清晰的章节、小节和标题层级的文档,例如技术手册、教科书、研究论文、长篇报告、企业知识库文档。当你希望用户能够根据文档的自然逻辑结构进行检索,或者LLM需要理解某个特定章节的完整语境时,这种方法是首选。它也是实现分层切块(Hierarchical Chunking)的基础。
- 优点:
a.高度贴合文档原意: 切块结果与文档的逻辑结构保持一致,非常自然。
b.上下文丰富: 每个块通常包含一个完整的章节或小节内容,提供足够的上下文。
c.便于导航和理解: 用户和LLM都能清晰地知道信息所属的章节位置。
- 缺点:
a.依赖文档格式: 需要文档有明确的标题或章节标记,对于非结构化文本无效。
b.解析复杂: 需要更智能的解析器来识别不同级别的标题和其对应的内容。
c.块大小不均: 不同章节的长度可能差异巨大,导致一些块过大。
def document_based_chunking(text):
"""
基于文档的自然结构(如Markdown的章节和子章节)进行切块。
这里我们将捕获顶级标题下的所有内容作为一个块,直到下一个同级或更高级的标题。
"""
chunks = []
lines = text.split('\n')
current_chunk_lines = []
for line in lines:
if line.startswith('#'): # 匹配任何级别的标题
if current_chunk_lines: # 如果当前块有内容,就结束并添加
chunks.append("\n".join(current_chunk_lines).strip())
current_chunk_lines = []
current_chunk_lines.append(line) # 将标题作为新块的开始
else:
current_chunk_lines.append(line)
if current_chunk_lines: # 添加最后一个块
chunks.append("\n".join(current_chunk_lines).strip())
return [chunk for chunk in chunks if chunk]
print("\n--- 8. 基于文档结构切块 ---")
chunks_doc_struct = document_based_chunking(sample_text_structured)
for i, chunk in enumerate(chunks_doc_struct):
print(f"Chunk {i+1}:\n'{chunk}'")
二、进阶篇:聪明切分,解决复杂问题!
这些方法需要更多的策略和算法支持,能应对更复杂的场景。
9. 基于关键词切块(Keyword-based Chunking):
- 场景使用:当文档没有明确的标题结构,但特定的关键词或短语总是标志着新主题或重要信息的开始时,这种方法非常有效。比如,法律合同中的“WHEREAS”、“THEREFORE”,医疗记录中的“Diagnosis:”、“Treatment:”,或者产品说明书中的“Warning:”、“Troubleshooting:”。
- 优点:
a.聚焦特定信息: 能有效地将包含特定关键词的重要信息切分出来。
b.规则灵活: 可以根据业务需求自定义关键词列表。
c.适用于半结构化文本: 对缺乏严格结构,但有固定标记的文本很有用。
- 缺点:
a.关键词依赖: 严重依赖预定义的关键词,如果关键词选择不当或缺失,切块效果会很差。
b.上下文丢失: 关键词可能出现在句子中间,切块时可能导致句子被截断。
c.人工成本: 确定有效的关键词列表可能需要人工分析和迭代。
def keyword_based_chunking(text, keywords):
"""
在指定关键词处进行切块。
"""
chunks = []
# 构建正则表达式,匹配所有关键词并保留关键词本身
# 使用非捕获组 (?:...) 结合 | 运算符
pattern = '|'.join(re.escape(k) for k in keywords)
parts = re.split(f'({pattern})', text) # 使用捕获组保留分隔符
current_chunk = ""
for part in parts:
if part.strip() in keywords: # 如果当前部分是关键词
if current_chunk.strip(): # 将之前的累积作为新块
chunks.append(current_chunk.strip())
current_chunk = part # 关键词作为新块的开始
else:
current_chunk += part
if current_chunk.strip(): # 添加最后一个块
chunks.append(current_chunk.strip())
return [chunk for chunk in chunks if chunk] # 过滤空块
print("\n--- 9. 基于关键词切块 ---")
keywords_for_chunking = ["Note:", "首先是", "接着是"]
chunks_keyword = keyword_based_chunking(sample_text_long + sample_text_structured, keywords_for_chunking)
for i, chunk in enumerate(chunks_keyword):
print(f"Chunk {i+1}:\n'{chunk}'")
10. 基于实体切块(Entity-based Chunking):
- 场景使用:适用于文档中特定实体(人名、地名、公司、产品等)是核心信息的场景,如新闻文章(围绕特定人物或事件)、法律合同(围绕当事人)、医学报告(围绕患者、疾病)、电影剧本(围绕角色)。当你希望检索结果能聚焦于某个实体及其相关描述时,这种方法能提供高度相关的上下文。
- 优点:
a.高度聚焦: 每个切块都围绕一个或一组实体,保证了信息的强相关性。
b.提升检索精度: 用户查询某个实体时,能精准召回所有与该实体相关的描述。
c.知识图谱构建: 为后续构建知识图谱提供了结构化的基础。
- 缺点:
a.依赖NER模型: 需要高质量的命名实体识别(NER)模型,模型性能直接影响切块效果。
b.计算开销大: NER处理本身有计算成本,且切块逻辑可能更复杂。
c.通用性受限: 对于没有明显实体的文本,效果不佳。
# pip install spacy
# python -m spacy download en_core_web_sm
import spacy
try:
nlp = spacy.load("en_core_web_sm")
except OSError:
print("Downloading spacy model 'en_core_web_sm'...")
spacy.cli.download("en_core_web_sm")
nlp = spacy.load("en_core_web_sm")
def entity_based_chunking(text):
"""
使用NER模型识别实体,并围绕实体聚合文本。
这里为了简化,我们找到实体所在句子,并以句子为单位聚合。
"""
doc = nlp(text)
entities = {} # {entity_text: [sentences containing this entity]}
for sent in doc.sents:
found_entities_in_sent = False
for ent in sent.ents:
if ent.label_ in ["PERSON", "ORG", "GPE", "PRODUCT"]: # 关注人、组织、地理、产品等实体
if ent.text notin entities:
entities[ent.text] = []
entities[ent.text].append(sent.text.strip())
found_entities_in_sent = True
ifnot found_entities_in_sent: # 如果句子没有实体,作为独立块或添加到“无实体”块
if"NO_ENTITY"notin entities:
entities["NO_ENTITY"] = []
entities["NO_ENTITY"].append(sent.text.strip())
# 将字典转换为列表,每个实体或无实体组一个块
chunks = []
for entity, sents in entities.items():
chunk_content = f"Related to {entity}:\n" + "\n".join(list(set(sents))) # 使用set去重
chunks.append(chunk_content)
return chunks
print("\n--- 10. 基于实体切块 ---")
sample_ner_text = "Apple公司发布了新的iPhone 15。Tim Cook在发布会上强调了其强大的A17芯片。用户可以在纽约的Apple Store购买。"
chunks_entity = entity_based_chunking(sample_ner_text)
for i, chunk in enumerate(chunks_entity):
print(f"Chunk {i+1}:\n'{chunk}'")
11. 基于Token切块(Token-based Chunking):
- 场景使用:主要用于需要精确控制LLM输入token数量的场景,比如LLM有严格的上下文窗口限制(token limit),或者你希望最大化单个token的使用效率。它通常作为其他切块方法(如句子切块)的补充或后处理步骤,以确保最终的块大小符合LLM要求,同时避免语义被完全破坏。
- 优点:
a.精确控制块大小: 能够严格控制每个块的token数量,避免超出LLM的输入限制。
b.适用于非结构化文本: 对于没有明确语义结构(如标题、段落)的文本,可以作为一种有效的切块方式。
c.与LLM兼容性好: 直接以LLM理解的token为单位进行切块,减少了LLM处理时的额外计算。
- 缺点:
a.语义完整性风险: 和固定大小切块类似,可能在token级别直接截断句子或单词,导致语义不完整。
b.需要与语义策略结合: 单独使用时容易丢失上下文,通常需要与句子切块等语义方法结合,先按语义切小段,再对过长的段落进行token切分。
c.依赖分词器: 切块结果依赖于所选分词器的行为,不同分词器结果可能不同。
# pip install transformers
def token_based_chunking(text, tokenizer_name="bert-base-uncased", max_tokens=128):
"""
使用分词器按token数量进行切块。
"""
tokenizer = AutoTokenizer.from_pretrained(tokenizer_name)
tokens = tokenizer.encode(text, add_special_tokens=False) # 不添加特殊token
chunks = []
for i in range(0, len(tokens), max_tokens):
chunk_tokens = tokens[i:i + max_tokens]
chunk_text = tokenizer.decode(chunk_tokens)
chunks.append(chunk_text)
return chunks
print("\n--- 11. 基于Token切块 ---")
chunks_token = token_based_chunking(sample_text_long, max_tokens=60)
for i, chunk in enumerate(chunks_token[:3]):
print(f"Chunk {i+1}:\n'{chunk}'")
print("...")
12. 基于主题切块(Topic-based Chunking):
- 场景使用:当你的文档涵盖多个主题,且主题之间有清晰的界限(但可能没有明确的标题或关键词标记),或者你希望每个切块都能高度聚焦于一个单一主题时。比如,一个关于科技趋势的报告可能同时讨论AI、区块链和元宇宙,用主题切块可以确保每个块只包含一个主题的内容。
- 优点:
a.高语义相关性: 每个切块都包含一个或少数几个紧密相关的主题,检索命中后能提供高度聚焦的信息。
b.应对复杂文档: 能够处理主题交织、没有明确结构的长文档。
c.提升检索质量: 减少了切块中的无关信息,提高了召回的精确性。
- 缺点:
a.实现复杂: 需要主题模型(如LDA、NMF)或高级聚类算法,计算成本较高。
b.主题边界模糊: 在主题过渡平滑的文档中,确定清晰的主题边界可能很困难。
c.参数调优: 聚类算法的参数(如主题数量、相似度阈值)需要仔细调优。
# pip install scikit-learn
def topic_based_chunking(text, min_sentences_per_topic=3):
"""
通过句子相似度模拟主题切块。
将相似的句子聚类成一个主题块。
"""
sentences = sent_tokenize(text)
if len(sentences) < min_sentences_per_topic:
return [text] # 句子太少,无法有效分主题
# 使用TF-IDF向量化句子
vectorizer = TfidfVectorizer().fit(sentences)
sentence_vectors = vectorizer.transform(sentences)
chunks = []
current_topic_sentences = [sentences[0]]
for i in range(1, len(sentences)):
# 计算当前句子与当前主题块中所有句子的平均相似度
current_sentence_vector = sentence_vectors[i]
# 将当前主题块的句子向量合并
current_topic_vectors = vectorizer.transform(current_topic_sentences)
avg_similarity = np.mean(cosine_similarity(current_sentence_vector, current_topic_vectors))
# 如果相似度低于某个阈值,或者当前主题块句子太多,就认为主题切换
# 这里阈值和数量都是启发式的,实际应用中需调优
if avg_similarity < 0.3or len(current_topic_sentences) >= 5: # 假设相似度低于0.3或句子多于5句视为新主题
chunks.append(" ".join(current_topic_sentences))
current_topic_sentences = [sentences[i]]
else:
current_topic_sentences.append(sentences[i])
if current_topic_sentences:
chunks.append(" ".join(current_topic_sentences))
return chunks
print("\n--- 12. 基于主题切块 ---")
chunks_topic = topic_based_chunking(sample_text_long)
for i, chunk in enumerate(chunks_topic):
print(f"Chunk {i+1}:\n'{chunk}'")
13. 表格感知切块(Table-aware Chunking):
- 场景使用:当你的文档中包含重要的表格数据,并且你希望这些表格能够作为一个完整的语义单元被处理和检索时。这在财务报表、产品规格、统计数据、研究数据等文档中尤为常见。通过将表格独立切块,LLM能更好地理解表格的结构和内容。
- 优点:
a.保留表格结构和完整性: 确保表格作为一个整体,不会被中间切断,方便LLM理解其数据关系。
b.提升表格数据检索: 用户查询表格内容时,能精确召回整个表格。
c.有利于LLM处理: LLM对结构化表格数据(如Markdown或JSON格式)的处理能力通常优于纯文本。
- 缺点:
a.解析复杂: 需要强大的表格解析能力,尤其对于非标准格式的表格或图像中的表格(需要OCR+表格检测)。
b.上下文丢失: 表格周围的文本上下文可能与表格内容紧密相关,但如果表格被单独切块,这种关联可能会被削弱。
c.依赖于格式: 仅适用于能够识别出表格结构的文档。
def table_aware_chunking(text):
"""
识别并单独切块表格。将表格内容转换为Markdown格式。
"""
chunks = []
# 匹配Markdown表格的正则表达式
# 捕获表格内容,包括表头、分隔线和行
table_pattern = re.compile(r'(\|.*\|\n\|[-: ]+\|\n(?:\|.*\|\n?)+)', re.MULTILINE)
last_end = 0
for match in table_pattern.finditer(text):
# 添加表格前的内容
pre_table_text = text[last_end:match.start()].strip()
if pre_table_text:
chunks.append(pre_table_text)
# 添加表格本身
chunks.append(match.group(0).strip())
last_end = match.end()
# 添加最后一个表格后的内容
post_table_text = text[last_end:].strip()
if post_table_text:
chunks.append(post_table_text)
return [chunk for chunk in chunks if chunk]
print("\n--- 13. 表格感知切块 ---")
chunks_table_aware = table_aware_chunking(sample_text_with_tables)
for i, chunk in enumerate(chunks_table_aware):
print(f"Chunk {i+1}:\n'{chunk}'")
14. 内容感知切块(Content-aware Chunking):
- 场景使用:适用于包含多种内容类型和结构的复杂文档,如网页文章(包含段落、列表、图片、嵌入视频)、学术论文(包含正文、图表、公式、参考文献)、商业报告(包含文字、表格、图示)。它是一种“智能”的切块方法,能根据不同内容的特点采用最合适的分割策略。
- 优点:
a.语义完整性强: 能够根据内容类型灵活调整,最大程度地保持语义完整性,例如段落不被切断,表格保持完整。
b.通用性广: 能够处理混合格式的复杂文档,适应性强。
c.提升检索质量: 每个切块的内容更加聚焦和完整,提高了检索的准确性和LLM的理解能力。
- 缺点:
a.实现复杂: 需要一套复杂的规则引擎来识别和区分不同类型的内容,并应用相应的切块逻辑。
b.性能开销: 解析和识别内容类型可能增加处理时间。
c.规则维护: 随着文档格式的变化,可能需要不断更新和维护切块规则。
def content_aware_chunking(text):
"""
根据内容类型(段落、列表、表格、标题等)应用不同的切块规则。
这是一个结合了多种策略的示例。
"""
chunks = []
lines = text.split('\n')
current_chunk_lines = []
in_table = False
for line in lines:
stripped_line = line.strip()
# 检查是否是表格行(简单的启发式判断)
if stripped_line.startswith('|') and'|'in stripped_line[1:]:
ifnot in_table: # 如果刚进入表格,先结束前一个非表格块
if current_chunk_lines:
chunks.append("\n".join(current_chunk_lines).strip())
current_chunk_lines = []
in_table = True
current_chunk_lines.append(line)
elif in_table: # 如果在表格中,但当前行不是表格行,则表格结束
if current_chunk_lines: # 添加完整的表格块
chunks.append("\n".join(current_chunk_lines).strip())
current_chunk_lines = []
in_table = False
current_chunk_lines.append(line) # 当前行作为新块的开始
elif stripped_line.startswith('#'): # 匹配标题
if current_chunk_lines: # 如果有内容,结束前一个块
chunks.append("\n".join(current_chunk_lines).strip())
current_chunk_lines = []
current_chunk_lines.append(line) # 标题作为新块的开始
elifnot stripped_line and current_chunk_lines: # 空行作为段落分隔符
if current_chunk_lines[-1].strip() != "": # 避免连续空行导致空块
chunks.append("\n".join(current_chunk_lines).strip())
current_chunk_lines = []
else: # 普通文本行
current_chunk_lines.append(line)
if current_chunk_lines: # 添加最后一个块
chunks.append("\n".join(current_chunk_lines).strip())
return [chunk for chunk in chunks if chunk['content']]
print("\n--- 14. 内容感知切块 ---")
chunks_content_aware = content_aware_chunking(sample_text_mixed_format)
for i, chunk in enumerate(chunks_content_aware):
print(f"Chunk {i+1}:\n'{chunk}'")
15. 上下文切块(Contextual Chunking):
- 场景使用:当你的知识库内容复杂、主题关联性强,且LLM的上下文窗口足够大,能够容纳额外注入的上下文信息时。这对于金融报告、法律合同、技术规范等需要深入理解文本背后逻辑和关联性的场景非常有用。LLM可以生成关于某个切块的摘要、主题标签或与相关切块的链接,从而丰富每个块的信息。
- 优点:
a.提升理解深度: 通过LLM添加额外上下文,增强了每个切块的语义丰富性,帮助下游LLM更好地理解和推理。
b.降低幻觉: LLM对检索到的信息理解更全面,减少了生成错误答案的风险。
c.灵活适应: LLM可以根据具体需求生成不同类型的上下文信息。
- 缺点:
a.成本高昂: 需要调用LLM进行额外处理,会增加API调用成本和计算延迟。
b.token消耗: 添加额外上下文会增加每个切块的token数量,可能更快达到LLM的上下文限制。
c.LLM依赖: 效果严重依赖LLM的生成能力和对知识库的理解程度。
def mock_llm_add_context(chunk_text, knowledge_base_overview):
"""
模拟LLM为每个切块添加相关上下文。
在实际中,这需要调用一个真正的LLM。
"""
if"RAG"in chunk_text and"检索"in chunk_text:
returnf"Context: This chunk details the core retrieval mechanism of RAG and its purpose related to knowledge bases. ---\n{chunk_text}"
elif"切块"in chunk_text and"影响"in chunk_text:
returnf"Context: This chunk elaborates on the criticality of chunking strategies and their impact on LLM performance and context. ---\n{chunk_text}"
else:
# 模拟一个通用上下文
returnf"Context: This text fragment discusses general AI concepts or system components. ---\n{chunk_text}"
def contextual_chunking(text, base_chunking_strategy=paragraph_based_chunking, knowledge_base_overview="Overview of AI and RAG systems."):
"""
先进行基础切块,然后用LLM为每个切块添加上下文。
"""
base_chunks = base_chunking_strategy(text, **{}) # 确保可以传入空字典
contextualized_chunks = [mock_llm_add_context(chunk, knowledge_base_overview) for chunk in base_chunks]
return contextualized_chunks
print("\n--- 15. 上下文切块 ---")
chunks_contextual = contextual_chunking(sample_text_long)
for i, chunk in enumerate(chunks_contextual[:3]):
print(f"Chunk {i+1}:\n'{chunk}'")
print("...")
16. 语义切块(Semantic Chunking):
- 场景使用:当你的文档主题连贯但缺乏明确结构,或者不同主题的句子交织在一起时,如访谈记录、会议纪要的自由转录、长篇小说中人物情感的起伏、对某个复杂概念的多角度阐述。这种方法通过识别句子或段落的语义相似性,将真正“谈论同一件事”的内容聚合在一起。
- 优点:
a.高语义纯度: 确保每个切块中的内容在语义上高度相关,减少无关信息的干扰。
b.应对无结构文本: 在没有明确结构的情况下,也能找到自然的语义边界。
c.提升检索质量: 用户查询某个概念时,能召回所有语义上相关的片段,即使它们在原文中不相邻。
- 缺点:
a.实现复杂: 需要使用句子嵌入模型(如Sentence Transformers),并进行向量计算和聚类分析。
b.计算开销: 嵌入生成和相似度计算会增加处理时间。
c.阈值敏感: 相似度阈值的设置非常关键,过高可能导致块过小,过低可能导致块过大并包含多个主题。
d.模型依赖: 效果取决于所选嵌入模型的语义理解能力。
# pip install sentence-transformers # 实际应用会用这个
from sentence_transformers import SentenceTransformer
# 加载一个预训练的句子嵌入模型 (首次运行可能需要下载)
try:
embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
except Exception:
print("Failed to load sentence-transformers model. Please ensure you have internet or download it manually.")
# 提供一个备用/跳过策略
embedding_model = None
def semantic_chunking(text, model=embedding_model, similarity_threshold=0.7):
"""
先嵌入所有句子,然后根据相似度聚合。
"""
if model isNone:
print("Embedding model not loaded, skipping embedding chunking demo.")
return [text] # 返回原始文本或进行其他默认切块
sentences = sent_tokenize(text)
if len(sentences) <= 1:
return sentences
sentence_embeddings = model.encode(sentences)
chunks = []
current_chunk_sentences = [sentences[0]]
for i in range(1, len(sentences)):
# 计算当前句子与前一个句子嵌入的余弦相似度
similarity = cosine_similarity([sentence_embeddings[i]], [sentence_embeddings[i-1]])[0][0]
if similarity < similarity_threshold:
# 如果相似度低,则认为语义不连续,结束当前块
chunks.append(" ".join(current_chunk_sentences))
current_chunk_sentences = [sentences[i]]
else:
current_chunk_sentences.append(sentences[i])
if current_chunk_sentences: # 添加最后一个块
chunks.append(" ".join(current_chunk_sentences))
return chunks
print("\n--- 16. 语义切块 ---")
if embedding_model:
chunks_semantic = semantic_chunking(sample_text_long, similarity_threshold=0.5) # 调整阈值以观察不同效果
for i, chunk in enumerate(chunks_semantic[:3]):
print(f"Chunk {i+1}:\n'{chunk}'")
print("...")
else:
print("跳过嵌入切块演示,因为SentenceTransformer模型未加载。")
17. 递归切块(Recursive Chunking):
- 场景使用:对于长度不确定、结构不规则的文本,如采访记录、自由形式的写作、用户评论、非结构化文档等。当你想确保每个切块都满足LLM的最大token限制,同时尽可能保持语义完整性时,递归切块是一个非常强大的通用解决方案。它会优先使用大的语义分隔符,如果仍超出限制,则尝试更小的分隔符,直至满足要求。
- 优点:
a.灵活性高: 能够处理各种长度和结构的文本,适应性强。
b.平衡完整性与粒度: 优先保留较大的语义单元(如段落),在必要时才进一步细分到句子或单词,尽量减少上下文破坏。
c.通用性强: 适合作为大多数RAG系统的通用切块策略。
- 缺点:
a.实现略复杂: 相较于简单切块,逻辑更复杂,需要定义分隔符优先级。
b.分隔符依赖: 分隔符的选择和顺序会影响切块质量,需要一定的经验和实验。
c.可能仍然截断: 在极端情况下,如果所有分隔符都用完仍无法满足长度要求,最终可能还是会强制截断文本。
def recursive_chunking(text, separators, max_chunk_size_char=500):
"""
递归切块,尝试不同的分隔符,直到块大小符合要求。
separators: 分隔符列表,从大到小排列 (如 ['\n\n', '\n', '. ', ' '])
max_chunk_size_char: 最大切块字符数
"""
chunks = []
ifnot text:
return []
# 如果文本已经小于最大块大小,直接返回
if len(text) <= max_chunk_size_char:
return [text]
# 尝试当前最大的分隔符
if separators:
current_separator = separators[0]
remaining_separators = separators[1:]
parts = text.split(current_separator)
for part in parts:
part_stripped = part.strip()
if part_stripped: # 确保不是空字符串
if len(part_stripped) > max_chunk_size_char:
# 如果部分仍然太大,递归调用更小的分隔符
chunks.extend(recursive_chunking(part_stripped, remaining_separators, max_chunk_size_char))
else:
chunks.append(part_stripped)
else: # 没有更多分隔符可用,直接按字符切分(作为兜底)
for i in range(0, len(text), max_chunk_size_char):
chunks.append(text[i:i + max_chunk_size_char])
return [chunk for chunk in chunks if chunk] # 过滤空块
print("\n--- 17. 递归切块 ---")
# 模拟一个非常长的段落,需要递归切分
long_paragraph = "这是一个非常非常长的段落,它包含了多句话,并且可能在语义上可以被分割。我们希望这个段落能够被智能地切分成更小的部分,以便于RAG系统处理。如果直接固定大小切块,可能会切断句子的上下文,导致信息丢失。所以,我们需要一个更灵活的策略来处理这种长文本。RAG的成功很大程度上取决于切块的质量。我们在这里模拟一个非常长的输入,以测试递归切块的能力。请注意,这个段落的长度远远超过了我们设定的最大块大小,所以它将被进一步切分。切块的艺术在于平衡信息的完整性和粒度。适当的切块能够帮助大模型更好地理解检索到的信息,从而生成更准确、更相关的回答。这是一项技术挑战,也是RAG优化的关键一步。通过不同的分隔符进行递归切分,我们可以确保每个块都不会过大,同时尽量保持语义的完整性。当遇到一个超长的段落时,首先尝试用段落符切分,如果还超长,就用句号切分,再超长就用逗号,直到达到预设的最大长度。"
separators = ['\n\n', '. ', ',', ' '] # 尝试从大到小的分隔符
chunks_recursive = recursive_chunking(long_paragraph, separators, max_chunk_size_char=100)
for i, chunk in enumerate(chunks_recursive):
print(f"Chunk {i+1} (len={len(chunk)}):\n'{chunk}'")
18. 嵌入切块(Embedding Chunking):
- 场景使用:当你的文档完全非结构化,缺乏任何标点、标题或清晰的段落分隔,或者简单的启发式切块效果不佳时。这种方法特别适合处理口语化的转录文本、网络爬取的混乱数据流等。它基于语义相似度来决定切块边界,从而在缺乏显式结构的情况下创建有意义的块。
- 优点:
a.应对无结构文本: 对没有明确结构的信息非常有效,能自动识别语义边界。
b.语义准确性高: 直接利用句子嵌入的语义信息,确保切块内容的相关性。
c.自动化程度高: 无需手动定义规则或关键词,自动化程度高。
- 缺点:
a.计算成本高: 需要为所有句子生成嵌入,这比简单的文本分割计算量更大。
b.模型依赖: 效果严重依赖所使用的嵌入模型的质量和适用性。
c.阈值敏感: 相似度阈值的设置对最终切块结果有很大影响,需要仔细调优。
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer
# 加载一个预训练的句子嵌入模型 (首次运行可能需要下载)
try:
embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
except Exception:
print("Failed to load sentence-transformers model. Please ensure you have internet or download it manually.")
# 提供一个备用/跳过策略
embedding_model = None
def embedding_chunking(text, model=embedding_model, similarity_threshold=0.7):
"""
先嵌入所有句子,然后根据相似度聚合。
"""
if model isNone:
print("Embedding model not loaded, skipping embedding chunking demo.")
return [text] # 返回原始文本或进行其他默认切块
sentences = sent_tokenize(text)
if len(sentences) <= 1:
return sentences
sentence_embeddings = model.encode(sentences)
chunks = []
current_chunk_sentences = [sentences[0]]
for i in range(1, len(sentences)):
# 计算当前句子与前一个句子嵌入的余弦相似度
similarity = cosine_similarity([sentence_embeddings[i]], [sentence_embeddings[i-1]])[0][0]
if similarity < similarity_threshold:
# 如果相似度低,则认为语义不连续,结束当前块
chunks.append(" ".join(current_chunk_sentences))
current_chunk_sentences = [sentences[i]]
else:
current_chunk_sentences.append(sentences[i])
if current_chunk_sentences: # 添加最后一个块
chunks.append(" ".join(current_chunk_sentences))
return chunks
print("\n--- 18. 嵌入切块 ---")
if embedding_model:
chunks_embedding = embedding_chunking(sample_text_long, similarity_threshold=0.5) # 调整阈值以观察不同效果
for i, chunk in enumerate(chunks_embedding[:3]):
print(f"Chunk {i+1}:\n'{chunk}'")
print("...")
else:
print("跳过嵌入切块演示,因为SentenceTransformer模型未加载。")
19. Agentic / 基于LLM切块(Agentic / LLM-based Chunking):
- 场景使用:适用于极其复杂、高度非结构化且难以用规则或启发式方法有效切块的文本。例如,包含大量口语、多主题交织、推理链条复杂的会议讨论、自由形式的用户反馈、专业领域的专家报告等。当人类判断是最佳的切块方式,但又需要自动化时,可以考虑让LLM来“智能”地完成这个任务。
- 优点:
a.高度智能: LLM能够理解文本的深层含义、逻辑关系和上下文,从而做出更符合语义的切块决策。
b.灵活性和适应性强: 可以应对各种复杂和未知的文本结构。
c.减少人工干预: 在一些传统方法难以处理的场景下,可以自动化切块过程。
- 缺点:
a.成本高昂: 调用大型LLM进行切块会产生显著的API费用和计算延迟。
b.速度较慢: LLM推理速度通常比基于规则或嵌入的切块慢得多。
c.不可控性: LLM的切块决策可能不够稳定或可解释,有时会出现“意料之外”的分割。
d.token限制: 需要将文本分成LLM可以处理的较小段落进行处理。
def mock_llm_chunking_decision(text_segment):
"""
模拟LLM决定如何切块。
在实际中,需要给LLM提供文本段落和切块规则,让它返回分割点或直接返回切好的块。
例如,可以给LLM一个Prompt:
"给定以下文本,请将其分割成语义连贯的、不超过200字的独立片段,并以'---CHUNK---'作为分隔符返回:"
"""
# 模拟LLM智能地将文本分割成几个逻辑块
if"RAG"in text_segment and"幻觉"in text_segment:
return ["RAG技术有效解决了大模型幻觉问题。", "其核心在于结合检索和生成能力。"]
elif"切块"in text_segment and"影响"in text_segment:
return ["切块策略直接影响检索的质量。", "如果切块过大,会增加LLM处理负担;如果过小,则可能丢失上下文。"]
else:
# 如果LLM无法智能切分,就回退到句子切块
return sent_tokenize(text_segment)
def agentic_llm_based_chunking(text, max_segment_for_llm=500):
"""
使用LLM来决定切块边界。
由于LLM调用成本,通常我们会先将大文本切分成适合LLM处理的段落,
然后让LLM对这些段落进行细粒度切块。
"""
# 先进行一个粗粒度的切块(例如,按段落或固定大小),确保每个段落大小适合LLM处理
coarse_chunks = fixed_size_chunking(text, max_segment_for_llm, overlap=0)
final_chunks = []
for chunk in coarse_chunks:
# 模拟LLM对每个粗粒度块进行智能切分
llm_decided_sub_chunks = mock_llm_chunking_decision(chunk)
final_chunks.extend(llm_decided_sub_chunks)
return [c.strip() for c in final_chunks if c.strip()]
print("\n--- 19. Agentic / 基于LLM切块 ---")
chunks_llm_based = agentic_llm_based_chunking(sample_text_long, max_segment_for_llm=300)
for i, chunk in enumerate(chunks_llm_based[:5]):
print(f"Chunk {i+1}:\n'{chunk}'")
print("...")
20. 分层切块(Hierarchical Chunking):
- 场景使用:对于结构清晰、具有多级标题的复杂文档,如书籍、学术论文、法律法规、复杂的公司规章制度、带有严格目录的技术文档。当你需要支持用户在不同粒度(从章节概览到具体段落)进行检索,并且LLM在生成回答时需要理解信息的层级关系时,这种方法是理想选择。
- 优点:
a.全面性与粒度兼顾: 提供了多粒度的检索能力,用户可以先获取高层级概览,再深入细节。
b.保留结构上下文: 每个块都带有其所属的层级信息(如章节标题),LLM在处理时能更好地理解其在文档中的位置和作用。
c.提升检索效率: 可以根据查询的广度在不同层级进行检索,提高效率。
- 缺点:
a.实现最复杂: 需要复杂的解析器来识别和构建文档的层级结构,并处理各种边缘情况。
b.存储冗余: 某些内容可能在不同层级的块中重复出现(如一个段落既是其小节块的一部分,也是其章节块的一部分),增加存储负担。
c.依赖文档结构: 对于非结构化文档或结构混乱的文档,无法应用。
class HierarchicalChunk:
def __init__(self, content, level, title=None, children=None):
self.content = content
self.level = level
self.title = title
self.children = children if children isnotNoneelse []
def __repr__(self):
returnf"Level {self.level} '{self.title or self.content[:30]}...'"
def parse_markdown_hierarchy(text):
"""
解析Markdown文本,构建分层结构。
返回一个包含顶级HierarchicalChunk对象的列表。
"""
lines = text.split('\n')
# 存储当前的层级路径,方便构建嵌套结构
# Stack stores (level, parent_chunk)
root_chunks = []
current_path = [(0, None)] # (level, parent_chunk)
for line in lines:
stripped_line = line.strip()
ifnot stripped_line:
continue
match = re.match(r'^(#+)\s*(.*)$', stripped_line)
if match:
level = len(match.group(1)) # 标题级别
title = match.group(2).strip()
new_chunk = HierarchicalChunk(cnotallow="", level=level, title=title)
# 回溯到正确的父级
while current_path and current_path[-1][0] >= level:
current_path.pop()
if current_path and current_path[-1][1]: # 有父级
current_path[-1][1].children.append(new_chunk)
else: # 顶级标题
root_chunks.append(new_chunk)
current_path.append((level, new_chunk))
else: # 普通内容,添加到当前最低层级块的内容
if current_path and current_path[-1][1]:
# 如果是第一个内容行,直接赋值,否则追加
if current_path[-1][1].content:
current_path[-1][1].content += "\n" + line
else:
current_path[-1][1].content = line
else: # 没有标题的开头内容,作为顶级块
ifnot root_chunks or root_chunks[-1].level != 0or root_chunks[-1].title: # 如果没有顶级块或者上一个是标题,就创建一个新的
root_chunks.append(HierarchicalChunk(cnotallow=line, level=0))
else: # 追加到第一个无标题顶级块
root_chunks[-1].content += "\n" + line
# 递归清理并整合内容
def consolidate_chunks(chunk_list):
final_chunks = []
for chunk in chunk_list:
# 将标题本身和内容整合到content中
full_content = ""
if chunk.title:
full_content += "#" * chunk.level + " " + chunk.title + "\n"
full_content += chunk.content.strip()
if full_content: # 确保内容不为空
final_chunks.append(HierarchicalChunk(full_content, chunk.level, chunk.title))
if chunk.children:
final_chunks.extend(consolidate_chunks(chunk.children))
return final_chunks
return consolidate_chunks(root_chunks)
print("\n--- 20. 分层切块 ---")
# 使用一个更适合分层切块的结构化文本
hierarchical_text = """
# 第一章 RAG概述
RAG是一种强大的AI技术。
## 1.1 RAG的原理
结合检索和生成。
### 1.1.1 检索部分
从知识库中获取信息。
## 1.2 RAG的优势
减少幻觉,提升准确性。
# 第二章 切块策略
切块是RAG的关键一步。
"""
hierarchical_chunks = parse_markdown_hierarchy(hierarchical_text)
for i, chunk_obj in enumerate(hierarchical_chunks):
print(f"Chunk {i+1} (Level {chunk_obj.level}, Title: '{chunk_obj.title}'):\n'{chunk_obj.content}'")
21. 模态感知切块(Modality-Aware Chunking):
- 场景使用:适用于包含不同类型数据(文本、图像、表格、图表、代码等)的多模态文档,如多媒体报告、带有图表的PDF文档、网页内容。当每种模态的信息都需要以其最适合的方式处理(例如,文本切块,图像生成描述,表格转换为结构化数据)时,这种方法至关重要。
- 优点:
a.优化信息处理: 针对不同模态采用最佳处理方式,确保每种信息的完整性和可读性。
b,提升多模态检索: 能够支持跨模态的查询,例如查询“关于产品销量的图表”。
c.丰富LLM上下文: 为LLM提供更全面的信息视图,包括文本描述和结构化数据。
- 缺点:
a.实现最复杂: 需要图像识别、表格检测、文本内容分析等多种技术结合,甚至需要多模态LLM支持。
b.工具依赖: 需要集成多个不同的解析库和AI模型。
c.成本高昂: 多模态处理通常涉及更复杂的模型和更高的计算资源。
def modality_aware_chunking(text):
"""
分离不同模态的内容(文本、表格)。
这里只处理文本和Markdown表格。
"""
chunks = []
lines = text.split('\n')
current_chunk_lines = []
in_table_block = False
for line in lines:
stripped_line = line.strip()
# 检查是否是Markdown表格行
is_table_line = stripped_line.startswith('|') and'|'in stripped_line[1:]
if is_table_line:
ifnot in_table_block:
# 结束之前的文本块
if current_chunk_lines:
chunks.append({"type": "text", "content": "\n".join(current_chunk_lines).strip()})
current_chunk_lines = []
in_table_block = True
current_chunk_lines.append(line)
else:
if in_table_block:
# 结束表格块
if current_chunk_lines:
chunks.append({"type": "table", "content": "\n".join(current_chunk_lines).strip()})
current_chunk_lines = []
in_table_block = False
current_chunk_lines.append(line)
# 处理最后一个块
if current_chunk_lines:
chunk_type = "table"if in_table_block else"text"
chunks.append({"type": chunk_type, "content": "\n".join(current_chunk_lines).strip()})
return [chunk for chunk in chunks if chunk['content']]
print("\n--- 21. 模态感知切块 ---")
chunks_modality = modality_aware_chunking(sample_text_mixed_format)
for i, chunk in enumerate(chunks_modality):
print(f"Chunk {i+1} (Type: {chunk['type']}):\n'{chunk['content']}'")
BONUS:混合切块(Hybrid Chunking):集大成者,无往不利!
- 场景使用:当你的数据非常复杂,单一的切块策略无法完美解决问题时。这是一种实践中非常常见的方案,你可以根据具体的数据特点和业务需求,灵活地组合上述一种或多种策略。比如,先用段落切块,再对过长的段落进行递归切块;或者先识别表格并单独处理,然后对剩余文本进行语义切块。
- 优点:
a.高度定制化: 可以根据特定文档类型和应用场景,设计出最匹配的切块流程。
b.兼顾多种需求: 结合不同策略的优势,在语义完整性、块大小、处理效率等方面找到最佳平衡。
c.解决复杂问题: 能有效应对单一策略无法处理的复杂文档结构和内容。
- 缺点:
a.实现和调试复杂: 组合多种策略会显著增加代码的复杂性和调试难度。
b.参数调优: 多个策略的参数需要协同调优,工作量大。
c.无通用模板: 混合切块是高度定制的,没有一个放之四海而皆准的方案。
def hybrid_chunking(text, primary_strategy, secondary_strategy, primary_args={}, secondary_args={}):
"""
混合切块策略示例:先用一种策略粗切,再用另一种策略细切。
primary_strategy: 第一阶段切块函数 (如 paragraph_based_chunking)
secondary_strategy: 第二阶段切块函数 (如 recursive_chunking)
"""
# 步骤1:用主要策略进行粗粒度切块
coarse_chunks = primary_strategy(text, **primary_args)
final_chunks = []
# 步骤2:对每个粗粒度块,再用次要策略进行细粒度切块
for chunk in coarse_chunks:
# 如果粗粒度块仍然太大或需要进一步细分
if len(chunk) > 500: # 假设一个启发式条件,可以根据token数或语义复杂度来定
# 注意:这里需要确保secondary_strategy能够处理传入的参数
fine_grained_chunks = secondary_strategy(chunk, **secondary_args)
final_chunks.extend(fine_grained_chunks)
else:
final_chunks.append(chunk)
return [chunk for chunk in final_chunks if chunk.strip()]
print("\n--- BONUS: 混合切块 ---")
# 示例:先按段落切块,然后对超过一定长度的段落进行递归切块
chunks_hybrid = hybrid_chunking(
sample_text_long + sample_text_structured,
primary_strategy=paragraph_based_chunking,
primary_args={}, # 段落切块不需要额外参数
secondary_strategy=recursive_chunking,
secondary_args={'separators': ['. ', ','], 'max_chunk_size_char': 200}
)
for i, chunk in enumerate(chunks_hybrid[:5]):
print(f"Chunk {i+1} (len={len(chunk)}):\n'{chunk}'")
print("...")
深入理解每种切块策略的应用场景、优缺点,再结合代码实现,你就能在RAG的实践中更加游刃有余。记住,切块是RAG成功的基石之一,选择合适的策略,往往能让你的RAG系统事半功倍!
本文转载自Halo咯咯 作者:基咯咯
