文档上传到RAG检索完整链路讲解

一、先把整条链路的位置和切分点立起来

这条链路解决的事情很具体:用户从前端点"上传"那一刻起,到这份文档"解析完成、切块策略也推荐好、随时可以进入索引构建"为止,中间发生的所有事

整条链路被一刀切成两段:

第一段是同步上传——用户点了上传按钮、到接口返回响应这中间发生的事。这一段的设计目标只有一个:接口快,亚秒级返回。重活一概不干。

第二段是 Kafka 异步处理——接口返回之后,消费者后台慢慢跑的那一长串。Tika 解析、结构树提取、画像生成、策略推荐,全在这段里。

为什么要这么切?因为这两段的诉求根本不一样。

上传接口必须快,因为它直接挂在前端用户的手指头上。用户点完按钮等三十秒看不到反馈,他要么以为系统挂了再点一次,要么直接走人。所以上传接口能做的事被严格限定在四件——校验、存文件、写记录、发消息——加起来全是网络 IO 和数据库写入,CPU 几乎不动,能控制在一秒内。

后面的解析就完全不同了。Tika 解析一份大 PDF 可能就要十几秒,结构树提取里如果调到 LLM 做歧义消解又要几秒,画像生成再调一次 LLM 又要几秒,最后还要算 token、评结构等级、推策略——加起来跑个几分钟都正常。这种活儿如果同步做,用户那边浏览器早就超时了。

所以这条链路贯穿一个核心思想:轻同步、重异步。同步只做"接进系统",异步才是"真正干活"。

二、同步上传段:七步走,每一步都有它的位置

第 1 步:文件校验

入口在 DocumentManageController.upload(),第一件事就是校验——格式对不对、大小超不超。

这一步看着简单,但要放在最前面。任何不该进系统的东西,都要拦在系统门外,不能让它污染后面的存储和数据库。校验失败直接返回 400,也省得后面所有步骤白做一遍。

第 2 步:读字节、生成文档 ID

DocumentManageServiceImpl.getFileBytes() 把文件内容读进内存,同时给这份文档分配一个全局唯一的 documentId。

这里有个细节值得讲——ID 是在写库之前就生成好的,不依赖数据库自增。原因是后面 MinIO 的对象路径要用 documentId 拼接,如果等数据库 insert 完拿自增 ID,对象存储和数据库就要绑成一个事务,太重。先生成 ID、再存对象、再写库——三件事彼此解耦,每一步失败的爆炸半径都小。

第 3 步:上传到 MinIO

DocumentStorageService.uploadOriginalFile() 把原始文件丢到 MinIO。

为什么要用对象存储而不是直接塞数据库?因为文件可能几十 MB 甚至几百 MB,大文件存数据库会把主从复制、备份、查询性能全部拖慢。MinIO 这种对象存储天生就是干这个的——大文件吞吐高、分布式、便宜。数据库只存路径,内容存对象存储,是这种系统的标准切法。

第 4 步:构建文档主记录

在内存里 new 一个 SuperAgentDocument 实体,字段填好。

这里特别要看的是 parseStatus = PARSING——还没真正解析,状态就先标成"解析中"。为什么?因为这条记录一旦落库,前端就能查到这份文档的存在了,必须给一个能反映真实状态的字段——告诉前端"这份文档系统已经收到,正在解析"。如果一开始就标成 PARSE_SUCCESS,前端会以为已经能用,结果点进去查不到任何切块;如果什么都不标,前端就不知道该展示什么。中间态是为了让前端有东西可展示

第 5 步:构建任务记录

new 一个 SuperAgentDocumentTask,taskType 填 PARSE_ROUTE(这次任务是"解析+路由"),taskStatus 填 NEW(已创建、还没被消费者接手)。

这里就要回答一个常见的疑问:为什么不把状态字段都堆在 document 表里?为什么要分一张 task 表?

因为 document 是"资产",task 是"动作"——两者的生命周期完全不一样。

一份文档可能被解析很多次(重新上传新版本、策略变了要重跑、解析失败要重试),但 document 记录始终只有一条。每次解析都是一次新的"动作",都对应一条新的 task 记录。如果把这两个状态混在 document 表上,重新解析时要么覆盖历史、要么互相打架。拆成两张表,document 反映"这份资产现在怎么样",task 反映"这次动作进行到哪了"——视角清晰、互不干扰。

第三张表 task_log 再补一刀,记录每一步的事件流——什么时候开始、什么时候完成、哪里出错。资产、动作、证据,三张表各管一个维度

