欢迎光临
我们一直在努力

如何利用分块策略优化RAG系统的检索质量

在构建RAG(检索增强生成)系统时,很多人把精力集中在选择更好的向量模型或更大的LLM上,却忽略了一个最基础却影响深远的环节——文档分块(Chunking)。分块策略的好坏直接决定了检索阶段能否找到真正相关的内容,进而影响最终生成答案的质量。本文将从实际工程角度出发,详解几种主流分块策略的原理与实现,帮助你为自己的RAG系统选择最合适的方案。

RAG系统架构

为什么分块策略如此重要

RAG系统的核心流程是:先将知识库文档切分成小块(chunk),为每个块生成向量并存储,用户提问时检索最相关的块,再拼接上下文交给LLM生成答案。如果分块太大,向量表示会变得模糊,检索精度下降;分块太小,则丢失上下文语义,答案不完整。

举一个直观的例子:一篇技术文档中有一段关于”MySQL索引优化”的完整说明,如果按固定500字切块,可能正好把这段内容一分为二,导致检索时无法召回完整信息。因此,选择合理的分块策略是RAG工程化的第一步。

固定大小分块:简单但有局限

最简单的分块方式是按固定字符数或token数切分,通常设置一个重叠窗口(overlap)来缓解语义断裂问题。

from langchain.text_splitter import CharacterTextSplitter

text_splitter = CharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separator="\n"
)

chunks = text_splitter.split_text(document_content)
print(f"共切分为 {len(chunks)} 个块")
print(f"第1个块前100字: {chunks[0][:100]}")

这种方式实现简单、速度快,适合结构化程度不高的文本(如聊天记录、日志)。但缺点也很明显:它不关心句子或段落的边界,可能把一句话从中间截断。

递归字符分块:尊重文本结构

递归字符分块是LangChain推荐的默认策略。它按层级分隔符(段落 → 换行 → 句号 → 空格)依次尝试切分,优先在自然断点处分块。

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=100,
    separators=["\n\n", "\n", "。", ",", " ", ""]
)

chunks = splitter.split_text(document)
for i, chunk in enumerate(chunks[:3]):
    print(f"--- Chunk {i+1} (长度: {len(chunk)}) ---")
    print(chunk[:150])
    print()

这种方法在大多数中文技术文档场景下表现良好,是快速上手RAG的首选方案。

语义分块:按含义切分

语义分块的核心思想是:相邻句子如果语义相似则归入同一块,语义差异大则切开。实现方式是先按句子拆分,计算相邻句子的向量余弦相似度,在相似度骤降处切分。

import numpy as np
from sentence_transformers import SentenceTransformer

def semantic_chunking(text, model_name="BAAI/bge-small-zh-v1.5", threshold=0.5):
    model = SentenceTransformer(model_name)
    # 按中文句号拆分
    sentences = [s.strip() for s in text.split("。") if s.strip()]
    embeddings = model.encode(sentences)

    chunks = []
    current_chunk = [sentences[0]]

    for i in range(1, len(sentences)):
        sim = np.dot(embeddings[i], embeddings[i-1]) / \
              (np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i-1]))
        if sim < threshold:
            chunks.append("。".join(current_chunk) + "。")
            current_chunk = [sentences[i]]
        else:
            current_chunk.append(sentences[i])

    if current_chunk:
        chunks.append("。".join(current_chunk) + "。")
    return chunks

chunks = semantic_chunking("你的长文本内容...")
print(f"语义分块结果: 共 {len(chunks)} 个块")

语义分块的检索质量通常最优,但计算成本较高,适合对精度要求严格且文档量不大的场景。

基于文档结构的分块

对于有明确结构的文档(Markdown、HTML、PDF),利用标题层级进行分块是最自然的方式。每个标题及其下属内容作为一个完整的块,既保留了语义完整性,又附带了标题作为元数据。

import re

def markdown_chunking(md_text, max_size=1000):
    """按Markdown标题层级分块"""
    sections = re.split(r'(?=^#{1,3} )', md_text, flags=re.MULTILINE)
    sections = [s.strip() for s in sections if s.strip()]

    chunks = []
    for section in sections:
        if len(section) <= max_size:
            chunks.append(section)
        else:
            # 超长段落递归切分
            sub_splitter = RecursiveCharacterTextSplitter(
                chunk_size=max_size, chunk_overlap=100
            )
            chunks.extend(sub_splitter.split_text(section))
    return chunks

# 使用示例
with open("technical_doc.md", "r") as f:
    doc = f.read()
chunks = markdown_chunking(doc)
for c in chunks:
    print(c[:80], "...")

数据处理流程

分块策略对比与选型建议

策略 优点 缺点 适用场景
固定大小 实现简单、速度快 语义断裂 日志、聊天记录
递归字符 平衡效果与速度 依赖分隔符选择 通用技术文档
语义分块 检索精度最高 计算开销大 高精度问答系统
结构化分块 保留文档结构 需要结构化输入 Markdown/HTML文档

在实际项目中,建议采用混合策略:先用结构化分块处理有标题的文档,对超长段落再用递归字符分块兜底。同时设置合理的chunk_overlap(通常为块大小的10%-20%),避免边界处信息丢失。

实践中的优化技巧

最后分享几个工程实践中验证有效的优化点:

# 1. 分块后为每个块添加元数据,提升检索时的过滤能力
for i, chunk in enumerate(chunks):
    metadata = {
        "source": "技术文档.pdf",
        "chunk_index": i,
        "total_chunks": len(chunks),
        "word_count": len(chunk)
    }

# 2. 使用Parent-Child策略:小块检索,大块喂给LLM
small_chunks = splitter_small.split_text(doc)   # 200字,用于检索
big_chunks = splitter_big.split_text(doc)       # 1000字,用于生成

# 3. 定期评估分块质量
# 可用检索命中率、答案准确率作为指标,迭代优化chunk_size

分块没有万能方案,关键是根据你的文档类型和业务场景做实验、看数据、持续优化。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » 如何利用分块策略优化RAG系统的检索质量
分享到: 更多 (0)