【RAG 实战】基于文档结构特征分析的切片策略自动推荐引擎设计与实现

摘要:在 RAG(Retrieval-Augmented Generation)系统中,文档切片策略的选择直接影响检索质量。本文介绍了一种基于文档结构特征分析的切片策略自动推荐引擎,通过"规则引擎 + LLM 轻量判定"的两阶段架构,自动为不同类型的文档推荐最优切片策略。文章涵盖架构设计、特征提取、规则打分、LLM 决策、参数自动计算等完整实现细节,并附带可直接运行的代码示例。


文章目录

📌 一、问题背景

1.1 切片策略对 RAG 的影响

在 RAG 系统中,文档切片(Chunking) 是决定检索质量的核心环节。不同的文档类型适合不同的切片策略:

文档类型 推荐策略 原因
Markdown 格式文档(法规、合同、API 文档) markdown 按标题层级切片,保持章节完整性
语义连贯的长文(报告、会议记录) semantic 按语义相似度切片,避免切断语义
表格/列表密集型文档(数据报告) markdown 保持结构化元素完整性
FAQ/问答文档(客服话术) window 句子窗口切片保留问答上下文
超长文档(财报、白皮书) hierarchical 多粒度层级切片,支持粗/细粒度检索
短文本/碎片(词条说明) sentence 按句切片获得更精确粒度

1.2 痛点

用户往往不知道该选哪种策略,只能凭经验盲猜。选错策略会导致:

  • 表格被截断 → 检索到不完整的表格
  • 语义被切断 → 检索到断章取义的片段
  • 超长文档直接语义切分 → 触碰 Embedding Token 上限报错

1.3 解决方案

构建一个自动推荐引擎,基于文档的结构特征分析,自动推荐最适合的切片策略。


🏗️ 二、整体架构设计

2.1 设计理念

采用 “规则引擎 + LLM 轻量判定” 结合的方式:

  1. 规则引擎先打分:基于文档结构特征,运行多条规则,筛选 Top 3 候选策略
  2. LLM 做最终决策:从 Top 3 候选中,结合文档内容语义选出最优策略
  3. 特征提取与策略判定分离:便于独立测试和调优
  4. 支持用户手动覆盖:用户指定策略时直接返回,跳过自动判定

2.2 核心组件

┌─────────────────────────────────────────────────────────────────┐
│                  AutoChunkStrategyRecommender                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │           MDStructureAnalyzer(特征提取器)                 │ │
│  │  - 标题层级分析        - 段落长度统计                       │ │
│  │  - 表格/列表占比       - FAQ/步骤型模式检测                 │ │
│  └───────────────────────────────────────────────────────────┘ │
│                              ↓                                  │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │         DocumentStructureFeatures(特征数据类)             │ │
│  │  - total_chars / heading_density                           │ │
│  │  - table_ratio / list_ratio                                │ │
│  │  - faq_pattern_ratio / numbered_steps_ratio                │ │
│  └───────────────────────────────────────────────────────────┘ │
│                              ↓                                  │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │              规则引擎(Rule Engine)                        │ │
│  │  - 超长文档     → hierarchical                             │ │
│  │  - 多级标题     → markdown                                 │ │
│  │  - 表格/列表密集 → markdown                                │ │
│  │  - FAQ 密集    → sentence_window                           │ │
│  │  - 步骤型文本   → sentence_window                          │ │
│  │  - 语义连贯     → semantic                                 │ │
│  │  - 短文本      → sentence                                 │ │
│  └───────────────────────────────────────────────────────────┘ │
│                              ↓                                  │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │          LLM 轻量判定(从 Top 3 中选择最优)                │ │
│  │  - 失败时回退到规则引擎第一候选                             │ │
│  └───────────────────────────────────────────────────────────┘ │
│                              ↓                                  │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │           StrategyRecommendation(推荐结果)                │ │
│  │  - strategy_type / strategy_params / confidence / reason   │ │
│  └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

2.3 执行流程

输入文档 → 特征提取 → 规则打分(7条) → 排序去重 → Top 3 候选
                                                    ↓
                                            LLM 选择最优
                                                    ↓
                                            输出推荐结果
                                            (策略+参数+原因)

📊 三、数据模型设计

3.1 文档结构特征集合

from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class DocumentStructureFeatures:
    """文档结构特征集合

    所有字段均为从 MD 文本中提取的统计指标,用于策略判定。
    """

    # --- 基础统计 ---
    total_chars: int = 0
    """文档总字符数"""
    total_lines: int = 0
    """文档总行数(非空行)"""
    total_paragraphs: int = 0
    """文档总段落数(以空行分隔)"""

    # --- 标题特征 ---
    heading_counts: dict = field(default_factory=dict)
    """各级标题数量,如 {1: 5, 2: 12, 3: 8, 4: 3}"""
    max_heading_level: int = 0
    """最大标题层级深度(如 3 表示最深到 ###)"""
    total_headings: int = 0
    """标题总数"""
    heading_density: float = 0.0
    """标题密度 = 标题总数 / 段落数"""

    # --- 段落特征 ---
    avg_paragraph_length: float = 0.0
    """平均段落长度(字符数)"""
    max_paragraph_length: float = 0.0
    """最长段落长度(字符数)"""
    short_paragraph_ratio: float = 0.0
    """短段落占比(段落长度 < 50 字符的比例)"""

    # --- 结构化元素占比 ---
    table_line_count: int = 0
    """表格行数(包含 | 的行)"""
    table_ratio: float = 0.0
    """表格行占总非空行的比例"""
    list_line_count: int = 0
    """列表行数(以 - / * / 数字. 开头的行)"""
    list_ratio: float = 0.0
    """列表行占总非空行的比例"""
    code_block_count: int = 0
    """代码块数量(```包裹的块)"""
    image_count: int = 0
    """图片数量(![...] 格式)"""
    link_count: int = 0
    """链接数量([...]() 格式,不含图片)"""

    # --- 分隔符与特殊元素 ---
    horizontal_rule_count: int = 0
    """水平分隔线数量(--- / *** / ___)"""
    blockquote_count: int = 0
    """引用块行数(> 开头)"""

    # --- 语义连贯性指标 ---
    avg_sentence_length: float = 0.0
    """平均句子长度(字符数,按 。!?.!? 分句)"""
    sentence_count: int = 0
    """句子总数"""

    # --- 场景特征 ---
    faq_pattern_count: int = 0
    """FAQ 模式段落数(问句+答句交替)"""
    faq_pattern_ratio: float = 0.0
    """FAQ 模式段落占比"""
    numbered_steps_count: int = 0
    """编号步骤数(1. 2. 3. 或 一、二、三、)"""
    numbered_steps_ratio: float = 0.0
    """编号步骤占比"""

