跳到主要内容

结构节点提取的四阶段流水线

上一篇讲到 TikaDocumentParserService.parse() 完成了文本提取和清洗。按照 parse() 的执行顺序,接下来就是 structureNodeExtractor.extract()——它负责从清洗后的纯文本中提取出文档的结构节点(标题、列表、步骤等)。这个方法内部是一条相当复杂的四阶段流水线,我们这篇单独拆开来讲。

四阶段流水线总览

先看一张总览图:

结构节点提取四阶段流水线
结构节点提取四阶段流水线

总入口:DocumentStructureNodeExtractor

/**
* 文档结构节点提取器。
* <p>
* 这是整条结构解析流水线的总入口,负责把“文档标题 + 解析后的纯文本”
* 逐步转换成可落库的结构节点候选列表。
* </p>
* <p>
* 内部固定采用四阶段流水线:
* 1. 信号提取:逐行识别标题、列表、步骤、正文、噪声等结构信号;
* 2. 歧义消解:对低置信度候选行做二次判定;
* 3. 层级构建:把扁平信号组装成带父子关系的 draft 树;
* 4. 树校验:修复路径、层级和兄弟关系,生成最终节点。
* </p>
*/
@AllArgsConstructor
@Component
public class DocumentStructureNodeExtractor {

/** 第一阶段:信号提取器,负责将文本逐行扫描并识别出标题、列表、正文等结构信号 */
private final DocumentStructureSignalExtractor signalExtractor;
/** 第二阶段:歧义消解器,对低置信度的信号借助 LLM 进行二次判定 */
private final DocumentStructureAmbiguityResolver ambiguityResolver;
/** 第三阶段:层级解析器,将扁平的信号列表组装成带有父子关系的草稿树 */
private final DocumentStructureHierarchyResolver hierarchyResolver;
/** 第四阶段:树校验器,修复无效父节点、重算深度、重建路径,输出最终候选节点 */
private final DocumentStructureTreeValidator treeValidator;

/**
* 文档结构提取的总入口方法。
* <p>
* 这个方法本身不承载复杂规则判断,它更像一个“编排器”:
* 负责把标题和正文送入四阶段流水线,并保证每一阶段的输出都成为下一阶段的输入。
* </p>
* <p>
* 其中有两个特别重要的边界语义:
* 1. 如果正文为空,不进入任何复杂规则,直接返回一个只有 DOCUMENT 根节点的结果;
* 2. 如果正文非空,哪怕后续没有抽出任何显式标题,也会在树构建阶段保留根节点,
* 从而保证下游始终面对的是一棵合法结构树,而不是空列表。
* </p>
*
* @param documentTitle 文档标题,可为空;为空时统一兜底为“文档”
* @param parsedText 解析后的纯文本正文
* @return 结构化节点候选列表,供后续结构节点落库、导航索引和图谱投影复用
*/
public List<DocumentStructureNodeCandidate> extract(String documentTitle, String parsedText) {
// 对标题和文本做空值保护和去除首尾空白
String normalizedTitle = StrUtil.blankToDefault(documentTitle, "文档").trim();
String normalizedText = StrUtil.blankToDefault(parsedText, "").trim();

// 如果正文为空,说明当前文档没有任何可供解析的结构内容;
// 此时直接返回一个根节点,既能保持数据结构稳定,也能让下游明确知道“文档存在但无结构”。
if (normalizedText.isBlank()) {
return List.of(new DocumentStructureNodeCandidate(
1,
DocumentStructureNodeTypeEnum.DOCUMENT.getCode(),
null,
0,
0,
0,
"",
normalizedTitle,
normalizedTitle,
"/document",
"",
"",
null
));
}

// 第一阶段:信号提取 —— 逐行扫描文本,识别标题、列表、噪声等结构信号
DocumentStructureSignalBatch signalBatch = signalExtractor.extract(normalizedTitle, normalizedText);
// 从信号批次中取出原始信号列表(防御性空值处理)
List<DocumentStructureSignal> rawSignals = signalBatch == null ? List.of() : signalBatch.signals();
// 从信号批次中取出所有行的规范化文本,供歧义消解时作为上下文窗口使用
List<String> allLines = signalBatch == null ? List.of() : signalBatch.contextLines();

// 第二阶段:歧义消解 —— 对“像标题又像列表”的候选行做二次判定,
// 目的是减少误把列表当标题、或误把标题当正文的情况。
List<DocumentStructureSignal> resolvedSignals = ambiguityResolver.resolve(normalizedTitle, allLines, rawSignals);

// 第三阶段:层级构建 —— 将扁平信号列表组装为带有父子关系的草稿节点树。
List<DocumentStructureNodeDraft> drafts = hierarchyResolver.resolve(normalizedTitle, resolvedSignals);

// 第四阶段:树校验与构建 —— 修复层级异常、重算深度、重建路径,输出最终候选节点。
return treeValidator.validateAndBuild(normalizedTitle, drafts);
}
}

这个类本身不承载复杂逻辑,更像一个"编排器"——把四个阶段串起来,每一阶段的输出作为下一阶段的输入。

第一阶段:信号提取(SignalExtractor)

这是整条流水线中代码量最大的一个阶段。DocumentStructureSignalExtractor 的职责是:逐行扫描文档纯文本,通过正则模式匹配和启发式规则,把每一行分类为标题、列表项、步骤、表格行、引用、正文、噪声等结构信号。

付费内容提示

该文档的全部内容仅对「码力全开」项目实战&技术讲解 知识星球用户开放

加入星球,一次获得完整项目资料、全栈技术知识库和长期答疑服务。

100万+字全栈技术知识库深入讲解技术核心、数据库、中间件和分布式等内容
8套热门的实战项目持续更新的企业级项目覆盖高并发、微服务、数据中台 和 AI Agent 等方向
AI 技术知识大模型面试详解覆盖 AI 模型原理、Agent、RAG、MCP、Skills、Harness 等核心知识点
文档 + 视频两种讲解形式既能系统阅读,也能跟随视频理解核心业务

完整项目实战资料

每套项目均包含从 0 到 1 讲解文档核心业务讲解视频

从基础项目到复杂业务场景,项目资料会持续更新。

8 套项目
  • 01Nexus Agent AI 智能体
  • 02Nexus Agent Pro 完全版
  • 03黑马点评Plus
  • 04大麦
  • 05大麦Pro
  • 06大麦AI
  • 07流量切换
  • 08数据中台

加入后还能获得

进入星球后,即可享受上述所有服务,保证不会再有其他隐藏费用。

从学习、面试到项目启动,都可以继续获得支持。

  • 1 对 1 解答项目和技术问题都可以提问
  • 针对性补充没有讲清楚的内容会继续补充
  • 面试与简历指导梳理回答技巧和项目亮点
  • 中间件云环境项目依赖可以直接接入使用
  • 面试后复盘被问住的问题可以继续交流
  • 远程问题解决项目启动问题可协助排查
知识星球二维码

扫码进入知识星球

  1. 打开微信,扫描左侧二维码,加入「码力全开」项目实战&技术讲解 知识星球
  2. 查看星球使用指导,获取完整项目讲解资料索引
解锁全部付费内容
🎁优惠