RAG分块主流+前沿方法全解析

6 阅读19分钟

前言

在构建RAG应用程序的整个流程中,拆分/分块是最具挑战性的环节之一,它显著影响检索效果。目前还没有通用的方法可以明确指出哪一种分块策略最为有效。不同的使用场景和数据类型都会影响分块策略的选择。因此对主流和前沿分块方法进行了整理,希望为开发者落地提供清晰、可直接复用的选型与实现参考。

一、递归结构分块(Recursive Chunking)

  1. 规则: 先尝试用 最高级分隔符(## 章节) 切 切完如果块还是太长,再用 下一级(### 小节) 再太长用 列表 1. /- 最后才用 换行 只要一块 ≤chunk_size 字符,就不再往下切
  2. 优点:保留逻辑、语义完整、易实现、速度快
  3. 缺点:对无结构文本(纯长文)效果一般
  4. langchain示例:
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 待切分的企业文档(产品手册)
text = """
# 智能客服系统 V2.0
## 功能介绍
### 核心能力
1. 多轮对话:支持用户连续提问,记忆上下文
2. 意图识别:精准识别用户咨询、投诉、建议等意图
### 部署方式
- 私有云部署:适合数据敏感的大型企业
- 公有云SaaS:适合中小微企业,快速上线
## 价格方案
基础版:9999元/年,包含10万次对话
专业版:29999元/年,包含50万次对话
"""

# 初始化递归分块器(按天然结构切,目标块大小50个token)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=50,          # 目标块大小
    chunk_overlap=10,       # 重叠token数(防断句)
    separators=["\n## ", "\n### ", "\n1. ", "\n- ", "\n"],  # 优先切分符(按结构层级)
)

# 执行分块
chunks = text_splitter.split_text(text)

# 输出结果(按章节→小节→列表项逐层切分,每个块不超50token)
for i, chunk in enumerate(chunks):
    print(f"块{i+1}{chunk}")

结果:

块1:# 智能客服系统 V2.0
块2:## 功能介绍
### 核心能力
1. 多轮对话:支持用户连续提问,记忆上下文
块3:2. 意图识别:精准识别用户咨询、投诉、建议等意图
块4:### 部署方式
- 私有云部署:适合数据敏感的大型企业
块5:- 公有云SaaS:适合中小微企业,快速上线
块6:## 价格方案
基础版:9999元/年,包含10万次对话
块7:专业版:29999元/年,包含50万次对话
  1. langchain4j示例:
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.openai.OpenAiTokenizer;

import java.util.List;

import static dev.langchain4j.model.openai.OpenAiModelName.GPT_3_5_TURBO;

public class RagRecursiveDemo {

    public static void main(String[] args) {
        // 1. 准备企业文档数据(产品手册)
        String text = "# 智能客服系统 V2.0\n" +
                "## 功能介绍\n" +
                "### 核心能力\n" +
                "1. 多轮对话:支持用户连续提问,记忆上下文\n" +
                "2. 意图识别:精准识别用户咨询、投诉、建议等意图\n" +
                "### 部署方式\n" +
                "- 私有云部署:适合数据敏感的大型企业\n" +
                "- 公有云SaaS:适合中小微企业,快速上线\n" +
                "## 价格方案\n" +
                "基础版:9999元/年,包含10万次对话\n" +
                "专业版:29999元/年,包含50万次对话";

        // 将字符串转换为 LangChain4j 的 Document 对象
        Document document = Document.from(text);

        // 2. 初始化递归分块器 (Recursive Document Splitter)
        // 参数 1: 50  -> maxSegmentSizeInTokens (目标块大小,注意是 Token 而非字符)
        // 参数 2: 10  -> maxOverlapSizeInTokens (重叠大小,防止语义在切分点断裂)
        // 参数 3: Tokenizer -> 关键点!使用模型对应的分词器能保证计数精确,防止超长报错
        DocumentSplitter splitter = DocumentSplitters.recursive(
                50, 
                10, 
                new OpenAiTokenizer(GPT_3_5_TURBO) 
        );

        // 3. 执行切分操作
        // LangChain4j 会自动根据 \n\n, \n, " " 等层级进行递归尝试
        List<TextSegment> segments = splitter.split(document);

        // 4. 结果展示与实际应用说明
        System.out.println("=== RAG 分块结果 ===");
        System.out.println("总分块数: " + segments.size());
        System.out.println("--------------------");

        for (int i = 0; i < segments.size(); i++) {
            TextSegment segment = segments.get(i);
            
            System.out.println("【块 " + (i + 1) + "】内容:");
            // 打印内容,并将换行符替换为可视符号方便查看切分点
            System.out.println(segment.text().replace("\n", " [↵] "));
            
            // 在生产环境中,你会在这里调用 EmbeddingModel 将 segment 转化为向量
            // 比如:Embedding embedding = embeddingModel.embed(segment).content();
            
            System.out.println("--------------------");
        }
    }
}