设计说明

  • 特征分为 6 大类:基础统计、标题特征、段落特征、结构化元素、分隔符、语义连贯性、场景特征
  • heading_density(标题密度)是关键指标:标题多且密度合理 → 适合按标题切片
  • faq_pattern_rationumbered_steps_ratio 是场景特征,用于识别特定文档类型

3.2 策略推荐结果

@dataclass
class StrategyRecommendation:
    """策略推荐结果"""
    strategy_type: ChunkStrategyType
    """推荐的切片策略类型"""
    strategy_params: dict = field(default_factory=dict)
    """推荐的切片策略参数"""
    features: Optional[DocumentStructureFeatures] = None
    """提取到的文档结构特征"""
    reason: str = ""
    """推荐原因说明"""
    confidence: str = "medium"
    """推荐置信度:high / medium / low"""

🔍 四、特征提取器实现

MDStructureAnalyzer 负责从 Markdown 文本中提取各类结构特征。

4.1 正则模式定义

import re

class MDStructureAnalyzer:
    """Markdown 文档结构分析器

    从 MD 文本中提取各类结构特征,供策略推荐引擎使用。
    """

    # --- 正则模式 ---
    _HEADING_RE = re.compile(r'^(#{1,6})\s+(.+)$', re.MULTILINE)
    _TABLE_LINE_RE = re.compile(r'^\s*\|.*\|', re.MULTILINE)
    _LIST_LINE_RE = re.compile(r'^\s*(?:[-*+]|\d+[.)])\s+', re.MULTILINE)
    _CODE_BLOCK_RE = re.compile(r'^```', re.MULTILINE)
    _IMAGE_RE = re.compile(r'!\[.*?\]\(.*?\)')
    _LINK_RE = re.compile(r'(?<!!)\[.*?\]\(.*?\)')
    _HR_RE = re.compile(r'^\s*(?:---+|\*\*\*+|___+)\s*$', re.MULTILINE)
    _BLOCKQUOTE_RE = re.compile(r'^\s*>\s*', re.MULTILINE)
    _SENTENCE_SPLIT_RE = re.compile(r'[。!?.!?]+')

    # 场景特征检测
    _FAQ_QUESTION_RE = re.compile(r'^[^\n]*[??]\s*$', re.MULTILINE)
    _NUMBERED_STEP_RE = re.compile(
        r'^(?:\d+|[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341])[、..]\s*',
        re.MULTILINE,
    )

各正则的作用

