[控场AI]
· 4 分钟阅读· 2,124 字

HTML转Markdown缩进陷阱:列表内代码块为何消失

HTML转Markdown缩进陷阱:列表内代码块为何消失

HTML转Markdown时按视觉列缩进会导致列表内代码块被静默吞掉,威胁RAG管线。

文章讲述了一位开发者在调试HTML-to-Markdown转换器时,花费近一周定位到一个隐蔽bug:列表项内的围栏代码块被静默转换为普通段落。根本原因是转换器在DOM转Markdown后按视觉列位置重新缩进,而非按AST深度跟踪列表项内容列,导致代码围栏缩进少了一个空格,被CommonMark解析器当作惰性延续吞掉。修复方法是在DOM遍历时显式跟踪列表项内容列,并将缩进应用到围栏的每一行。作者强调,这类静默丢失对RAG和Agent管线尤其危险,因为输出看似正常但内容已残缺,embedding和检索都会照常运行却基于不完整的信息。核心教训是:永远不要在生成的Markdown上做后处理缩进,应在发射阶段从解析树一次做对。

一个看似正常的转换结果,藏着静默丢失

把 HTML 转成 Markdown,大多数时候输出看起来都没问题:标题正常、列表正常、链接正常。但有一类 bug 极其阴险——输出没有任何报错,也没有截断,只是内容悄悄没了。

一位开发者在调试自己的 HTML-to-Markdown 转换器时,花了将近一周时间,才定位到一个藏在嵌套列表里的问题:列表项内部的围栏代码块(fenced code block)被转成了普通段落。顶层代码块没问题,列表也没问题,偏偏“列表项里包着代码块”这种 GitHub README 最常见的结构,被无声地压平了。

rss source: My HTML-to-Markdown converter kept eating code blocks inside list items — indentation is a contract

缩进不是排版细节,而是语法契约

问题出在“重新缩进”这一步

作者在 DOM 转成 Markdown 之后,做了一遍 re-indentation(重新缩进),让嵌套内容对齐列表深度。问题就在这里:Markdown 规定,列表项里的围栏代码块,必须缩进到与列表项内容起始列对齐——无序列表 - 标记是 4 个空格,或标记宽度加一。

而他的实现是按“视觉列位置”重新缩进的,不是按 AST 深度。结果:处于列表深度 2 的代码围栏被缩进了 3 个空格,而不是 4 个。CommonMark 解析器于是把这个围栏当成上一段落的“惰性延续”(lazy continuation)——这不是语法错误,但代码块直接消失了。

修复方式很朴素,但方向对了

真正的修复不花哨:在 DOM 遍历过程中显式跟踪列表项内容列,而不是事后处理。根据父级列表标记计算每个块的必需缩进,并且把这个缩进应用到围栏的每一行,而不只是第一行。

这类 bug 只有在人手动把输出和源文件做 diff 时才会暴露——你才会突然发现,一个函数定义不见了。作者坦言,这已经是他第二次因为“把 Markdown 当纯文本格式对待”而踩坑,而它本质上是一门对空白敏感的语法(whitespace-sensitive grammar)。

惰性延续(lazy continuation) 是 CommonMark 规范里一个容易被忽视的解析规则。在列表项中,如果某一行没有正确缩进到列表项内容起始列,但又能被解析为段落文本的延续,解析器不会报错,而是把它当作上一段落的一部分继续处理。这意味着一个缩进不足的围栏代码块起始行 ``` 会被吞进段落,导致整个代码块坍缩成普通文字。这不是语法错误,因此大多数转换器、linter 和渲染管线都不会发出任何警告。要避免这种静默失败,就必须理解 Markdown 的空白规则本质上是在声明结构层级——缩进量直接决定节点归属。

为什么这类 bug 对 RAG 和 Agent 管线特别危险

作者在总结里强调了一个关键判断:如果你正在为 agent 的上下文窗口构建“文档转文本”管线,这类 bug 属于最阴险的一种,因为输出看起来合理。

没有错误,没有截断,只是内容静默缺失。而你的 RAG 管线会毫无察觉地把一份缺少使用示例的 README 拿去做 embedding——检索、召回、生成全都跑得通,但答案的基础已经残缺。

核心教训只有一句:**永远不要在生成的 Markdown 上做后处理缩进。**要在发射(emission)阶段、从解析树里,在嵌套深度还已知的时候一次做对。

作者最终把整个转换器打包成了一个在线工具,方便直接使用。

Embedding 是将文本映射为高维向量的过程,RAG(检索增强生成)管线依赖它来计算查询与文档片段的语义相似度。当一份 README 中的代码示例被静默压平为普通段落时,embedding 仍然能成功计算——模型不会检测到内容缺失。问题在于,代码示例通常承载着最具体、最可操作的语义信息,缺失后会导致检索召回的片段在语义上偏向描述性文字,而丢失了实际用法。用户得到的答案可能语法正确、语气流畅,却缺少关键的可执行细节。更棘手的是,这类退化无法通过监控 embedding 维度或向量相似度阈值来发现,只能通过源文档与转换结果的语义比对来暴露。

给构建文档管线的开发者三条可操作建议

  1. 缩进基于 AST 深度,而非视觉列:在每个节点发射时就确定所需缩进,并应用于该块的所有行。
  2. 把“列表项内代码块”列为必测用例:README、教程、API 文档里这种结构极常见,却最容易在转换中被压平。
  3. 输出与源做 diff,而不仅是肉眼扫:这类静默丢失不会报错,只有对比才能捕捉到“某段代码消失了”。

Markdown 的缩进从来不是排版偏好,而是一份契约。破坏契约的代价,往往是内容本身。

#HTML转Markdown#Markdown缩进#代码块丢失#CommonMark#RAG管线#文档转换工具#Agent上下文
分享:

相关推荐