二、固定长度+重叠(Fixed Size + Overlap)

  1. 规则:按token或字符硬切,Overlap 建议固定在10%~15%,不仅仅是为了防断句,更是为了让相邻块之间有语义衔接。
  2. 优点:速度极快、可预测(块多大、切在哪等可预测)、适合海量无结构数据
  3. 缺点:易切碎语义、检索精度低
  4. 适用场景:数据量大、结构混乱场景(日志、爬虫文本)常用
  5. python实现:
def fixed_size_chunking(text, chunk_size=100, overlap_ratio=0.2):
    """固定长度分块+重叠"""
    overlap = int(chunk_size * overlap_ratio)
    chunks = []
    start = 0
    # 按字符数硬切(也可换token计数)
    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)
        # 下一个块从“当前起始+块长-重叠”开始
        start = end - overlap
    return chunks

# 待切分的无结构文本(用户日志)
log_text = "2026-02-25 10:01:用户反馈登录失败,提示密码错误,核实为用户输入大写字母未区分;2026-02-25 10:05:用户咨询会员退费政策,告知7天无理由退费,需提供订单号;2026-02-25 10:08:用户投诉商品质量问题,要求换货,转接人工客服处理;..."

# 执行分块(块长100字符,重叠20字符)
log_chunks = fixed_size_chunking(log_text, 100, 0.2)
print(f"生成{len(log_chunks)}个块,前2个块:")
print(log_chunks[0])
print(log_chunks[1])
  1. java实现:
import java.util.ArrayList;
import java.util.List;

public class FixedSizeChunker {

    /**
     * 固定长度分块逻辑(带重叠)
     * * @param text         待切分文本
     * @param chunkSize    块大小(字符数)
     * @param overlapRatio 重叠比例(0.0 ~ 1.0)
     * @return 分块后的字符串列表
     */
    public static List<String> fixedSizeChunking(String text, int chunkSize, double overlapRatio) {
        List<String> chunks = new ArrayList<>();
        
        // 健壮性检查
        if (text == null || text.isEmpty() || chunkSize <= 0) {
            return chunks;
        }

        // 计算重叠的字符数
        int overlap = (int) (chunkSize * overlapRatio);
        int start = 0;
        int textLength = text.length();

        while (start < textLength) {
            // 计算当前块的结束位置,防止越界
            int end = Math.min(start + chunkSize, textLength);
            
            // 截取分块
            String chunk = text.substring(start, end);
            chunks.add(chunk);

            // 如果已经处理到文本末尾,跳出循环
            if (end == textLength) {
                break;
            }

            // 下一个块的起始点 = 当前结束位置 - 重叠长度
            // 确保 start 始终在向前推进,避免死循环
            start = end - overlap;
            
            // 安全保护:如果重叠设置过大导致 start 不动,强制推进
            if (start <= (end - chunkSize)) {
                start = end;
            }
        }
        return chunks;
    }