正则 匹配目标 示例
_HEADING_RE Markdown 标题 ## 二级标题
_TABLE_LINE_RE 表格行 | 列1 | 列2 |
_LIST_LINE_RE 列表行 - 项目 / 1. 步骤
_CODE_BLOCK_RE 代码块标记 ```python
_IMAGE_RE 图片 ![alt](url)
_LINK_RE 链接(排除图片) [text](url)
_HR_RE 水平分隔线 --- / ***
_BLOCKQUOTE_RE 引用块 > 引用内容
_SENTENCE_SPLIT_RE 句子分隔符 。!?.!?
_FAQ_QUESTION_RE 问句(以问号结尾的行) 什么是 RAG?
_NUMBERED_STEP_RE 编号步骤(中英文) 1. 第一步 / 一、概述

4.2 特征提取主方法

@classmethod
def analyze(cls, md_text: str) -> DocumentStructureFeatures:
    """分析 MD 文本,提取结构特征

    :param md_text: Markdown 格式的纯文本
    :return: 文档结构特征集合
    """
    if not md_text or not md_text.strip():
        return DocumentStructureFeatures()

    features = DocumentStructureFeatures()
    features.total_chars = len(md_text)

    # 非空行
    all_lines = md_text.split('\n')
    non_empty_lines = [line for line in all_lines if line.strip()]
    features.total_lines = len(non_empty_lines)

    # 段落(以空行分隔)
    paragraphs = cls._split_paragraphs(md_text)
    features.total_paragraphs = max(len(paragraphs), 1)

    # --- 标题特征 ---
    heading_matches = cls._HEADING_RE.findall(md_text)
    heading_counts = {}
    for hashes, _ in heading_matches:
        level = len(hashes)
        heading_counts[level] = heading_counts.get(level, 0) + 1
    features.heading_counts = heading_counts
    features.total_headings = sum(heading_counts.values())
    features.max_heading_level = max(heading_counts.keys()) if heading_counts else 0
    features.heading_density = features.total_headings / features.total_paragraphs

    # --- 段落特征 ---
    para_lengths = [len(p.strip()) for p in paragraphs if p.strip()]
    if para_lengths:
        features.avg_paragraph_length = sum(para_lengths) / len(para_lengths)
        features.max_paragraph_length = max(para_lengths)
        short_count = sum(1 for length in para_lengths if length < 50)
        features.short_paragraph_ratio = short_count / len(para_lengths)

    # --- 结构化元素 ---
    features.table_line_count = len(cls._TABLE_LINE_RE.findall(md_text))
    features.table_ratio = features.table_line_count / max(features.total_lines, 1)

    features.list_line_count = len(cls._LIST_LINE_RE.findall(md_text))
    features.list_ratio = features.list_line_count / max(features.total_lines, 1)

    code_fence_count = len(cls._CODE_BLOCK_RE.findall(md_text))
    features.code_block_count = code_fence_count // 2  # 每个代码块有开闭两个标记

    features.image_count = len(cls._IMAGE_RE.findall(md_text))
    features.link_count = len(cls._LINK_RE.findall(md_text))

    # --- 分隔符与特殊元素 ---
    features.horizontal_rule_count = len(cls._HR_RE.findall(md_text))
    features.blockquote_count = len(cls._BLOCKQUOTE_RE.findall(md_text))

    # --- 语义连贯性 ---
    sentences = [s.strip() for s in cls._SENTENCE_SPLIT_RE.split(md_text) if s.strip()]
    features.sentence_count = max(len(sentences), 1)
    total_sentence_len = sum(len(s) for s in sentences)
    features.avg_sentence_length = total_sentence_len / features.sentence_count

    # --- 场景特征 ---
    # FAQ 模式检测:以问号结尾的行
    faq_questions = cls._FAQ_QUESTION_RE.findall(md_text)
    features.faq_pattern_count = len(faq_questions)
    features.faq_pattern_ratio = features.faq_pattern_count / max(features.total_paragraphs, 1)

    # 编号步骤检测
    numbered_steps = cls._NUMBERED_STEP_RE.findall(md_text)
    features.numbered_steps_count = len(numbered_steps)
    features.numbered_steps_ratio = features.numbered_steps_count / max(features.total_lines, 1)

    return features

4.3 段落分割辅助方法

@staticmethod
def _split_paragraphs(text: str) -> List[str]:
    """按空行分割段落"""
    paragraphs = []
    current = []
    for line in text.split('\n'):
        if line.strip():
            current.append(line)
        else:
            if current:
                paragraphs.append('\n'.join(current))
                current = []
    if current:
        paragraphs.append('\n'.join(current))
    return paragraphs

🧠 五、策略推荐引擎实现

5.1 可调阈值集中管理

所有阈值定义为类属性,便于后续调优:

class AutoChunkStrategyRecommender:
    """切片策略自动推荐引擎"""

    # --- 可调阈值(集中管理,便于后续调优) ---

    # 超长文档阈值(字符数)
    VERY_LONG_DOC_THRESHOLD = 100_000       # > 10 万字符视为超长文档
    LONG_DOC_THRESHOLD = 50_000             # > 5 万字符视为长文档

    # 标题结构判定
    MIN_HEADING_LEVELS_FOR_MARKDOWN = 2     # 至少 2 级标题才适合 markdown 切片
    MIN_HEADING_DENSITY = 0.05              # 标题密度下限(太低说明标题稀疏)
    MAX_HEADING_DENSITY = 0.5               # 标题密度上限(太高说明标题过密)

    # 结构化元素判定
    HIGH_TABLE_RATIO = 0.15                 # 表格行占比 > 15% 视为表格密集
    HIGH_LIST_RATIO = 0.20                  # 列表行占比 > 20% 视为列表密集

    # 语义连贯性判定
    MIN_AVG_PARA_LENGTH_FOR_SEMANTIC = 100  # 平均段落长度 > 100 字符才适合语义切分
    MAX_SHORT_PARA_RATIO_FOR_SEMANTIC = 0.3 # 短段落占比 < 30% 才适合语义切分
    MIN_AVG_SENTENCE_LENGTH = 15            # 平均句长 > 15 字符

    # 短文本判定
    SHORT_DOC_THRESHOLD = 5_000             # < 5000 字符视为短文档
    VERY_SHORT_DOC_THRESHOLD = 2_000        # < 2000 字符视为极短文档

    # 场景特征阈值
    FAQ_PATTERN_RATIO_THRESHOLD = 0.15      # FAQ 模式段落占比 > 15% 视为 FAQ 密集
    NUMBERED_STEPS_RATIO_THRESHOLD = 0.10   # 编号步骤占比 > 10% 视为步骤型文档

5.2 推荐主入口

@classmethod
def recommend(
    cls,
    source: str | List[Document],
    user_strategy: Optional[ChunkStrategyType] = None,
) -> StrategyRecommendation:
    """根据文档内容推荐切片策略

    流程:提取特征 → 规则引擎打分取 Top 3 → LLM 从候选中选最优

    :param source: MinerU 输出的 Markdown 文本,或 LlamaIndex Document 列表
    :param user_strategy: 用户手动指定的策略(非 None 时直接返回,跳过自动判定)
    :return: 策略推荐结果
    """
    # 用户手动覆盖优先
    if user_strategy is not None:
        return StrategyRecommendation(
            strategy_type=user_strategy,
            reason=f'用户手动指定: {user_strategy.value}',
            confidence='high',
        )

    # Document 列表 → 拼接为纯文本
    md_text = (
        '\n\n'.join(doc.text or '' for doc in source)
        if isinstance(source, list)
        else source
    )

    features = MDStructureAnalyzer.analyze(md_text)

    # 空文档兜底
    if features.total_chars == 0:
        return StrategyRecommendation(
            strategy_type=ChunkStrategyType.SENTENCE,
            strategy_params={},
            features=features,
            reason='文档为空,使用默认 sentence 策略',
            confidence='low',
        )

    # 规则引擎打分 → 排序去重取 Top 3
    top = cls._score_strategies(features)
    if not top:
        top = [cls._rule_fallback(features)]

    for i, rec in enumerate(top, 1):
        logger.info(
            f"[AUTO_STRATEGY] 候选 #{i}: {rec.strategy_type.value}"
            f" (置信度={rec.confidence}) | {rec.reason}"
        )

    # LLM 从 Top 3 中选最优,失败则回退到规则引擎第一候选
    try:
        sample = md_text[:_LLM_SAMPLE_MAX_CHARS]
        llm_result = cls._llm_select_strategy(top, features, sample)
        if llm_result is not None:
            llm_result.features = features
            return llm_result
    except Exception as e:
        logger.warning(f"[AUTO_STRATEGY] LLM 判定失败,使用规则引擎第一候选: {e}")

    result = top[0]
    result.features = features
    return result

关键设计点

  1. 用户手动覆盖优先:如果用户指定了策略,直接返回,不做分析
  2. 支持两种输入:纯文本 str 或 LlamaIndex List[Document]
  3. 空文档保护:空文档直接返回 sentence 兜底
  4. LLM 失败回退:LLM 调用失败时,使用规则引擎的第一候选

5.3 规则打分与排序

# 常量定义
_TOP_K = 3
_CONFIDENCE_WEIGHT: dict[str, int] = {'high': 3, 'medium': 2, 'low': 1}

@classmethod
def _score_strategies(cls, features: DocumentStructureFeatures) -> list[StrategyRecommendation]:
    """运行所有规则,按置信度排序去重后返回 Top K 候选"""
    results: list[StrategyRecommendation] = []
    for rule_method in [
        cls._rule_very_long_document,
        cls._rule_heading_structure,
        cls._rule_structured_elements,
        cls._rule_faq_dense,
        cls._rule_procedural_text,
        cls._rule_semantic_text,
        cls._rule_short_text,
    ]:
        result = rule_method(features)
        if result is not None:
            results.append(result)

    if not results:
        results.append(cls._rule_fallback(features))

    # 按置信度降序排序,同一策略类型去重(保留最高置信度)
    results.sort(key=lambda r: _CONFIDENCE_WEIGHT.get(r.confidence, 0), reverse=True)
    seen: set[str] = set()
    unique: list[StrategyRecommendation] = []
    for rec in results:
        key = rec.strategy_type.value
        if key not in seen:
            seen.add(key)
            unique.append(rec)

    return unique[:_TOP_K]

设计要点

  • 运行所有 7 条规则,收集命中的结果
  • 按置信度降序排序(high > medium > low
  • 同一策略类型去重,保留最高置信度的那条
  • 取 Top 3 候选给 LLM 选择

📏 六、规则链详解

规则按优先级从高到低排列,每条规则返回 Optional[StrategyRecommendation]

6.1 规则 1:超长文档 → hierarchical

@classmethod
def _rule_very_long_document(cls, f: DocumentStructureFeatures) -> Optional[StrategyRecommendation]:
    """超长文档使用 hierarchical 多粒度切片

    适用场景:财报、白皮书、长篇技术文档
    原因:超长文档需要多粒度检索(粗粒度定位章节,细粒度检索段落)
    """
    if f.total_chars >= cls.VERY_LONG_DOC_THRESHOLD:
        return StrategyRecommendation(
            strategy_type=ChunkStrategyType.HIERARCHICAL,
            strategy_params=cls._compute_strategy_params(ChunkStrategyType.HIERARCHICAL, f),
            reason=(
                f'超长文档({f.total_chars:,} 字符),'
                f'使用 hierarchical 多粒度切片以支持章节级和段落级检索'
            ),
            confidence='high',
        )
    return None

判定逻辑:文档总字符数 > 100,000 → 推荐 hierarchical

为什么:超长文档如果只用单层切片,要么粒度太粗丢失细节,要么粒度太细丢失全局。hierarchical 策略生成多层级的 Node(父-子关系),检索时可以粗粒度定位章节,再下钻到具体段落。

6.2 规则 2:多级标题结构 → markdown

@classmethod
def _rule_heading_structure(cls, f: DocumentStructureFeatures) -> Optional[StrategyRecommendation]:
    """有清晰多级标题的文档使用 markdown 按标题切片"""
    if f.max_heading_level < cls.MIN_HEADING_LEVELS_FOR_MARKDOWN:
        return None

    if not (cls.MIN_HEADING_DENSITY <= f.heading_density <= cls.MAX_HEADING_DENSITY):
        return None

    # 构建标题层级描述
    level_desc = ', '.join(
        f'H{level}: {count}'
        for level, count in sorted(f.heading_counts.items())
    )

    return StrategyRecommendation(
        strategy_type=ChunkStrategyType.MARKDOWN,
        strategy_params=cls._compute_strategy_params(ChunkStrategyType.MARKDOWN, f),
        reason=(
            f'文档具有 {f.max_heading_level} 级标题结构'
            f'({level_desc}),标题密度 {f.heading_density:.2f},'
            f'使用 markdown 按标题层级切片'
        ),
        confidence='high',
    )

判定逻辑

  • 至少 2 级标题层级(max_heading_level >= 2
  • 标题密度在合理范围内(0.05 <= heading_density <= 0.5

为什么:有清晰标题结构的文档,按标题切片能保持章节完整性,检索时还能带上标题路径(如 第三章 > 3.2 核心架构)。

6.3 规则 3:表格/列表密集 → markdown

@classmethod
def _rule_structured_elements(cls, f: DocumentStructureFeatures) -> Optional[StrategyRecommendation]:
    """表格或列表密集的文档使用 markdown 切片"""
    reasons = []

    if f.table_ratio >= cls.HIGH_TABLE_RATIO:
        reasons.append(f'表格密集(表格行占比 {f.table_ratio:.1%})')

    if f.list_ratio >= cls.HIGH_LIST_RATIO:
        reasons.append(f'列表密集(列表行占比 {f.list_ratio:.1%})')

    if reasons:
        return StrategyRecommendation(
            strategy_type=ChunkStrategyType.MARKDOWN,
            strategy_params=cls._compute_strategy_params(ChunkStrategyType.MARKDOWN, f),
            reason=','.join(reasons) + ',使用 markdown 切片以保持结构化元素完整性',
            confidence='medium',
        )
    return None

判定逻辑

  • 表格行占比 > 15% → 表格密集
  • 列表行占比 > 20% → 列表密集

为什么semanticsentence 策略可能会把一个完整的表格从中间切断,导致检索到半截表格。markdown 策略能识别并保护结构化元素。

6.4 规则 3.5:FAQ/问答密集 → window

@classmethod
def _rule_faq_dense(cls, f: DocumentStructureFeatures) -> Optional[StrategyRecommendation]:
    """FAQ/问答密集文档使用 window 切片"""
    if f.faq_pattern_ratio < cls.FAQ_PATTERN_RATIO_THRESHOLD:
        return None

    return StrategyRecommendation(
        strategy_type=ChunkStrategyType.SENTENCE_WINDOW,
        strategy_params=cls._compute_strategy_params(ChunkStrategyType.SENTENCE_WINDOW, f),
        reason=(
            f'FAQ 模式密集({f.faq_pattern_ratio:.0%} 段落为问答对),'
            f'使用 window 切片以保留完整问答上下文'
        ),
        confidence='high',
    )

判定逻辑:以问号结尾的行占总段落比例 > 15%

为什么:FAQ 文档中,问句和答句通常紧密相邻。window 策略在检索时会自动扩展上下文窗口——问句命中后能展示完整答案,而非孤立的句子。

6.5 规则 3.6:步骤型文本 → window

@classmethod
def _rule_procedural_text(cls, f: DocumentStructureFeatures) -> Optional[StrategyRecommendation]:
    """步骤型文档使用 window 切片"""
    if f.numbered_steps_ratio < cls.NUMBERED_STEPS_RATIO_THRESHOLD:
        return None

    # 如果有大量标题,更适合 markdown
    if f.max_heading_level >= cls.MIN_HEADING_LEVELS_FOR_MARKDOWN:
        return None

    return StrategyRecommendation(
        strategy_type=ChunkStrategyType.SENTENCE_WINDOW,
        strategy_params=cls._compute_strategy_params(ChunkStrategyType.SENTENCE_WINDOW, f),
        reason=(
            f'步骤型文本(编号步骤占比 {f.numbered_steps_ratio:.1%}),'
            f'使用 window 切片保持步骤上下文连贯'
        ),
        confidence='medium',
    )

判定逻辑

  • 编号步骤占比 > 10%
  • 且没有多级标题(有标题的优先走 markdown)

为什么:操作手册、SOP 中的步骤间有强上下文依赖("第 3 步"依赖"第 2 步"的结果),窗口扩展能避免切断步骤链。

6.6 规则 4:语义连贯无标题 → semantic

@classmethod
def _rule_semantic_text(cls, f: DocumentStructureFeatures) -> Optional[StrategyRecommendation]:
    """语义连贯但无标题结构的文档使用 semantic 按语义切片"""
    if f.heading_density >= cls.MIN_HEADING_DENSITY:
        return None

    if f.avg_paragraph_length < cls.MIN_AVG_PARA_LENGTH_FOR_SEMANTIC:
        return None

    if f.short_paragraph_ratio > cls.MAX_SHORT_PARA_RATIO_FOR_SEMANTIC:
        return None

    if f.avg_sentence_length < cls.MIN_AVG_SENTENCE_LENGTH:
        return None

    return StrategyRecommendation(
        strategy_type=ChunkStrategyType.SEMANTIC,
        strategy_params=cls._compute_strategy_params(ChunkStrategyType.SEMANTIC, f),
        reason=(
            f'文档语义连贯但无明显标题结构'
            f'(标题密度 {f.heading_density:.2f},'
            f'平均段落长度 {f.avg_paragraph_length:.0f} 字符),'
            f'使用 semantic 按语义相似度切片'
        ),
        confidence='medium',
    )

判定逻辑(需同时满足):

  • 标题密度 < 0.05(标题极少)
  • 平均段落长度 > 100 字符(段落较长且连贯)
  • 短段落占比 < 30%(不是碎片文本)
  • 平均句长 > 15 字符(不是超短句子)

为什么:没有标题结构但语义连贯的文档(如会议记录、报告),按语义相似度切分能在主题转换处自然断开。

6.7 规则 5:短文本 → sentence

@classmethod
def _rule_short_text(cls, f: DocumentStructureFeatures) -> Optional[StrategyRecommendation]:
    """短文档使用 sentence 按句切片"""
    if f.total_chars >= cls.SHORT_DOC_THRESHOLD:
        return None

    return StrategyRecommendation(
        strategy_type=ChunkStrategyType.SENTENCE,
        strategy_params=cls._compute_strategy_params(ChunkStrategyType.SENTENCE, f),
        reason=(
            f'短文档({f.total_chars:,} 字符),'
            f'使用 sentence 按句切片以获得更精确的检索粒度'
        ),
        confidence='medium',
    )

判定逻辑:文档总字符数 < 5,000

为什么:短文档用语义切分粒度太粗(可能整个文档就一个语义段),按句切片更精确。

6.8 规则 6:兜底 → sentence

@classmethod
def _rule_fallback(cls, f: DocumentStructureFeatures) -> StrategyRecommendation:
    """兜底策略:使用 sentence 切片"""
    return StrategyRecommendation(
        strategy_type=ChunkStrategyType.SENTENCE,
        strategy_params=cls._compute_strategy_params(ChunkStrategyType.SENTENCE, f),
        reason=(
            f'未匹配到明确的策略特征'
            f'(标题层级 {f.max_heading_level},'
            f'标题密度 {f.heading_density:.2f},'
            f'平均段落长度 {f.avg_paragraph_length:.0f}),'
            f'使用 sentence 兜底策略'
        ),
        confidence='low',
    )

为什么选 sentence 兜底sentence 策略对各类文档都有基本适应性,不会出现严重问题(如表格截断、Token 超限等)。


🤖 七、LLM 轻量判定

规则引擎输出 Top 3 候选后,调用 LLM 做最终决策。

7.1 LLM 选择逻辑

_LLM_SAMPLE_MAX_CHARS = 2000  # LLM 判定时的文档内容采样长度

@classmethod
def _llm_select_strategy(
    cls,
    candidates: list[StrategyRecommendation],
    features: DocumentStructureFeatures,
    sample: str,
) -> Optional[StrategyRecommendation]:
    """调用 LLM 从规则引擎 Top 3 候选中选择最优策略"""
    from module_rag.config.settings import get_llm

    llm = get_llm()

    # 构建候选描述
    candidate_desc = '\n'.join(
        f'{i+1}. {c.strategy_type.value}(置信度: {c.confidence},原因: {c.reason})'
        for i, c in enumerate(candidates)
    )

    # 构建特征摘要
    feature_summary = (
        f'文档总字符: {features.total_chars:,},'
        f'总段落: {features.total_paragraphs},'
        f'最大标题层级: {features.max_heading_level},'
        f'标题密度: {features.heading_density:.2f},'
        f'平均段落长度: {features.avg_paragraph_length:.0f}字符,'
        f'表格行占比: {features.table_ratio:.1%},'
        f'列表行占比: {features.list_ratio:.1%},'
        f'FAQ段落占比: {features.faq_pattern_ratio:.0%},'
        f'编号步骤占比: {features.numbered_steps_ratio:.1%}'
    )

    valid_names = '/'.join(c.strategy_type.value for c in candidates)

    prompt = f"""你是一个文档切片策略选择专家。请根据以下文档特征和内容摘要,
从候选策略中选择最适合的切片策略。

## 文档特征摘要
{feature_summary}

## 文档内容摘要(前 {_LLM_SAMPLE_MAX_CHARS} 字符)
{sample[:1500]}

## 候选策略(由规则引擎预筛选)
{candidate_desc}

## 要求
1. 请仅从候选策略中选择一个最合适的策略
2. 综合考虑文档结构、内容特点、检索场景
3. 严格按以下 JSON 格式回复,不要添加其他内容:
{{"strategy": "策略名称", "reason": "选择原因的简要说明"}}

可选策略名称: {valid_names}"""

    response = llm.complete(prompt)
    response_text = str(response).strip()

    # 解析 LLM 响应(提取 JSON)
    import json
    json_match = re.search(r'\{[^}]+\}', response_text)
    if not json_match:
        return None

    try:
        result = json.loads(json_match.group())
    except json.JSONDecodeError:
        return None

    strategy_name = result.get('strategy', '')
    reason = result.get('reason', '')

    # 验证策略名称
    try:
        strategy_type = ChunkStrategyType(strategy_name)
    except ValueError:
        return None

    # 计算对应策略的参数
    strategy_params = cls._compute_strategy_params(strategy_type, features)

    return StrategyRecommendation(
        strategy_type=strategy_type,
        strategy_params=strategy_params,
        reason=f'[LLM] {reason}',
        confidence='high',
    )

设计要点

  1. 轻量判定:只采样前 2000 字符,不传全文,降低 Token 消耗
  2. 约束选择范围:LLM 只能从 Top 3 候选中选择,不能凭空发明
  3. JSON 格式输出:便于解析,容错处理(正则提取 JSON)
  4. 失败回退:LLM 调用失败或解析失败时,回退到规则引擎第一候选

⚙️ 八、参数自动计算

推荐引擎不仅推荐策略类型,还会根据文档特征动态计算策略的内部参数。

@classmethod
def _compute_strategy_params(
    cls,
    strategy_type: ChunkStrategyType,
    f: DocumentStructureFeatures,
) -> dict:
    """根据文档特征为指定策略计算最优参数"""

    # --- Sentence 策略参数 ---
    if strategy_type == ChunkStrategyType.SENTENCE:
        avg_para = f.avg_paragraph_length
        if avg_para < 80:
            chunk_size = 256
        elif avg_para < 150:
            chunk_size = 384
        elif avg_para < 300:
            chunk_size = 512
        elif avg_para < 500:
            chunk_size = 768
        else:
            chunk_size = 1024
        # 极短文档用更小的 chunk_size
        if f.total_chars < 2000 and chunk_size > 256:
            chunk_size = 256
        return {'chunk_size': chunk_size, 'chunk_overlap': max(20, chunk_size // 10)}

    # --- Semantic 策略参数 ---
    if strategy_type == ChunkStrategyType.SEMANTIC:
        # 短段落多 → 降低阈值(更敏感地切分)
        if f.short_paragraph_ratio > 0.5:
            threshold = 98
        elif f.short_paragraph_ratio > 0.3:
            threshold = 95
        elif f.avg_paragraph_length > 200:
            threshold = 90
        else:
            threshold = 93
        return {
            'threshold': threshold,
            'buffer_size': 2 if f.avg_sentence_length > 30 else 1,
        }

    # --- Hierarchical 策略参数 ---
    if strategy_type == ChunkStrategyType.HIERARCHICAL:
        total = f.total_chars
        if total >= 500_000:
            return {'chunk_sizes': [4096, 2048, 1024, 512]}
        if total >= 200_000:
            return {'chunk_sizes': [2048, 1024, 512]}
        return {'chunk_sizes': [1024, 512, 256]}

    # --- Window 策略参数 ---
    if strategy_type == ChunkStrategyType.SENTENCE_WINDOW:
        # FAQ 密集或段落很短 → 更大的窗口
        if f.faq_pattern_ratio > 0.3 or f.avg_paragraph_length < 80:
            return {'window_size': 3}
        return {'window_size': 2}

    return {}

参数自适应逻辑

策略 参数 自适应逻辑
sentence chunk_size 段落越长 → chunk_size 越大(256~1024)
sentence chunk_overlap chunk_size 的 1/10,最小 20
semantic threshold 短段落越多 → 阈值越高(更敏感切分)
semantic buffer_size 句子越长 → buffer 越大(上下文窗口)
hierarchical chunk_sizes 文档越长 → 层级越多、每层越大
window window_size FAQ 密集或段落短 → 窗口更大(3 vs 2)

🔗 九、与切片策略工厂的集成

9.1 注册式工厂模式

# factory.py
class ChunkStrategyFactory:
    """切片策略工厂 - 注册式机制"""
    _strategies: Dict[ChunkStrategyType, Type[BaseChunkStrategy]] = {}

    @classmethod
    def register(cls, strategy_type: ChunkStrategyType, 
                 strategy_class: Type[BaseChunkStrategy]) -> None:
        cls._strategies[strategy_type] = strategy_class

    @classmethod
    def get_strategy(cls, strategy_type: ChunkStrategyType) -> BaseChunkStrategy:
        strategy_class = cls._strategies.get(strategy_type)
        if not strategy_class:
            raise ValueError(f"Unsupported chunk strategy: {strategy_type}")
        return strategy_class()

9.2 策略基类

# strategy.py
class BaseChunkStrategy(ABC):
    """切片策略抽象基类"""

    strategy_type: ClassVar["ChunkStrategyType"]

    @abstractmethod
    def get_parser(self, params: dict) -> NodeParser:
        """获取 LlamaIndex 的 NodeParser 实例"""
        pass

    def execute(self, documents: List[Document], params: dict) -> list:
        """执行切片并返回 Node 列表,自动注入 strategy_type 元数据"""
        parser = self.get_parser(params)
        nodes = parser.get_nodes_from_documents(documents)
        strategy_value = self.__class__.strategy_type.value
        for node in nodes:
            node.metadata["strategy_type"] = strategy_value
        return nodes

9.3 策略自注册示例(以 Window 策略为例)

# strategies/window.py
class SentenceWindowStrategy(BaseChunkStrategy):
    strategy_type: ClassVar[ChunkStrategyType] = ChunkStrategyType.SENTENCE_WINDOW

    def get_parser(self, params: dict):
        return SentenceWindowNodeParser(
            window_size=params.get("window_size", 2),
            sentence_splitter=chinese_sentence_splitter,
            window_metadata_key="window",
            original_text_metadata_key="original_text"
        )

    def execute(self, documents: List[Document], params: dict) -> list:
        nodes = super().execute(documents, params)
        window_size = params.get("window_size", 2)

        for node in nodes:
            node.metadata["window_size"] = window_size
            # 将 window 句子列表合并为可直接检索的文本
            window_sentences = node.metadata.get("window", [])
            if isinstance(window_sentences, list) and window_sentences:
                node.metadata["window_text"] = "".join(window_sentences)

        return nodes

# 自注册
ChunkStrategyFactory.register(ChunkStrategyType.SENTENCE_WINDOW, SentenceWindowStrategy)

9.4 在文档解析任务中的集成

# tasks/document_parse_task.py
from module_rag.rag_common.chunking.auto_strategy import AutoChunkStrategyRecommender

# 获取用户配置的策略
strategy_type, strategy_params = get_chunk_strategy_params(document.chunk_strategy)

# 自动模式:基于文档内容分析推荐切片策略
if strategy_type == ChunkStrategyType.AUTO:
    recommendation = AutoChunkStrategyRecommender.recommend(documents)
    logger.info(
        f'[AUTO_STRATEGY] doc_id={doc_id} 自动推荐: '
        f'{recommendation.strategy_type.value} | {recommendation.reason}'
    )
    strategy_type = recommendation.strategy_type
    strategy_params = recommendation.strategy_params

# 后续使用推荐的策略执行切片...

📋 十、统一元数据协议

所有策略切出的 Node 必须遵循统一元数据协议,确保下游(检索、展示、上下文增强)能统一处理:

@dataclass
class ChunkMetadataProtocol:
    """切片元数据统一协议"""

    # === 策略级元数据(由各切片策略自行注入)===
    strategy_type: str  # "sentence" / "window" / "hierarchical" / ...

    # === 关系元数据(由 ChunkPostProcessor 注入)===
    prev_chunk_id: Optional[str] = None
    next_chunk_id: Optional[str] = None
    parent_chunk_id: Optional[str] = None
    child_chunk_ids: List[str] = field(default_factory=list)

    # === 位置元数据 ===
    heading_path: Optional[str] = None    # "第一章 > 1.2 核心架构"
    heading_level: Optional[int] = None
    page_number: Optional[int] = None
    page_start: Optional[int] = None
    page_end: Optional[int] = None

    # === Window 专用 ===
    window_text: Optional[str] = None
    window_size: Optional[int] = None

    # === Hierarchical 专用 ===
    chunk_sizes: Optional[List[int]] = None
    has_children: bool = False

    # === 上下文增强元数据 ===
    chunk_summary: Optional[str] = None
    keywords: List[str] = field(default_factory=list)
    hypothetical_questions: List[str] = field(default_factory=list)

元数据注入责任分工

Strategy.execute()        → strategy_type + 策略专有字段
ChunkPostProcessor        → prev/next (所有策略) + heading_path (markdown)
MetadataEnrichment        → doc_id, kb_id, file_name 等文档级字段
ContextEnhancement        → chunk_summary, keywords 等增强字段

🚀 十一、完整使用示例

11.1 基础使用

from module_rag.rag_common.chunking.auto_strategy import AutoChunkStrategyRecommender

# 方式 1:传入 Markdown 文本
md_text = open("document.md", encoding="utf-8").read()
result = AutoChunkStrategyRecommender.recommend(md_text)

print(f"推荐策略: {result.strategy_type.value}")
print(f"推荐参数: {result.strategy_params}")
print(f"置信度: {result.confidence}")
print(f"原因: {result.reason}")

11.2 传入 Document 列表

from llama_index.core import Document
from module_rag.rag_common.chunking.auto_strategy import AutoChunkStrategyRecommender

documents = [
    Document(text="第一章 概述\n..."),
    Document(text="第二章 架构设计\n..."),
]

result = AutoChunkStrategyRecommender.recommend(documents)
print(f"推荐策略: {result.strategy_type.value}")

11.3 用户手动覆盖

from module_rag.rag_common.chunking.schemas import ChunkStrategyType

# 用户强制指定策略
result = AutoChunkStrategyRecommender.recommend(
    md_text,
    user_strategy=ChunkStrategyType.SEMANTIC
)
# 直接返回用户指定的策略,跳过自动分析

11.4 配合 ChunkingService 使用

from module_rag.rag_common.chunking.service import ChunkingService
from module_rag.rag_common.chunking.auto_strategy import AutoChunkStrategyRecommender

# 1. 自动推荐策略
result = AutoChunkStrategyRecommender.recommend(md_text)

# 2. 使用推荐的策略执行切片
response = ChunkingService.process_chunking(
    documents=documents,
    strategy_type=result.strategy_type,
    params=result.strategy_params,
)

print(f"切片数: {response.total_chunks}")
print(f"耗时: {response.cost_time_ms}ms")
for chunk in response.chunks:
    print(f"  [{chunk.node_id}] {chunk.text[:50]}...")

📊 十二、效果验证

12.1 各类文档的推荐结果

文档类型 文档特征 推荐策略 置信度
API 文档(多级标题) H1:3, H2:15, H3:28, 密度=0.12 markdown high
财报(超长+结构化) 120,000 字符, 表格占比 18% hierarchical high
FAQ 问答文档 FAQ 占比 25%, 短段落为主 window high
操作手册(无标题) 步骤占比 15%, 无标题层级 window medium
会议记录(长文连贯) 标题密度 0.02, 平均段落 180 字符 semantic medium
短词条说明 总字符 1,200 sentence medium

12.2 日志输出示例

[AUTO_STRATEGY] 候选 #1: markdown (置信度=high) | 文档具有 3 级标题结构(H1: 3, H2: 15, H3: 28),标题密度 0.12,使用 markdown 按标题层级切片
[AUTO_STRATEGY] 候选 #2: hierarchical (置信度=medium) | 表格密集(表格行占比 18.5%),使用 markdown 切片以保持结构化元素完整性
[AUTO_STRATEGY] 候选 #3: semantic (置信度=medium) | 文档语义连贯但无明显标题结构(标题密度 0.02,平均段落长度 180 字符),使用 semantic 按语义相似度切片
[AUTO_STRATEGY] LLM 判定结果: markdown | 文档具有清晰的多级标题结构,且表格占比较高,markdown 策略能同时保护标题层级和表格完整性

🎯 十三、设计亮点总结

13.1 规则引擎 + LLM 混合决策

  • 规则引擎保证确定性:不会因为 LLM 幻觉推荐出不存在的策略
  • LLM 提供灵活性:能在候选中根据语义做更精细的判断
  • 失败回退保证鲁棒性:LLM 挂了也不影响使用

13.2 参数自适应

不仅推荐策略类型,还根据文档特征动态计算策略参数:

  • 段落越长 → chunk_size 越大
  • 文档越长 → hierarchical 层级越多
  • FAQ 越密集 → window_size 越大

13.3 可扩展性

  • 新增规则只需添加一个 _rule_xxx 方法,注册到 _score_strategies 的规则列表中
  • 新增策略只需继承 BaseChunkStrategy 并调用 ChunkStrategyFactory.register()
  • 阈值集中管理,便于后续基于实际数据调优

13.4 统一元数据协议

所有策略切出的 Node 遵循统一的 ChunkMetadataProtocol,确保下游(检索、展示、上下文增强)能统一处理,不需要针对不同策略做特殊适配。


📦 十四、项目结构

module_rag/rag_common/chunking/
├── __init__.py              # 模块导出
├── schemas.py               # 数据模型(枚举、VO、元数据协议)
├── strategy.py              # 策略抽象基类 BaseChunkStrategy
├── factory.py               # 注册式策略工厂
├── auto_strategy.py         # 自动推荐引擎(本文核心)
├── service.py               # 切片服务入口
├── strategies/              # 具体策略实现
│   ├── __init__.py          # 触发所有策略自动注册
│   ├── sentence.py          # SentenceStrategy
│   ├── semantic.py          # SemanticStrategy(含超长文档保护)
│   ├── window.py            # SentenceWindowStrategy
│   ├── hierarchical.py      # HierarchicalStrategy
│   ├── markdown.py          # MarkdownStrategy
│   └── json_parser.py       # JsonStrategy
└── ...

📝 十五、总结

本文介绍了一种基于文档结构特征分析的切片策略自动推荐引擎,核心思路是:

  1. 特征提取:从 Markdown 文本中提取 6 大类结构特征(标题、段落、表格、列表、FAQ、步骤)
  2. 规则打分:7 条规则按优先级运行,输出 Top 3 候选策略
  3. LLM 决策:LLM 从 Top 3 中选择最优策略(失败回退到规则引擎)
  4. 参数自适应:根据文档特征动态计算策略内部参数
  5. 统一协议:所有策略遵循 ChunkMetadataProtocol,确保下游兼容

这种"规则 + LLM"的混合架构,既保证了决策的确定性,又具备一定的灵活性,能够适应各种类型的文档。


作者:RAG 工程实践

标签RAG LangChain LlamaIndex 文档切片 智能推荐 Python

参考:LlamaIndex 官方文档、Semantic Chunking 论文

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