第 6 步:手动事务提交(这一步是整段同步的核心)

TransactionTemplate.execute() 把 document 和 task 的 insert 包在同一个事务里,事务结束、提交、然后才去发 Kafka 消息。

为什么不用 @Transactional 注解?为什么要手动控制事务边界?

因为这里要解决的是一个特别隐蔽但特别致命的顺序问题:Kafka 消息一定要在数据库事务 commit 之后才能发

设想反过来——事务还没提交就把消息发出去了。消费者那边手速一快,拿着 documentId 去查数据库——查不到。因为这边事务还没提交,记录对它不可见。结果就是"消息已经投递了、消费者已经消费了、但数据库里啥也没有",任务直接丢。

@Transactional 是基于 AOP 切面的,事务边界的精细控制比较受限。TransactionTemplate 让你能精确控制"事务在哪一行提交"——提交完之后才执行下一行的 Kafka 发送。哪怕 Kafka 发送失败,前面的数据是已落库的,可以靠定时任务扫"长期处于 NEW 状态的 task"做补偿重试。先 DB 后消息,是分布式系统里处理"本地事务+消息队列"的一个铁律。

但要老实说——这套朴素方案不是百分百严格的分布式一致性。事务 commit 之后、Kafka 还没发出去这一瞬间如果服务挂了,消息就丢了。完美方案是事务消息(RocketMQ Transactional Message)或者本地消息表。但工程上接受"小概率不一致+消费端幂等+定时补偿"的组合,性价比最高

第 7 步:发 Kafka 消息

kafkaProducer.sendParseRoute() 把消息发出去。消息体里只有 documentId 和 taskId 两个字段,别的什么都不放。

为什么消息这么瘦?

因为 Kafka 不是数据库,它的角色是"通知",不是"数据"。消息只负责告诉消费者"该干活了,文档 ID 和任务 ID 在这里",至于这份文档叫什么名字、什么类型、谁上传的——消费者自己回数据库查就行。

胖消息的两个坑:第一是消息体大,Kafka 网络压力高;第二也是更要命的,消息里的数据可能跟数据库里的最新状态不一致。比如消息发出去那一刻文档名是 A,几秒后管理员把文档名改成了 B,消费者拿着旧文档名 A 去处理,处理结果就错了。真实状态永远以数据库为准,消息只传指针。

到这里同步段就结束了,接口返回成功响应,整段耗时压在亚秒级。剩下的事 Kafka 消费者后台慢慢干。

三、Kafka 异步段:从拿到消息到推荐方案落库,21 步全过

第一小段:加载上下文 + 文件解析(步骤 8-11)

第 8 步:消费入口

DocumentKafkaConsumer.consumeParseRoute() 接到消息。

这层故意做得非常薄——反序列化消息、调一下 handleParseRoute(documentId, taskId),就完了。真正的业务逻辑全在 handleParseRoute 里。

为什么要这么分?两个好处:第一,Consumer 这层是 Kafka 框架细节集中地——监听配置、序列化、消费组——业务代码混进来会让这层很难维护。第二,****handleParseRoute 抽出来之后,可以被多个入口复用——除了 Kafka 触发,后台管理界面里"重新解析"按钮、定时任务做补偿,都可以直接调它,不用非走 Kafka 不可。

第 9 步:加载上下文 + 推进状态

DocumentAsyncProcessServiceImpl.handleParseRoute(),第一件事就是用 documentId 和 taskId 回查数据库,把 document 和 task 完整对象拿出来——消息里只有 ID,数据从 DB 来,呼应前面瘦消息的设计。

拿到对象之后,把 task 状态从 NEW 推到 RUNNING。这个状态切换的意义是让监控可以一眼看出系统状态:NEW 一直堆积说明 Consumer 消费跟不上,RUNNING 一直堆积说明单个任务处理太慢——两种问题对应的处理动作完全不同。

第 10 步:从 MinIO 重新下载原始文件

storageService.downloadObject() 把之前存进去的原始文件捞回来。

上传接口里不是已经把文件字节读到内存里了吗?为什么不直接把字节传过来?

因为上传接口和异步消费完全是两个进程、两个线程、两次执行。上传接口早就 return 了,它的内存里的字节早就被 GC 回收了。异步消费这边只能拿到消息里的 documentId,唯一拿到原始文件的方式就是回 MinIO 下载。这也是为什么前面要把文件存对象存储——它是异步消费唯一的真实数据源。

