AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


分块的艺术:提升 RAG 效果的关键
发布日期:2025-01-10 02:19:15 浏览次数: 1547 来源:ChallengeHub



聪明人往往很“懒”,但这种“懒”其实是高效的体现。他们总能找到解决复杂问题的最佳路径,用最少的力气获得最大的成果。

在RAG系统中,这种高效的实现往往是通过“分块”来实现的。你可以把它想象成把一本厚书分成几章——这样一来,阅读和理解就轻松多了。同样地,分块技术把大段复杂的文本拆分成更小、更容易处理的片段,让AI能更快、更准确地理解和处理信息。

不过,在深入探讨分块之前,咱们得先聊聊它背后的“大框架”——检索增强生成,也就是RAG。这个技术可是分块能发挥作用的关键哦!

什么是RAG

长话短说,检索增强生成(RAG)是一种巧妙的方法,它把检索机制和大型语言模型(LLM)结合在了一起。简单来说,就是让AI在生成回答之前,先“翻翻资料”——通过检索相关文档来增强自己的能力。这样一来,AI给出的回答不仅更准确,还能带上更多上下文信息,显得更有深度和针对性。

引入分块

分块,说白了就是把一大段文字“切”成更小、更容易处理的片段。这个过程主要分两步走:

第一步:数据准备
首先,我们需要把靠谱的数据源“切”成一个个小文档块,然后存到数据库里。如果这些小文档块还生成了对应的“嵌入”(可以理解成一种数学表示),那这个数据库就可以升级成向量存储,方便后续的快速查找。

第二步:检索
当用户抛出一个问题时,系统就会动起来,通过向量搜索、全文搜索,或者两者结合的方式,去数据库里翻找最相关的小文档块。简单来说,就是快速定位到那些和用户问题最匹配的信息片段。

这样一来,大模型不仅能更快找到答案,还能确保回答得更精准、更有针对性!

为什么分块在 RAG 架构中如此重要?

分块在 RAG 架构里可是个“关键角色”,因为它直接决定了生成式 AI 应用的准确性。可以说,它是整个流程的“第一道关卡”。

1.小块头,大智慧:提高准确性

分块让系统能把文本切成更小的片段,方便索引和搜索。这样一来,当用户提问时,系统能快速找到最相关的片段,大大提高了检索的准确性。想象一下,与其在一整本书里翻找答案,不如直接定位到某一页的某一段——效率高多了!

2.大小适中,上下文更给力

不过,分块也不是越小越好。块太小可能会丢失上下文信息,块太大又会让模型难以聚焦。理想的分块大小能让生成模型更好地理解每个片段的上下文,从而生成更连贯、更准确的回答。这样一来,模型就不用在一大堆无关信息里“大海捞针”了。

3.可扩展性与性能:高效处理大数据

分块还能让 RAG 系统更高效地处理海量数据。通过把数据切成可管理的部分,系统可以并行处理这些块,减少计算负担,提升整体性能。这样一来,RAG 系统不仅能处理更多数据,还能跑得更快、更稳。

总之,分块不仅是 RAG 系统的技术必需品,更是一种战略方法。它提高了检索准确性、处理效率和资源利用率,是 RAG 应用成功的“秘密武器”。

优化分块的技巧:从基础到高级

分块可不是随便“切一切”就完事了,想要让 RAG 系统表现得更出色,得用上一些技巧。下面这些方法,从简单到复杂,总有一款适合你:

1.固定字符大小:简单粗暴

这是最直接的方法——把文本按固定字符数切成块。比如每 500 个字符切一刀。虽然简单,但有时候效果还不错,尤其是对格式规整的文本。

2.递归字符文本分割:按标点“下刀”

这种方法更聪明一点,它会根据空格、标点符号(比如句号、逗号)来切分文本。这样一来,切出来的块更有上下文意义,不会把一句话硬生生切成两半。

3.特定文档的拆分:因地制宜

不同类型的文档(比如 PDF、Markdown)结构不一样,分块方法也得跟着变。比如,PDF 可以按段落切,Markdown 可以按标题切。这种“量身定制”的方法能让分块更精准。

4.语义分割:按意思切块

这种方法更高级,它会用嵌入技术(embedding)来分析文本的语义,然后根据意思来分块。比如,把讨论同一个话题的句子归到一起。这样一来,切出来的块不仅更连贯,还能更好地保留上下文。

