构建高效RAG系统的常用策略

示例代码:

代码1 cleaning.py:

def clean_text(text: str) -> str:
    text = re.sub(r"[^\w\s.,!?]", " ", text)
    text = re.sub(r"\s+", " ", text)
    return text.strip()

代码2 chunking.py:

from langchain.text_splitter import RecursiveCharacterTextSplitter, SentenceTransformersTokenTextSplitter

def chunk_text(text: str, chunk_size: int = 500, chunk_overlap: int = 50) -> list[str]:
    character_splitter = RecursiveCharacterTextSplitter(separators=["\n\n"], chunk_size=chunk_size, chunk_overlap=0)
    text_split_by_characters = character_splitter.split_text(text)

    token_splitter = SentenceTransformersTokenTextSplitter(
        chunk_overlap=chunk_overlap,
        tokens_per_chunk=embedding_model.max_input_length,
        model_name=embedding_model.model_id,
    )
    chunks_by_tokens = []
    for section in text_split_by_characters:
        chunks_by_tokens.extend(token_splitter.split_text(section))

    return chunks_by_tokens


def chunk_document(text: str, min_length: int, max_length: int) -> list[str]:
    """Alias for chunk_article()."""

    return chunk_article(text, min_length, max_length)


def chunk_article(text: str, min_length: int, max_length: int) -> list[str]:
    sentences = re.split(r"(?, text)

    extracts = []
    current_chunk = ""
    for sentence in sentences:
        sentence = sentence.strip()
        if not sentence:
            continue

        if len(current_chunk) + len(sentence) <= max_length:
            current_chunk += sentence + " "
        else:
            if len(current_chunk) >= min_length:
                extracts.append(current_chunk.strip())
            current_chunk = sentence + " "

    if len(current_chunk) >= min_length:
        extracts.append(current_chunk.strip())

    return extracts

文本预处理工具

文本预处理的核心操作函数,主要有两个文件:cleaning.pychunking.py
这两个文件包含的函数就像是文本处理的"工具箱",负责清洗和分割文本。


1. cleaning.py - 文本清洗

函数:clean_text

def clean_text(text: str) -> str:
    text = re.sub(r"[^\w\s.,!?]", " ", text)
    text = re.sub(r"\s+", " ", text)
    return text.strip()

功能解释

这个函数做了三件事:

  • 移除特殊字符:移除除了字母、数字、空格和一些基本标点(.,!?)以外的所有字符
  • 规范空格:将多个连续的空格替换为单个空格
  • 去除首尾空白:去掉文本开头和结尾的空白字符

举例说明

假设我们有这样一段文本:

Hello   world! This is a #test@ with some $special% characters.

经过 clean_text 处理后,会变成:

Hello world! This is a test with some special characters.

注意以下变化:

  • 多个空格变成了单个空格
  • 特殊字符如 #@$% 都被替换为空格
  • 基本标点如 .! 被保留了

这个函数的目的是让文本更干净、更标准化,便于后续处理。


2. chunking.py - 文本分块

这个文件包含了几个函数,用于将长文本分割成更小的块。


(1) chunk_text 函数

def chunk_text(text: str, chunk_size: int = 500, chunk_overlap: int = 50) -> list[str]:
    character_splitter = RecursiveCharacterTextSplitter(separators=["\n\n"], chunk_size=chunk_size, chunk_overlap=0)
    text_split_by_characters = character_splitter.split_text(text)

    token_splitter = SentenceTransformersTokenTextSplitter(
        chunk_overlap=chunk_overlap,
        tokens_per_chunk=embedding_model.max_input_length,
        model_name=embedding_model.model_id,
    )
    chunks_by_tokens = []
    for section in text_split_by_characters:
        chunks_by_tokens.extend(token_splitter.split_text(section))

    return chunks_by_tokens
功能解释

这个函数分两步进行文本分块:

  1. 第一步:按段落(双换行符 \n\n)分割文本,每块最大500字符
  2. 第二步:对每个段落进行基于 token 的分割,确保每块不超过嵌入模型的最大输入长度
举例说明

假设我们有一篇1500字符的文章,包含3个段落:

  • 第一段:包含了大约400个字符的内容…
  • 第二段:包含了大约600个字符的内容…
  • 第三段:包含了大约500个字符的内容…

处理过程:

  • 首先按段落分割,得到3个块(因为每个段落都小于500字符)
  • 然后对每个段落进行 token 分割:
    • 第一段:400字符,可能是100个 token,保持为一块
    • 第二段:600字符,可能是150个 token;如果模型最大输入是128个 token,会分成两块,中间有50个 token 重叠
    • 第三段:500字符,可能是125个 token,保持为一块
  • 最终可能得到4个文本块。

(2) chunk_article 函数

def chunk_article(text: str, min_length: int, max_length: int) -> list[str]:
    sentences = re.split(r"(?, text)

    extracts = []
    current_chunk = ""
    for sentence in sentences:
        sentence = sentence.strip()
        if not sentence:
            continue

        if len(current_chunk) + len(sentence) <= max_length:
            current_chunk += sentence + " "
        else:
            if len(current_chunk) >= min_length:
                extracts.append(current_chunk.strip())
            current_chunk = sentence + " "

    if len(current_chunk) >= min_length:
        extracts.append(current_chunk.strip())

    return extracts
功能解释

这个函数更智能地分割文章:

  • 按句子分割:首先使用复杂的正则表达式识别句子边界来按句子分割文本
  • 贪婪合并:将句子组合成块,确保每块:
    • 不超过最大长度 max_length
    • 不小于最小长度 min_length
    • 不会在句子中间断开
举例说明

假设我们有以下文本:

我喜欢编程。Python是一种很棒的语言。它简单易学。我每天都在使用它。机器学习是一个有趣的领域。深度学习更是令人着迷。

假设 min_length=50max_length=100,每个句子的长度如下:

  • “我喜欢编程。” - 15字符
  • “Python是一种很棒的语言。” - 30字符
  • “它简单易学。” - 15字符
  • “我每天都在使用它。” - 20字符
  • “机器学习是一个有趣的领域。” - 30字符
  • “深度学习更是令人着迷。” - 25字符

处理过程:

  • 第一块:添加前4个句子 = 80字符(小于 max_length=100
  • 第二块:添加后2个句子 = 55字符(大于 min_length=50

最终得到两个文本块:

  • 块1:

    我喜欢编程。Python是一种很棒的语言。它简单易学。我每天都在使用它。
    
  • 块2:

    机器学习是一个有趣的领域。深度学习更是令人着迷。
    

(3) chunk_document 函数

def chunk_document(text: str, min_length: int, max_length: int) -> list[str]:
    """Alias for chunk_article()."""
    return chunk_article(text, min_length, max_length)

这只是 chunk_article 函数的一个别名,功能完全相同。


这些函数的实际应用

这些函数在项目中的作用非常关键:

  • 清洗文本:移除噪音和不必要的字符,使文本更规范
  • 分块处理:将长文本分成适合模型处理的小块
    • 对于嵌入模型:确保不超过模型的最大输入长度
    • 对于检索系统:创建合适大小的文本块,便于精确检索

这些预处理操作是构建高效 RAG(检索增强生成) 系统的基础。
通过合理的清洗和分块,可以:

  • 提高嵌入向量的质量
  • 增强检索的精确度
  • 减少无关信息的干扰

简单来说,这些函数就像是文本的"切菜师傅",它们把原始的长文本"切"成大小合适、干净整洁的小块,为后续的 AI 模型处理做好准备。


chunk_text 方法和 chunk_article 方法的区别与联系

它们都用于将长文本分割成更小的块,但采用了不同的策略和目的。

主要区别

  1. 分割策略不同

    • chunk_text 方法:

      def chunk_text(text: str, chunk_size: int = 500, chunk_overlap: int = 50) -> list[str]:
          character_splitter = RecursiveCharacterTextSplitter(separators=["\n\n"], chunk_size=chunk_size, chunk_overlap=0)
          text_split_by_characters = character_splitter.split_text(text)
      
          token_splitter = SentenceTransformersTokenTextSplitter(
              chunk_overlap=chunk_overlap,
              tokens_per_chunk=embedding_model.max_input_length,
              model_name=embedding_model.model_id,
          )
          chunks_by_tokens = []
          for section in text_split_by_characters:
              chunks_by_tokens.extend(token_splitter.split_text(section))
      
          return chunks_by_tokens
      
      • 使用两级分割策略:
        1. 首先按段落(双换行符 \n\n)分割
        2. 然后按 token 进一步分割
      • 使用专业的分割器类:RecursiveCharacterTextSplitterSentenceTransformersTokenTextSplitter
      • 考虑了嵌入模型的 token 限制
      • 默认参数:chunk_size=500(字符),chunk_overlap=50(token)
    • chunk_article 方法:

      def chunk_article(text: str, min_length: int, max_length: int) -> list[str]:
          sentences = re.split(r"(?, text)
      
          extracts = []
          current_chunk = ""
          for sentence in sentences:
              sentence = sentence.strip()
              if not sentence:
                  continue
      
              if len(current_chunk) + len(sentence) <= max_length:
                  current_chunk += sentence + " "
              else:
                  if len(current_chunk) >= min_length:
                      extracts.append(current_chunk.strip())
                  current_chunk = sentence + " "
      
          if len(current_chunk) >= min_length:
              extracts.append(current_chunk.strip())
      
          return extracts
      
      • 使用正则表达式按句子分割
      • 手动实现了贪婪合并句子的逻辑
      • 考虑了最小长度和最大长度的约束
      • 参数需要明确指定 min_lengthmax_length
  2. 参数不同

    • chunk_text 使用字符数和重叠 token 数作为参数
    • chunk_article 使用最小和最大字符长度作为参数
  3. 应用场景不同

    • chunk_text 更适合用于嵌入模型处理,因为它考虑了 token 限制
    • chunk_article 更适合用于生成摘要或提取,因为它保持了句子的完整性

联系

  • 目的相同:两者都是为了将长文本分割成更小的、可管理的块

  • 别名关系chunk_documentchunk_article 的别名

    def chunk_document(text: str, min_length: int, max_length: int) -> list[str]:
        """Alias for chunk_article()."""
        return chunk_article(text, min_length, max_length)
    
  • 尊重文本自然边界

    • chunk_text 尊重段落边界
    • chunk_article 尊重句子边界
  • 预处理管道中的角色:它们都是文本处理管道的一部分

实际应用举例

假设有一篇包含多个段落的长文章:

人工智能正在改变世界。它影响了从医疗到金融的各个领域。

机器学习是AI的一个子领域。深度学习是机器学习中特别强大的一种方法。它基于神经网络,可以学习复杂的模式。

自然语言处理让计算机能够理解人类语言。这使得像聊天机器人这样的应用成为可能。它还支持机器翻译和情感分析。
  • 使用 chunk_text 处理:

    • 首先按 \n\n 分割成3个段落
    • 然后每个段落按 token 进一步分割(如果需要)
    • 可能的结果:3个或更多块,取决于每个段落的 token 数
  • 使用 chunk_article 处理:

    • 首先按句子分割成8个句子
    • 然后根据 min_lengthmax_length 合并句子
    • 可能的结果:2-3个块,每个块包含完整的句子
总结
  • chunk_text 更技术性,专注于为嵌入模型优化文本块
  • chunk_article 更语义性,专注于保持文本的可读性和连贯性
  • 两者在项目中扮演互补角色,用于不同的预处理需求

同时使用 chunk_text 与 chunk_article 方法能同时使用么?

这两个方法可以同时使用,它们在不同的处理阶段有各自的优势。

同时使用的场景

  • 多级处理:先用 chunk_article 按语义分块,再用 chunk_text 确保每块符合嵌入模型的要求
  • 不同用途
    • chunk_article 处理要展示给用户的内容
    • chunk_text 处理要输入到模型的内容
  • A/B 测试:比较两种分块方法的效果,选择更好的结果

代码示例:多级处理

假设我们有一篇长文章,需要先按语义分块,然后确保每块都适合嵌入模型处理:

from llm_engineering.application.preprocessing.operations.chunking import chunk_article, chunk_text

def process_long_document(document: str) -> list[str]:
    """
    处理长文档的两阶段分块策略:
    1. 首先使用chunk_article按语义分块,保持句子完整性
    2. 然后使用chunk_text确保每块不超过嵌入模型的限制
    """
    # 第一阶段:按语义分块,每块500-2000字符
    semantic_chunks = chunk_article(document, min_length=500, max_length=2000)
    
    # 第二阶段:确保每块适合嵌入模型
    final_chunks = []
    for chunk in semantic_chunks:
        # 如果语义块太长,进一步分割
        if len(chunk) > 1000:  # 假设1000字符是一个安全阈值
            sub_chunks = chunk_text(chunk, chunk_size=800, chunk_overlap=100)
            final_chunks.extend(sub_chunks)
        else:
            final_chunks.append(chunk)
    
    return final_chunks

# 使用示例
long_article = """
人工智能(AI)正在彻底改变我们的生活和工作方式。从智能手机上的语音助手到自动驾驶汽车,AI技术无处不在。

机器学习是AI的核心技术之一。它允许计算机从数据中学习,而无需明确编程。深度学习是机器学习的一个子集,它使用多层神经网络来模拟人脑的工作方式。这些网络可以识别模式、分类数据并做出预测。

自然语言处理(NLP)是AI的另一个重要分支。它使计算机能够理解、解释和生成人类语言。NLP技术支持了从机器翻译到情感分析的各种应用。最近的大型语言模型(LLM)如GPT和BERT在这一领域取得了突破性进展。

计算机视觉让机器能够"看到"和理解视觉世界。它在医疗诊断、安全监控和自动驾驶等领域有广泛应用。结合深度学习技术,现代计算机视觉系统可以识别物体、人脸和活动,有时甚至比人类更准确。

强化学习是AI的另一个关键领域,它专注于如何通过试错来学习最佳行动。这种方法已被用于训练AI玩游戏、控制机器人和优化系统。

尽管AI技术取得了令人印象深刻的进展,但它也面临着伦理、隐私和安全方面的挑战。确保AI的负责任发展和使用是研究人员、企业和政策制定者的共同责任。
"""

chunks = process_long_document(long_article)
for i, chunk in enumerate(chunks):
    print(f"块 {i+1} ({len(chunk)} 字符):")
    print(chunk[:100] + "..." if len(chunk) > 100 else chunk)
    print("-" * 50)

代码示例:不同用途

假设我们在构建一个 RAG 系统,需要同时为用户展示和为模型准备内容:

from llm_engineering.application.preprocessing.operations.chunking import chunk_article, chunk_text

def prepare_document_for_rag(document: str):
    """
    为RAG系统准备文档:
    1. 使用chunk_article创建用户友好的摘要块
    2. 使用chunk_text创建适合嵌入模型的块
    """
    # 为用户界面创建可读性好的块
    user_friendly_chunks = chunk_article(document, min_length=200, max_length=1000)
    
    # 为嵌入模型创建优化的块
    embedding_chunks = chunk_text(document, chunk_size=500, chunk_overlap=50)
    
    # 创建映射关系,将每个嵌入块关联到最相似的用户友好块
    chunk_mapping = {}
    for emb_chunk in embedding_chunks:
        best_match = None
        max_overlap = 0
        
        for uf_chunk in user_friendly_chunks:
            # 简单计算文本重叠度
            overlap = sum(1 for word in emb_chunk.split() if word in uf_chunk.split())
            if overlap > max_overlap:
                max_overlap = overlap
                best_match = uf_chunk
        
        chunk_mapping[emb_chunk] = best_match
    
    return {
        "user_chunks": user_friendly_chunks,  # 用于展示给用户
        "embedding_chunks": embedding_chunks,  # 用于向量数据库
        "mapping": chunk_mapping  # 关联两种块
    }

# 使用示例
document = """
检索增强生成(RAG)是一种结合了检索系统和生成AI的强大技术。它通过从外部知识库检索相关信息来增强语言模型的输出。

RAG的工作原理是首先将查询发送到检索系统,该系统从知识库中找到相关文档。然后,这些文档与原始查询一起提供给语言模型,使其能够生成更准确、更相关的回答。

这种方法解决了大型语言模型的一个关键限制:它们只能基于训练数据生成内容,而训练数据可能过时或不完整。通过RAG,模型可以访问最新和专业的信息。

实现高效的RAG系统需要几个关键组件:文档处理管道、嵌入模型、向量数据库和生成模型。文档处理包括清洗、分块和嵌入文本。向量数据库存储这些嵌入,并支持相似性搜索。

RAG系统的性能很大程度上取决于文本分块的质量。块太大会包含太多信息,可能稀释相关内容;块太小可能会丢失上下文。找到合适的平衡是构建有效RAG系统的关键。
"""

result = prepare_document_for_rag(document)
print(f"用户友好块数量: {len(result['user_chunks'])}")
print(f"嵌入模型块数量: {len(result['embedding_chunks'])}")
print("\n示例用户友好块:")
print(result['user_chunks'][0][:150] + "..." if len(result['user_chunks'][0]) > 150 else result['user_chunks'][0])
print("\n示例嵌入块:")
print(result['embedding_chunks'][0][:150] + "..." if len(result['embedding_chunks'][0]) > 150 else result['embedding_chunks'][0])

通俗解释

想象你是一个图书管理员,需要整理一本厚重的百科全书:

  • chunk_article 就像按章节分割

    • 你首先按照章节、段落等自然分界线将书分成几个部分
    • 每个部分都是完整的、有意义的内容单元
    • 这样分割出来的内容对人类读者友好,容易理解
  • chunk_text 就像制作索引卡片

    • 为了方便电脑检索,你需要制作标准大小的索引卡片
    • 每张卡片不能太大(否则电脑处理不了)
    • 卡片之间可以有一些重叠内容(确保连贯性)
  • 同时使用两种方法就像你既为人类读者准备了章节目录,又为电脑检索系统准备了索引卡片:

    • 人类可以通过章节目录轻松找到完整、连贯的内容
    • 电脑可以通过索引卡片快速、精确地检索信息
    • 两者之间有映射关系,当电脑找到相关索引卡片时,可以引导读者到对应的完整章节

这种组合使用的方法既满足了人类对内容连贯性和可读性的需求,又满足了 AI 模型对输入大小和格式的技术要求,是构建高效 RAG 系统的常用策略。

你可能感兴趣的:(RAG+langchain,RAG优化方法,人工智能,RAG,RAG优化,自然语言处理,数据处理)