第 11 步:Tika 解析

DocumentParserService.parse() 底下走的是 TikaDocumentParserService,用 Apache Tika 把不同格式的原始文件——PDF、Word、Excel、HTML、Markdown——统一抽成纯文本。

但这里要强调一点:Tika 抽出来的文本不能直接用。里面常见一堆问题——多余空格、PDF 的硬换行、页眉页脚、控制字符、乱码字符。所以解析阶段还要做文本清洗,把这些噪声去掉。这一步看着不起眼,但整个知识库的质量地基就在这里——清洗没做好,后面的标题识别、切块、向量化全部受影响,垃圾进、垃圾出。

第二小段:结构节点提取——四步流水线(步骤 12)

这一步是整条链路里最值得讲的部分。它在做一件事:把扁平的纯文本,识别成一棵带父子关系的文档结构树

很多人做 RAG 是直接把纯文本丢去切块,省了这一步。但这样会丢一个非常关键的信息——章节归属。用户问"第三章讲了什么",没有结构树根本答不上来。

入口是 DocumentStructureNodeExtractor.extract(),里面拆成四步:

第一步:信号提取DocumentStructureSignalExtractor)。逐行扫描清洗后的文本,用 15 种规则识别每一行的"角色"——是 Markdown 标题(# 开头)、是中文章节("第 X 章")、是多级编号("1.2.3")、是中文大纲("一、")、是列表项、是步骤、还是普通正文。这一步原则是宁可多标、不要漏标——漏掉一个标题后面就补不回来了,多标了后面还能在歧义消解里修正。

第二步:歧义消解DocumentStructureAmbiguityResolver)。有些行天生有歧义。比如 "1、项目背景" 这种——可能是一级标题,也可能只是个列表项;比如 "一、" 开头的——可能是大纲,也可能就是序号。这种规则定不下来的,就交给 LLM 做二次判定。

这就是规则+LLM 混合策略的核心场景。规则负责大部分确定性场景——毫秒级、零成本;LLM 只处理边界和歧义的少量行——慢、贵、但量可控。如果每一行都问 LLM,成本和延迟都扛不住,稳定性还不如规则;但反过来全靠规则,那些天生有歧义的行又判不准。两者搭配是性价比最高的组合

第三步:层级构建DocumentStructureHierarchyResolver)。把前面识别出的扁平信号组装成父子关系。看到 1、1.1、1.1.1、1.2、2 这样的序列,要能正确判断 1.1.1 是 1.1 的儿子、1.2 还是 1 的儿子、2 和 1 是兄弟。实现上用一个 section 栈做线性扫描——栈本身就编码了从根到当前位置的完整路径,遇到新标题做"弹栈到合适层级、再压栈"两个动作。用栈这种数据结构让算法变简单,比靠正则硬切优雅得多。

第四步:树校验DocumentStructureTreeValidator)。前面的构树过程不可能完美——可能有重复层级要折叠、有非法的父子关系要修复、深度要重算、规范化路径要重建。这一步做"体检+修复",确保产出的树干净可用。

最后输出的每个节点上都带两套路径:canonicalPath 给机器用——格式稳定、做唯一定位;sectionPath 给人看——用真实标题拼起来,比如"产品手册 / 第二章 系统设计 / 2.1 架构设计",前端目录展示和 RAG 引用来源都用它。两套路径职责不同,不是冗余。

第三小段:统计、切分、质量评估(步骤 13-16)

第 13 步:标题数量统计

countHeadings() 数出文档里有多少个标题节点。这个数后面要决定结构等级——5 个以上算结构清晰、2 个以上算有一定结构、再少就是结构差。

第 14 步:段落切分

extractParagraphs() 把文本按段落切开。这一步是预处理,给后面的统计和切块提供基础单位。

第 15 步:统计 + 质量评估

这里实际跑了三个评估:

estimateTokenCount() 估算 token 数。这里不用真正的 tokenizer(比如 tiktoken),而是用一个简化算法——中文按字算、英文按词算、其他字符按 4:1 估。真 tokenizer 慢、重、还和模型绑定,这里 token 只用来"统计展示",不参与计费、不参与限流,轻量估算就够,误差 ±20% 完全可以接受

evaluateStructureLevel() 评结构等级。基于标题数量分档——HIGH / MEDIUM / LOW / UNKNOWN。这个等级直接影响后面策略推荐时怎么选切块方式。

