
做RAG项目最容易被低估的环节其实是数据导入。我见过不少团队把精力全扑在向量模型选型和提示词调优上结果知识库里的文档压根没洗干净检索出来的内容七零八落——问一个连续的问题模型拿到的却是被硬生生切碎的半句话这锅真不能让召回算法背。这个系列我打算从最基础的文本类型开始写第一篇先聚焦大多数知识库最常见的原料txt文件以及如何把它处理成对RAG最友好的Markdown结构。标题里说通用文本指的是那些没有固定排版约束的纯文本文档——笔记导出、网页另存、电子书复制、OCR识别结果都属于这一类。这类文本最大的问题不是内容复杂而是没有结构而RAG的检索质量恰恰建立在结构之上。这篇会把我的完整处理链路拆开讲从编码识别、噪声清理到转Markdown的方法选型再到结构化解对检索效果的量化影响。适合正在搭本地知识库、用RAG做问答但效果不理想的人参考。1. 为什么RAG的第一步是文本脏活解析优先级判断1.1 检索效果的上限在导入阶段就定了很多人对RAG有个误区认为向量化之后一切就都靠embedding模型了。实际上embedding模型只能对传入它的一段文本做语义编码它管不了这段文本本身是否完整、是否有语义边界、是否包含干扰信息。如果你把一个章节拆成了三段不连贯的碎片embedding再强也没办法还原上下文。我在实际项目里做过一次对比实验同一份产品手册第一种方式直接把txt按固定字符数切成512字符的块第二种方式先解析成Markdown、再按标题层级切块。在完全相同的embedding模型和检索参数下第二组的top-5命中准确率提升了大约三成。这充分说明解析阶段才是决定RAG质量天花板的地方。这也是为什么我把数据导入和解析单独做成一个系列——它不是工程里的边角料而是整套RAG系统的地基。1.2 通用文本的真实来源和它们各自的问题说通用文本到项目里实际会碰到的是这几类笔记软件导出的纯文本通常有规律的分隔线或缩进但缺少明确的层级标记网页抓取后去标签的正文段落基本完整但可能残留导航文字、广告位文本电子书章节复制出来的内容经常有目录页、页眉页脚、页码混在里面OCR跑出来的文字断行位置随机标点符号丢失偶尔有错别字这些文本的共同特点是人类读起来没问题模型切起来全是问题。因为RAG处理文本需要的是机器可识别的结构而txt恰恰只保留了线性字符流。1.3 结构化的目标层级先别急着上知识图谱提到结构化有人马上想到实体关系抽取、知识图谱构建这个方向在RAG里确实有应用场景但对大多数中小项目来说性能开销和工程复杂度都太高了。我的建议是分三个层级来看结构化层级工作内容对RAG的实际价值基础层段落边界、标题层级、列表结构决定分块是否完整直接影响检索命中率增强层表格识别、代码块、引用块避免跨格式的语义污染改善回答精度高级层实体链接、知识图谱三元组适合对可解释性和多跳推理有要求的场景对于通用文本先做到基础层和增强层就够了。把txt清洗成Markdown本质就是完成这两个层级的解析。别一上来就追求高级层否则很容易陷入做了三个月知识图谱问答效果还不如简单切块的尴尬。2. txt文件的入场体检编码、换行与噪声清理2.1 编码识别UTF-8是常态GBK是常态的坑先从最不起眼但最容易翻车的地方说起编码。一个txt文件如果编码识别错误后面所有环节都会输出乱码而且这个乱码会被直接向量化。我推荐用charset-normalizer这个库做编码识别它在中文场景下比chardet更准。示例代码import charset_normalizer def detect_encoding(file_path): with open(file_path, rb) as f: raw f.read(10000) result charset_normalizer.from_bytes(raw).best() return result.encoding if result else utf-8读取前先采样前10KB足够判断大多数情况。注意一个隐蔽问题带BOM的UTF-8文件。如果识别到utf-8-sig建议在转存时统一去掉BOM否则后续按行处理时第一行会带着不可见字符影响标题识别。2.2 换行符与段落边界CRLF、LF、空行的三种处理策略txt文件的换行符五花八门。Windows记事本导出的是\r\nmacOS和Linux下保存的多是\n还有个别文本会用单独的\r做分隔。处理时统一归一化到\ncontent content.replace(\r\n, \n).replace(\r, \n)真正影响RAG分块的是空行边界的处理。空行在txt里通常代表段落分隔但并不是所有空行都该保留。我的经验是分两种情况正文段落之间的空行保留后续转Markdown时它就是段落的天然分隔符连续两个以上空行压缩成单个空行避免产生大量空白块进入向量库另外还要留意短行问题。OCR出来的文本经常一行就是一个小短句然后用换行符硬拆开。遇到这种文件建议先做行合并如果某一行不以句号、问号、感叹号、冒号结尾且下一行长度超过20个字符就把两行拼接在一起。2.3 噪声清理目录页、页眉页脚、连续重复段的清洗清单我总结了一个清洗清单按优先级从高到低目录页识别如果文件前20行里连续出现第X章、.....、纯数字页码这类模式说明是目录页。规则是第X章/节配合点号或页码命中两个以上特征就整体截掉直到出现第一次正文特征比如超过50字的连续段落。页眉页脚电子书导出的文本里经常有书名章节名反复出现的模式。如果同一行文本在文件里出现超过3次且间隔均匀基本可以判定是页眉全局删除。页码行独立成行且内容只有数字或第X页的直接删除。连续重复段落某些抓取工具会重复拼接正文检测方法是取每段前30个字符做哈希如果同一哈希值出现在多个位置且完全重复的行超过50行就要怀疑是重复内容。实际操作中我不建议写一套万能清洗规则因为不同来源的txt噪声特征差异太大。更靠谱的方案是先抽样3到5个文件人工看一下噪声类型然后针对性地写清洗规则最后批量跑。RAG导一次数据往往需要反复迭代清洗规则也应当跟着数据源的变化持续维护。3. 从纯文本到Markdown的三条路径与方法选型3.1 规则转换稳定、可解释、成本最低把txt转成Markdown最直接的方式是写规则。核心逻辑就一句话通过模式匹配识别标题、列表、表格等元素然后打上对应的Markdown标记。一份典型的转换规则全数字编号行如1、2.1、3.2.4配合短文本识别为标题以-或数字加.开头的行识别为无序/有序列表以制表符或空格对齐的列且连续多行模式一致识别为表格有明确第X章、Chapter X等关键词的行强制设为一二级标题下面是我经常用的一个最小实现思路def txt_to_markdown(content): lines content.split(\n) md_lines [] for line in lines: stripped line.strip() # 标题识别形如2.1 背景介绍 if re.match(r^\d(\.\d)*\s\S, stripped): level stripped.split( )[0].count(.) 2 md_lines.append(f{# * level} {stripped.split( , 1)[1]}) # 无序列表 elif re.match(r^[-*•]\s, stripped): md_lines.append(f- {re.sub(r^[-*•]\s, , stripped)}) # 有序列表 elif re.match(r^\d[.、]\s, stripped): md_lines.append(f{stripped}) # 普通段落 elif stripped: md_lines.append(stripped) else: md_lines.append() return \n.join(md_lines)这段代码只是一个骨架真实场景里要处理的是大量边缘情况比如2.1后面没有空格、标题行本身就是加粗文本、列表项跨行等。但它演示了一个关键思想规则转换的每一步都可解释、可调试、可针对失败样例补充规则。3.2 LLM辅助解析适合复杂文本但要控制成本和幻觉如果txt文本本身没有规律——比如章节编号混乱、段落边界模糊、甚至混用了多种排版风格——规则转换会变成一个无底洞。这时候可以考虑用LLM做结构化抽取。做法是构造一个解析Prompt让模型输出Markdown格式的正文你是一个文档结构化引擎。请把下面输入的正文重写为Markdown格式 1. 识别标题层级使用#、##、###标记 2. 保留段落缩进与列表结构 3. 表格数据使用Markdown表格语法 4. 不要做任何内容改写、摘要或翻译 输入内容如下 {chunk}注意这里的措辞是重写为而不是提取目的是让模型保持原文措辞减少信息损耗。我测过几个模型在结构化抽取任务上把输入切成2000字以内的小段效果最稳超过3000字很容易出现标题丢失或列表结构错乱。使用LLM辅助解析的最大风险是幻觉——模型可能生成原文里没有的章节标题或者把普通段落脑补成列表。对策是在解析结果里做对比校验用difflib比较原始文本和解析后文本的字符重叠率。如果重写后的内容被大段替换说明模型做了改写需要重新解析。3.3 工具链参考pandoc与textutil的适用边界除了自己写代码有些现成工具可以省不少力pandoc能把多种格式转成Markdown支持自定义模板。但它默认处理的是有结构的格式docx、html、epub等对无结构的txt无能为力。它的用武之地在二次精修——把已经带基础Markdown标记的文本统一规范化。textutilmacOS自带可以把txt转成html再通过html间接获得一些段落结构。多一步转换结果可控性一般适合linux/mac环境下快速批量处理。VS Code / Sublime Text插件Markdown All in One、Markdown Preview Enhanced这类插件主要用于预览和编辑不适合批处理但在抽样检查转换效果时非常好用——直接在编辑器里看标题树是否正常。工具选型的原则规则能解决的不用LLMLLM能解决的不用人工。pandoc这类通用工具适合做格式转换不适合做内容清洗。4. Markdown结构化解解析后的信息骨架如何决定检索质量4.1 标题层级树分块切分的天然锚点Markdown对RAG最大的价值是#标题提供了明确的层级信息。这意味着分块不再依赖固定字符数硬切而是可以以标题为单位把相邻内容聚合到同一块里。我常用的分块策略是标题树聚合解析Markdown提取标题间的包含关系从某个二级标题开始把下属内容组合成一个候选块如果候选块超过分块上限比如1500字符沿三级标题继续拆分在每块的文本前自动拼上所属的完整路径如## 安装指南 ### 环境要求第四步是关键给子块拼上祖先标题路径相当于给每个块注入了上下文信息。这种方法比单纯切字符分的块语义要完整得多实测检索效果提升非常明显。4.2 表格的语义保留别让关系型信息变成碎片txt里如果混着表格是最容易被解析环节毁掉的。典型错误是把表格每一行当成独立段落切块向量化之后行与行之间的对应关系全丢了。转成Markdown之后表格有了明确的语法边界|竖线分隔建议单独处理小表格行数少于10行整体作为一个块保留不要切分大表格按逻辑行分组切分每组保留表头作为上下文前缀检索时把表格块的标题路径加进块内容避免只知道表格内容、不知道表格主题如果你后续有把Markdown表格转成Excel分析的需求注意保留表头的完整性和分隔行|---|---|的格式。有些工具导出表格时会把分隔行丢掉导致解析器识别不了表头。4.3 代码块、引用块、数学公式的边界处理这三个元素在RAG场景里的共同点是不能和普通正文混在一起切块。代码块用围栏包裹切块时优先保留代码块完整性。如果代码超过分块大小宁可整块作为一个超长块也不要从中间切开——切开的代码没有任何语义。引用块一般对应原文中的强调或回填说明可以跟后续正文合并但要保留标记让向量模型识别到这是引用语气。数学公式如果用的是带$分隔的LaTeX语法切块时避开$...$的中间位置否则公式符号会被拦腰截断。网上搜markdown数学公式插件能查到不少渲染方案处理逻辑上核心就一条公式边界优先于分块边界。4.4 元数据注入给每个块配一张身份证结构化解除了做格式转换还应该顺带提取元数据。元数据是很多RAG项目完全忽略的一块但它的作用非常大。至少建议注入以下字段字段来源作用source_file文件名溯源来源source_path原始路径定位原文档title_path标题层级路径上下文补充chunk_typetext/table/code/quote检索时按类型过滤updated_at导入时间增量更新时对比checksum文本哈希判断文件是否变更有了这份元数据你后续做RAG的过滤器、时间衰减、增量更新都有抓手。很多人等到检索效果不好才回头补元数据那时候数据已经向量化入库了补起来非常痛苦。5. 结构化解的实际收益一次可复现的对比验证5.1 测试设计同源文本两种处理路径光说结构化好没有说服力我分享一个实测的对比验证你可以直接在项目里复现。取一份约5000字的产品开发文档做两组处理A组纯文本路径识别编码后按512字符固定长度切块overlap设64字符不做任何结构化处理B组Markdown路径先用规则转为Markdown按标题树聚合切块子块自动拼接标题路径两组使用完全相同的embedding模型我测的时候用的bge-large-zh-v1.5向量检索用相同的TopK参数。设计20个覆盖各章节细节的问题按命中正确答案所在块作为评估指标。5.2 对比结果与统计分析我的测试结果如下表指标A组纯文本切块B组Markdown结构化解正确答案命中率55%85%无效块召回占比答非所问30%10%单次检索平均耗时210ms187ms碎片化截断现象频繁出现基本消失最直观的差异是碎片化截断A组里有好几个问题检索到的都是把一句话拦腰切断的片段模型回答时只能靠猜B组这种问题几乎不存在。另一个有意思的发现是B组的无效块召回明显更少——原因很简单纯文本切块会把正文和目录、页脚混在一起这些垃圾块经常被无关问题检索到白白占用上下文窗口。5.3 收益边界不是所有文档都值得做结构化不过也要客观说一句结构化解析不是银弹。我总结了三个不值得做的场景全文档无层级比如一份纯FAQ列表没有标题树可依结构化能做的只是列表识别收益有限单次丢进去跑着玩的测试数据如果只导入几个文件做验证多花的时间可能比检索收益还大已有高质元数据的源头如果源系统比如数据库或知识库已经带了结构化字段直接从源头拿数据就好没必要从txt再造一遍结构化解析的最大价值在于批量、长期、持续更新的知识库构建——成本一次性收益持续发生。6. 落地过程中的高频坑位与补救手段6.1 标题被切碎与层级错乱规则转换里最常见的问题是假标题。一个段落开头写了2024.03.18 项目启动会记录正则如果匹配数字.数字就会误判成三级标题。对策是给标题识别加约束条件标题行结尾不能以标点符号。收尾且标题长度不超过30个字。这两条规则能过滤掉八成以上的误判。另一个问题是同一份txt里不同章节的标题风格不一致——第一章用第X章第二章用X.X. 标题第二章里又有居中大字。这种情况下先统计全文的标题格式分布选最频繁的一种作为主规则剩余的交由人工抽查修正比强求一套规则覆盖所有情况更高效。6.2 转义与特殊符号的坑Markdown里触发语法的字符很多txt里经常原样出现。最典型的表格里的竖线|。如果原文是一段说使用A | B进行对比转成Markdown表格语法时竖线会被解析成分隔符。这种情况需要统一转义为\|否则表格列数乱掉。代码块里如果本身包含三个反引号围栏语法会被提前终止。我的处理是用四个反引号围栏包裹内含三个反引号的块或者干脆对内容做HTML实体编码。建议在转Markdown后的校验环节加一个语法完整性检查统计反引号围栏是否成对、表格行竖线数是否一致、标题层级是否连续。6.3 分块器与Markdown结构的配合别让后道工序毁掉前道成果即使你把文本完美转成了Markdown如果后端的文本分块器不理解Markdown语法之前的工作就白做了。不要直接用通用文本Splitter处理Markdown。推荐两种方案使用Markdown感知的分块器很多主流框架比如LangChain的MarkdownHeaderTextSplitter或是LlamaIndex的MarkdownNodeParser已经内置了标题感知逻辑自研两步走分块第一步按标题层级切分得到大块第二步对大块内部再按段落或固定字符数细分自研的好处是可控性更强——比如你可以决定哪些标题层级参与切块通常只用H2和H3哪些需要合并H4及以下一般合并到父标题里避免块太碎。6.4 中小项目的一条省心成绩单最后给一个可以直接参考的管线配置适合中小规模的本地RAG知识库数据接入读取txt用charset-normalizer识别编码并统一UTF-8噪声清洗按2.3节的清单规则批量处理结构转换正则规则做第一轮Markdown转换人工抽查每批次随机抽5个文件用Sublime Text或VS Code打开检查标题树和表格占位复杂文档兜底规则转换效果差的文件走LLM辅助解析通道元数据注入写入source_file、title_path、chunk_type等字段Markdown感知分块按标题层级切分并拼接标题路径到子块向量化入库前最后的校验统计空块、超长块、孤儿标题的数量异常数据单独拉出排查这套流程我跑过不少项目最大的体会有两个一是先小步迭代再批量跑别一开始就在几千个文件上跑全流程二是每个环节都保留中间产物清洗前后的diff、Markdown解析的结果都存下来排查问题时能直接溯源。把这块做好你的RAG知识库才真正有了可靠的地基。