5.自主分割:让 AI 自己决定

这是最“智能”的方法——直接让大型语言模型(LLM)根据内容和上下文来决定怎么分块。AI 会自己判断哪里该切,哪里该留,确保每个块都有完整的意义。

通过灵活运用这些技巧,RAG 系统的性能和准确性都能大幅提升。分块不仅是技术活儿,更是一门艺术——掌握得好,AI 的表现就能更上一层楼!

固定字符大小分块:简单但有点“呆”

固定字符大小分块是最基础的分块方法,简单来说,就是不管内容如何,直接按固定的字符数把文本“切”成块。比如,每 300 个字符切一刀,完事儿。

固定字符大小分块的优缺点

优点

  1. 简单省事:这种方法实现起来超级简单,几乎不需要什么计算资源,特别适合快速上手。
  2. 整齐划一:生成的块大小均匀,方便后续处理,比如存储或检索。

缺点

  1. 忽略上下文:它完全不管文本的结构和意思,切出来的块可能会把完整的信息拆得支离破碎。
  2. 效率不高:重要的内容可能会被“拦腰截断”,想要重新拼出有意义的信息,还得额外费功夫。

下面是如何使用代码实现固定字符大小分块的示例:

# 将示例文本分块
text = "This is the text I would like to ch up. It is the example text for this exercise." 

# 设置块大小
chunk_size = 35 
# 初始化一个列表来保存块 chunks
 = [] 
# 遍历文本以创建块
for i in  range ( 0 , len (text), chunk_size): 
    chunk = text[i:i + chunk_size] 
    chunks.append(chunk) 
# 显示块
print (chunks) 
# 输出:['This is the text I would like to ch', 'unk up. It is the example text for ', 'this exercise']

使用LangChainCharacterTextSplitter来实现相同的结果:


from langchain.text_splitter import CharacterTextSplitter 

# 使用指定的块大小初始化文本分割器
text_splitter = CharacterTextSplitter(chunk_size= 35 , chunk_overlap= 0 , Separator= '' , strip_whitespace= False ) 
# 使用文本分割器创建文档
documents = text_splitter.create_documents([text]) 
# 显示创建的文档
for doc in documents: 
    print (doc.page_content) 

固定字符大小分块是一种简单但基础的技术,通常在转向更复杂的方法之前用作基准。

递归字符文本分割:保持上下文关联

递归字符文本分割是一种更高级的技术,它考虑了文本的结构。它使用一系列分隔符以递归方式将文本分成块,确保块更有意义且与上下文更相关。

在上面的例子中,我们设置了一个块大小为30个字符,重叠部分为20个字符。RecursiveCharacterTextSplitter这个工具会尽量在保持文本逻辑结构的同时进行拆分。不过,这也暴露了一个问题:由于块大小设置得比较小,它还是有可能在单词或句子中间“咔嚓”一刀,这显然不是我们想要的效果。

优点:

  • 上下文更连贯:这种方法通过使用段落或句子作为分隔符,能够更好地保留文本的自然结构。
  • 灵活性强:你可以根据需要调整块的大小和重叠部分,这样就能更精细地控制整个分块过程。

缺点:

  • 块大小是个关键:块的大小得适中,既要方便处理,又要确保每个块至少包含一个完整的短语或更多内容。否则,我们在检索这些块时,可能会遇到精度问题。
  • 性能消耗:由于采用了递归拆分和处理多个分隔符的方式,这种方法会消耗更多的计算资源。而且,与固定大小的块相比,它生成的块数量也会更多。

以下是在 Langchain 中如何实现递归字符文本拆分的示例:

from langchain_text_splitters import RecursiveCharacterTextSplitter
# Sample text to chunk
text = """
The Olympic Games, originally held in ancient Greece, were revived in 1896 and
have since become the world’s foremost sports competition, bringing together 
athletes from around the globe.
"""

# Initialize the recursive character text splitter with specified chunk size
text_splitter = RecursiveCharacterTextSplitter(
    # Set a really small chunk size, just to show.
    chunk_size=30,
    chunk_overlap=20,
    length_function=len,
    is_separator_regex=False,
)

# Create documents using the text splitter
documents = text_splitter.create_documents([text])
# Display the created documents
for doc in documents:
    print(doc.page_content)
