别再无脑 RAG 了!11 种分块策略我只留了 3 种(附实测数据)

别再无脑 RAG 了!11 种分块策略我只留了 3 种(附实测数据)

RAG(检索增强生成)你肯定听过。但你知道吗?同样的文档、同样的 Embedding 模型、同样的 LLM,换一种分块策略,回答准确率能差 40%。

我信了网上"直接按 500 token 切"的教程,做出来的 RAG 系统被用户骂"答非所问"。后来花了一周时间测了 11 种分块策略,最终只留了 3 种。

这篇文章就是我的实测过程和最终选择。


为什么分块策略这么重要

RAG 的流程是:用户提问 → 检索相关文档 → 把文档喂给 LLM → LLM 生成回答。

问题出在"检索相关文档"这一步。如果你的文档切得不好——

  • 切太大:检索到的 chunk 包含太多无关信息,LLM 被干扰
  • 切太小:关键信息被切断,LLM 拿到的是碎片
  • 切的位置不对:一个完整的概念被切成两半,哪一半都搜不到
  • # 举个例子:一段关于"JWT 认证流程"的文档
    
    原始文档:
    "JWT 认证分为三个步骤。第一步,用户提交用户名和密码。
    第二步,服务端验证通过后生成 JWT Token,包含 Header、
    Payload、Signature 三部分。第三步,客户端将 Token 存储
    在 localStorage 中,后续请求在 Authorization 头中携带。"
    
    如果按 50 字硬切,可能变成:
    chunk1: "JWT 认证分为三个步骤。第一步,用户提交用户名和密码。"
    chunk2: "第二步,服务端验证通过后生成 JWT Token,包含 Header、"
    chunk3: "Payload、Signature 三部分。第三步,客户端将 Token 存储"
    
    用户问"JWT 的 Payload 是什么"→ 检索到 chunk2 和 chunk3
    → 两个 chunk 都不完整,LLM 回答不出。

    11 种分块策略速览

    我测了以下 11 种,按复杂度从低到高排列:

    #策略原理复杂度 1固定长度切分按字符/token 数硬切⭐ 2按段落切分以 `\n\n` 为分隔符⭐ 3按句子切分以句号为分隔符⭐ 4递归字符切分先按段落,再按句子,再按字符⭐⭐ 5按 Markdown 标题切分以 `#` `##` 为分隔符⭐⭐ 6按语义相似度切分相邻句子语义变化大时切一刀⭐⭐⭐ 7按代码结构切分识别函数/类边界⭐⭐⭐ 8滑动窗口重叠固定长度 + 重叠区域⭐⭐ 9按文档结构切分识别目录、章节、附录⭐⭐⭐ 10混合策略结构 + 语义 + 重叠⭐⭐⭐⭐ 11Agentic Chunking用 LLM 判断哪里该切⭐⭐⭐⭐⭐

    实测:我怎么评估的

    测试数据集:我们内部的安全知识库,约 200 篇文档(技术文档 + 操作手册 + 历史告警记录),总计约 50 万字。

    评估方法:准备了 50 个测试问题,每个问题有标准答案。用不同分块策略构建 RAG 系统,看回答准确率。

    # 评估脚本核心逻辑
    import json
    from langchain.text_splitter import (
        RecursiveCharacterTextSplitter,
        MarkdownHeaderTextSplitter,
        CharacterTextSplitter
    )
    
    def evaluate_strategy(chunks, test_questions):
        """评估某个分块策略的效果"""
        correct = 0
        total = len(test_questions)
        
        for q in test_questions:
            # 检索最相关的 3 个 chunk
            relevant_chunks = retrieve_top_k(q["question"], chunks, k=3)
            
            # 拼成 context 喂给 LLM
            context = "\n\n".join(relevant_chunks)
            answer = llm_answer(q["question"], context)
            
            # 检查答案是否包含关键信息
            if q["key_point"].lower() in answer.lower():
                correct += 1
        
        return correct / total  # 准确率

    实测结果:3 个赢家

    冠军:递归字符切分 + 滑动窗口重叠

    准确率:82%

    from langchain.text_splitter import RecursiveCharacterTextSplitter
    
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,        # 每个 chunk 最大 500 字符
        chunk_overlap=100,     # 重叠 100 字符(约 2-3 句话)
        separators=["\n\n", "\n", "。", ",", " ", ""]
        # 先按段落切,段落太长按换行切,再不行按句号切……
    )
    
    chunks = splitter.split_text(document)

    为什么好separators 参数让它优先在"自然断点"(段落、句子)处切分,不会把一句话切成两半。chunk_overlap 确保相邻 chunk 有重叠,关键信息不会因为恰好落在边界上而丢失。

    一句话总结:90% 的场景用这个就够了,别折腾花哨的。

    亚军:按 Markdown 标题切分

    准确率:79%

    from langchain.text_splitter import MarkdownHeaderTextSplitter
    
    headers_to_split_on = [
        ("#", "标题"),
        ("##", "二级标题"),
        ("###", "三级标题"),
    ]
    
    markdown_splitter = MarkdownHeaderTextSplitter(
        headers_to_split_on=headers_to_split_on
    )
    
    # 对每篇 Markdown 文档
    md_header_splits = markdown_splitter.split_text(markdown_content)

    为什么好:技术文档天然有标题结构,按标题切分出来的 chunk 语义完整——一个 ## 登录流程 下面的内容,肯定都是讲登录的。

    适用场景:你的文档是 Markdown 或 RST 格式(技术博客、Wiki、README)。

    一句话总结:文档有标题结构的,直接用这个,效果比硬切好很多。

    季军:按语义相似度切分

    准确率:77%

    # 需要安装: pip install semantic-text-splitter
    from semantic_text_splitter import TextSplitter
    from tokenizers import Tokenizer
    
    # 用 token 计数而不是字符数
    tokenizer = Tokenizer.from_pretrained("bert-base-uncased")
    splitter = TextSplitter.from_huggingface_tokenizer(
        tokenizer,
        500,  # 最大 token 数
        overlap=50
    )
    
    chunks = splitter.chunks(document)

    为什么好:当相邻两句话的语义突然变化时(比如从"安装步骤"跳到"配置参数"),它会在这里切一刀。切出来的每个 chunk 都是"一个话题"。

    缺点:慢。比递归切分慢 10 倍左右,因为要做 Embedding 计算。50 万字的文档集大概要跑 20 分钟。

    一句话总结:追求极致准确率可以用,但性价比不如前两个。

    那 8 种被淘汰的,为什么不行

    策略淘汰原因 固定长度硬切经常把句子切断,准确率只有 58% 按段落切分段落长度差异太大(有的 20 字,有的 2000 字) 按句子切分切得太碎,丢失上下文,LLM 拿到的是碎片 按代码结构切分只对代码有效,对文档反而更差 按文档结构切分依赖文档格式,格式不规范就崩了 Agentic Chunking太慢了!200 篇文档要跑 3 小时,成本约 15 元 滑动窗口(无递归)固定窗口经常在句子中间切,重叠也没用 混合策略调参太复杂,4 个超参数排列组合,收益不大

    最终方案:两层策略

    我最终用的是两层策略——先用 Markdown 标题切大块,再用递归字符切小块:

    def two_level_split(markdown_content):
        """两层分块策略"""
        # 第一层:按标题切分
        headers = [("#", "h1"), ("##", "h2"), ("###", "h3")]
        title_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers)
        sections = title_splitter.split_text(markdown_content)
        
        # 第二层:对超过 500 字的 section 做递归切分
        recursive_splitter = RecursiveCharacterTextSplitter(
            chunk_size=500,
            chunk_overlap=100,
            separators=["\n\n", "\n", "。", ",", " ", ""]
        )
        
        final_chunks = []
        for section in sections:
            if len(section.page_content) > 500:
                sub_chunks = recursive_splitter.split_text(section.page_content)
                final_chunks.extend(sub_chunks)
            else:
                final_chunks.append(section.page_content)
        
        return final_chunks

    准确率:85%,比单独用任何一种都高。

    一句话总结

    分块是 RAG 里最容易被忽视但影响最大的环节。别再无脑 500 token 硬切了——先用递归字符切分 + 滑动窗口重叠,文档有标题结构的再加一层 Markdown 切分,够用了。

    如果你试了还是不行,那八成不是分块的问题——是 Embedding 模型或检索策略的问题。


    关注「安全值班室」公众号

    每天AI安全早报 + 实战攻防案例 + 网安学习路线连载

    关注安全值班室

    posted on 2026-05-28 18:06  明.Sir  阅读(35)  评论(0)    收藏  举报

    导航