Ordered list
2026/9/16 18:57:15 网站建设 项目流程

Ordered list

【免费下载链接】spring-aiAn Application Framework for AI Engineering项目地址: https://gitcode.com/GitHub_Trending/spr/spring-ai

  1. Lorem ipsum dolor sitamet, consectetur adipiscing elit.Curabiturdiam eros, laoreet sitametcursus vitae, varius sed nisi.
  2. Cras sit amet quam quis velit commodo porta consectetur id nisi. Phasellus tincidunt pulvinar augue.
  3. Proin vel laoreet leo, sed luctus augue. Sed et ligula commodo, commodo lacus at, consequat turpis. Maecenas eget sapien odio.
    1. Pellentesque auctor pharetra eros, viverra sodales lorem aliquet id. Curabitur semper nisi vel sem interdum suscipit.
    2. Maecenas urna lectus, pellentesque in accumsan aliquam, congue eu libero. Ut rhoncus nec justo a porttitor.

Unordered list

  • Aenean eu leo eu nibh tristique posuere quis quis massa.
  • Aenean imperdiet libero dui, nec malesuada dui maximus vel. Vestibulum sed dui condimentum, cursus libero in, dapibus tortor.
    • Etiam facilisis enim in egestas dictum.
这份样本刻意覆盖了 Markdown 列表的全部典型形态: | 结构特征 | 示例位置 | 说明 | |---|---|---| | 二级标题 | `## Ordered list`、`## Unordered list` | 作为列表块的“分区头” | | 有序列表 | `1.`、`2.`、`3.` | 顺序编号项 | | 无序列表 | `*` 开头 | 项目符号项 | | 嵌套有序列表 | 第 3 项下的 `1.`、`2.` | 缩进 4 空格,两层结构 | | 嵌套无序列表 | 第 2 项下的 `*` | 缩进 4 空格,两层结构 | | 内联格式化 | `*amet*`、`**Curabitur**`、`_amet_` | 斜体、加粗、下划线斜体混合 | 这里使用的正文全部是 Lorem ipsum 占位文本,目的是把解析行为与语义内容解耦,让测试断言只关注“结构如何被转换”,这正是理解阅读器行为的最佳切入点。 ## 三、解析引擎:AST 访问者机制如何工作 `MarkdownDocumentReader` 的核心实现位于 [MarkdownDocumentReader.java](https://link.gitcode.com/i/56e1f2d33b053fcde3cc2ef2a870a277),其工作流程如下: 1. **资源解析**:构造器接收字符串形式的资源路径(如 `classpath:/lists.md`),通过 `PathMatchingResourcePatternResolver` 解析为 `Resource[]`,因此也支持 `classpath:/dir/*.md` 这类通配符批量加载; 2. **语法解析**:使用 `Parser.builder().build()` 构建 commonmark 解析器,将 Markdown 源码解析为 AST([源码 L104](https://link.gitcode.com/i/56e1f2d33b053fcde3cc2ef2a870a277#L104)); 3. **访问遍历**:`DocumentVisitor` 继承 commonmark 的 `AbstractVisitor`,以访问者模式遍历 AST 节点(标题、列表项、段落、代码块、引用块等),把匹配的文本累积进 `currentParagraphs`; 4. **文档产出**:在遇到“文档边界”节点时调用 `buildAndFlush()`,将累积文本与元数据打包成一个 `Document`。 关键访问方法及其作用如下(均在 [MarkdownDocumentReader.java](https://link.gitcode.com/i/56e1f2d33b053fcde3cc2ef2a870a277) 中): | 访问方法 | 行号 | 对列表解析的影响 | |---|---|---| | `visit(Heading)` | [L166-L169](https://link.gitcode.com/i/56e1f2d33b053fcde3cc2ef2a870a277#L166-L169) | 遇到标题先 `buildAndFlush()`,标题成为新文档的起点 | | `visit(Text)` | [L232-L242](https://link.gitcode.com/i/56e1f2d33b053fcde3cc2ef2a870a277#L232-L242) | 标题文本写入 `category=header_N`、`title` 元数据;普通文本追加进 `currentParagraphs` | | `visit(ListItem)` | [L192-L195](https://link.gitcode.com/i/56e1f2d33b053fcde3cc2ef2a870a277#L192-L195) | 每个列表项之间插入一个空格(`translateLineBreakToSpace`) | | `visit(SoftLineBreak)` / `visit(HardLineBreak)` | [L180-L189](https://link.gitcode.com/i/56e1f2d33b053fcde3cc2ef2a870a277#L180-L189) | 列表项内部的软/硬换行一律转为空格 | | `buildAndFlush()` | [L250-L265](https://link.gitcode.com/i/56e1f2d33b053fcde3cc2ef2a870a277#L250-L265) | 用 `String.join("", currentParagraphs)` 拼接文本并构建 `Document` | 从源码结构可以推断:**列表在 AST 中只是普通节点的容器,阅读器并没有为 `List` 节点定义专门的访问方法**,列表的序号(`1.`、`2.`)和项目符号(`*`)也不会进入文本。列表内容之所以能完整保留,完全依赖 `ListItem` 内部 `Text` 节点的累积,以及 `translateLineBreakToSpace()` 对换行的空格化处理。 ## 四、测试验证:testLists 的期望输出逐段解读 `MarkdownDocumentReaderTest` 中的 `testLists()` 用例([L254-L266](https://link.gitcode.com/i/43239e2108755babd36cfc2b4626eb0e))是理解列表解析行为的权威依据: ```java @Test void testLists() { MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/lists.md"); List<Document> documents = reader.get(); assertThat(documents).hasSize(2) .extracting(Document::getMetadata, Document::getText) .containsOnly( tuple(Map.of("category", "header_2", "title", "Ordered list"), "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur diam eros, laoreet sit amet cursus vitae, varius sed nisi. Cras sit amet quam quis velit commodo porta consectetur id nisi. Phasellus tincidunt pulvinar augue. Proin vel laoreet leo, sed luctus augue. Sed et ligula commodo, commodo lacus at, consequat turpis. Maecenas eget sapien odio. Pellentesque auctor pharetra eros, viverra sodales lorem aliquet id. Curabitur semper nisi vel sem interdum suscipit. Maecenas urna lectus, pellentesque in accumsan aliquam, congue eu libero. Ut rhoncus nec justo a porttitor."), tuple(Map.of("category", "header_2", "title", "Unordered list"), "Aenean eu leo eu nibh tristique posuere quis quis massa. Aenean imperdiet libero dui, nec malesuada dui maximus vel. Vestibulum sed dui condimentum, cursus libero in, dapibus tortor. Etiam facilisis enim in egestas dictum.")); }

