RAG(六)大語言模型應用中的分塊策略詳解

1、分塊在不同應用場景的作用
語義搜索
在語義搜索中,索引一組文檔,每個文檔包含特定主題的有價值信息。通過應用有效的分塊策略,可以確保搜索結果準確捕捉用戶查詢的核心。分塊的大小和方式直接影響搜索結果的準確性和相關性:
- 分塊過小:可能會丟失上下文信息,導致搜索結果無法準確理解用戶查詢的意圖。
- 分塊過大:可能會引入過多無關信息,稀釋關鍵內容的重要性,降低搜索結果的精確度。
一般來說,如果一段文本在沒有上下文的情況下對人類有意義,那么它對語言模型也是有意義的。因此,找到文檔庫中的最佳分塊大小至關重要。
對話代理
在對話代理中,使用嵌入的分塊來構建對話代理的上下文,基于一個知識庫,使代理能夠基于可信信息進行對話。選擇合適的分塊策略非常重要,原因有二:
- 上下文的相關性:合適的分塊策略可以確保上下文與提示真正相關,從而提高對話的質量和準確性。
- token數量限制:在將檢索到的文本發送給外部模型提供商(如OpenAI)之前,需要考慮每次請求可以發送的token數量的限制。例如,使用具有32k上下文窗口的GPT-4時,分塊大小可能不是問題,但當使用較小上下文窗口的模型時,過大的分塊可能會超出限制,導致無法有效利用檢索到的信息。 此外,使用非常大的分塊可能會會引入過多噪聲,降低檢索結果的質量,進而對結果的相關性產生不利影響。
2、分塊的嵌入行為分析
嵌入短內容(如句子)
當一個句子被嵌入時,生成的向量主要聚焦于該句子的具體含義。這是因為句子級別的嵌入模型通常會將注意力集中在句子內部的詞匯和語法結構上,以提取其核心語義。例如,句子“蘋果是一種水果”會被嵌入成一個向量,該向量主要表示“蘋果”和“水果”之間的關系,以及“蘋果”這一具體概念的特征。
優點
- 精確匹配:句子級別的嵌入能夠精確捕捉句子的具體含義,因此在與其他句子嵌入進行比較時,可以更準確地識別出語義上的相似性或差異性。
- 適合細節查詢:對于較短的查詢,如單個句子或短語,句子級別的嵌入更容易找到與之匹配的內容。例如,當用戶查詢“蘋果的營養價值”時,句子級別的嵌入可以快速定位到包含“蘋果”和“營養”相關詞匯的句子。
缺點
- 缺乏上下文信息:句子級別的嵌入可能會錯過段落或文檔中的更廣泛上下文信息。例如,一個句子可能在孤立狀態下意義明確,但在整個段落或文檔中,它的含義可能會受到前后文的影響。
- 難以捕捉主題:句子級別的嵌入難以捕捉到文本的更廣泛主題和整體結構。例如,在一個關于“健康飲食”的文檔中,單獨嵌入的句子可能無法體現出整個文檔的核心主題。
嵌入長內容(如段落或整個文檔)
當嵌入整個段落或文檔時,嵌入過程會考慮整體上下文以及文本中句子和短語之間的關系。這會生成一個更全面的向量表示,捕捉到文本的更廣泛含義和主題。例如,一個段落可能包含多個句子,這些句子共同表達了一個主題或觀點,嵌入過程會將這些信息整合到一個向量中。
優點
- 捕捉主題和上下文:長內容的嵌入能夠捕捉到文本的整體主題和上下文信息。例如,在一個關于“氣候變化”的文檔中,嵌入向量可以反映出整個文檔的核心觀點和論據。
- 適合主題查詢:對于跨越多個句子或段落的較長查詢,段落或文檔級別的嵌入更適合匹配。這是因為長查詢通常需要更廣泛的上下文信息來理解其意圖。例如,當用戶查詢“氣候變化對全球生態系統的影響”時,段落或文檔級別的嵌入可以更好地找到與之相關的長文本內容。
缺點
- 引入噪聲:較大的輸入文本大小可能會引入噪聲或稀釋單個句子或短語的重要性。例如,一個段落中可能包含一些不相關的信息,這些信息會在嵌入過程中混入,導致向量表示不夠精確。
- 難以精確匹配:由于長內容的嵌入包含了更多的信息,因此在查詢索引時更難找到精確匹配。例如,一個包含多個主題的段落可能會與多個查詢相關,但無法精確匹配到某個具體的查詢。
查詢長度對嵌入匹配的影響
查詢的長度也會影響嵌入之間的關系:
- 短查詢:較短的查詢,如單個句子或短語,會集中在具體細節上,更適合與句子級別的嵌入進行匹配。這是因為短查詢通常需要精確的語義匹配,而句子級別的嵌入能夠提供更精確的語義表示。
- 長查詢:跨越多個句子或段落的較長查詢可能更適合與段落或文檔級別的嵌入進行匹配。這是因為長查詢通常需要更廣泛的上下文信息來理解其意圖,而段落或文檔級別的嵌入能夠提供更全面的語義表示。
非均勻索引的挑戰與優勢
索引也可能是非均勻的,包含不同大小的分塊嵌入。這可能會在查詢結果相關性方面帶來挑戰,但也可能產生一些積極的影響:
- 挑戰:查詢結果的相關性可能會因為長內容和短內容的語義表示之間的差異而波動。例如,一個查詢可能與某個句子的嵌入高度相關,但與整個段落的嵌入相關性較低,這可能導致查詢結果的不一致性。
- 優勢:非均勻索引可能捕捉到更廣泛的上下文和信息,因為不同的分塊大小代表了文本中不同層次的粒度,這可能更靈活地適應不同類型的查詢。例如,對于一些需要精確匹配的查詢,句子級別的嵌入可以提供更好的結果;而對于一些需要更廣泛上下文的查詢,段落或文檔級別的嵌入可以提供更全面的信息。
3、分塊考慮因素
內容的性質
索引的內容是長文檔還是短內容,決定了哪種模型更適合目標,進而影響分塊策略的選擇。
嵌入模型
不同的嵌入模型在不同的分塊大小上表現最佳。例如,句子轉換器模型適合單個句子,而像text-embedding-ada-002這樣的通用模型在包含256或512個token的分塊上表現更好,能夠更好地捕捉段落或文檔級別的上下文信息,適合需要更廣泛語義理解的場景。
用戶查詢的長度和復雜性
用戶查詢的長度和復雜性會影響分塊內容的方式,以便嵌入查詢和嵌入分塊之間有更緊密的相關性。如果用戶查詢通常很短且具體(如單個句子或短語),那么句子級別的分塊可能更適合;如果用戶查詢通常較長且復雜(如跨越多個句子或段落),那么較大的分塊(如段落或文檔級別)可能更適合。
檢索結果的用途
檢索結果在特定應用中的用途,如語義搜索、問答、摘要等,也會影響分塊策略。
語義搜索:如果檢索結果用于語義搜索,需要考慮如何平衡分塊大小以保留上下文信息和提高檢索效率。較大的分塊可以提供更全面的上下文,但可能會降低檢索效率。
問答系統:如果檢索結果用于問答系統,需要考慮如何確保分塊與用戶問題的語義相關性。較小的分塊可以提供更精確的匹配,但可能會丟失上下文信息。
摘要生成:如果檢索結果用于摘要生成,需要考慮如何選擇分塊大小以保留關鍵信息。較小的分塊可以提供更精確的信息,但可能需要更多的分塊來覆蓋整個文檔。
其他用途:如果檢索結果需要輸入到另一個具有token限制的LLM中(如OpenAI的GPT模型),則需要根據token限制來調整分塊大小。例如,如果模型的上下文窗口為32k tokens,分塊大小可以相對較大;但如果上下文窗口較小,則需要更小的分塊。
4、經典分塊方法 固定大小分塊
最常見且直接的分塊方法,該方法只需要通過決定分塊中的token數量,并可選地決定分塊之間是否有重疊來實現。通常,分塊之間會保留一些重疊,以確保語義上下文不會在分塊之間丟失。
優點:這種方法計算成本低且易于使用,不需要使用任何NLP庫。
缺點:無法根據內容的語義結構進行動態調整。如果分塊過大,可能會稀釋關鍵信息;如果分塊過小,可能會丟失上下文信息。
適用于大多數常見情況,尤其是當文本內容較為均勻且不需要復雜語義分析的情況。
text = "..." # 你的文本
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator = "\n\n",
chunk_size = 256,
chunk_overlap = 20
)
docs = text_splitter.create_documents([text])5、經典分塊方法 “內容感知”分塊
“內容感知”分塊方法利用分塊內容的性質,應用更復雜的分塊策略。這些方法通常基于自然語言處理技術,能夠更好地保留語義結構。
句子分割
句子分割是將文本分割成句子級別的分塊。許多模型都針對句子級別的嵌入進行了優化。
適用場景于需要精確句子匹配的場景(如問答系統、語義搜索)和文本內容較為均勻且以句子為單位的場景(如新聞文章、學術論文等)。
- 簡單分割:通過句號(“.”)和換行符分割句子。快速且簡單,但可能無法處理所有邊緣情況。
text = "..." # 你的文本
docs = text.split(".")- NLTK:自然語言工具包(NLTK)提供了一個句子分詞器,可以準確地識別句子邊界,避免在句子中間進行分塊,從而確保每個分塊都包含完整的句子。,
text = "..." # 你的文本
from langchain.text_splitter import NLTKTextSplitter
# 創建 NLTKTextSplitter 實例
text_splitter = NLTKTextSplitter(
chunk_size=256, # 分塊大小
chunk_overlap=20 # 分塊重疊部分
)
docs = text_splitter.split_text(text)- spaCy:spaCy 提供了多種預訓練模型,支持多種語言,這些模型能夠準確地識別句子邊界、詞性等信息,并可以通過設置分塊之間的重疊部分, 在一定程度上保留上下文信息。
text = "..." # 你的文本
from langchain.text_splitter import SpacyTextSplitter
# 創建 SpacyTextSplitter 實例
text_splitter = SpacyTextSplitter(
nlp=nlp, # spaCy 模型
chunk_size=256, # 分塊大小
chunk_overlap=20 # 分塊重疊部分
)
docs = text_splitter.split_text(text)遞歸分塊
遞歸分塊使用一組分隔符以分層和迭代的方式將輸入文本分割成較小的塊。如果初始的分割嘗試沒有生成所需大小或結構的塊,該方法會使用不同的分隔符或標準遞歸調用自身,直到達到所需的塊大小或結構。
優點:可以根據不同的分隔符和標準動態調整分塊大小,能夠處理不同結構的文本。
缺點:遞歸調用可能導致較高的計算開銷。
適用于需要處理復雜文本結構的場景(如長文檔、多級標題的文檔)和需要動態調整分塊大小的場景(如根據內容的語義結構進行分塊)。
text = "..." # 你的文本
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
# 設置一個非常小的塊大小,僅用于展示。
chunk_size = 256,
chunk_overlap = 20
)
docs = text_splitter.create_documents([text])專用分塊
專用分塊方法針對特定格式的文本(如Markdown和LaTeX)進行優化,以在分塊過程中保留內容的原始結構。
優點:能夠根據特定格式的語法和結構進行分塊,生成更具語義連貫性的塊。
缺點:僅適用于特定格式的文本,通用性較差。需要解析特定格式的語法和結構。
適用于特定格式的文本處理。
- Markdown
from langchain.text_splitter import MarkdownTextSplitter
markdown_text = "..."
markdown_splitter = MarkdownTextSplitter(chunk_size=100, chunk_overlap=0)
docs = markdown_splitter.create_documents([markdown_text])- LaTeX
from langchain.text_splitter import LatexTextSplitter
latex_text = "..."
latex_splitter = LatexTextSplitter(chunk_size=100, chunk_overlap=0)
docs = latex_splitter.create_documents([latex_text])語義分塊
語義分塊是一種新的實驗性分塊技術,通過語義分析創建由談論相同主題或話題的句子組成的塊。
優點:能夠根據語義內容動態調整分塊,生成更有意義的文本片段。
缺點:需要生成嵌入并進行語義分析,實現復雜度較高。效果依賴于嵌入模型的質量。
實現步驟:
- 將文檔分割成句子。
- 創建句子組:對于每個句子,創建一個包含該句子前后一些句子的組。
- 為每個句子組生成嵌入,并將它們與它們的“錨定”句子相關聯。
- 按順序比較每個組之間的距離:較低的語義距離表明主題相同,較高的語義距離表明主題已發生變化。
from langchain.text_splitter import SemanticTextSplitter
text = "..."
# 加載預訓練的語言模型
model = SentenceTransformer('all-MiniLM-L6-v2')
# 創建 SemanticTextSplitter 實例
text_splitter = SemanticTextSplitter(
model=model, # 預訓練的語言模型
chunk_size=256, # 分塊大小
chunk_overlap=20, # 分塊重疊部分
semantic_threshold=0.75 # 語義距離閾值
)
docs = text_splitter.split_text(text)5經典分塊方法確定最佳分塊大小
數據預處理:在確定最佳分塊大小之前,需要對數據進行預處理以確保質量,如刪除HTML標簽或去除噪聲元素。選擇分塊大小范圍:根據內容的性質(如短消息或長文檔)和嵌入模型的功能(如token限制),選擇一系列潛在的分塊大小進行測試。目標是找到保留上下文和保持準確性之間的平衡。評估每個分塊大小性能:使用多個索引或具有多個命名空間的單個索引,為測試的分塊大小創建嵌入并保存在索引中。然后運行一系列查詢來評估質量,并比較不同分塊大小的性能,通過迭代過程確定最適合內容和預期查詢的分塊大小。6、Agentic Chunking
Agentic Chunking是一種基于大語言模型(LLM)的先進文本分塊方法,旨在通過模擬人類在文本分割時的理解和判斷,生成語義連貫的文本塊。這種方法的核心在于關注文本中的“Agentic”元素(如人物、組織機構等),并將圍繞這些元素相關的句子聚合在一起,形成有意義的文本塊。
使用 agentic chunking 能夠解決遞歸字符分割和語義分割的局限性。它不依賴于固定的 token 長度或語義意義的變化,而是主動評估每一句話,并將其分配到相應的文本塊中。正因為如此,agentic chunking 能夠將文檔中相隔甚遠的相關句子歸入同一組。
優點:
- 語義連貫性:能夠將文檔中相隔較遠但主題相關的句子歸入同一組,生成語義連貫的文本塊。
- 動態調整:根據文本內容動態調整分塊大小,適應不同類型的文本。
- 上下文保留:通過句子獨立化和語義評估,保留上下文信息,提升檢索效率。
- 智能分塊:通過 LLM 的智能判斷,實現更高效的文本分塊。
缺點:
- 成本較高:每次調用 LLM 都會消耗成本,且增加延遲。
- 處理速度較慢:由于需要進行復雜的語義評估,處理速度相對較慢。
- 資源需求高:需要足夠的計算資源和內存來處理大型文檔。
Agentic Chunking 特別適用于以下場景
非結構化文本:如客服對話記錄、播客內容等。
主題反復變化的內容:如技術沙龍實錄、會議記錄等。
需要跨段落關聯的 QA 系統:如智能問答系統、語義搜索等。
下面來看下該方法的實現:
句子獨立化(Propositioning)
將文本中的每個句子獨立化,確保每個句子都有自己的主語。例如,將句子 “He was leading NASA’s Apollo 11 mission.” 轉換為 “Neil Armstrong was leading NASA’s Apollo 11 mission.”。這一步驟可以看作是對文檔進行“句子級整容”,確保每個句子獨立完整。
from langchain.chains import create_extraction_chain_pydantic
from langchain_core.pydantic_v1 import BaseModel
from typing import Optional
from langchain.chat_models import ChatOpenAI
import uuid
import os
from typing import List
from langchain import hub
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel
# 定義pydantic模型
class Sentences(BaseModel):
sentences: List[str]
# 初始化 LLM 和提取鏈
obj = hub.pull("wfh/proposal-indexing")
llm = ChatOpenAI(model="gpt-4o")
extraction_llm = llm.with_structured_output(Sentences)
extraction_chain = obj | extraction_llm
# 調用提取鏈并處理文本
sentences = extraction_chain.invoke(
"""
On July 20, 1969, astronaut Neil Armstrong walked on the moon.
He was leading the NASA's Apollo 11 mission.
Armstrong famously said, "That's one small step for man, one giant leap for mankind" as he stepped onto the lunar surface.
"""
)創建文本塊
第一次啟動時,沒有任何文本塊。因此,必須創建一個文本塊來存儲第一個 proposition。
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(temperature=0)
chunks = {}
def create_new_chunk(chunk_id, proposition):
summary_llm = llm.with_structured_output(ChunkMeta)
summary_prompt_template = ChatPromptTemplate.from_messages(
[
(
"system",
"Generate a new summary and a title based on the propositions.",
),
(
"user",
"propositions:{propositions}",
),
]
)
summary_chain = summary_prompt_template | summary_llm
chunk_meta = summary_chain.invoke(
{
"propositions": [proposition],
}
)
chunks[chunk_id] = {
"summary": chunk_meta.summary,
"title": chunk_meta.title,
"propositions": [proposition],
}更新文本塊
每個后續的 proposition 都需要被添加到一個文本塊中。當添加一個 proposition 時,文本塊的標題和摘要可能并不完全準確地反映其內容。因此,要對它們進行重新評估,并在必要時進行重寫。
from langchain_core.pydantic_v1 import BaseModel, Field
class ChunkMeta(BaseModel):
title: str = Field(descriptinotallow="The title of the chunk.")
summary: str = Field(descriptinotallow="The summary of the chunk.")
def add_proposition(chunk_id, proposition):
summary_llm = llm.with_structured_output(ChunkMeta)
summary_prompt_template = ChatPromptTemplate.from_messages(
[
(
"system",
"If the current_summary and title is still valid for the propositions return them."
"If not generate a new summary and a title based on the propositions.",
),
(
"user",
"current_summary:{current_summary}\n\ncurrent_title:{current_title}\n\npropositions:{propositions}",
),
]
)
summary_chain = summary_prompt_template | summary_llm
chunk = chunks[chunk_id]
current_summary = chunk["summary"]
current_title = chunk["title"]
current_propositions = chunk["propositions"]
all_propositions = current_propositions + [proposition]
chunk_meta = summary_chain.invoke(
{
"current_summary": current_summary,
"current_title": current_title,
"propositions": all_propositions,
}
)
chunk["summary"] = chunk_meta.summary
chunk["title"] = chunk_meta.title
chunk["propositions"] = all_propositions語義評估與合并
LLM 會根據句子的語義內容,將其分配到現有的文本塊中,或者在找不到合適的文本塊時創建新的文本塊。如果新的句子被添加到文本塊中,LLM 可以更新文本塊的摘要和標題,以反映新信息。
def find_chunk_and_push_proposition(proposition):
class ChunkID(BaseModel):
chunk_id: int = Field(descriptinotallow="The chunk id.")
allocation_llm = llm.with_structured_output(ChunkID)
allocation_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You have the chunk ids and the summaries"
"Find the chunk that best matches the proposition."
"If no chunk matches, return a new chunk id."
"Return only the chunk id.",
),
(
"user",
"proposition:{proposition}" "chunks_summaries:{chunks_summaries}",
),
]
)
allocation_chain = allocation_prompt | allocation_llm
chunks_summaries = {
chunk_id: chunk["summary"] for chunk_id, chunk in chunks.items()
}
best_chunk_id = allocation_chain.invoke(
{"proposition": proposition, "chunks_summaries": chunks_summaries}
).chunk_id
if best_chunk_id not in chunks:
best_chunk_id = create_new_chunk(best_chunk_id, proposition)
return
add_proposition(best_chunk_id, proposition)7、總結
分塊內容在大多數情況下相對簡單,但當偏離常規路徑時可能會帶來挑戰。沒有一種適用于所有情況的分塊解決方案,不同的用例需要不同的分塊策略。通過理解分塊策略的關鍵點和權衡,可以為特定的應用程序找到最適合的分塊方法。


































