本教程系统性地讲解 RAG(检索增强生成)系统中文本数据预处理的三大环节——转换、清洗与切分——的概念、方法与工程实现。教程覆盖 Word、PDF、PPT、Excel、图片、网页等常见文档格式统一转换为 Markdown 的方法,推荐主流开源工具,分析转换后内容的结构重组与元数据追踪,介绍基于正则的定向清洗与三种切分策略,并通过自定义工具 text-process 与端到端工作流演示完整的落地路径。
前置教程
如想快速开始学习本教程,你可能需要先完成以下前置教程:
- Embedding、Rerank与向量数据库概念详解与应用教程,本教程 语义切分直接使用 Embedding 模型计算相邻句子的相似度来判断主题边界,切分后的文本块最终也会向量化存入向量数据库用于召回。
- GREP概念详解与在RAG应用教程,正则表达式是文本清洗与自定义定向去除的核心工具,本教程的清洗模块大量使用正则。
- Python的安装与简单的代码示例,本教程的示例项目使用 Python 编写。
- Python环境管理详解与UV的安装与使用教程,使用 uv 创建虚拟环境并安装示例项目的依赖。
资源下载
1. 文本数据预处理在 RAG 中的价值
1.1 RAG 数据管道全景
RAG(Retrieval-Augmented Generation,检索增强生成) 的核心思路是:先从知识库中检索出与问题相关的资料,再交给大语言模型生成回答。知识库里的资料并不是原文直接入库,而是需要经过一条完整的预处理管道:
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 保留了 # 标题,但这些标题仍然是一行行平铺的文本。为了让"同一单元"的内容聚合在一起、可整体召回,需要把平面文本重组为文档结构树——按标题层级还原出"章节套小节"的树形关系:
构建结构树的算法并不复杂:维护一个标题栈,遇到一级标题就弹出所有更深层标题、把该标题挂到根节点;遇到二级标题就挂到最近的一级标题下,依此类推。
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)机制——相邻两块之间共享一段文本:
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 相似度
当文档主题跳跃、结构不清晰(如访谈记录、会议纪要、聊天记录)时,标题感知切分无能为力,需要语义理解模型判断在哪里切。核心思路是:把句子向量化,计算相邻句子的语义相似度,相似度骤降的地方就是主题切换的边界:
同一主题" .-> 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
实现包含三个关键步骤:
- 句子切分:把文本按句号、问号、感叹号等结束标点拆成句子。
- 向量化:用本地 Embedding 模型(sentence-transformers)为每个句子生成向量。
- 边界判定:计算相邻句子向量的余弦相似度,相似度低于阈值
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.py 的 CUSTOM_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 把各环节串成端到端管道:转换 → 清洗 → 重组 → 切分 → 输出 JSONL。cli.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.py 与 text-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% 起步,观察召回效果再调整。
问:语义切分比机械式切分一定更好吗?
答:不一定。语义切分在主题跳跃的文本(访谈、纪要)上优势明显,但对结构清晰的文档反而"过度设计"——既慢(每句都要过本地模型)又要额外加载模型,效果未必比按标题切分好。建议优先结构感知切分,仅在不适用时引入语义切分。
问:自定义清洗规则会不会误删正文?
答:会。正则无法理解上下文,设计不当会把正常内容一并删除。降低误删的三个办法:尽量用精确匹配而非宽泛匹配;规则先在样本集上验证再上线;把"可疑删除"改为"标记待人工确认"而不是直接删除。
问:清洗与切分应该先做哪个?
答:先清洗、后切分。清洗会把文本变短、合并断行,改变字符位置;若先切分再清洗,块边界会因清洗而错位,部分噪点还会残留进块中。正确顺序是先转换、再清洗、最后切分。
举手提问