    public static void main(String[] args) {
        // 待切分的无结构文本(用户日志)
        String logText = "2026-02-25 10:01:用户反馈登录失败,提示密码错误,核实为用户输入大写字母未区分;" +
                         "2026-02-25 10:05:用户咨询会员退费政策,告知7天无理由退费,需提供订单号;" +
                         "2026-02-25 10:08:用户投诉商品质量问题,要求换货,转接人工客服处理;...";

        // 执行分块:块长100,重叠20%
        List<String> logChunks = fixedSizeChunking(logText, 100, 0.2);

        // 输出结果
        System.out.println("生成分块数: " + logChunks.size());
        for (int i = 0; i < Math.min(logChunks.size(), 2); i++) {
            System.out.println("块 " + (i + 1) + ": " + logChunks.get(i));
        }
    }
}

三、层次化 / 父子分块(Hierarchical/Parent-Child)

  1. 原理:父块(章节/段落)存完整上下文;子块(句子/短句)存向量做检索;检索先召回子块,再拉父块补全。
  2. 优点:兼顾检索精度(子块)+ 上下文完整(父块)、幻觉少
  3. 缺点:索引与检索逻辑稍复杂
  4. 使用场景:企业级RAG、LlamaIndex、LangChain 高级方案标配
  5. LlamaIndex代码(LlamaIndex是专门为RAG打造的开源数据框架,核心是帮你把私有数据变成LLM能使用的上下文,主打数据摄入->分块->索引->检索->生成全链路。)
from llama_index.core import Document
from llama_index.core.node_parser import HierarchicalNodeParser, get_leaf_nodes, get_parent_nodes

# 待切分的技术文档
doc_text = """
# Python性能优化指南
## 内存优化
### 避免内存泄漏
- 及时关闭文件句柄和数据库连接
- 使用生成器代替列表推导式(减少一次性内存占用)
### 减少重复对象
- 使用sys.intern()缓存字符串
- 用__slots__限制类属性
## CPU优化
### 并行计算
- 使用multiprocessing处理CPU密集型任务
- 用numba加速数值计算
"""

# 初始化父子分块器(父块:章节/小节;子块:句子)
node_parser = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[200, 50]  # 父块200token,子块50token
)

# 生成父子节点
doc = Document(text=doc_text)
nodes = node_parser.get_nodes_from_documents([doc])
leaf_nodes = get_leaf_nodes(nodes)    # 子块(用于建向量索引)
parent_nodes = get_parent_nodes(nodes)# 父块(用于补全上下文)

# 检索逻辑示例:先查子块,再拉父块
def retrieve_with_parent(query="如何减少Python内存占用?"):
    # 1. 向量检索召回相关子块(模拟)
    matched_leaf = [n for n in leaf_nodes if "内存" in n.text][0]
    # 2. 根据子块ID找到对应的父块
    matched_parent = [p for p in parent_nodes if p.node_id == matched_leaf.parent_node_id][0]
    # 3. 返回子块(精准)+ 父块(完整上下文)
    return {
        "子块(检索用)": matched_leaf.text,
        "父块(补上下文)": matched_parent.text
    }

# 执行检索
result = retrieve_with_parent()
print(result)

结果:

