尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RAG数据解析全攻略:从txt清洗到Markdown结构化与分块策略

RAG数据解析全攻略:从txt清洗到Markdown结构化与分块策略 1. 先搞清楚RAG 解析为什么值得单开一篇攻略我见过太多团队把 RAG 搭起来之后检索效果差得离谱最后排查半天发现根因根本不是模型选得不好、向量库配置不对而是最前面的数据导入环节就出了问题——原始文档里面的文本是乱七八糟的有乱码、有没清洗掉的噪声、有大段大段没有结构的纯文本。你往向量库里塞进去的是垃圾检索出来的自然也是垃圾。数据解析这一步决定了整个 RAG 系统的上限。这个系列的第一篇我把最常用、也最容易被忽视的 txt 和 Markdown 两种格式放在一起讲。txt 是几乎所有文档系统都能导出的底线格式但它的缺点是纯文本没有结构标题、列表、表格全靠人工或工具二次识别Markdown 则自带轻量级结构标记是目前 RAG 知识库建设里性价比最高的中间格式之一。理解从 txt 到 Markdown 的通用文本与结构化解析方法你后续处理 PDF、Word、HTML 都会轻松很多因为它们的解析链路最终也常常收敛到转成带结构的文本这一步。这篇文章适合谁一种是正在搭 RAG 知识库、但检索效果一直不理想的开发者另一种是手里有大量 txt 文档想给这些文档建立知识库但不知道怎么下手的产品或运营同学。我会把解析链路拆开讲清楚并给出可以直接抄走的代码和工具组合。先说个大前提RAG 的解析不是把文件读出来那么简单它至少包含三层任务——第一层是字符级处理编码识别、乱码修复、噪声清理、统一换行符。第二层是结构级处理识别标题层级、段落边界、列表、表格、引用块把它们标记出来。第三层是语义级处理决定哪些内容该合并、哪些该拆分最终生成适合向量化的文本块。很多教程只讲了第三层切片导致前面两层出了问题后面怎么调参都救不回来。本篇的重点在前两层第三层会在涉及分块的章节里初步展开后续系列文章再深入。所以这篇攻略的核心价值就是帮你把 RAG 的进料口彻底打通。2. 文本读取最基础却最容易翻车的环节2.1 编码识别与乱码修复的实战经验txt 文件最折磨人的就是编码。UTF-8 是现在的绝对主流但国内存量文档里 GBK、GB2312、GB18030 仍然大量存在尤其是从老系统导出、从网上下载的电子书、从某些 Windows 软件里保存的文本文件。如果你直接用open(file.txt, encodingutf-8)去读一个 GBK 文件大概率会直接抛UnicodeDecodeError或者读出来一整屏的乱码。我常用的方案是先用chardet或charset-normalizer做编码探测再兜底用errorsreplace读入最后做一轮乱码特征检测。这里给出一个比较稳的读取函数import charset_normalizer def smart_read_text(file_path): # 1. 先读原始字节 raw open(file_path, rb).read() # 2. 用 charset_normalizer 探测编码 result charset_normalizer.from_bytes(raw).best() if result is None: # 探测失败尝试常见编码 for enc in [utf-8, gb18030, gbk, latin-1]: try: return raw.decode(enc) except UnicodeDecodeError: continue return raw.decode(utf-8, errorsreplace) return str(result)这里有几个细节值得展开latin-1是一个神奇的编码它永远不会报解码错误因为所有字节都被映射到 Unicode 对应码位。但它会把中文字符解成完全错误的内容。所以latin-1只能作为最后兜底不能作为正常路径。chardet和charset_normalizer我都用过实际体验是charset_normalizer对中文文本的探测准确率更高而且它返回的结果里有encoding、language等字段方便你做日志记录。但探测毕竟只是概率问题最稳妥的做法是在文件导出的源头就统一转成 UTF-8这个后面会讲。如果你在命令行环境里批量处理可以考虑用iconv做一次性转换# 把单个 GBK 文件转成 UTF-8 iconv -f GBK -t UTF-8 input.txt output.txt # 批量转换遍历当前目录所有 txt for f in *.txt; do iconv -f GBK -t UTF-8 $f utf8_$f; done提示iconv遇到无法映射的字符会直接报错并中断。批量处理时建议加上-c参数忽略无效字符但要注意这可能会丢数据所以转换前最好先备份原文件。2.2 超大型文件的流式读取策略RAG 知识库里常见的一种场景是一个 txt 文件几百 MB里面是多年的日志、聊天记录或者电子书全文。如果你用read()一次性读入内存瞬间爆掉或者卡死。这时必须用流式读取按行或者按固定块处理。Python 里最简单的写法是直接迭代文件对象with open(big.txt, r, encodingutf-8) as f: for line in f: # 处理每一行 process(line)这种写法的好处是 Python 内部会帮你做缓冲不会把整个文件读进内存。但如果你需要在行与行之间保持状态比如判断上一行是不是标题就得自己维护一个上下文变量。如果是超大型文件还有一个思路是分块读入后再按换行符切分。因为read(chunk_size)可能在半行处切断所以一般会保留一个buffer等找到完整换行符再处理def read_chunks(file_path, chunk_size8192): with open(file_path, r, encodingutf-8) as f: buffer while True: chunk f.read(chunk_size) if not chunk: if buffer: yield buffer break buffer chunk lines buffer.splitlines(keependsTrue) # 最后一行可能是残缺的保留到下一轮 buffer lines.pop() if lines else yield from lines这个函数的意义在于既不会把文件全部加载进内存又能保证每次拿到的都是完整的行。你在处理超大日志、超大电子书时可以直接套用。我个人还建议对大文件做先抽样、后全量的策略。比如先读前 2 万字符判断编码、观察噪声类型、统计标题密度然后再决定用什么解析流程。这样可以避免一次性投入大量计算资源后发现方向错了。3. 清洗与规范化入库前一定要做的脏活累活3.1 统一换行符与空白字符txt 文件的换行符有三种Linux 的\n、旧 Mac 的\r、Windows 的\r\n。如果你不做统一后面做正则匹配、行号对齐、Markdown 转换时都会出幺蛾子。比如\r单独出现时很多文本处理库不会把它当作换行结果你的段落全部黏在一起。第一步永远是统一换行符def normalize_newlines(text): # 顺序很重要先处理 \r\n再处理单独的 \r text text.replace(\r\n, \n) text text.replace(\r, \n) return text除了换行符还有一类东西容易忽略不可见字符。比如\u00a0不换行空格经常从网页复制过来、全角空格\u3000、零宽空格\u200b、BOM 头\ufeff。这些字符在屏幕上看不见但会污染文本切分和向量化结果。建议统一清洗import re def clean_invisible_chars(text): # 去掉 BOM 和零宽字符 text text.replace(\ufeff, ).replace(\u200b, ) # 不换行空格和全角空格归一为普通空格 text text.replace(\u00a0, ).replace(\u3000, ) # 将连续空白压成一个空格但保留换行 text re.sub(r[^\S\n], , text) return text.strip()你可能会问为什么要把空白压掉因为很多从 PDF 或网页转出来的 txt 里每个词中间可能夹着好几个空格这会影响后续的英文分词、中文断句和嵌入模型的效果。但这里有一个坑压空白的时候必须保留换行。正则[^\S\n]的意思是空白但排除换行这样段落结构不会被破坏。3.2 章节编号与标题模式的规整化txt 文档的标题识别是解析里的重头戏但直接靠行短就是标题这种启发式方法效果很差。根据我的经验要分几步走第一步先规整章节编号。中文文档里常见的章节编号模式有全角数字第一章、这种全角字符需要转半角。中文序号一、、一、1.1.1、第1章。西文序号Chapter 1、1. Introduction、A.。我一般会写一个专门函数把全角字符统一转半角这样后面用正则匹配标题时能少踩很多坑。def fullwidth_to_halfwidth(text): result [] for ch in text: code ord(ch) # 全角字符范围65281 ~ 65374 if 0xFF01 code 0xFF5E: result.append(chr(code - 0xFEE0)) elif ch \u3000: # 全角空格 result.append( ) else: result.append(ch) return .join(result)第二步才是真正识别标题。我这里分享一个判断函数它综合了行长度、行尾符号、序号模式和前后文特征def looks_like_heading(line, prev_line, next_line): line line.strip() if not line: return False if len(line) 30: # 标题通常不会太长 return False if line.endswith((。, , , )): # 标题一般不加句读 return False # 匹配常见序号模式 patterns [ r^第[一二三四五六七八九十百千0-9-][章节篇回部分], r^\d(\.\d)*[、.\s], r^[一二三四五六七八九十][、.], r^\(?[一二三四五六七八九十]\), r^[(][一二三四五六七八九十0-9][)], r^[A-Za-z0-9][\.、)]\s*, ] for p in patterns: if re.search(p, line): return True # 前后文启发上一行是空行且下一行是正常长段落 if (prev_line is None or not prev_line.strip()) and next_line and len(next_line.strip()) 30: return True return False这套启发式在大多数规范文档上表现不错但遇到标题是网络小说式短句如第一章 雨夜效果尚可遇到那种标题和正文混排、全靠加粗区分的文档就不行了。那种情况下建议直接转入 Markdown 源文件而不是从 txt 硬猜。提示这里介绍的标题识别只是入门方案。真实场景中我通常还会结合后续段落是否以句号结尾、标题行是否有独立空行包围等上下文特征做二次校验。别指望一个正则打天下解析逻辑要能接受人工校正结果的反哺。3.3 OCR 残留与常见噪声处理如果你的 txt 是从扫描 PDF 或图片 OCR 来的那清洗工作会更重。OCR 结果里常见噪声包括多余的换行OCR 引擎经常把一个段落内的行切成多条。识别错误字符比如中文识别成日文汉字、0和O混淆。页眉页脚残留文档页眉的标题、页码混进正文。表格内容错位列与列之间靠多个空格分隔但空格数量不稳定。针对 OCR 文本我的建议是优先做段落重排Rewrap再谈结构化。段落重排的核心思路是把连续的非空行拼接成一个逻辑段落除非某行明显是标题、列表项或表格行。一个简化的重排函数大致是这样def rewrap_paragraph(text): lines text.split(\n) paragraphs [] current [] for line in lines: line line.strip() if not line: if current: paragraphs.append(.join(current)) current [] continue if looks_like_heading(line, None, None): if current: paragraphs.append(.join(current)) current [] paragraphs.append(line) else: current.append(line) if current: paragraphs.append(.join(current)) return \n\n.join(paragraphs)这个函数做了一件事把散行的内容拼回完整段落同时保留标题作为独立段落。做完这步再来做结构识别准确率会显著提升。4. 分段策略为什么切片方式直接决定检索效果4.1 固定长度切分的问题很多人做 RAG 时第一个想到的分段方式就是这样def fixed_split(text, chunk_size500, overlap50): chunks [] for i in range(0, len(text), chunk_size - overlap): chunks.append(text[i:i chunk_size]) return chunks这套逻辑简洁但实战中问题特别明显。比如你的文本是第三章 系统架构 本章介绍系统的整体架构包括前端、后端和数据库三大部分。前端采用 React 框架后端采用 Spring Boot数据库使用 MySQL。如果固定 500 字切分很可能把第三章 系统架构这个标题和它下面的正文切到两个不同的块里。检索出来的是脱离了标题的正文碎片模型根本不知道这段内容讲的是哪个章节。切分线落在表格中间、列表中间也经常发生导致信息不完整甚至语义错乱。固定长度切分的本质问题是它完全无视文本的结构边界。结构是语义的骨架你要是把骨架切碎了肉自然挂不住。4.2 面向结构的切分让分块跟着语义走我目前在用的方案是先定结构再切内容。具体来说就是先把文本解析成带标记的结构化对象标题层级、段落、表格、列表然后以标题为锚点做分块如果一个大章节下的内容不长比如 800 字以内整个章节作为一个块。如果内容超过阈值再按段落边界切分并把所属的标题路径如第三章 3.2 数据库设计作为前缀附加到每个子块。表格无论多长尽量作为一个整体块不拆开。除非表格实在太大才考虑按行分组并且每组前加入表格标题和表头。举个例子我的分块输出对象会是这样的{ title_path: 第三章 系统架构 3.2 数据库设计, content: 数据库采用 MySQL 8.0主要表结构包括用户表、订单表……, metadata: { source_file: chapter3.txt, heading_level: 2, char_count: 620 } }title_path是我强烈推荐的一个字段。它把当前块在整个文档中的位置坐标记录了下来。检索到某个块时你可以把title_path和content一起拼进 Prompt让大模型明确知道这段内容的上下文归属。这个技巧对提升回答质量的效果非常明显。4.3 三种分块策略的对比与选型为了方便你直接选型我把三种常见策略的适用场景整理成了一张表策略实现成本检索精度适用场景主要缺点固定长度切分极低中低日志类、无结构短文本切断语义、标题与正文分离段落/标题感知切分中高书籍、教程、规范文档需要结构解析能力、实现稍复杂父子分块小块检索、大块生成高高知识密集、需要精确引用的文档存储量增大、检索延迟增加父子分块是目前 RAG 实战里公认效果较好的一种设计检索时用更小、更精确的块去匹配问题生成时把小块所属的大块通常是整个章节作为上下文喂给大模型。这种设计既照顾了检索的召回精度又保证了答案生成时上下文足够完整。父子分块的块内容由两套切片逻辑生成并用父子 ID 关联起来class ChunkNode: def __init__(self, chunk_id, content, meta): self.chunk_id chunk_id self.content content self.meta meta self.child_ids [] # 每个大块包含的小块 ID 列表如果你刚接触 RAG我建议先实现第二种标题感知切分它性价比最高等检索结果不能满足需求时再升级到父子分块。5. 从 txt 到 Markdown让文本长出结构5.1 为什么 Markdown 是知识库的黄金中间格式在做 RAG 数据导入时我一直坚持一个原则文本入库前最好先转成 Markdown而不是直接用纯文本。原因有三个第一Markdown 的结构标记是轻量级的人类可读大模型也能很好理解。#、##、-、|这些符号在向量化和 Prompt 拼接时占用的 token 很少却提供了宝贵的结构信息。相比 PDF 的复杂版面或 Word 的 XMLMarkdown 就是直给。第二Markdown 有成熟的生态。pandoc可以把 Markdown 转成 PDF、Word、HTML也可以反向转回来。你把文档统一成 Markdown 后后续无论做分块、做摘要、做问答都方便。第三Markdown 天然适合嵌入模型。很多开源嵌入模型在训练时就把 Markdown 文本作为重要语料所以结构标记对嵌入质量有一定正向作用。至少在实际对比中带#标题前缀的文本块检索命中率通常优于纯文本块。5.2 用 Pandoc 做批量转换的完整流程如果你手头有一批 txt 文件要转成 Markdown首选的工具就是 Pandoc。它免费、开源、跨平台处理常见格式的转换非常可靠。基本命令很简单pandoc input.txt -o output.md但直接这样转换你会发现一个问题Pandoc 默认会拿 txt 的空白行来推断段落但对标题的识别能力很弱因为 txt 里没有显式的标题标记。所以我在实际工作中会走一条分步走的路第一步先用上面第 3 节的方法做清洗和标题识别把标题行提前标记为 Markdown 标题def add_md_heading_markers(text): lines text.split(\n) result [] prev None for i, line in enumerate(lines): nxt lines[i 1] if i 1 len(lines) else None if looks_like_heading(line, prev, nxt): result.append(# line.strip()) else: result.append(line) prev line return \n.join(result)第二步再交给 Pandoc 做最终的段落整理和转义。这样转换出来的 Markdown 不仅标题层级清晰段落也不会乱断pandoc preprocessed.txt -o output.md如果你有 Word 版式比如 docx 里有标题 1样式作为原始素材那就更简单了可以一步到位pandoc input.docx -o output.mdPandoc 会依据 Word 的样式把标题层级转成对应的 Markdown 标题这是我最推荐的流程。所以工作流的设计可以这样能拿到 docx 就用 docx拿不到就 txt 手动预标记再转。5.3 自定义 Python 转换脚本的核心逻辑有些场景下你不想引入 Pandoc 这种重型依赖那就需要自己写转换脚本。纯 Python 实现从 txt 到 Markdown 的核心逻辑其实围绕三件事标题、列表、表格。标题的转换在前面的函数里已经实现了核心判断逻辑。列表的转换我一般用正则匹配行首符号模式比如1.、-、•、*等list_re re.compile(r^\s*([0-9][.)]|[-•*])\s) def txt_line_to_md(line): stripped line.strip() if list_re.match(stripped): # 统一转成 Markdown 无序/有序列表 if stripped[0].isdigit(): return stripped # 保持有序列表原格式 else: return - re.sub(r^[-•*]\s, , stripped, count1) return line表格的转换最麻烦因为 txt 里表格通常是多空格对齐的伪表格。我的建议是如果源 txt 里表格数量少且清晰列之间有明显分隔符用正则把连续多空格替换成|再补上分割行如果表格混乱建议不要自动转换宁可保留下原始文本也不要产生一个结构错乱的 Markdown 表格。错误的结构比没有结构更坑。转换脚本的整体骨架大概是def txt_to_markdown(text): lines text.split(\n) md_lines [] for i, line in enumerate(lines): prev_text lines[i - 1] if i 0 else next_text lines[i 1] if i 1 len(lines) else # 1. 标题识别 if looks_like_heading(line.strip(), prev_text.strip(), next_text.strip()): md_lines.append(# line.strip()) # 2. 列表识别 elif list_re.match(line): md_lines.append(txt_line_to_md(line)) # 3. 空行处理 elif not line.strip(): md_lines.append() else: md_lines.append(line) return \n.join(md_lines)这段代码不复杂但它能应对大部分文本型文档。当你后续遇到更复杂的逻辑比如表格、代码块、引用块在这三个分支上继续补充即可。6. 结构化解析实战标题层级、表格、列表的处理细节6.1 标题层级的正确推断与还原标题识别的下一步是给标题确定层级。txt 没有#符号所以层级只能靠同级标题的相似模式和标题之间的内容长度来推断。我的做法是先收集所有疑似标题的行。统计它们匹配的序号模式比如第X章通常是最高级X.Y.Z是次级。把序号模式的层级映射到 Markdown 的#数量。一个实用的映射规则是这样def infer_heading_level(heading_text): txt heading_text.strip() if re.match(r^第[一二三四五六七八九十百千0-9][章节篇回部分], txt): return 1 # 章级别 if re.match(r^\d\.\d\.\d, txt): return 3 if re.match(r^\d\.\d, txt): return 2 if re.match(r^\d[、.], txt): return 2 if re.match(r^[一二三四五六七八九十][、.], txt): return 2 if re.match(r^[(][一二三四五六七八九十0-9][)], txt): return 3 return 2 # 默认二级标题这套规则在绝大多数规范文档上是可用的但你要注意中文书籍里常见的结构是章 节 小节所以第X章映射为#或者##需要看你的整体设计。我通常会以第X章作为#X.Y作为##X.Y.Z作为###这样阅读和检索都清晰。注意层级推断的本质仍是猜测。如果你手头有原始书籍的目录TOC文件直接利用目录来规范标题层级准确率远高于从正文猜测。6.2 表格的智能识别与 Markdown 表格生成表格解析是所有结构化解析里最容易翻车的地方。txt 里的表格通常长这样城市 人口 面积 北京 2189万 16410平方公里 上海 2487万 6340平方公里列与列之间用多个空格分隔但每行的空格数量可能不同。我的转换思路是先统一分隔符再判断是否符合表格特征。第一步把连续空格折叠为单个制表符或|def spaces_to_pipe(line): # 将连续两个以上空格替换为 |并在两侧保留空格便于阅读 return re.sub(r\s{2,}, | , line.strip())第二步判断相邻两行是否都是管道分隔行。如果连续 3 行以上都能被分隔成相同的列数就认定为表格def is_table_block(lines, start, end): cols_count None for line in lines[start:end]: parts [p for p in line.split(|) if p.strip()] if cols_count is None: cols_count len(parts) elif len(parts) ! cols_count: return False return cols_count and cols_count 2第三步生成 Markdown 表格时要插入表头分隔行| 城市 | 人口 | 面积 | | --- | --- | --- | | 北京 | 2189万 | 16410平方公里 |这里的分隔行是 Markdown 表格的必需要素不补上渲染器会认为这不是表格。分割行的写法固定列数多就写多少个---。不过我得提醒一句当表格单元格内部也有连续空格、或者表格是从 PDF 转过来导致严重错位时这套方法就不太可靠了。此时最稳妥的办法是跳过自动转换把表格区域原样保留等人工介入修正或者在文档 source 里标注此处疑似表格待校对。把错乱的表格强行转成 Markdown检索时不仅不会加分反而会产出大量噪声。6.3 列表层级、引用块、代码块的保留列表在 Markdown 里分无序-、*和有序1.两种。从 txt 转 Markdown 的时候我一般做以下处理把-、*、•开头的行统一转成-前缀避免混用导致渲染不一致。把1.、1)、1开头的行统一转成1.前缀的有序列表。如果列表有缩进层级比如两个空格缩进的子项保留缩进Markdown 渲染器会自动识别嵌套层级。引用块是另一种常见结构原文里以开头的引言、评注、注释可以直接保留前缀def convert_blockquote(line): stripped line.strip() # 常见的引用开头模式 if re.match(r^[]\s*, stripped) or stripped.startswith(【): return re.sub(r^[]?\s*, , stripped) return line代码块的保留相对特殊。如果 txt 里有缩进的程序代码或命令行输出建议用 Markdown 代码块包裹def wrap_code_block(text_block): # 根据内容粗略判断语言 lang if re.search(r\b(def|import|class)\b, text_block[:200]): lang python elif re.search(r\b(function|var|const)\b, text_block[:200]): lang javascript return lang \n text_block.strip() \n我自己使用了更复杂的语言检测逻辑但最核心的仍然是用代码块包裹避免代码被误解析为正文或列表这一点。代码缩进在 Markdown 里的规则很敏感宁可包成代码块也不要裸放。6.4 保留 Markdown 的元数据头另一个值得养成的习惯是在转出来的 Markdown 文件顶部保留 YAML 元数据头。这样可以记录来源、日期、标签、原始文件名等信息后续在 RAG 的 metadata 字段里可以直接引用。--- title: RAG 数据导入与解析全攻略一 source: chapter3.txt author: 未知 date: 2024-06-01 tags: [RAG, txt, 解析] ---这个元数据头不参与向量化内容或者参与度极低但非常适合在检索后展示来源。很多人搭 RAG 知识库时没有做这个记录结果检索出来的答案根本不知道出自哪份文档这其实是个很可惜的疏漏。实现上非常简单写文件时在前面拼接一段字符串即可def write_md_with_meta(output_path, content, meta_dict): meta_lines [---] [f{k}: {v} for k, v in meta_dict.items()] [---, ] md_text \n.join(meta_lines) content with open(output_path, w, encodingutf-8) as f: f.write(md_text)7. 踩坑实录我在真实项目里遇到的解析问题与解法7.1 标题识别正确但层级错乱有一次我处理一批老电子书总体识别效果不错但出现了一个典型问题第一章被标为#但第一章下的1.1小节却也被标成了#。追溯原因是我在预标记阶段用了两套判断函数它们的返回结果没有统一等级映射。后来我把所有标题识别的出口收敛到一个infer_heading_level函数里问题就消失了。这个坑的本质是同一份文档里混用了多套标题风格。比如某些章节用第X章某些章节用Chapter X某些章节直接用一、。如果你的规则只覆盖了其中一部分漏掉的那些就会落入默认等级导致层级忽上忽下。解决方案是在批量转换前先抽样看一遍文档确认标题风格的数量再写全这些规则。7.2 表格数字被 Python 的 strip 弄丢另一个至今想起来都后怕的坑我在处理表格时用line.strip()去掉行首尾空格这本没有错。但有一个表格的单元格内容是 1897 这种带首尾空格的对齐文本strip之后的数字没有丢问题出在我用连续空格做分隔时把单元格内部的空格也当成了分隔符导致一列被拆成两列。解决方法是在转表格前先做行内结构分析用split()前先统计空格段长度如果某一处的空格数明显小于列间距的平均值就不应该作为分隔符。但这套启发式在真实文档里仍然不稳定。后来我的策略变得更加务实自动转换成 Markdown 表格后必须做一轮列数一致性校验列数不一致的行直接降级为纯文本保留宁可少转换不乱转换。7.3 Pandoc 转换时把全角标点转乱还有一次我自信满满地用 Pandoc 把一批 txt 转 Markdown结果打开发现中文引号变成了英文引号破折号变成了连字符。原因是 Pandoc 默认启用了某些智能标点扩展smart punctuation对英文文本是友好的但对中文文档反而画蛇添足。解决方法是关闭 smart 扩展pandoc input.txt -o output.md -f markdown-smart或者在转换前干脆把中文标点用占位符保护起来转完再还原。如果你是写 Python 脚本做转换也同样要注意正则替换不能误伤中文全角标点。一个实用的建议是转换脚本里所有针对英文语法的正则都只在 ASCII 范围内生效绝不直接对中文文本做全局替换。7.4 分块时把标题吞进上一块这个问题发生在固定长度切分之外的另一种情况我用段落感知切分时判断段落结束的规则是遇见空行或下一个标题。但有些文档的标题前面没有空行紧跟上一段末尾结果标题被吞进了上一块下一块内容就失去了标题上下文。我的修法很直接在分块之前先统一标题前插入空行人为制造出段落边界def ensure_blank_before_heading(text): lines text.split(\n) result [] prev_blank True for line in lines: if line.strip().startswith(#): if not prev_blank: result.append() result.append(line) prev_blank False else: result.append(line) prev_blank (line.strip() ) return \n.join(result)不要小看这个处理。结构解析和切分的很多异常源头都是边界线不干净。你花十分钟写这个函数后面能省好几个小时的排查时间。7.5 图片和附件路径的问题虽然 RAG 知识库本身以文本为主体但 Markdown 文档里经常有图片引用和附件链接。txt 转 Markdown 时图片链接![alt](path)里的相对路径很容易失效因为源文件的目录结构没有一起搬过来。我的实践是在 YAML 元数据头里记录source_dir和asset_dir扫描 Markdown 里的图片引用路径批量重写为新知识库目录下的相对路径。如果只是文本解析阶段路径问题可以向后放但一旦进入生产环境路径失效会导致文档加载失败进而影响整篇文本入库这个小细节值得提前处理。8. 实操过程中留下的几个经验数据解析这件事做得好的人看起来只是写了几行代码但背后的判断逻辑、兜底策略和对异常的容忍度才是真正拉开差距的地方。我个人的建议是先把文本清洗 → 标题识别 → Markdown 转换 → 面向结构的切分这条链路完整跑通哪怕每一步都用最简单的方案。跑通之后再逐步引入更复杂的表格识别、父子分块、元数据关联不要一上来就追求一步到位的完美解析器。解析器的进化应该跟着真实语料走你手上的文档长什么样你就优化哪一环。还有一点想特别强调任何自动解析方案都做不到 100% 准确所以在设计流程时一定要给人工修正留下入口。比如转换后的 Markdown 文件可以单独标注待校对状态检索日志里可以把置信度低的分块标记出来让后续可以针对性地优化。RAG 系统的质量不是一次解析决定的而是持续迭代出来的。这套链路跑完之后后续自然要面对的问题是如何把企业里的 PDF、Word、HTML 也归拢到同样的解析框架里如何处理扫描件和表格密集的文档这些内容我计划在系列的第二篇展开。但在那之前先把 txt 和 Markdown 这两块地基打牢后面的所有玩法都会顺畅很多。
返回列表