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咯咯?? 作者:基咯咯

