{
    "子块(检索用)": "- 及时关闭文件句柄和数据库连接\n- 使用生成器代替列表推导式(减少一次性内存占用)",
    "父块(补上下文)": "## 内存优化\n### 避免内存泄漏\n- 及时关闭文件句柄和数据库连接\n- 使用生成器代替列表推导式(减少一次性内存占用)\n### 减少重复对象\n- 使用sys.intern()缓存字符串\n- 用__slots__限制类属性"
}
  1. java代码
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.openai.OpenAiTokenizer;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class ParentChildChunkingDemo {

    public static void main(String[] args) {
        String docText = "# Python性能优化指南\n" +
                "## 内存优化\n" +
                "### 避免内存泄漏\n" +
                "- 及时关闭文件句柄\n" +
                "- 使用生成器代替列表\n" +
                "### 减少重复对象\n" +
                "- 使用sys.intern()\n" +
                "## CPU优化\n" +
                "### 并行计算\n" +
                "- 使用multiprocessing";

        // 1. 先切父块 (Parent Chunks) - 较大
        DocumentSplitter parentSplitter = DocumentSplitters.recursive(200, 0, new OpenAiTokenizer());
        List<TextSegment> parentSegments = parentSplitter.split(Document.from(docText));

        List<TextSegment> allChildren = new ArrayList<>();

        // 2. 遍历每个父块,进一步切分子块 (Child Chunks)
        for (TextSegment parent : parentSegments) {
            // 为父块生成唯一ID
            String parentId = UUID.randomUUID().toString();
            
            // 记录父块内容(实际生产中会存入数据库)
            // parent.metadata().add("node_id", parentId);

            // 切分子块 - 较小
            DocumentSplitter childSplitter = DocumentSplitters.recursive(50, 0, new OpenAiTokenizer());
            List<TextSegment> children = childSplitter.split(Document.from(parent.text()));

            for (TextSegment child : children) {
                // 关键步骤:子块通过 Metadata 绑定父块 ID 和完整内容
                child.metadata().add("parent_id", parentId);
                child.metadata().add("parent_text", parent.text()); // 直接存入或索引引用
                allChildren.add(child);
            }
        }

        // 3. 模拟检索逻辑
        System.out.println("子块总数 (用于向量检索): " + allChildren.size());
        
        // 假设检索到了第一个子块
        TextSegment hitChild = allChildren.get(0);
        System.out.println("\n[召回子块]: " + hitChild.text());
        System.out.println("[补充父块上下文]: " + hitChild.metadata().get("parent_text"));
    }
}

四、语义感知分块(Semantic Chunking)

  1. 原理:它不是硬切,而是通过计算相邻句子的余弦相似度(Cosine Similarity)。
  2. 实现思路:将文档拆成句子。调用 Embedding 模型获取每个句子的向量。计算余弦相似度。当相似度低于阈值(如 0.8)时,划定切分点。
  3. 优点:块内语义高度一致、跨块无主题跳跃、检索准。
  4. 缺点:需Embedding计算、速度比结构分块慢
  5. 应用场景:知识问答、学术/技术文档、法律合同场景主流。
  6. python实现:
import numpy as np
from sentence_transformers import SentenceTransformer

# 加载Embedding模型
model = SentenceTransformer("all-MiniLM-L6-v2")

# 待切分的法律文档
legal_text = """
本合同由甲方(XX科技有限公司)与乙方(XX广告有限公司)于2026年2月25日签订。
第一条 合作内容:乙方为甲方提供2026年度线上广告投放服务,涵盖抖音、小红书、B站等平台。
第二条 费用及支付:甲方需支付年度服务费100万元,分4期支付,每期25万元,支付时间为每季度末。
第三条 违约责任:若甲方逾期支付费用,每日按未支付金额的0.05%支付违约金;若乙方未按约定投放广告,需退还对应服务费。
第四条 争议解决:双方因本合同产生的争议,优先协商解决;协商不成的,提交合同签订地法院诉讼解决。
"""

# 步骤1:拆分成句子
sentences = [s.strip() for s in legal_text.split("\n") if s.strip()]
# 步骤2:计算每个句子的Embedding
embeddings = model.encode(sentences)
# 步骤3:计算相邻句子的余弦相似度(相似度突降处切分)
def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

similarities = []
for i in range(len(embeddings)-1):
    sim = cosine_similarity(embeddings[i], embeddings[i+1])
    similarities.append(sim)

# 步骤4:设定阈值(0.7),相似度低于阈值则切分
threshold = 0.7
chunk_boundaries = [0]  # 初始边界
for i, sim in enumerate(similarities):
    if sim < threshold:
        chunk_boundaries.append(i+1)
chunk_boundaries.append(len(sentences))  # 最后一个边界

