小美是文档仓库的管理员,每天都会收到一箱箱用 Markdown 写成的货物说明。她发现,如果不先看清货物的标签(标题)、包装(代码块)和分类(列表),就随手把货物切块上架,那么取货的人(RAG 里的检索模块)就永远找不到正确的零件。这篇文章要讲的,就是如何像整理仓库一样解析 Markdown:先看懂骨架、再决定怎么下刀、给每个块贴上「来路标签」、最后测评切得是否合格。读完你会对 RAG 的文档处理环节建立起一套完整的直觉,不再靠感觉调参数。
对 AI 从业者:Markdown 是知识库最常见的格式,掌握结构化解析与 chunk 切分,是从「能跑通 RAG」到「检索效果好用到能上线」的分水岭,也是 AI 应用工程面试的高频考点。
可视化演示
\u901a\u8fc7\u6807\u9898\u5c42\u7ea7\u5207\u5206\u3001\u5207\u5206\u7b56\u7565\u5bf9\u6bd4\u548c\u8d28\u91cf\u8bc4\u4f30\u6307\u6807\uff0c\u76f4\u89c2\u5c55\u793aMarkdown\u6587\u6863\u5982\u4f55\u6cbf\u7ed3\u6784\u7ebf\u62c6\u5206\u4e3a\u8bed\u4e49\u5b8c\u6574\u7684RAG\u68c0\u7d22chunk\u3002
本节配有交互动效(进入视口后自动加载,约 3KB)。
\u6807\u9898\u5c42\u7ea7\u5207\u5206 H1\uff1a\u4e00\u7ea7\u6807\u9898\u521b\u5efa\u4e3b\u5207\u5206\u8fb9\u754c H2\uff1a\u4e8c\u7ea7\u6807\u9898\u7ec6\u5206\u5b50\u4e3b\u9898 H3\uff1a\u4e09\u7ea7\u6807\u9898\u5212\u5206\u6700\u5c0f\u8bed\u4e49\u5757 Chunk=Heading+Content\u2193Hn \u6cbf\u6807\u9898\u7ed3\u6784\u7ebf\u5207\u5206\u4fdd\u8bc1\u8bed\u4e49\u5b8c\u6574
看懂Markdown骨架
小美打开一箱刚到货的项目文档,没有急着把东西往货架上塞,而是先摊开看看里面长什么样。箱子内部分了好几层:最上面是一层大隔板(一级标题 #),下面压着几块小隔板(二级标题 ##),最小的格子里放着零件(三级标题 ###);玻璃罩里有一台精密仪器(代码块),整罩摆放不能拆;贴着编号的小纸片(列表项)按顺序串在一起。看懂这些分隔和层次,才知道该从哪儿下刀。
在 RAG 系统里,这份「货物结构图」就是 Markdown 解析器做的事。解析器用正则表达式匹配行首的井号数量来判断标题层级,用成对的反引号围栏识别代码块,用行首符号识别列表和表格。解析完成后,文档会被组织成一棵树:每个标题是子树根节点,它下面的正文、列表、代码块都是子节点。这棵树就是后续所有切分决策的依据。
我们可以用一条路径公式记录每个标题节点的「来路」。假设某个标题是 ### 环境要求,它从根到自己的各级祖先标题依次是 项目文档、安装、环境要求,那么这条路径就是:
1. 节点定位:$node$ 表示 Markdown 中的某个标题节点,比如 ### 环境要求。
2. 祖先序列:$t_1$ 到 $t_k$ 是从根标题到当前标题的各级标题文本,$k$ 就是当前标题的层深度。
3. 路径拼接:$[\,t_1,\ t_2,\ \dots,\ t_k\,]$ 把这些标题按顺序排列成一条从根到叶的路径。
4. 后续用途:这条路径会被写进 chunk 的元信息,作为检索时定位的「来路标签」。
小美把这张树状结构图画在仓库墙上之后,整个团队的取货速度都变快了——因为大家第一次知道,每件货物应该放在哪个隔板下面。
切分策略选择
货架整理有两种粗暴做法:一是完全按隔板自然分区,把每块隔板里的东西装进一个盒子;二是统一买一种固定尺寸的收纳盒,东西多了就硬压、少了就空着。第一种是「按标题切分」,第二种是「按长度切分」。如果文档本身有清晰的标题结构,按标题切能让每个盒子里的内容语义完整;但遇到纯文本日志、无标题的说明文件,就只能退而求其次按长度硬切。
聪明的仓库管理员会混合下刀:先看标题结构,如果某个标题下的内容不算太长,就整个作为一个 chunk;只有内容超长时才动用固定长度做二次切分,把超长部分再拆成几个小段。这样既保住了标题带来的语义边界,又不会出现一个块大到超过向量模型输入上限的情况。
把这条规则写成数学语言,就是一个分段函数:
1. 候选块:$c$ 表示某个标题节点下的正文内容块。
2. 长度上限:$len(c)$ 是它的字符数,$L_{max}$ 是预先设定的单块最大长度。
3. 分支判断:当长度在上限内时选择按标题切($\text{heading\_split}$),否则选择按长度切($\text{length\_split}$)。
4. 策略含义:这个分段函数就是「结构优先、兜底保底」的数学表达。
小美试了几周后发现,混合策略下,客户几乎没有再抱怨过「找到的零件缺了一角」——因为大多数盒子都完整地保留了原包装。
给Chunk贴上「来路标签」
每个上架的盒子上,小美都会贴一张进货单,写上它来自哪个货架、哪个隔板。顾客来取货时只要报出「文档 > 安装 > 环境要求」,取货员立刻就知道去哪个货架第几层找。在 RAG 系统里,这张进货单就是 chunk 的元信息:标题路径、文档 ID、块序号、锚点链接。
为什么必须这么干?因为向量检索是一个「模糊匹配」的过程,chunk 本身如果只有孤零零的正文,检索模型根本不知道这段话在整篇文档里的位置。把标题路径拼在正文之前,相当于给检索模型递了一张地图,让「环境要求」里的内容能和「安装」这个主题建立上下文关联。
拼装公式非常简单直接:
1. 正文部分:$T_{body}$ 是该标题节点下的实际内容。
2. 标题路径:$T_{heading}$ 是形如「项目文档 > 安装 > 环境要求」的路径前缀。
3. 拼接操作:$+$ 表示字符串拼接,把路径放在正文之前。
4. 存储阶段:拼接后的完整文本才送入 embedding 向量化,字段顺序对检索效果有直接影呛。
小美的经验是:别小看这张「进货单」,它让同一个盒子的命中概率至少翻了一倍——因为检索模型终于知道自己在哪一层楼了。
切片质量评估
每隔一段时间,小美会做一次盘点抽检:请几位老顾客随机报出货物的名字,看取货员能不能在货架上快速找到。找到了算命中,找不到算漏检。把命中次数除以总询问次数,就得到一个能横向比较的分数。在 RAG 工程里,这对应着一套用真实问答构造的测试集。
最常用的指标是 Recall@k:系统返回前 k 个候选块,看答案所在的块是否在其中。它的数学定义是:
1. 正确答案集合:$G$ 表示测试集里人工标注的、包含答案的 chunk 集合。
2. 返回集合:$R_k$ 表示检索系统对每个问题返回的前 $k$ 个 chunk。
3. 求交集:$R_k \cap G$ 表示返回结果里真正命中的正确答案块。
4. 取模求比例:$|\cdot|$ 是集合大小,$\frac{|R_k \cap G|}{|G|}$ 是命中块占全部正确答案的比例。
除了召回率,小美还会随机打开几十个盒子检查语义是否完整:一段话有没有被腰斩、一个列表有没有被拆散、代码块有没有缺行。数值指标加人工抽检,双管齐下才能判断切分策略值不值得上线。
工程落地优化
仓库越来越忙之后,小美想了三招提速。第一招:在相邻货架的边界都多放一份畅销品,这样客户无论从哪边走都能拿到货——对应「重叠窗口」,让相邻 chunk 之间交叠一部分文本。第二招:新货到货时先只拆开箱顶看标签,客户真下单才拆包装——对应「惰性解析」,先只扫描标题行,正文按需处理。第三招:只有某个货架的区域发生变化时才重新整理那一片——对应「增量更新」,按文件哈希判断是否需要重新解析。
重叠窗口的滑动规律可以用一个偏移公式描述:
1. 窗口大小:$S_{window}$ 是每个 chunk 的目标字符长度。
2. 重叠量:$O_{overlap}$ 是相邻两个块重复的字符数。
3. 滑动间距:$S_{window} - O_{overlap}$ 是每次实际向前推进的距离。
4. 起点计算:$offset_i = i \times (S_{window} - O_{overlap})$ 是第 $i$ 块的起始偏移。
这三招让小美的仓库在大促期间也没有瘫痪:解析速度快了,检索命中率也没掉。边界修正则是锦上添花——如果块的首尾正好落在代码块中间,就把这个块整体向后挪几行,避免切断程序语法。
文本清洗
上架之前,小美还会把货物里的泡沫纸、空纸箱、缠歪的标签全部清掉。Markdown 文档里也有这类杂质:连续多个空行、图片链接、导航栏文字、站内跳转的锚点。这些内容不会被用户检索,却会稀释 chunk 里有效信息的密度,让向量更「模糊」。
清洗规则要区分文本类型:正文里的连续空行可以压缩成一个;代码块里的缩进和空行不能随便压,否则程序语义就变了;图片链接和导航文字可以直接删除,因为它们对问答没有贡献。清洗效果可以用信息密度来衡量:
1. 有效字符数:$C_{effective}$ 表示清洗后保留的有意义文本字符数。
2. 总字符数:$C_{total}$ 表示清洗前的原始字符总数。
3. 求密度:两者相除得到 $D$,范围在 0 到 1 之间。
4. 优化方向:$D$ 越接近 1,说明每单位文本的语义载荷越高,检索越容易命中。
小美把这条公式贴在墙上当作「卫生标准」,从此每个上架的盒子都轻装上阵。仓库的空间利用率提高了,客户取货的精准度也上了一个台阶。
AI 应用场景
企业客服知识库问答
阶段:文档加载与处理
解决的问题:客服系统要上传数百篇产品手册(Markdown 格式),传统做法直接整篇喂给模型,不仅超出上下文窗口,检索时也定位不到具体段落。用标题解析 + 路径元信息切分后,每个 chunk 自带「文档 > 章节 > 小节」的定位,检索命中率大幅提升。
具体案例:某 SaaS 公司把 1000 篇使用手册按二级标题切成约 8000 个 chunk,每个 chunk 携带标题路径。上线后,用户问「如何重置密码」,系统能精确返回「账号管理 > 安全设置」下的段落,而不是整篇手册。
代码库问答助手
阶段:文档切分与清洗
解决的问题:开发者提问「这段代码怎么用」时,如果代码块在切分时被拦腰截断,嵌入向量会严重失真。通过代码块隔离与边界修正,让代码块整块保留,再配合周围文本组成 chunk。
具体案例:某 DevOps 团队的内部代码问答机器人,对 README 做切分时识别所有代码块并整块保护。工程师问「CI 流水线如何配置缓存」,机器人引用的代码块完整无缺,直接可以复制使用。
企业知识库增量更新
阶段:索引构建与更新
解决的问题:知识库文档每周更新,如果每次都全量重新解析,耗时且浪费算力。通过文件哈希判断文档是否变化,只对变更文件做增量切分和重新向量化。
具体案例:某银行合规部门每周更新制度文档,系统记录每个文件的哈希值,发现变更后只把对应文档重新解析成 chunk 并更新向量库。全量重跑需 2 小时,增量更新只需 3 分钟。
Python 演示
# -*- coding: utf-8 -*-"""根据标题层级把 Markdown 拆成带路径的 chunk"""import re def parse_headings(md_text): """返回 [(level, title, start_line)]""" headings = [] for i, line in enumerate(md_text.splitlines()): m = re.match(r'^(#{1,6})\s+(.*?)\s*$', line) if m: headings.append((len(m.group(1)), m.group(2).strip(), i)) return headings def split_by_headings(md_text): headings = parse_headings(md_text) lines = md_text.splitlines() chunks = [] if not headings: return chunks for idx, (level, title, start) in enumerate(headings): end = headings[idx + 1][2] if idx + 1 < len(headings) else len(lines) body = "\n".join(lines[start:end]).strip() if not body: continue chunks.append({"level": level, "path": title, "text": body}) return chunks def merge_small_chunks(chunks, min_chars=200): merged = [] for c in chunks: if merged and (c["level"] > merged[-1]["level"] or len(c["text"]) < min_chars): prev = merged[-1] prev["path"] = prev["path"] + " > " + c["path"] prev["text"] = prev["text"] + "\n\n" + c["text"] prev["level"] = min(prev["level"], c["level"]) else: merged.append(dict(c)) return merged def main(): sample = """# 项目文档 ## 简介 这是项目的简介内容,说明项目用途。 ## 安装
# 识别标题层级,再把每个标题及其下属内容组装成一个 chunk,最后把小标题就近合并到父块。re.match(r'^(#{1,6})\s+(.*?)\s*$', line)用正则识别 Markdown 标题行,井号数量就是标题层级。headings.append((len(m.group(1)), m.group(2).strip(), i))记录标题层级、标题文本和所在行号。end = headings[idx + 1][2] if idx + 1 < len(headings) else len(lines)下一个标题所在的行,就是当前标题块的结束位置。chunks.append({"level": level, "path": title, "text": body})每个标题子树作为一个 chunk 存入列表。merge_small_chunks把过短的小标题块合并到上一个父块中,避免出现碎片上下文。
常见误区
误区我以为只要把每个三级标题、四级标题都单独切成一块,检索时肯定更容易命中,于是把一篇文档切成了几百个碎块。
正解块太小会丢失上下文。检索模型看到的是一个只有两行字的「孤岛」,根本不知道它属于哪个章节。正确做法是设置块体积下限(如 200 字),小标题就近合并到父块中,保证每一块都有完整的语义单元。
为何常见:人们直觉上觉得「小块更容易精确匹配」,却忽略了语义完整对向量表征的影响,属于典型的局部最优思维。
误区RAG 反正都要向量化,直接按 500 字一个块切开就行,标题不标题的无所谓。
正解固定长度切分会把标题和正文、表格和代码拦腰截断,检索到的常常是残缺的碎片。只有对完全没有结构的纯文本才适合用固定长度;有结构的 Markdown 文档应当优先按标题切。
为何常见:很多入门教程为了省代码,都用固定长度切分演示,导致初学者误以为这是通用最优解。
误区代码块不就是普通文本吗?按长度切的时候从中间断开也没什么关系。
正解代码一旦被切断,语法结构就破坏了,嵌入向量的语义会严重失真。正确做法是把代码块整体隔离,绝不允许切分点落在代码块内部;必要时可把代码块压缩后与周围文本一起组成块。
为何常见:代码块在 Markdown 渲染中看起来只是「底色不同的文本」,初学者看不出它在结构上和普通段落的本质区别。
面试题
Q1. 在 Markdown 中,以 ## 开头的行表示什么?
- A.一个一级标题
- B.一个二级标题← 正确答案
- C.正文被加粗
- D.无序列表项
答案:B。井号数量代表标题层级,一个井号是一级标题,两个井号是二级标题。加粗用 `,列表用 - 或 *`,所以 B 正确。
Q2. 按标题切分 chunk 的主要优点是什么?
- A.块大小完全一致
- B.保持语义完整← 正确答案
- C.无需解析文档
- D.计算开销最低
答案:B。每个标题及其下属内容构成一棵子树,天然是完整的语义单元,按标题切分能让块与块之间边界清晰。块大小一致是固定长度切分的特点,按标题切不需要解析文档也是错的。
Q3. 混合切分策略的核心思想是什么?
- A.完全靠标题定界
- B.标题为主长度为辅← 正确答案
- C.随机挑选一种方式
- D.全部按段落切分
答案:B。混合策略的第一优先级是标题边界,只有标题块超长时才用固定长度兜底,即「标题优先、长度兜底」。A 忽略了兜底,C 和 D 都不是标准做法。
Q4. 在 chunk 中携带标题路径的主要作用是什么?
- A.减小存储体积
- B.补全上文语境← 正确答案
- C.加快向量化速度
- D.替代正文内容
答案:B。标题路径相当于给 chunk 加了一段「来路标签」,让检索模型知道这段内容在文档中的位置,从而补齐语义上下文。它不会减少存储、不会加快向量化,更不能替代正文。
Q5. 切分 Markdown 时,哪些内容应当整块保留不切断?
- A.代码块内容← 正确答案
- B.表格行列← 正确答案
- C.文档空行
- D.标题层级
答案:AB。代码块和表格都有严格的结构,一旦被拦腰截断语义就损坏了,所以切分点必须绕开。空行本身没有语义,标题层级是切分的依据而非需要保留的块。
Q6. 重叠窗口主要解决什么问题?
- A.块间语义出现断层← 正确答案
- B.存储空间严重不足
- C.检索速度变得很慢
- D.标题特征难以识别
答案:A。固定边界切分后,同一句话可能被拆到两个块里,导致检索哪边都搜不全。重叠窗口让相邻块共享一段文本,把跨越边界的内容在两边都保留一份,从而解决语义断层。
Q7. 评价切分质量时,最核心的量化指标是什么?
- A.总块数尽可能少
- B.检索召回率达标← 正确答案
- C.单块体积尽量大
- D.解析速度更快
答案:B。Recall@k 直接衡量「答案所在的块有没有被检索回来」,是切分质量最直接的量化指标。块数少、体积大、解析快都不等于切得好,切分的最终目的是让检索命中。
Q8. 下列哪些做法能有效提升 Markdown 切分质量?
- A.压缩文档空行← 正确答案
- B.隔离保护代码块← 正确答案
- C.就近合并小标题← 正确答案
- D.粗暴截断长段落
答案:ABC。空行压缩减少噪声,代码块隔离避免语法断裂,小标题合并避免碎片上下文,这三者都能提升质量。粗暴截断长段落是典型的反面操作,会破坏语义。
Q9. 固定长度切分最典型的副作用是什么?
- A.文档加载速度更快
- B.正文被拦腰截断← 正确答案
- C.标题层级自动提升
- D.代码错误自动修复
答案:B。固定长度完全不看文档结构,切分点可能落在句子中间、代码块内部或表格行之间,把完整的语义单元切断。其余选项都不是它的典型副作用。
Q10. 关于切分质量评估,下列哪些做法是正确的?
- A.用真实问答验证召回← 正确答案
- B.人工抽样检查语义完整← 正确答案
- C.只统计块的字数大小
- D.对比不同策略消融结果← 正确答案
答案:ABD。真实问答算 Recall@k、人工抽检语义完整性、对不同切分策略做消融对比,三者都是科学的评估手段。只统计字数是自欺欺人,无法反映切分质量。