小林第一天到“向量食府”后厨报到。大厨在小白板上写着今日任务:“客人点了一份 RAG 大餐,先备料!”所谓 RAG 大餐,就是大模型在回答之前要先“查菜谱”——从企业知识库里检索相关资料,再结合资料作答。而知识库里的料,都来自散落在各处的原材料:网页上的 HTML、Wiki 导出的 Markdown、技术文档站点的长页面。这些原材料就像刚从菜市场买回来的菜,带泥、带土、带烂叶、带包装袋。备菜员的第一道工序,就是把乱糟糟的食材,洗成干净、切成整齐、分好类的净菜,送进冷柜(向量库),等着大厨下锅。 这个过程就是 RAG 的“文档加载与清洗”。加载就是“买菜”——选什么工具把文档读进内存;清洗就是“洗菜切菜”——去掉标签噪音(泥土),保留结构信息(纹理),小心处理表格和代码块(易碎食材),决定图片链接的取舍(装饰与引用)。只有备菜到位,检索才能找到对味的料,回答才能色香味俱全。今天,我们就跟着小林在后厨从零学起,把 Markdown 和 HTML 处理这门手艺练扎实。
掌握文档加载与清洗,是 RAG 工程师的第一块敲门砖。它直接决定知识库的检索质量和回答准确率,让你搭建企业级 RAG 时少踩一半的坑,是 AI 应用落地中最基础也最值钱的硬功夫。
目录
可视化演示
展示归并排序的分治策略:递归拆分数组至单元素,再逐层合并有序子数组,通过步进控制观察每个阶段的数组状态变化。
本节配有交互动效(进入视口后自动加载,约 3KB)。
拆分阶段 递归将数组对半拆分,直到每部分仅含1个元素 T(n) = 2T(n/2) + O(n) 分治策略的核心:分解为子问题
加载器选型:采购要用对工具
菜市场里的西红柿带着泥巴和蒂把,直接用手去撕,只会把灶台弄得一塌糊涂。老师傅带小林看了墙上挂的三把刀:斩骨刀、剔骨刀、削皮刀。斩骨刀什么都能砍,但切豆腐会压碎;剔骨刀专门对付骨骼关节,削皮刀则只处理去了粗皮的净菜。选对工具,菜才完整,手上也不沾泥。
RAG 里的“采购工具”就是文档加载器(Loader)。不同文档格式就像不同食材,需要不同刀具。通用加载器(如 Unstructured)像斩骨刀,什么格式都能读,但精细度有限;HTML 专用工具(如 BeautifulSoup 配合自写解析)像剔骨刀,专门拆解网页骨架;Markdown 解析器(如 markdown-it、python-markdown)像削皮刀,轻量、精准、适合结构本来就规整的文档。
选加载器时看三个维度:一是格式适配,HTML 用 BeautifulSoup/lxml,Markdown 用对应解析库,PDF 用 PyMuPDF;二是结构保留能力,好的加载器会把标题、列表、表格暴露成结构对象,而不是拍平成纯文本;三是性能与批量,企业文档动辄上万份,加载器要支持流式读取和并行处理。落地前先拿一批真实文档试跑,用成功率说话。
1. 统计成功解析数:分子是加载过程中无异常、文本完整读出的文档数量,反映加载器对格式的兼容程度。
2. 统计候选总数:分母是本次要入库的全部文档,包括损坏、加密、格式不支持的“带病食材”。
3. 换算为百分比:乘 100% 得到直观通过率,一般以 95% 作为验收线,低于则换加载器或加补偿清洗。
小林把三把刀都试了一遍,发现没有哪把刀是万能的——HTML 页面用剔骨刀拆得干干净净,批量导出的 Markdown 用削皮刀效率最高。选加载器,本质就是选对“刀”。
标签噪声清洗:洗去泥土与烂叶
洗菜是备菜的基本功。菜叶上的泥巴,是 <div>、<span> 这类布局标签——它们只负责“摆放位置”,不承载语义;根须上的烂叶,是 <script>、<style> 标签里的内容——它们是植株的一部分,但做菜用不上。小林起初把整颗菜直接放进筐里,结果大厨炒菜时满嘴沙子。
HTML 文档里,标签字符合计往往能占到全文的三成以上。如果原样喂给大模型,token 被白白消耗,检索时还会被导航栏、广告位等噪音干扰。清洗的目标很明确:去掉一切“不影响语义”的标签与符号,只留下干净的文本和结构。
清洗有两条路:其一是解析式清洗,用 HTMLParser 或 BeautifulSoup 解析 DOM,只取文本节点,稳但不快;其二是正则粗清洗,用规则直接删标签,快但不稳。具体动作包括:剔除 <div>、<span> 等布局标签;整段跳过 script/style 内容;把连续空白压缩成单个空格;统一编码,处理 & 之类的 HTML 实体和乱码。洗好的文本里,不应该再看到尖括号和残留属性。
1. 记录原始字符数:清洗前统计总字符数,作为基准分母,相当于称一下带泥蔬菜的重量。
2. 统计残留噪音:清洗后仍残留的标签、HTML 实体、多余空白等,分子,相当于洗完后搓下来的沙砾。
3. 取补集并转百分比:用 1 减去噪声占比得到“干净度”,乘 100% 便于与验收标准对比。
小林把洗好的青菜递给大厨,大厨捏起一片叶子在灯下照了照:没有沙砾、没有烂叶、水珠滚落干净——这才是合格的备菜。
结构信息保留:像剥洋葱一样分清层次
洋葱一层裹着一层,标题就是洋葱的层。切菜的时候如果不管不顾一刀拍扁,表层、中层、里层全混在一起,大厨根本分不清哪些是引言、哪些是细节、哪些是结论。小林发现,文档的标题层级(h1 到 h6)就是这样的洋葱层,拆掉了,文本就成了一锅粥。
在 RAG 里,标题层级、列表缩进、链接引用是“文本的骨架”。如果清洗时把骨架全拆掉,切块时就会把不属于同一主题的内容混在一起,检索时明明命中了关键词,却答非所问。保留结构,是为了让每个知识块都能“溯源”到它在原文里的位置。
具体的做法:h1 转成单 #、h2 转成 ##,这是明确的“层级信号”而不是 Markdown 语法残留;列表保留缩进(- 加空格),让嵌套关系可读;链接保留其文字内容作为引用线索;元数据(标题、作者、日期、来源 URL)抽出来放在知识块前缀,检索命中后能一路回溯到它的“洋葱层”。
1. 统计原始结构块:原始文档中 h1~h6、有序无序列表、表格等有语义的结构总数,相当于洋葱的总层数。
2. 统计保留结构块:清洗后仍保留为层级记号或独立块的数量,相当于完整剥下来的洋葱层。
3. 求比值:保真度越接近 1,检索时命中的内容越能回溯到正确层级,回答越有据可依。
小林把剥好的洋葱一层层码在保鲜盒里,每层都贴着标签:表皮、中层、芯。大厨取用时只需要看一眼盒盖上的层级记号,就能精准下刀。
表格代码处理:豆腐切块,牛排保鲜
嫩豆腐要切块保型,一刀拍碎就全完了;带血牛排要原封不动地用保鲜膜包好,不能提前调味。小林做表格时图省事把整张表压成一段话,结果“包名”和“版本号”全串了位;转头又把代码块的换行全删了,大厨看了一眼直接摔锅铲:“程序都废了。”
表格和代码块是清洗流水线上最娇气的食材。表格一旦失去行列关系,单元格就会乱跑,切块后检索到的信息是错位的;代码块一旦被压缩换行,语法就被破坏,喂进知识库只会误导模型。
处理表格:把每一行转成一行文本,单元格用分隔符(| 或制表符)隔开,保持行列对应;表格太大时独立成块,避免与其他段落混切。处理代码块:识别 <pre>/<code> 或 Markdown 的 `` 围栏,内部原样保留,不压缩空白、不改换行,最好打上语言标签(如 lang=python`)。换行规范要区分场景:正文按段落换行,代码按语法换行,两者标准不同,不能一刀切。
1. 统计原始单元格:一个 m 行 n 列的表格,格子总数是 m×n,这是表格完整度的基准,相当于一整块豆腐的体积。
2. 统计还原单元格:转换后仍能按行列关系读出的格子数,相当于完整切出的豆腐块。
3. 求损失率:用 1 减去还原比例,损失率越高,说明表格串位越严重,需要调整分隔符策略。
小林把表格切成六块豆腐码齐,把代码块用保鲜膜包好放进冷柜。大厨满意地点点头:“豆腐还是豆腐,牛排还是牛排。”
图片链接取舍:装饰与引用的去留
后厨里有一盆萝卜雕花,好看但炒不熟,上菜时只能当摆盘。菜谱上写“酱汁参考隔壁老王”,这句话必须记下——客人问起来,你得说得出出处。小林对着文档里的图片和链接犯了难:删了怕丢信息,留着又不知道怎么处理。
图片和链接,一个占地方、一个不占地方,但处理原则相通:留下“语义线索”,丢掉“物理实体”。模型是文本模型,看不了二进制图片,但图片的 alt 文本描述的是“图里讲了什么”,这正是可检索的语义。
图片处理策略:优先保留 alt 文本,转成 [图片:alt] 占位符放回原文位置;alt 缺失时看周围上下文,能概括就概括,纯装饰性图片直接删除。链接处理:保留链接文字(anchor text),必要时标注 [链接] 和目标 URL 摘要;对外链打上“外部引用”标记,提示该知识点可能涉及站外内容。取舍的目标,是让视觉信息尽量转成可检索的文本。
1. 统计视觉元素总数:文档中图片与链接的总条数,相当于所有需要决策的摆盘和引用。
2. 统计有效线索数:保留的 alt 文本、链接文字和引用标注条数,相当于真正写进菜谱的备注。
3. 求语义增量:比值越高,视觉信息流失越少,检索时越容易通过图片描述命中知识。
小林把萝卜雕花扔进垃圾桶,却把“酱汁参考隔壁老王”这句话工工整整记在了菜谱的备注栏。装饰可以丢,出处必须留。
实战最佳实践:开市前的出品检查
开市前 15 分钟,老师傅会把备好的菜抽查三盘:没有头发丝、没有沙砾、摆盘整齐。小林问:“我们不是按流程做完了吗?”老师傅说:“按流程做完了,不代表做对了。检查,是流程的一部分。”
清洗流程搭好之后,必须有“出品检查”的闭环,否则坏料进了冷柜,后面全盘皆输。RAG 的文档清洗也是如此,脚本不报错不代表结果合格。
最佳实践有四件事:一是固定加载器选配标准,HTML 走 BeautifulSoup/Readability,Markdown 走统一解析库,避免每个人各自为政;二是流水线清洗,加载 → 去噪 → 结构提取 → 表格与代码保护 → 分块,每一步都留中间产物,方便出问题时回溯定位;三是抽样质检,随机抽 10~20 份文档,人工核对标题是否保留、表格是否串位、代码块是否完整;四是性能与缓存,抓取时加缓存、多进程并行解析,避免同一份文档反复清洗。用质检通过率量化成果,低于阈值就加规则再跑一轮。
1. 确定抽样样本数:随机抽取 10~20 份清洗结果作为样本,相当于随机抽查三盘菜。
2. 统计无误样本数:人工核对标题层级、表格、代码块均正确的份数,相当于三盘里几盘合格。
3. 换算通过率:乘 100% 与阈值(如 95%)对比,达标才允许批量入库。
开市铃响,大厨站在灶台前,看着冷柜里码得整整齐齐的净菜,对小林竖起大拇指:“备菜无小事。检查过了,我才敢放心下锅。”
AI 应用场景
企业 Wiki 知识库迁移
阶段:文档加载与清洗
解决的问题:传统做法是运维用脚本把 Wiki 页面导出成 HTML 后直接扔进向量库,结果标签噪音让 token 浪费三成,检索还总命中导航栏和菜单文案。用文档加载与清洗流程后,噪声被剔除,标题层级被保留,检索质量显著提升。
具体案例:某公司的 Confluence 存着 5000 个产品页面,工程师在清洗流水线里加入统一加载器、跳过 script/style、把 h2 转成二级记号、表格转分隔符文本。上线后,内部问答机器人在“部署权限”类问题上的命中准确率从 61% 提升到 89%。
官方技术文档站点入库
阶段:文档加载与清洗
解决的问题:爬虫抓下来的文档页往往带着导航栏、页脚、广告位等噪音,直接切块会让大量无关内容进入知识库。结合图片 alt 保留与链接引用标注,知识块变得更干净、更可溯源。
具体案例:一家 SaaS 公司为 SDK 搭建问答机器人,先把官方文档站的上百个 HTML 页面清洗成带层级记号的 Markdown,再按标题切块。用户问“如何初始化客户端”时,系统能精确命中“快速开始”章节下的代码块,回答里还附带了原文链接。
代码仓库 README 与教程清洗
阶段:文档加载与清洗
解决的问题:GitHub 仓库的 README、教程类 Markdown 里满是代码块,如果清洗时压缩换行,程序示例就变成乱码。代码块保护和语言标签保留,让代码语义完整地进入知识库。
具体案例:某团队的 DevOps 知识助手定期清洗内部仓库的 Markdown 文档,检测 ``` 围栏并原样保留代码,标注语言类型。开发者提问“怎么配置 CI 缓存”,助手能返回带完整缩进的 YAML 示例,直接复制可用。
Python 演示
import refrom html.parser import HTMLParser class DocCleaner(HTMLParser): """把 HTML 洗成保留结构的纯文本,便于 RAG 分块""" def __init__(self): super().__init__() self.lines = [] self.skip_depth = 0 self.in_code = False self.code_buf = [] self.in_row = False self.cells = [] def handle_starttag(self, tag, attrs): if self.skip_depth: self.skip_depth += 1 return if tag in ("script", "style"): self.skip_depth = 1 return if tag in ("pre", "code"): self.in_code = True return if re.fullmatch(r"h[1-6]", tag): self.lines.append("#" * int(tag[1]) + " ") elif tag == "li": self.lines.append("- ") elif tag == "tr": self.in_row = True self.cells = [] elif tag in ("td", "th"): self.cells.append("") def handle_endtag(self, tag): if self.skip_depth: self.skip_depth -= 1 return if tag in ("pre", "code"): self.in_code = False self.lines.append("".join(self.code_buf)) self.code_buf = [] return if tag == "tr": self.lines.append(" | ".join(self.cells)) self.in_row = False return if tag in ("p", "div", "li") or re.fullmatch(r"h[1-6]", tag): self.lines.append("") def handle_data(self, data): if self.skip_depth: return text = re.sub(r"\s+", " ", data).strip() if not text: return if self.in_code: self.code_buf.append(data) elif self.in_row and self.cells: self.cells[-1] += text else: self.lines.append(text) def clean(self, html): self.lines = [] self.code_buf = [] self.cells = [] self.skip_depth = 0 self.in_code = False self.feed(html) return "\n".join(self.lines) def split_by_heading(text, max_chars=200): chunks = [] current = "" for line in text.splitlines(): if re.match(r"^#{1,6} ", line) and current: chunks.append(current.strip()) current = "" current += line + "\n" if len(current) >= max_chars and current.count("\n") > 2: chunks.append(current.strip()) current = "" if current.strip(): chunks.append(current.strip()) return chunks def main(): sample = """ <html><head><style>body{color:red}</style></head> <body><h1>部署指南</h1><p>先安装依赖</p> <table><tr><td>包名</td><td>版本</td></tr> <tr><td>langchain</td><td>0.1</td></tr></table> <pre><code>pip install langchain</code></pre></body></html> """ cleaner = DocCleaner() text = cleaner.clean(sample) print(text) print("---- 分块结果 ----") for i, chunk in enumerate(split_by_heading(text)): preview = chunk[:40] + "..." if len(chunk) > 40 else chunk print("[块 %d] %s" % (i, preview)) if __name__ == "__main__": main()
self.skip_depth = 1遇到 script/style 后进入“跳过模式”,把里面的内容整段当烂叶扔掉。re.fullmatch(r"h[1-6]", tag)识别 h1~h6 标题标签,把它转成#数量的层级记号,这就是保洋葱层。self.lines.append("#" * int(tag[1]) + " ")用#重复次数表达标题深度,检索时能按层级切块。self.cells.append("")每遇到 td/th 就登记一个格子,为表格的行列对应做准备。" | ".join(self.cells)在行结束时用分隔符把单元格拼成一行,保住行列关系,豆腐不碎。self.code_buf.append(data)代码块内的数据原样进缓冲,不压缩空白不拆换行,牛排保鲜。split_by_heading(text)按# 标题切分知识块:遇到新标题就把上一块收尾,防止跨主题混切。if len(current) >= max_chars and current.count("\n") > 2块太长时兜底切分,避免超大块撑爆向量库。print(text)先看清洗结果再分块,相当于开市前的抽样检查。
常见误区
误区用正则把所有 `<...>` 都删掉,以为标签删得越干净越好,最后连标题层级、列表结构也一起删没了。
正解标签分两类:布局标签(div/span)是泥巴,结构标签(h1/h2/li/tr)是洋葱层。正确的做法是“分层处理”——布局标签剔除,结构标签转成层级记号或分隔符,而不是一刀切全删。
为何常见:人眼看渲染后的页面只看到文字,容易忽略结构本身携带的语义,所以想当然认为“凡是标签都是脏的”。
误区觉得用 html2text、Jina Reader 转换一下就是全部,不需要任何后处理,转换结果直接入库。
正解现成工具能保底,但企业文档通常有自定义模板:内部组件、特殊表格、面包屑导航、页脚版权,工具都会留下盲区。转换后的 Markdown 依然要过清洗规则,做二次去噪和结构校验。
为何常见:工具的宣传语“一键转 Markdown”太有迷惑性,让人低估了脏数据的多样性。
误区为了省 token 把代码块的所有缩进和换行压成一行,看起来“体积小、更干净”。
正解代码的缩进和换行是语法的一部分,压缩后语义被破坏,模型检索到的是“语法损坏的代码”。代码块应该整体保护,保留原样,还可以打上语言标签帮助检索。
为何常见:因为压缩正文空白确实很有效,容易把同一规则误用到代码块上,没有意识到两者语义标准不同。
误区反正大模型是文本模型,看不了图片,清洗时索性把所有图片一律丢弃。
正解图片的 alt 文本和上下文描述是有价值的检索线索,删掉等于弄丢“图里讲了什么”的入口。正确做法是把 alt 转成占位文本,缺失时尝试用上下文概括。
为何常见:图片确实无法直接参与文本匹配,但忽略了 alt 文本和上下文同样可以作为文本被检索。
误区只要脚本没报错,清洗结果就一定合格,直接灌进向量库完事。
正解程序不报错不代表语义正确。表格串位、标题丢失、编码乱码都不会抛异常。上线前必须抽样打印清洗结果人工核对,并用质检通过率设置门槛。
为何常见:因为“加载成功”和“清洗正确”是两码事,初学者容易把运行无异常当成质量合格。
面试题
Q1. RAG 系统里,喂给大模型之前的第一步是什么?
- A.文档加载与清洗← 正确答案
- B.向量索引构建
- C.检索重排序
- D.提示词调优
答案:A。备菜在前,炒菜在后。RAG 的流程是先把文档加载清洗成干净的知识块,再向量化、建索引、检索,最后才轮到提示词和回答。选项 B、C、D 都是清洗之后的下游环节。
Q2. 清洗 HTML 时,遇到 <script> 标签里面的内容应该怎么处理?
- A.整段跳过删除掉← 正确答案
- B.原样保留进正文
- C.转为代码块保存
- D.提取变量值存储
答案:A。script 是网页的脚本逻辑,相当于菜根上的烂叶,做菜用不上,应该整段跳过,避免污染正文 token。选项 C 的“转代码块”只适用于正文里的演示代码,不适用于脚本逻辑。
Q3. 清洗时遇到 <h2> 标题,最推荐的处理方式是什么?
- A.整行直接删除
- B.合并进正文段落
- C.转成二级层级记号← 正确答案
- D.保留标签原文
答案:C。标题是洋葱层,应该转成 ## 这样的层级记号,既去掉标签噪音又保住结构信息。A 丢了骨架,B 让标题和正文混在一起,D 把标签噪音留给了模型。
Q4. 一个 3 行 4 列的表格,在清洗转文本时推荐怎么做?
- A.压缩成一段长文本
- B.直接丢弃表格数据
- C.截图存成图片
- D.用分隔符分行展示← 正确答案
答案:D。表格是嫩豆腐,要切块保型。把每行转成一行、单元格用分隔符隔开,才能保住行列关系。A 会让单元格串位,B 直接丢数据,C 引入模型看不了的二进制。
Q5. 下列哪些做法属于文档清洗的正确操作?
- A.压缩代码块换行
- B.跳过脚本与样式内容← 正确答案
- C.提取页面元数据← 正确答案
- D.保留标题层级记号← 正确答案
答案:BCD。跳过 script/style 是洗烂叶,提取元数据是贴溯源标签,保留标题层级是保洋葱层,都是正确操作。A 是常见误区——代码缩进和换行是语法的一部分,不能压缩。
Q6. 图片在清洗文本知识库时应该如何处理?
- A.Base64 完整嵌入
- B.一律删除不保留
- C.保留 alt 文本占位← 正确答案
- D.交给 OCR 管线
答案:C。模型看不了图,但 alt 文本是“图里讲了什么”的可检索线索,应该转成占位文本留在原位。A 浪费 token,B 丢掉语义入口,D 的 OCR 属于另一条处理管线,不是文本清洗的默认动作。
Q7. 选择文档加载器时,需要考虑哪些因素?
- A.结构保留能力← 正确答案
- B.源文档格式匹配← 正确答案
- C.通吃所有格式
- D.解析速度与内存← 正确答案
答案:ABD。选加载器像选刀,要看格式是否匹配、结构保留能力强不强、批量解析时速度快不快、内存稳不稳。C 是误区——不存在真正通吃所有格式且保真的加载器,通用型工具精细度有限。
Q8. Markdown 行首的 # 符号在清洗中的含义是什么?
- A.标记注释不解析
- B.无序列表的符号
- C.代码块的围栏
- D.表示标题的层级← 正确答案
答案:D。# 是标题层级记号,# 是一级、## 是二级,是清洗时要特意保留的结构信号。它是切块回溯的依据,不是注释,也不是列表符号。
Q9. 清洗流程上线之前,最有效的质检方式是什么?
- A.直接灌进向量库
- B.仅检查有无报错
- C.抽样打印人工核对← 正确答案
- D.只看总字符数量
答案:C。程序不报错不代表语义正确。表格串位、标题丢失、乱码都不抛异常,必须抽样打印清洗结果,人工核对层级、表格和代码块,再计算质检通过率决定是否上线。
Q10. 清洗过程中,哪些结构一旦丢失会造成语义损失?
- A.正文句号逗号
- B.标题嵌套的层级← 正确答案
- C.表格行列对应← 正确答案
- D.列表缩进层级← 正确答案
答案:BCD。标题层级决定知识块的可回溯性,表格行列决定单元格是否串位,列表缩进决定嵌套关系的可读性,都是清洗时要保护的结构。正文标点属于常规文本,清洗不会专门丢失它们。