# 步骤5:生成语义块
semantic_chunks = []
for i in range(len(chunk_boundaries)-1):
    start = chunk_boundaries[i]
    end = chunk_boundaries[i+1]
    chunk = "\n".join(sentences[start:end])
    semantic_chunks.append(chunk)

# 输出结果
print("语义相似度:", [round(s, 2) for s in similarities])
print("语义块:")
for i, chunk in enumerate(semantic_chunks):
    print(f"块{i+1}{chunk}")

结果:

语义相似度: [0.91, 0.85, 0.62, 0.58]
语义块:
块1:本合同由甲方(XX科技有限公司)与乙方(XX广告有限公司)于2026225日签订。
第一条 合作内容:乙方为甲方提供2026年度线上广告投放服务,涵盖抖音、小红书、B站等平台。
第二条 费用及支付:甲方需支付年度服务费100万元,分4期支付,每期25万元,支付时间为每季度末。
块2:第三条 违约责任:若甲方逾期支付费用,每日按未支付金额的0.05%支付违约金;若乙方未按约定投放广告,需退还对应服务费。
块3:第四条 争议解决:双方因本合同产生的争议,优先协商解决;协商不成的,提交合同签订地法院诉讼解决。
  1. java代码:
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.data.embedding.Embedding;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class SemanticChunkerDemo {

    public static void main(String[] args) {
        // 1. 待切分文档
        String legalText = "本合同由甲方(XX科技有限公司)与乙方(XX广告有限公司)于2026年2月25日签订。\n" +
                "第一条 合作内容:乙方为甲方提供2026年度线上广告投放服务。\n" +
                "第二条 费用及支付:甲方需支付年度服务费100万元。\n" +
                "第三条 违约责任:若甲方逾期支付,每日按未支付金额的0.05%支付违约金。\n" +
                "第四条 争议解决:双方因本合同产生的争议,优先协商解决。";

        // 2. 将文档拆分为句子
        List<String> sentences = Arrays.stream(legalText.split("\n"))
                .filter(s -> !s.trim().isEmpty())
                .collect(Collectors.toList());

        // 3. 加载本地 Embedding 模型 (这里使用 LangChain4j 自带的 ONNX 模型,性能极高)
        EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();

        // 4. 计算所有句子的向量
        List<Embedding> embeddings = sentences.stream()
                .map(embeddingModel::embed)
                .map(response -> response.content())
                .collect(Collectors.toList());

        // 5. 计算相邻句子相似度并切分
        double threshold = 0.7; // 语义断裂阈值
        List<TextSegment> semanticChunks = new ArrayList<>();
        StringBuilder currentChunk = new StringBuilder(sentences.get(0));

        for (int i = 0; i < embeddings.size() - 1; i++) {
            // 计算 Cosine Similarity
            double similarity = cosineSimilarity(embeddings.get(i).vector(), embeddings.get(i + 1).vector());
            System.out.printf("句子 %d 与 %d 的相似度: %.2f\n", i, i + 1, similarity);

            if (similarity < threshold) {
                // 相似度过低,切分:保存当前块,开启新块
                semanticChunks.add(TextSegment.from(currentChunk.toString()));
                currentChunk = new StringBuilder(sentences.get(i + 1));
            } else {
                // 语义连贯,合并
                currentChunk.append("\n").append(sentences.get(i + 1));
            }
        }
        semanticChunks.add(TextSegment.from(currentChunk.toString()));

        // 6. 输出结果
        System.out.println("\n最终语义块数量: " + semanticChunks.size());
        semanticChunks.forEach(chunk -> System.out.println(">>> " + chunk.text()));
    }

    // 余弦相似度计算工具类
    private static double cosineSimilarity(float[] vectorA, float[] vectorB) {
        double dotProduct = 0.0;
        double normA = 0.0;
        double normB = 0.0;
        for (int i = 0; i < vectorA.length; i++) {
            dotProduct += vectorA[i] * vectorB[i];
            normA += Math.pow(vectorA[i], 2);
            normB += Math.pow(vectorB[i], 2);
        }
        return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
    }
}