# Output:
# “The Olympic Games, originally”
# “held in ancient Greece, were”
# “revived in 1896 and have”
# “have since become the world’s”
# “world’s foremost sports”
# “competition, bringing together”
# “together athletes from around”
# “around the globe.”

在这种方法中,文本首先会按照较大的结构(比如段落)进行拆分。如果拆分后的块还是太大,就会继续用更小的结构(比如句子)进一步切分。这样一来,每个块都能保留有意义的上下文,避免了重要信息被“拦腰截断”的情况。

递归字符文本分割法在简单和复杂之间找到了一个平衡点,既不会过于粗暴地切割文本,也不会让分块过程变得繁琐。它提供了一种既高效又尊重文本结构的分块方式,算得上是一种非常实用的工具。

特定文档的拆分:因地制宜

这种方法会根据不同的文档类型,量身定制分块策略。比如,Markdown 文件、Python 脚本、JSON 文档或者 HTML 文件,每种文档都会按照最适合其内容和结构的方式进行拆分。

举个例子,Markdown 文件在 GitHub、Medium 和 Confluence 等平台上非常常见,这也让它成为 RAG 系统中提取数据的首选。毕竟,干净且结构化的数据对于生成准确的响应至关重要。

不仅如此,针对特定编程语言的分割器也适用于多种语言,比如 C++、Go、Java、Python 等。这样一来,代码可以被高效地分块,方便后续的分析和检索。

简单来说,这种方法就是“看菜下饭”,根据文档的特点灵活处理,确保分块既高效又精准!

优点:

  • 相关性更强:针对不同类型的文档,采用最合适的分块方法,同时还能保留文档的逻辑结构。
  • 精确度更高:根据每种文档的特点量身定制分块过程,确保分块结果更精准。

缺点:

  • 实现起来有点复杂:因为不同类型的文档需要不同的分块策略和工具,所以实现起来会麻烦一些。
  • 维护成本较高:由于方法多样,维护起来也会相对复杂,需要投入更多精力。

这种方法虽然效果更好,但也需要更多的“技术含量”和“耐心”来支撑!

Markdown 切块

from langchain.text_splitter import MarkdownTextSplitter
# Sample Markdown text
markdown_text = """
# Fun in California
## Driving
Try driving on the 1 down to San Diego
### Food
Make sure to eat a burrito while you're there
## Hiking
Go to Yosemite
"""

# Initialize the Markdown text splitter
splitter = MarkdownTextSplitter(chunk_size=40, chunk_overlap=0)
# Create documents using the text splitter
documents = splitter.create_documents([markdown_text])
# Display the created documents
for doc in documents:
    print(doc.page_content)
# Output:
# # Fun in California\n\n## Driving
# Try driving on the 1 down to San Diego
# ### Food
# Make sure to eat a burrito while you're
# there
# ## Hiking\n\nGo to Yosemite

Python 代码切块

from langchain.text_splitter import PythonCodeTextSplitter
# Sample Python code
python_text = """
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
p1 = Person("John", 36)
for i in range(10):
    print(i)
"""

# Initialize the Python code text splitter
python_splitter = PythonCodeTextSplitter(chunk_size=100, chunk_overlap=0)
# Create documents using the text splitter
documents = python_splitter.create_documents([python_text])
# Display the created documents
for doc in documents:
    print(doc.page_content)
# Output:
# class Person:\n    def __init__(self, name, age):\n        self.name = name\n        self.age = age
# p1 = Person("John", 36)\n\nfor i in range(10):\n    print(i)

文档拆分这件事,听起来简单,但其实大有学问。我们采用的是一种“量身定制”的拆分方式,它会根据文档类型的不同,智能地保留原有的逻辑结构。这样做的好处是,每个拆分出来的文本块都自带完整的上下文,不会出现“断章取义”的情况。

举个例子你就明白了:处理Markdown文档时,系统会聪明地把标题和正文分开处理,就像整理书籍时会区分章节标题和正文内容一样;处理Python代码时,它又能准确识别类和函数的边界,就像程序员阅读代码时会自然地区分不同功能模块。

这种智能拆分方式,让文档处理系统变得更“懂行”。它不仅保持了文档的完整性,还让系统在检索信息时更精准,生成回答时更靠谱。你可以把它想象成一个特别细心的图书管理员,不仅知道每本书放在哪个书架,还能准确找到书中最相关的段落。