evaluateContentQuality() 评内容质量。看两个指标——文本是不是太短(可能是解析失败或扫描版 PDF)、乱码字符( 这种替换字符)的比例。质量分 LOW / MEDIUM / HIGH,质量差的话后面策略推荐会更谨慎,甚至考虑 LLM 智能切块兜底。

第 16 步:打包结果

把所有产物——清洗后的文本、结构节点、统计数据、质量等级——统一封装成一个 DocumentAnalysisResult 对象。

这个对象很关键——它是解析阶段的标准输出。后面的策略推荐根本不用关心 Tika 怎么解析、文本怎么清洗、结构树怎么提取,只需要看这个结果对象就行。接口和实现解耦,后面如果要换解析引擎或者升级结构树算法,只要保证产出的 DocumentAnalysisResult 结构不变,下游代码完全不用动。

第四小段:产物持久化(步骤 17-19)

第 17 步:上传清洗后的纯文本到 MinIO

storageService.uploadParsedText() 把清洗后的纯文本存成 parsed-text.txt,路径写回 document 表的 parseTextPath 字段。

为什么要把这份文本独立存一份,而不是每次切块都重新解析原始文件?

因为解析很贵——Tika 跑一遍、清洗一遍、结构提取一遍,加起来不便宜。而切块阶段只需要纯文本,结构信息已经在数据库里了。所以把"解析的产出"独立持久化,后续切块、索引、摘要、画像都可以直接复用,不用每次重新解析。

更重要的一点——重复解析可能因为模型/库版本差异产生不一致结果。把解析产物固化下来,从此切块基于稳定的输入,结果可复现。这是"解析"和"切块"完全解耦的关键。

第 18 步:结构节点落库

DocumentStructureNodeService.replaceDocumentNodes()——注意是 replace 不是 insert,全量替换。同一文档重新解析时,旧节点先删干净、新节点全量写入。

为什么要全量替换?因为如果只做增量更新,旧结构会残留——前端目录会同时显示新旧标题、Neo4j 图谱会有断裂的边。全量替换保证结构永远是最新的、一致的

第 19 步:导航产物同步

syncNavigationArtifacts() 把结构节点同步到两个外部系统:ESNeo4j。这两个都是可选的,看配置开关。

但这里要把功能讲准——

ES 这边写入的是结构节点(章节标题、路径),用于关键词检索。后续用户搜某个章节标题或者关键术语时,ES 能秒级命中并标黄定位。

Neo4j 这边把结构节点之间的层级关系投影成图——文档节点连接到一级章节、一级章节连接到二级章节,章节之间还有"前一兄弟、后一兄弟"的边。这样后面用户问"上一章讲了什么"、"第三章下面有哪些内容",可以靠图谱关系沿着边导航,而不是只靠向量相似度硬搜。

这一步是为后面 RAG 三通道检索提前埋好的种子。

第五小段:画像生成 + 任务推进(步骤 20-22)

第 20 步:文档画像生成

DocumentProfileService.generateProfile() 调 LLM 给整份文档打一组标签——文档类型(手册、规范、合同、纪要……)、主题、摘要、关键词。

相当于给文档发了一张"身份证"。后续用户问问题时,可以先用画像做粗筛——比如用户问"差旅报销怎么走流程",先用画像找到"财务类、报销主题"的文档,再在这些文档的 chunk 里做精细检索,比直接全库向量搜效率高得多。

这一步调 LLM,是异步链路里第二次用到大模型(第一次是结构提取里的歧义消解)。两次 LLM 调用都符合规则+LLM 混合的核心精神——只在规则搞不定的地方用 LLM,量可控、成本可控。

第 21 步:写解析完成日志

taskLogService.saveLog() 写一条 task_log,记录"解析阶段完成"这个事件。包括完成时间、耗时、本阶段的关键产物 ID。日志是事后排查的眼睛——任务表的字段会被覆盖(status 从 RUNNING 变成 SUCCESS),但日志是追加写的,串起来就能完整还原整个执行过程。

第 22 步:任务阶段推进

把 task 的 currentStageCONTENT_PARSE 推到 STRATEGY_ROUTE。意思是"解析这一阶段干完了,接下来该推荐策略了"。

第六小段:策略推荐(步骤 23-28)

第 23 步:策略推荐

DocumentStrategyService.recommendStrategy() 是策略推荐的核心。它根据前面 DocumentAnalysisResult 里的指标——结构等级、内容质量、token 数、段落数——决定四种切块策略各自适不适合:

策略 适用条件 一句话理由
结构化切块 文件类型适合 + 结构等级高 顺着章节切,边界最自然
递归切块 全文很长或单段超长 长度兜底,防止单块太大
语义切块 长 + 段落多 + 质量好 按主题边界优化,但成本较高
LLM 切块 质量差 + 配置允许 救火用,不是默认

选完策略之后还有一个关键设计——产出的不是单一切块方案,而是 Parent / Child 双层方案。Parent 块大、保留完整上下文、不进向量库;Child 块小、进向量库、负责被检索命中。检索时先在 Child 里召回小块,通过 parentId 反查到对应的 Parent 大块,把大块塞给模型回答。搜小块、读大块,既精准又完整。

为什么要这样?因为 RAG 里有一个根本矛盾——检索希望块越小越好(向量语义聚焦、召回精准),但回答希望块越大越好(上下文完整、不答半截)。Parent/Child 双层把这两件事彻底分开——小块管召回、大块管上下文,互不打架。

第 24-25 步:方案主记录 + 步骤明细落库

SuperAgentDocumentStrategyPlan(方案主记录,记元信息)和 SuperAgentDocumentStrategyStep(方案下的每一步具体操作)。

注意这里只是把推荐方案落库等用户确认,并不真正执行切块。状态置为 WAIT_CONFIRM,让用户在前端看到推荐结果,必要时还能调整。这种设计把"推荐"和"执行"彻底解耦——推荐层只判断该怎么切,执行层等用户确认后再真正动手。整个流程更可控,方便后续失败重试和过程回放

第 26 步:更新文档主表

把 document 的 parseStatusPARSING 切到 PARSE_SUCCESSstrategyStatus 切到 RECOMMENDED

这两个状态切换之后,前端列表页就能展示"解析成功、待确认方案"——用户点进去能看到推荐方案、能审核、能点"确认"开始下一段索引构建。

第 27 步:任务成功收尾

finishTaskSuccess() 把 taskStatus 从 RUNNING 切到 SUCCESS,记录 finishTime。算下来 finishTime - startTime 就是这次任务的实际处理耗时,startTime - createTime 是它在 Kafka 队列里的等待时间——三个时间字段拆开记,能精确分析瓶颈在排队还是处理

第 28 步:写策略推荐完成日志

再写一条 task_log,整个异步链路结束。


四、状态机:三个维度同时在流转

整条链路涉及三个独立的状态字段同时流转。把它们拆开看,逻辑就清楚多了:

parseStatus(文档解析状态): PARSINGPARSE_SUCCESS / PARSE_FAILED

taskStatus(任务执行状态): NEWRUNNINGSUCCESS / FAILED

strategyStatus(策略状态): WAIT_RECOMMENDRECOMMENDEDCONFIRMEDEXECUTINGEXECUTE_SUCCESS

注意 strategyStatus 跨链路——前两步(WAIT_RECOMMENDRECOMMENDED)在本链路完成,后面的 CONFIRMED → EXECUTING → EXECUTE_SUCCESS 是在下一段索引构建链路完成的。

为什么要拆三个独立字段,而不合成一个?因为三个维度互不耦合——解析成功了策略不一定推荐完、策略推荐完了任务不一定结束、任务结束了文档不一定可用。如果合成一个字段,组合数会爆炸——PARSING_RECOMMENDED_RUNNING 这种 case 写都写不过来。多状态字段是工程上的合理设计


五、异常处理:每一段都有兜底

整条链路有十几个步骤,每一步都可能挂。处理思路是按发生位置分层:

Kafka 发送失败: 上传接口直接返回错误。因为这里走的是 .get() 同步发送,发送失败会让事务回滚——document 和 task 记录都不会留下。前端看到失败提示让用户重试就行,不会出现"任务在但消息没发"的状态。

异步解析失败: 把 parseStatus 标 PARSE_FAILED、taskStatus 标 FAILED、把错误信息写进 errorMsg 字段、记一条 ERROR 级别的 task_log。库里所有相关记录的状态都标记成失败,不留半成品。同时定时任务会扫长期处于 RUNNING 状态的 task 做补偿重试——可能是消费者挂了、可能是 LLM 临时不可用。

结构提取异常: 这里做了特殊处理——四阶段流水线里如果某一阶段崩了,捕获异常、降级处理,但不阻断整条主流程。因为即便结构树没建好,文档的纯文本和画像还能用,切块策略可以降级到递归切块兜底,不至于整份文档作废。这就是多层兜底的精神——每个步骤都有降级路径,不让单点失败打断整条主链路。

