本教程系统性地讲解 RAG(检索增强生成)系统中文本数据预处理的三大环节——转换、清洗与切分——的概念、方法与工程实现。教程覆盖 Word、PDF、PPT、Excel、图片、网页等常见文档格式统一转换为 Markdown 的方法,推荐主流开源工具,分析转换后内容的结构重组与元数据追踪,介绍基于正则的定向清洗与三种切分策略,并通过自定义工具 text-process 与端到端工作流演示完整的落地路径。

前置教程

如想快速开始学习本教程,你可能需要先完成以下前置教程:

资源下载

1. 文本数据预处理在 RAG 中的价值

1.1 RAG 数据管道全景

RAG(Retrieval-Augmented Generation,检索增强生成) 的核心思路是:先从知识库中检索出与问题相关的资料,再交给大语言模型生成回答。知识库里的资料并不是原文直接入库,而是需要经过一条完整的预处理管道:

graph LR A[原始文档
docx/pptx/pdf/图片/网页] --> B[转换
转为 Markdown] B --> C[清洗
去除噪点] C --> D[重组
结构树与元数据] D --> E[切分
chunk] E --> F[Embedding
向量化] F --> G[向量数据库] H[用户查询] --> I[检索与重排序] G --> I I --> J[LLM 生成回答] style A fill:#fadbd8,stroke:#c0392b style B fill:#ffecd6,stroke:#d35400 style C fill:#fdebd0,stroke:#e67e22 style D fill:#d5f5e3,stroke:#1e8449 style E fill:#d5f5e3,stroke:#1e8449 style F fill:#d6eaf8,stroke:#2980b9 style G fill:#ebdef0,stroke:#6c3483 style H fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style I fill:#d6eaf8,stroke:#2980b9 style J fill:#ebdef0,stroke:#6c3483

本教程关注管道的前半段——转换、清洗、重组与切分,它们共同决定了进入向量库的资料质量,是后续 Embedding 与检索的地基。

1.2 转换、清洗、切分三大环节

转换 解决"读得懂"的问题。Word、PDF、PPT、Excel、图片、网页等格式各异,无法直接切分和向量化,需要统一转换为纯文本格式。

清洗 解决"干净不干净"的问题。转换结果中往往混入页眉页脚、超链接、采集平台标识、乱码字符等噪点,这些噪点会稀释语义,拉低检索质量。

切分 解决"拆得开"的问题。大模型和向量模型的输入长度有限,长文档必须切成多个小块(chunk),每个块独立向量化、独立召回。

环节 解决什么问题 典型输入 典型输出
转换 异构格式无法直接使用 docx/pptx/pdf/图片/网页 Markdown 文本
清洗 转换文本含噪点,稀释语义 原始 Markdown 干净文本
切分 长文本无法整体向量化与检索 清洗后的文本 若干可召回的小块

1.3 预处理决定 RAG 效果的上限

检索质量遵循"垃圾进、垃圾出"的规律。向量检索再强,也无法从被截断、被污染、边界错位的文本中找回正确内容。预处理环节常见的问题包括:

  • 结构丢失:PDF 转换后标题层级丢失,正文被拍扁成一段,导致后续无法按章节召回。
  • 噪点干扰:页眉页脚、网址、版权声明占据大量 token,污染向量语义。
  • 切分不当:块太大导致语义混杂、超出模型窗口;块太小导致上下文不完整;切分点恰好把一句话拦腰截断。

2. 文件转换为 Markdown 文本

2.1 为什么统一转换为 Markdown