语义分割:保留语义的完整性

以前的文档拆分方式,说白了就是“简单粗暴”——要么按固定长度切分,要么死板地按照标点符号来划分。但我们现在用的这种语义拆分,那可真是“聪明”多了!它就像个理解能力超强的读者,会根据文本的实际意思来决定怎么拆分,把文档处理带到了一个新高度。

具体是怎么做到的呢?这要归功于先进的嵌入技术。系统会先理解文本的含义,然后把意思相近的内容自动归类到一起。这样一来,每个拆分出来的文本块都是一个完整的“小故事”,前后文都对得上,不会出现前言不搭后语的情况。

你可以把它想象成一个特别会整理资料的小助手:它不会把你的文件随便撕成几半,而是会仔细阅读内容,把相关的部分整整齐齐地放在一起。这样不仅看起来舒服,用起来也特别顺手!

让我们来看看这个语义分块是怎么工作的,就像拆解一个精密的机器一样,它主要分三步走:

第一步,先把文档“切”成一句一句的。这可不是简单的断句哦,系统会像语文老师一样,准确地识别出每个完整的句子。

第二步,给每个句子穿上“智能外套”——也就是生成嵌入向量。这个过程就像是给句子打上独特的身份标签,让系统能理解它们的含义。

第三步最有趣,系统会像个细心的图书管理员,把意思相近的句子归类到一起。想象一下,它会把讨论同一个话题的句子自动归为一组,就像把同类型的书籍放在同一个书架上。

这样做的好处显而易见:每个分块都是一个完整的小故事,前后文都对得上号。当你需要查找信息时,系统就能快速找到最相关的内容,准确率杠杠的!

下面,我们就通过一个具体的例子,来看看这个方法的神奇之处。

这张图给我们展示了一个生动的例子,展示了如何用余弦相似度来给句子分组。简单来说,就是让聊同一件事的句子待在一起,而话题不同的句子就各走各的路。通过这种可视化的方式,我们能清楚地看到语义分块是怎么保持文本的连贯性的。

优点:

  1. 上下文拿捏得准:相似的内容总在一起,找信息时特别给力,准确率up up!
  2. 灵活度满分:不管什么类型的文本,它都能根据意思来调整,不会死板地按固定规则来。

小缺点:3. 有点费算力:毕竟要生成和比较那些嵌入向量,需要多花点计算资源。4. 实现起来不简单:比起直接按字数或标点拆分,这个方法确实复杂一些。

以下是如何使用嵌入实现语义分割的示例:

from sklearn.metrics.pairwise import cosine_similarity
from langchain.embeddings import OpenAIEmbeddings
import re
# Sample text
text = """
One of the most important things I didn't understand about the world when I was a child is the degree to which the returns for performance are superlinear.
Teachers and coaches implicitly told us the returns were linear. "You get out," I heard a thousand times, "what you put in." They meant well, but this is rarely true. If your product is only half as good as your competitor's, you don't get half as many customers. You get no customers, and you go out of business.
It's obviously true that the returns for performance are superlinear in business. Some think this is a flaw of capitalism, and that if we changed the rules it would stop being true. But superlinear returns for performance are a feature of the world, not an artifact of rules we've invented. We see the same pattern in fame, power, military victories, knowledge, and even benefit to humanity. In all of these, the rich get richer.
"""
# Splitting the text into sentences
sentences = re.split(r'(?<=[.?!])\s+', text)
sentences = [{'sentence': x, 'index' : i} for i, x in enumerate(sentences)]
# Combine sentences for context
def combine_sentences(sentences, buffer_size=1):
   for i in range(len(sentences)):
       combined_sentence = ''
       for j in range(i - buffer_size, i):
           if j >= 0:
               combined_sentence += sentences[j]['sentence'] + ' '
       combined_sentence += sentences[i]['sentence']
       for j in range(i + 1, i + 1 + buffer_size):
           if j < len(sentences):
               combined_sentence += ' ' + sentences[j]['sentence']
       sentences[i]['combined_sentence'] = combined_sentence
   return sentences
sentences = combine_sentences(sentences)
# Generate embeddings
oai_embeds = OpenAIEmbeddings()
embeddings = oai_embeds.embed_documents([x['combined_sentence'] for x in sentences])
# Add embeddings to sentences
for i, sentence in enumerate(sentences):
   sentence['combined_sentence_embedding'] = embeddings[i]