五、2025~2026前沿分块方法

1. LLM引导分块(LLM-Guided / LumberChunker)

  1. 原理:直接让LLM判断主题切换,插入分块符;或把文本拆成原子命题(Proposition)
  2. 优点:最贴合人类理解、语义最完整、适合复杂长文
  3. 缺点:成本高、延迟大、不适合超大规模数据
  4. 应用场景:高端知识库、法律/医疗专业文档
  5. python代码
from openai import OpenAI

client = OpenAI(api_key="你的API_KEY")

# 待切分的复杂医疗文档
medical_text = """
糖尿病诊疗指南(2026版)
1. 诊断标准:空腹血糖≥7.0mmol/L,或餐后2小时血糖≥11.1mmol/L,伴多饮、多食、多尿、体重下降等症状。
2. 分型:1型糖尿病(胰岛素依赖型,多见于青少年)、2型糖尿病(非胰岛素依赖型,多见于中老年人)、妊娠糖尿病、特殊类型糖尿病。
3. 治疗原则:1型糖尿病需终身胰岛素替代治疗;2型糖尿病先通过饮食控制+运动,无效则加用口服降糖药,最后考虑胰岛素。
4. 并发症管理:糖尿病肾病需控制血压(目标<130/80mmHg),糖尿病视网膜病变需定期眼底检查(每年1次)。
"""

# 调用LLM(GPT-4)判断分块边界并切分
def llm_guided_chunking(text):
    prompt = f"""
    请你作为专业的医疗文档分析师,将以下文本按**主题完整度**切分成语义块,要求每个块对应一个独立的诊疗主题,输出格式为JSON数组:
    文本:{text}
    """
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"}
    )
    # 解析LLM返回的分块结果
    import json
    result = json.loads(response.choices[0].message.content)
    return result["chunks"]

# 执行LLM引导分块
llm_chunks = llm_guided_chunking(medical_text)
print("LLM生成的语义块:")
for i, chunk in enumerate(llm_chunks):
    print(f"块{i+1}{chunk}")

2. 意图驱动动态分块(Intent-Driven Dynamic Chunking, IDC)

  1. 原理:LLM生成用户可能的查询,用动态规划找最优切分,让块与“预期问题”对齐
  2. 优点:检索精度提升5%–67%、块数减少40%–60%、答案覆盖率高
  3. 缺点:需离线预生成意图、工程复杂
  4. 前沿:2026年顶会论文,大厂内部RAG在验证
  5. python代码:
# 步骤1:LLM预生成用户可能的查询意图(离线)
def generate_intents(text):
    prompt = f"""
    基于以下产品文档,生成用户最可能的10个查询意图(问题形式):
    文档:{text}
    """
    # 调用LLM生成意图(示例结果)
    intents = [
        "智能客服系统支持哪些部署方式?",
        "智能客服系统专业版价格是多少?",
        "智能客服系统能识别哪些用户意图?",
        "智能客服系统多轮对话功能是什么?"
    ]
    return intents

# 步骤2:动态规划找最优分块(让块与意图对齐)
def intent_driven_chunking(text, intents, max_chunk_size=100):
    # 核心逻辑:计算每个候选块覆盖的意图数,选择覆盖最多的块作为最终分块
    # 模拟实现(简化版)
    optimal_chunks = [
        "# 智能客服系统 V2.0\n## 功能介绍\n### 核心能力\n1. 多轮对话:支持用户连续提问,记忆上下文\n2. 意图识别:精准识别用户咨询、投诉、建议等意图",
        "### 部署方式\n- 私有云部署:适合数据敏感的大型企业\n- 公有云SaaS:适合中小微企业,快速上线",
        "## 价格方案\n基础版:9999元/年,包含10万次对话\n专业版:29999元/年,包含50万次对话"
    ]
    return optimal_chunks