不同格式的文档经过转换后,理想的目标格式是 Markdown,原因有三:

  • 结构可保留:Markdown 的标题(#)、列表(-)、表格(|)天然表达文档层级,为后续结构感知切分提供依据。
  • 通用可读:Markdown 是纯文本,任何环境都能打开,也方便人工校对。
  • 对模型友好:大语言模型与向量模型都基于文本训练,Markdown 是它们最熟悉的"半结构化"输入。

转换的目标不只是"变成文字",更是要在变成文字的同时尽量保留文档结构

2.2 转换方案选择

RAG 知识库中的文档格式五花八门——Word、PDF、PPT、Excel、图片、网页——每种格式都有其对应的本地解析库。但逐个安装、配置这些库成本较高,尤其是扫描件 OCR 对硬件有要求。

MinerU 提供了一个云端一站式文档解析 API:将文档上传到服务器,由 MinerU 自动识别格式、完成版面分析、提取表格和公式,最后返回结构化 Markdown。一条 API 覆盖所有格式,无需在本地安装任何转换库。

本教程示例项目 text-process 以 MinerU API 作为文档转换的引擎,将重心放在更关键的后续环节——清洗与切分。

2.3 使用 MinerU API 智能解析文档

MinerU 的文档解析流程分为三步:获取上传链接上传文件轮询获取结果。整个过程通过 RESTful API 完成,输出为结构化 Markdown。

# 使用 MinerU API 解析文档(需 API Token)
import httpx
import os
import time

MINERU_API_KEY = "your-api-key-here"   # 在 https://mineru.net/apiManage 获取
MINERU_API_BASE = "https://mineru.net/api/v4"

def convert_mineru(path: str) -> str:
    """上传文件到 MinerU 云端解析,返回 Markdown 文本"""
    headers = {"Authorization": f"Bearer {MINERU_API_KEY}"}
    name = os.path.basename(path)

    # 第一步:获取文件上传链接与 task_id
    resp = httpx.post(
        f"{MINERU_API_BASE}/file-urls/batch",
        headers=headers,
        json={
            "files": [{"file_name": name, "file_size": os.path.getsize(path),
                        "model_version": "MinerU-HTML"}],
        },
        timeout=30,
    )
    resp.raise_for_status()
    body = resp.json()["data"]
    upload_info = body["files"][0]
    task_id = body["task_ids"][0]

    # 第二步:上传文件(使用预签名 URL)
    with open(path, "rb") as f:
        httpx.put(upload_info["upload_url"], content=f, timeout=300)

    # 第三步:轮询等待解析结果(最多 2 分钟)
    for _ in range(60):
        result = httpx.get(
            f"{MINERU_API_BASE}/extract/task",
            headers=headers,
            params={"task_id": task_id},
            timeout=30,
        ).json()
        status = result["data"]["status"]
        if status == "done":
            return result["data"]["result"]["content"]
        if status == "failed":
            raise RuntimeError(result["data"].get("error_msg", "解析失败"))
        time.sleep(2)
    raise TimeoutError("MinerU 解析超时")

MinerU API 的特点包括:

  • 覆盖格式广:支持 PDF、Word、PPT、Excel、图片、HTML 等常见格式,无需逐个安装转换库。
  • 智能版面分析:自动识别多栏排版、表格、数学公式、图表,输出保留结构的 Markdown。
  • 三种解析模型pipeline(通用)、vlm(视觉语言模型,深度理解图表)、MinerU-HTML(保留 HTML 结构的 Markdown 输出)。
  • 批量处理/api/v4/extract/task/batch 接口支持一次提交多个文件。

MinerU API 是云端服务,需联网且有使用次数上限,但一般情况下不会超限。convert_mineru 函数的完整代码请查看 text-process/convert.py,API Key 请在 config.py 中配置。

2.4 混合内容与转换要点

MinerU 的核心优势之一在于处理混合内容。以下几种常见场景可借助 MinerU 的智能版面分析自动完成,无需人工干预:

  • 图文混排的文档:MinerU 自动识别图片区域,对图片中的文字执行 OCR,与正文拼接输出。
  • 表格与公式MinerU-HTML 模型会保留表格的 HTML 结构,数学公式以 LaTeX 或图片形式输出。
  • 多栏排版:学术论文常见的双栏布局,MinerU 自动按栏拆分,避免文字串乱。
  • 扫描件与文字版混合:同一份 PDF 中既有文字层页面又有扫描页,MinerU 自动识别并分别处理。

3. 转换后的内容重组

3.1 标题层级与文档结构树

转换产出的 Markdown 保留了 # 标题,但这些标题仍然是一行行平铺的文本。为了让"同一单元"的内容聚合在一起、可整体召回,需要把平面文本重组为文档结构树——按标题层级还原出"章节套小节"的树形关系:

graph LR A[文档根节点] --> B[第一章 概述] B --> C[1.1 背景] B --> D[1.2 意义] A --> E[第二章 方法] E --> F[2.1 数据采集] E --> G[2.2 模型训练] G --> H[2.2.1 训练数据] G --> I[2.2.2 评估指标] style A fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style B fill:#ffecd6,stroke:#d35400 style C fill:#d5f5e3,stroke:#1e8449 style D fill:#d5f5e3,stroke:#1e8449 style E fill:#ffecd6,stroke:#d35400 style F fill:#d5f5e3,stroke:#1e8449 style G fill:#d5f5e3,stroke:#1e8449 style H fill:#d6eaf8,stroke:#2980b9 style I fill:#d6eaf8,stroke:#2980b9

构建结构树的算法并不复杂:维护一个标题栈,遇到一级标题就弹出所有更深层标题、把该标题挂到根节点;遇到二级标题就挂到最近的一级标题下,依此类推。

3.2 内容重组:从平面文本到逻辑单元

结构树的价值在于把"一段连续文字"还原为"一组有归属的逻辑单元"。每个逻辑单元由三部分组成:

  • 标题路径:如 第一章 概述 > 1.1 背景,说明内容在文档中的位置。
  • 正文内容:该标题下到下一个同级或更深标题之前的全部文本。
  • 来源信息:原始文件名、页码等,保证引用可溯源。
# 内容重组:把 Markdown 标题还原为文档结构树
def build_structure(md_text: str) -> dict:
    """解析 Markdown 标题,构建文档结构树"""
    tree = {"title": "文档根节点", "children": []}
    stack = [tree]  # 根节点深度为 0
    for line in md_text.splitlines():
        m = re.match(r"^(#{1,6})\s+(.+)$", line)
        if not m:
            continue
        level = len(m.group(1))
        node = {"title": m.group(2).strip(), "children": []}
        while len(stack) > level:
            stack.pop()
        stack[-1]["children"].append(node)
        stack.append(node)
    return tree

重组后的结构树既可以用于生成目录导航,也可以直接指导后续的结构感知切分——按标题路径把文档切成互不重叠、语义完整的单元。

完整代码请查看 text-process/workflow.py

3.3 元数据与来源追踪

重组过程中为每个逻辑单元挂载的元数据是 RAG 召回的重要资产。至少应记录:

元数据字段 含义 用途
source 原始文件名 回答时可给出出处引用
heading 标题路径 定位内容在文档中的位置
chunk_index 块序号 还原文档顺序
chunk_type 切分方式 区分不同切分策略的结果

有了元数据,检索命中一个块时,系统能明确告诉用户"这条结论出自哪份文档的哪个章节",这在实际知识库产品中是刚需。

3.4 重组与可召回性

"可召回"意味着一个块被检索命中后,其内容本身就能回答问题,而不是零碎到无法理解。碎片化的块(如半句话、被截断的表格行)即使被召回,LLM 也无法给出正确答案。

结构重组对可召回性的贡献在于:

  • 语义完整:按标题聚合的内容天然围绕一个主题,被召回时上下文自洽。
  • 边界正确:切分点落在标题边界而非句子中间,避免把一句话拦腰截断。
  • 命中率提升:用户的问题往往指向某个具体章节,按章节聚合的块与查询的匹配度更高。

4. 文本数据清洗

4.1 常见噪点类型

清洗的目标是去除噪点、保留语义。不同来源的文档携带不同类型的噪点:

噪点类型 示例 主要来源
页眉页脚页码 "第 3 页"、"目录" PDF 转换
超链接与 URL https://example.com/xx 网页抓取
采集平台标识 "本文转自 XX 公众号" 转载采集
乱码与控制字符 不可见字符、异常符号 编码问题
多余空白 连续空行、行尾空格 转换过程
中文断行 段落中间被硬换行 PDF 双栏、网页换行

4.2 通用清洗规则

对于高频噪点,可以沉淀为内置通用规则,开箱即用。这些规则本质上是"正则表达式 + 替换文本"的集合:

# 内置清洗规则:(名称, 正则表达式, 替换文本)
BUILTIN_RULES = [
    ("去除孤立页码", r"(?m)^\s*\d{1,4}\s*$", ""),
    ("去除多余空行", r"\n{3,}", "\n\n"),
    ("去除行尾空白", r"[ \t]+\n", "\n"),
    ("去除多余空格", r" {2,}", " "),
    ("去除控制字符", r"[\x00-\x08\x0b\x0c\x0e-\x1f]", ""),
    ("去除零宽字符", "[\u200b\u200c\u200d\u2060\ufeff]", ""),
    ("合并中文断行", r"(?<=[一-鿿])\n(?=[一-鿿])", ""),
]

def clean_text(text: str, extra_rules=None) -> str:
    """执行全部清洗规则:内置规则 + 传入规则 + 配置文件自定义规则"""
    rules = BUILTIN_RULES + (extra_rules or []) + CUSTOM_CLEAN_RULES
    for _, pattern, replacement in rules:
        text = re.sub(pattern, replacement, text)
    return text.strip()

其中"合并中文断行"规则比较巧妙:只有当换行符两侧都是中文字符时才删除换行,避免破坏中英文混合文本的空格语义。

完整代码请查看 text-process/clean.py

4.3 自定义定向去除

知识库往往有自己特有的噪点——例如某份资料固定的页脚水印、某平台转载固定带的标识。这类噪点无法穷举,需要支持自定义定向去除。示例项目把自定义规则放在 config.py 中,每项规则由"名称、正则表达式、替换文本"三元组构成:

# config.py 中的自定义清洗规则
CUSTOM_CLEAN_RULES = [
    ("去除软件版本水印", r"v\d+\.\d+\.\d+", ""),
    ("去除采集平台标识", r"本文转自[一-鿿]+", ""),
]

需要定向去除 URL、邮箱时,可以直接传入正则:

# 定向去除 URL 与邮箱
clean_text(
    raw_text,
    extra_rules=[("去除URL", URL_PATTERN, ""),
                 ("去除邮箱", EMAIL_PATTERN, "")],
)

正则表达式是清洗能力的核心。正则元字符、贪婪与懒惰匹配等细节可参考前置教程 GREP概念详解与在RAG应用教程。设计正则时先在在线工具(如 regex101)或通过AI生成后进行验证,再接入管道,可大幅减少误删。

4.4 规则与语义的平衡

规则清洗精确、可控、可解释,但需要人工维护正则,面对千变万化的噪点总有漏网之鱼。生产实践中常采用"规则为主、模型兜底"的组合策略:

  • 第一层:正则规则批量清除明确的噪点(页码、URL、平台标识)。
  • 第二层:对规则无法判定的内容,用 LLM 批量判读——例如识别"这段话是否像广告或模板话术"。
  • 第三层:人工抽检,把新发现的噪点模式沉淀回规则库,形成正反馈。

5. 文本数据切分

5.1 切分的目标与评估标准

切分(Chunking) 是把长文本切成若干小块(chunk)的过程,每个块独立向量化、独立检索。切分质量直接决定召回质量,需要同时满足三个目标:

目标 说明 后果(未满足时)
块大小适中 单块长度适配 Embedding 模型与 LLM 窗口 过大超限、过小信息不足
语义完整 一个块尽量围绕一个主题 块内语义混杂,匹配困难
边界正确 不在句子、段落中间硬切 答案被截断,无法使用

评估切分效果最直接的方式是端到端测试:准备一批典型问题,统计 RAG 召回率与回答准确率;也可以观察块的平均长度、跨块打断句子的比例等指标。

5.2 机械式切分:固定长度 + 重叠

最基础的切分是机械式切分:按固定字符数从头到尾滑动切块。为了防止切在句子中间导致语义截断,引入重叠(overlap)机制——相邻两块之间共享一段文本:

graph LR A["块1
0 - 500"] --> B["重叠
400 - 500"] B --> C["块2
400 - 900"] C --> D["重叠
800 - 900"] D --> E["块3
800 - 1300"] style A fill:#d5f5e3,stroke:#1e8449 style B fill:#fdebd0,stroke:#e67e22 style C fill:#d5f5e3,stroke:#1e8449 style D fill:#fdebd0,stroke:#e67e22 style E fill:#d5f5e3,stroke:#1e8449

其他资料中也常把 overlap 写作"over_loop"或"滑动窗口重叠",含义相同,指相邻块之间共享一部分文本,确保跨边界的信息不会因为被切分而丢失。

实现上就是"滑动窗口":

def chunk_fixed(text: str, chunk_size: int = 500, overlap: int = 100) -> list[Chunk]:
    """机械式切分:按固定字符数切分,相邻块保留 overlap 重叠"""
    chunks = []
    start = 0
    step = max(chunk_size - overlap, 1)
    while start < len(text):
        end = min(start + chunk_size, len(text))
        chunks.append(Chunk(text[start:end],
                            {"chunk_type": "fixed", "start": start, "end": end}))
        if end >= len(text):
            break
        start += step
    return chunks

机械式切分实现简单、速度最快、零成本,但不理解语义——它可能在最不合适的位置(句子中间、主题中间)硬切。适合对切分要求不高的通用长文本。

5.3 结构感知切分:按标题层级

如果文档是 Markdown 且标题层级清晰,结构感知切分是更优选择:直接按标题边界切分,天然满足"语义完整 + 边界正确",还附带标题路径作为元数据:

def chunk_by_heading(md_text: str) -> list[Chunk]:
    """结构感知切分:按 Markdown 标题层级划分逻辑单元"""
    matches = list(HEADING_RE.finditer(md_text))
    if not matches:
        return chunk_fixed(md_text)
    chunks = []
    stack = []  # 维护当前标题路径
    for i, m in enumerate(matches):
        level = len(m.group(1))
        title = m.group(2).strip()
        while stack and stack[-1][0] >= level:
            stack.pop()
        stack.append((level, title))
        content_end = matches[i + 1].start() if i + 1 < len(matches) else len(md_text)
        content = md_text[m.start():content_end].strip()
        if content:
            chunks.append(Chunk(content,
                                {"chunk_type": "heading",
                                 "heading": " > ".join(t for _, t in stack)}))
    return chunks

结构感知切分与第 3 章的内容重组共享同一套标题栈算法,两者天然衔接:先重组出结构树,再按树节点切块。该方式速度快、零成本,但对"标题缺失、结构混乱"的文档不适用,此时会退化为机械式切分。

5.4 语义理解切分:Embedding 相似度

当文档主题跳跃、结构不清晰(如访谈记录、会议纪要、聊天记录)时,标题感知切分无能为力,需要语义理解模型判断在哪里切。核心思路是:把句子向量化,计算相邻句子的语义相似度,相似度骤降的地方就是主题切换的边界:

graph LR A["句子1"] --> B["句子2"] B --> C["句子3"] C --> D["句子4"] D --> E["句子5"] C -. "相似度 0.82 高
同一主题" .-> C D -. "相似度 0.18 低
主题切换,在此切分" .-> D style A fill:#d6eaf8,stroke:#2980b9 style B fill:#d6eaf8,stroke:#2980b9 style C fill:#d5f5e3,stroke:#1e8449 style D fill:#fadbd8,stroke:#c0392b style E fill:#fadbd8,stroke:#c0392b

实现包含三个关键步骤:

  1. 句子切分:把文本按句号、问号、感叹号等结束标点拆成句子。
  2. 向量化:用本地 Embedding 模型(sentence-transformers)为每个句子生成向量。
  3. 边界判定:计算相邻句子向量的余弦相似度,相似度低于阈值 threshold 且当前块已有一定长度时切块。
def chunk_semantic(text: str, threshold: float = 0.5,
                   chunk_size: int = 500) -> list[Chunk]:
    """语义理解切分:计算相邻句子的向量相似度,在主题边界处切分"""
    sentences = split_sentences(text)
    if len(sentences) <= 1:
        return [Chunk(text, {"chunk_type": "semantic"})]

    embeddings = _get_embedding(sentences)
    sims = [_cosine_similarity(embeddings[i], embeddings[i + 1])
            for i in range(len(sentences) - 1)]

    chunks = []
    buffer = sentences[0]
    for i in range(1, len(sentences)):
        similarity = sims[i - 1]
        boundary = similarity < threshold and len(buffer) >= chunk_size * 0.5
        if boundary:
            chunks.append(Chunk(buffer,
                                {"chunk_type": "semantic",
                                 "boundary_score": round(similarity, 3)}))
            buffer = sentences[i]
        else:
            buffer += sentences[i]
    if buffer:
        chunks.append(Chunk(buffer, {"chunk_type": "semantic"}))
    return chunks

语义切分质量最高,但速度慢——每句话都要过一遍本地模型,首次加载模型还需数秒到数十秒(之后缓存在内存中复用)。threshold 是核心调参项:阈值越低,越少切分、块越大;阈值越高,越敏感切分、块越小。另外 len(buffer) >= chunk_size * 0.5 的保护条件避免切出过小的碎片块。

完整代码请查看 text-process/chunk.py

5.5 三种切分方式对比

维度 机械式(fixed) 结构感知(heading) 语义理解(semantic)
实现复杂度
速度 最快 慢(需本地推理向量)
语义完整度
边界质量 差(可能切在句子中间) 好(标题边界) 好(主题边界)
额外成本 本地模型加载与算力
适用场景 通用长文本 结构化 Markdown 文档 主题跳跃的对话、纪要

生产实践中常组合使用:先按标题结构切分,对过大的章节内部再用机械式或语义方式二次细分。

6. 自定义工具实现(text-process)

前面章节分别介绍了转换、重组、清洗、切分的方法,本节把它们组装成一个完整的自定义工具 text-process,并用命令行串联成工作流。

6.1 项目结构

示例项目直接放在本教程同级目录下,结构如下:

text-process/
├── config.py          # 全部配置:目录、切分参数、清洗规则、本地 Embedding 模型路径
├── convert.py         # 转换模块:使用 MinerU API 将文档解析为 Markdown
├── clean.py           # 清洗模块:内置通用规则 + 自定义定向去除
├── chunk.py           # 切分模块:fixed / heading / semantic 三种策略
├── workflow.py        # 端到端工作流:转换→清洗→重组→切分→JSONL
├── cli.py             # 命令行入口:convert / clean / chunk / pipeline
├── requirements.txt   # 依赖内容
└── README.md          # 使用说明

6.2 环境准备

示例项目使用 conda 创建 Python 3.10 虚拟环境,依赖统一用 uv 安装:

# 创建并激活虚拟环境
conda create -n text-process python=3.10
conda activate text-process

# 安装依赖
uv pip install -r requirements.txt

6.3 转换模块

convert.py 的核心函数是 convert_mineru,通过 MinerU 云端 API 将各类文档统一解析为 Markdown(见第 2.3 节)。该模块不依赖任何本地转换库,只需在 config.py 中配置 MinerU 的 API Key:

# config.py 中的 MinerU 配置
MINERU_API_KEY = "your-mineru-api-key-here"  # 在 https://mineru.net/apiManage 获取
MINERU_API_BASE = "https://mineru.net/api/v4"
MINERU_MODEL = "MinerU-HTML"

6.4 清洗模块

clean.py 实现内置通用规则与自定义定向去除,规则在 config.pyCUSTOM_CLEAN_RULES 中扩展,无需改代码即可加入新噪点规则。第 4 章的代码片段即出自该模块。

6.5 切分模块

chunk.py 实现三种切分策略,统一返回 Chunk 对象(文本 + 元数据),方便上层工作流统一处理。第 5 章的代码片段即出自该模块。语义切分使用本地 Embedding 模型,只需配置模型路径:

# config.py 中的语义切分 Embedding 模型配置
EMBEDDING_MODEL_PATH = r"C:\models\bge-small-zh-v1.5"  # 本地路径,或填 HuggingFace 模型名自动下载

6.6 命令行工作流

workflow.py 把各环节串成端到端管道:转换 → 清洗 → 重组 → 切分 → 输出 JSONLcli.py 用子命令(subparsers)组织操作,支持分步执行与一键执行:

# 一键执行完整工作流(转换 + 清洗 + 切分)
python cli.py pipeline --method heading

# 分步执行
python cli.py convert                      # 转换 docs/ 下所有文档
python cli.py clean                        # 清洗转换结果
python cli.py chunk --method fixed         # 机械式切分
python cli.py chunk --method semantic --threshold 0.5   # 语义切分

切分结果输出到 output/chunks/chunks.jsonl,每行一个 JSON 块,同时携带元数据供检索使用:

{"source": "产品手册.md", "chunk_index": 3, "text": "...", "metadata": {"chunk_type": "heading", "heading": "第3章 使用说明 > 3.2 常见问题"}}

完整代码请查看 text-process/workflow.pytext-process/cli.py

7. 生产实践建议

7.1 工具选型建议

场景 推荐方案
快速原型验证、多格式文档 MinerU API(云端一站式解析)
离线环境、大批量处理 MarkItDown / pymupdf4llm / Pandoc 本地库组合
自定义全格式管道 参考本教程 text-process,以 MinerU 为转换引擎

7.2 切分参数调优

  • 块大小:中文文本建议 300-800 字符(约 200-500 token),并低于 Embedding 模型的最大输入长度。
  • 重叠大小:约为块大小的 10%-20%,在"防截断"与"去冗余"之间权衡。
  • 语义阈值:先跑一批样本观察相邻句子相似度分布,再在 0.4-0.6 区间内微调 threshold
  • 评估闭环:每次调整后用一组固定测试问题跑端到端召回,用数据而非感觉决策。

7.3 常见问题排查

现象 可能原因 处理建议
召回内容答非所问 块边界切在主题中间 改用 heading 或 semantic 切分
块超出模型窗口 块大小设置过大 调小 CHUNK_SIZE
大量重复片段 重叠过大 降低 overlap
中文乱码 文件编码非 UTF-8 转换前统一转码
扫描件转换为空 缺少 OCR 环节 接入 RapidOCR
语义切分报错 ModuleNotFoundError 未安装 sentence-transformers 执行 uv pip install -r requirements.txt
语义切分报错模型加载失败 EMBEDDING_MODEL_PATH 路径无效 检查 config.py 中模型路径是否正确,或改用 HuggingFace 模型名

8. 总结

8.1 核心内容回顾

  • 转换 是 RAG 数据管道的地基,目标是把 docx/pptx/pdf/图片/网页等异构格式统一转换为保留结构的 Markdown。本教程以 MinerU API 为转换引擎,一站式处理所有格式;离线场景可选用 MarkItDown、pymupdf4llm 等本地库。
  • 混合内容 通过统一分派器按扩展名路由到对应转换函数,实现多格式一站式处理。
  • 内容重组 用标题栈算法把平面文本还原为文档结构树,为每个逻辑单元挂载标题路径与来源元数据,提升可召回性。
  • 清洗 通过"内置通用规则 + 自定义定向去除"两级正则引擎去除噪点,规则精确可控,必要时用 LLM 兜底。
  • 切分 有三种策略:机械式(固定长度 + overlap)最简单通用,结构感知(按标题)语义完整,语义理解(Embedding 相似度)适合主题跳跃文本,三者可组合使用。
  • 工作流text-process 项目完整实现,通过 cli.py pipeline 一键完成转换、清洗、重组、切分与 JSONL 输出。

8.2 常见问题与解答

问:转换后必须用 Markdown 格式吗?

答:不一定,但强烈推荐。Markdown 保留标题、列表、表格等结构信息,是后续结构感知切分的必要前提;纯文本会丢失层级,语义切分只能"盲切"。若工具只输出纯文本,可考虑先用规则把缩进、编号还原为标题。

问:机械式切分的重叠值应该设多大?

答:一般取块大小的 10%-20%。重叠太小起不到防截断作用,重叠太大则相邻块大量重复,既浪费 token 又稀释检索精度。可以先取块大小的 20% 起步,观察召回效果再调整。

问:语义切分比机械式切分一定更好吗?

答:不一定。语义切分在主题跳跃的文本(访谈、纪要)上优势明显,但对结构清晰的文档反而"过度设计"——既慢(每句都要过本地模型)又要额外加载模型,效果未必比按标题切分好。建议优先结构感知切分,仅在不适用时引入语义切分。

问:自定义清洗规则会不会误删正文?

答:会。正则无法理解上下文,设计不当会把正常内容一并删除。降低误删的三个办法:尽量用精确匹配而非宽泛匹配;规则先在样本集上验证再上线;把"可疑删除"改为"标记待人工确认"而不是直接删除。

问:清洗与切分应该先做哪个?

答:先清洗、后切分。清洗会把文本变短、合并断行,改变字符位置;若先切分再清洗,块边界会因清洗而错位,部分噪点还会残留进块中。正确顺序是先转换、再清洗、最后切分。