消息消费幂等: 消费端拿到消息后会先查 task 记录——如果 task 不存在(脏消息)直接丢弃;如果 task 状态已经是 SUCCESS(重复消费)直接跳过。这样即使 Kafka 投了两次或者补偿任务和正常消费撞上,结果也不会出错。


六、和 RAG 三通道检索的衔接

讲到最后回到一个根本问题——这条链路干的活,最终是为了什么

答案是:为 RAG 检索提供高质量的结构化数据。具体对应三个通道:

向量检索通道——后面索引构建阶段会基于 Child 块生成 embedding 写入向量库(PGVector 或 Milvus),用户提问时做语义相似度匹配召回最相关的片段。Child 块的边界、质量、token 数都由本链路推荐的切块策略决定

关键词检索通道——本链路里第 19 步已经把结构节点同步进了 ES,后面切块的 chunk 也会写进 ES,用户提问时做 BM25 关键词匹配召回包含关键术语的片段。这一通道补足向量检索的盲区——产品型号、唯一编号、专有名词这些向量搜不准的字面命中场景。

图检索通道——本链路第 19 步同步进 Neo4j 的结构节点关系,可以让用户沿着"章节 → 小节 → 段落"的层级路径做结构化检索,回答"上一章讲了什么"、"第三章包含哪些内容"这类问题。

最终用户提问时,三通道并行召回、用 RRF 之类的算法融合排序、再上一个 Cross-Encoder 精排——这才是工业级 RAG 检索的全貌。本链路是这一切的"上游供应商",产出质量直接决定下游的检索效果。垃圾进、垃圾出,这条铁律在 RAG 链路里特别明显——所以才要在解析、清洗、结构提取、画像、策略推荐每一步都做扎实。


七、收尾

整条链路的设计核心可以归到八个字——快进快出、异步干活、状态可控、异常可查

  • 快进快出:上传接口亚秒级返回,重活全部丢给后台。
  • 异步干活:Kafka 解耦同步和异步,消费者可以横向扩展。
  • 状态可控:document、task、task_log 三表分管资产、动作、证据,三维度状态机互不耦合。
  • 异常可查:每个步骤都有日志、有降级路径,失败时库里所有相关记录都标记成失败,不留半成品。

整条链路虽然 28 步看起来长,但逻辑是一条线串下来的,不绕。每一步都有它存在的理由——要么是为了让接口快、要么是为了让状态准、要么是为了让 RAG 后面能用得上。讲到任何一步都能把"它在做什么"和"它为什么这样做"两件事讲清楚,就算真正吃透了这条链路。


附:极简版(一分钟讲完)

这条链路从用户上传到策略推荐落库,分两段——同步上传段和 Kafka 异步段。

同步段做四件事:校验、存 MinIO、写三张表(document、task、task_log)、发 Kafka 消息。关键设计是 TransactionTemplate 手动控制事务边界、事务 commit 之后才发消息,避免消费者查不到上下文。消息体只放 documentId 和 taskId,真实数据从数据库查。

异步段在 handleParseRoute 里串起来:从 MinIO 重新下载原始文件、用 Tika 解析、做文本清洗,然后跑结构树提取的四阶段流水线——信号提取、歧义消解、层级构建、树校验——产出带父子关系的结构节点。歧义消解这一步用规则+LLM 混合,规则负责确定场景、LLM 只处理边界歧义,量可控成本可控。

提取完之后做统计和质量评估——token 数、结构等级、内容质量——打包成标准产物 DocumentAnalysisResult。然后把清洗后的纯文本独立存到 MinIO、结构节点全量替换落库、同步到 ES 和 Neo4j 给后续关键词检索和图检索用、再调 LLM 生成文档画像。

最后基于 DocumentAnalysisResult 推荐 Parent/Child 双层切块策略——Parent 大块管回答上下文、Child 小块管检索召回,搜小块读大块。方案落库等用户确认,确认后才进入下一段索引构建。

整条链路三个状态字段同时流转——parseStatus、taskStatus、strategyStatus,三表分管资产、动作、证据。任何一步失败由统一兜底处理,库里永远没有半成品。


企业级项目导航:⬅️ 00-聊天系统整体架构讲解 | 01-文档上传到RAG检索完整链路讲解 | ➡️ 02-执行计划准备的入口与整体流程