# 执行IDC分块
text = """# 智能客服系统 V2.0...(同递归分块示例)"""
intents = generate_intents(text)
idc_chunks = intent_driven_chunking(text, intents)
print("意图驱动分块结果:")
for chunk in idc_chunks:
    print(chunk)

3. 布局感知分块(Layout-Aware Chunking)

  1. 原理:结合PDF/Word/网页的视觉结构(标题、段落、表格、代码块)切分,不破坏排版语义
  2. 优点:充分利用文档视觉结构与排版信息,保证分块边界自然、语义完整、连贯性强,非常适合富文本
  3. 缺点:依赖文档解析质量、实现复杂、速度较慢、不可预测
  4. 使用场景:适合富文本、报告、论文、合同
  5. 基于 Unstructured 库解析 PDF 布局代码:
from unstructured.partition.pdf import partition_pdf

# 解析PDF(带表格/标题/代码块的技术论文)
elements = partition_pdf(
    "AI论文.pdf",
    extract_images=False,
    infer_table_structure=True,  # 识别表格结构
    chunking_strategy="by_layout",  # 按布局分块
    max_characters=500,
    new_after_n_chars=450,
    combine_text_under_n_chars=100,
)

# 按布局类型分类分块
layout_chunks = {
    "Title": [],       # 标题
    "NarrativeText": [],# 正文段落
    "Table": []        # 表格
}
for element in elements:
    if element.category in layout_chunks:
        layout_chunks[element.category].append(element.text)

# 输出结果
print("标题块:", layout_chunks["Title"][0])
print("表格块:", layout_chunks["Table"][0])
print("正文块:", layout_chunks["NarrativeText"][0])

结果:

标题块: 基于大语言模型的RAG分块方法研究
表格块: 表1 不同分块方法的精度对比
| 方法 | 准确率 | 召回率 |
|------|--------|--------|
| 递归分块 | 85% | 90% |
| 语义分块 | 92% | 88% |
正文块: 摘要:RAG(检索增强生成)是大语言模型应用的核心技术...

4. 混合分块(Hybrid)

  1. 原理:结构分块 + 语义分块 + LLM校验。先按标题切 → 超长块用语义分块 → LLM检查语义完整性。
  2. 优点:既保留了文档天然结构(递归分块),又保证了语义完整(语义分块 + LLM 校验),同时避免了纯语义分块的高成本。
  3. python代码:
# 步骤1:先按递归结构分块
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
raw_chunks = text_splitter.split_text(legal_text)  # legal_text同语义分块示例

# 步骤2:对超长块(>200字符)用语义分块
def semantic_filter(chunk):
    if len(chunk) > 200:
        # 调用语义分块函数(同语义感知分块示例)
        return semantic_chunks  # 返回切分后的小子块
    return [chunk]

filtered_chunks = []
for chunk in raw_chunks:
    filtered_chunks.extend(semantic_filter(chunk))

# 步骤3:LLM校验语义完整性
def llm_validate_chunk(chunk):
    prompt = f"请判断以下文本块是否语义完整(是/否):{chunk}"
    # 调用LLM校验(模拟返回)
    return "是"

final_chunks = []
for chunk in filtered_chunks:
    if llm_validate_chunk(chunk) == "是":
        final_chunks.append(chunk)
    else:
        # 不完整则合并相邻块
        if final_chunks:
            final_chunks[-1] += "\n" + chunk
        else:
            final_chunks.append(chunk)

# 输出最终混合分块结果
print("混合分块最终结果:")
for i, chunk in enumerate(final_chunks):
    print(f"块{i+1}{chunk}")

六、总结

  1. 企业级主流:递归结构分块适合普通有结构文档快速落地,固定长度 + 重叠适合海量无结构数据,父子分块兼顾精度与上下文,语义分块适合专业文档。
  2. 前沿方法:LLM 引导分块语义最完整但成本高,IDC 分块精度提升显著但工程复杂,布局感知分块适配富文本,混合分块是平衡速度 / 精度 / 成本的最终选型。