# Calculate cosine distances
def calculate_cosine_distances(sentences):
   distances = []
   for i in range(len(sentences) - 1):
       embedding_current = sentences[i]['combined_sentence_embedding']
       embedding_next = sentences[i + 1]['combined_sentence_embedding']
       similarity = cosine_similarity([embedding_current], [embedding_next])[0][0]
       distance = 1 - similarity
       distances.append(distance)
       sentences[i]['distance_to_next'] = distance
   return distances, sentences
distances, sentences = calculate_cosine_distances(sentences)
# Determine breakpoints and create chunks
import numpy as np
breakpoint_distance_threshold = np.percentile(distances, 95)
indices_above_thresh = [i for i, x in enumerate(distances) if x > breakpoint_distance_threshold]
# Combine sentences into chunks
chunks = []
start_index = 0
for index in indices_above_thresh:
   end_index = index
   group = sentences[start_index:end_index + 1]
   combined_text = ' '.join([d['sentence'] for d in group])
   chunks.append(combined_text)
   start_index = index + 1
if start_index < len(sentences):
   combined_text = ' '.join([d['sentence'] for d in sentences[start_index:]])
   chunks.append(combined_text)
# Display the created chunks
for i, chunk in enumerate(chunks):
   print(f"Chunk #{i+1}:\n{chunk}\n")

语义分割这招挺有意思,它用嵌入技术把意思相近的内容打包在一起,做成一个个“语义块”。这样做的好处可不少,特别是在RAG系统里,找东西更准了,生成的回答也更靠谱了。

它的秘诀就在于特别注重文本的实际含义。每个分出来的块都像是一个完整的小故事,里面的内容都是相关的、连贯的。这样一来,RAG应用的表现自然就上去了,既好用又可靠。

代理分割:自主智能切片

代理分割(Agentic Splitter)这招可厉害了!它靠着大语言模型的超强理解力,能像人一样聪明地给文本分块。

这个方法高级在哪呢?它会像我们读书时那样,先理解内容的意思,再看看上下文,然后找出最合适的地方“下刀”。完全不像以前那些死板的规则或者纯靠统计的方法。

说白了,它处理文本的方式就跟人一样灵活。想象一下你读文章时会怎么分段——肯定是根据意思和思路来的对吧?这个分裂器也是这么干的!所以分出来的块特别连贯,内容也都很相关,用起来特别顺手。优点:

  1. 准得很:靠着强大的语言模型,分出来的块不仅内容相关,上下文也特别准。
  2. 够灵活:不管什么类型的文本都能搞定,分块策略还能随时调整,特别聪明。

缺点:3. 有点烧资源:毕竟要用大语言模型,计算资源少不了,成本也会高一些。4. 实现起来不简单:得好好设置和微调语言模型,才能让它发挥最佳水平。

在LangGraph里,节点就像是流水线上的一个个工作站。每个节点都有明确的职责:接活儿、干活儿、交活儿。它会接收输入数据,处理一番,然后把结果传给下一个节点,就像接力赛一样。

from langgraph.nodes import InputNode, SentenceSplitterNode, LLMDecisionNode, ChunkingNode

# Step 1: Input Node
input_node = InputNode(name="Document Input")

# Step 2: Sentence Splitting Node
splitter_node = SentenceSplitterNode(input=input_node.output, name="Sentence Splitter")

# Step 3: LLM Decision Node
decision_node = LLMDecisionNode(
    input=splitter_node.output, 
    prompt_template="Does the sentence '{next_sentence}' belong to the same chunk as '{current_chunk}'?"
    name="LLM Decision"
)

# Step 4: Chunking Node
chunking_node = ChunkingNode(input=decision_node.output, name="Semantic Chunking")

# Run the graph
document = "Your document text here..."
result = chunking_node.run(document=document)
print(result)

总结

总结一下,分块这招对优化RAG系统来说,绝对是个关键策略。它能让系统的回答更准、更贴合上下文,还能轻松应对大规模需求。

说白了,把大段文本切成小块,不仅找东西更快更准,还能让AI应用的整体效率蹭蹭往上涨。

原文链接?:https://towardsdatascience.com/the-art-of-chunking-boosting-ai-performance-in-rag-architectures-acdbdb8bdc2b


添加微信,回复”RAG“进入交流群



53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询