该用例断言了两个文档,结合期望文本可以提炼出列表解析的四条硬规则:

  1. 标题即文档边界## Ordered list## Unordered list各产生一个Document,整个列表(含嵌套项)被并入标题所在的同一文档,绝不会被拆分成“一项一文档”;
  2. 序号与符号被丢弃:输出文本中看不到1.2.*等序号或项目符号,列表的“顺序语义”不保留;
  3. 嵌套列表被扁平化:有序列表第 3 项下的两层嵌套项(Pellentesque...Maecenas urna...)被直接拼接进同一文档,与父列表项之间以空格衔接;
  4. 内联格式被剥离*amet***Curabitur**_amet_等格式标记全部消失,只留下纯文本字面量(如consectetur adipiscing elit.),因为格式节点(Emphasis、Strong)在 AST 中只是容器,阅读器只收集其Text子节点的字面量。

这一行为与同目录下的其他测试资源形成对照:only-headers.md验证“标题+段落”的切分,code.md验证代码块独立成文档,horizontal-rules.md验证水平线切分——而lists.md专门验证列表结构在单个文档内的扁平化合并,是整个模块测试矩阵中不可替代的一环。

五、元数据行为:category 与 title 的生成规则

testLists的断言可以看到,每个文档的元数据为:

Map.of("category", "header_2", "title", "Ordered list")

这两条元数据的生成位置在visit(Text)方法中(L232-L242):

@Override public void visit(Text text) { if (text.getParent() instanceof Heading heading) { this.currentDocumentBuilder.metadata("category", "header_%d".formatted(heading.getLevel())) .metadata("title", text.getLiteral()); } else { this.currentParagraphs.add(text.getLiteral()); } super.visit(text); }

也就是说:

  • categoryheader_NN为标题级别。## Ordered list是二级标题,故category=header_2;若为#则为header_1
  • title:标题文本字面量,如Ordered listUnordered list

这套元数据对 RAG 检索有直接价值:向量化入库后,可以基于category字段筛选出“标题下的列表摘要”类文档,或基于title做按标题聚合的过滤查询,而无需回源解析原文。

六、配置项对解析策略的影响

MarkdownDocumentReaderConfig(MarkdownDocumentReaderConfig.java)通过 Builder 提供四个配置项,全部有默认值:

配置方法默认值作用
withHorizontalRuleCreateDocument(boolean)false水平线(---***等)分隔的文本是否各自生成新Document。关闭时整个文件合并为一个文档
withIncludeCodeBlock(boolean)false代码块是否并入所在段落文档。默认false表示代码块独立成文档,且带category=code_blocklang元数据
withIncludeBlockquote(boolean)false引用块是否并入所在段落文档。默认false表示引用块独立成文档,带category=blockquote元数据
withAdditionalMetadata(String, Object)空 Map为所有产出的Document追加自定义元数据,如withAdditionalMetadata("service", "docs")

需要注意:这四个配置项均不改变列表的合并行为。无论horizontalRuleCreateDocument取何值,列表项都始终属于其标题所在文档——列表解析是阅读器的固定行为,配置只影响代码块、引用块和水平线这三个“可分离元素”。例如测试 testDocumentDividedViaHorizontalRules 展示了水平线开关对文档数量的影响(开=7 个文档,关=1 个文档),可作为对照实验理解“可配置分离”与“列表固定合并”的差异。

七、实战:在 Spring AI 应用中解析 Markdown 列表

7.1 基础用法

引入依赖后(模块坐标org.springframework.ai:spring-ai-markdown-document-reader,版本与父 POM 的spring-ai-parent保持一致,当前仓库版本为2.0.2-SNAPSHOT,见 pom.xml),最简单的列表解析:

MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/lists.md"); List<Document> documents = reader.get(); for (Document document : documents) { System.out.println(document.getMetadata()); // {category=header_2, title=Ordered list} System.out.println(document.getText()); // 扁平化后的列表纯文本 }

7.2 批量加载与自定义元数据

MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder() .withAdditionalMetadata("source", "docs-guide") .withAdditionalMetadata("lang", "zh") .build(); // 支持通配符,一次加载目录下所有 md 文件 MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/docs/**/*.md", config); List<Document> documents = reader.get(); // 每个文档的 metadata 都会带上 source=docs-guide、lang=zh

7.3 与向量存储的衔接

Document是 Spring AI 的通用抽象(定义于spring-ai-commons),解析结果可直接交给VectorStore做嵌入入库:

List<Document> documents = reader.get(); vectorStore.add(documents); // documents 自带 category/title 元数据,便于后续过滤检索

【免费下载链接】spring-aiAn Application Framework for AI Engineering项目地址: https://gitcode.com/GitHub_Trending/spr/spring-ai

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询