知识库系统的入口工程
把“上传接口与文档主记录创建”这一块当成整个知识库系统的入口工程来学。它不是单纯把文件传上来,而是完成一件更重要的事:把一份用户上传的原始文件,登记成系统可追踪、可异步处理、可查询状态的文档资产,并投递后续解析任务。
一、先建立整体认知:这个接口在项目里解决什么问题?
在项目里,知识库不是用户上传一个 PDF 就立刻可以问答。真正可问答之前,文档还要经历很多阶段:格式解析、内容抽取、切块策略推荐、用户确认、父子块切分、向量化、写入向量库、写入 Elasticsearch、构建 Neo4j 文档结构图谱等。
所以“上传接口与文档主记录创建”这一块的核心职责不是做完所有事情,而是完成上传阶段的初始化工作。
它主要做六件事:
- 接收用户上传的文件和元信息;
- 校验文件是否合法;
- 把原始文件保存到 MinIO;
- 在 MySQL 中创建文档主记录;
- 创建第一条异步任务记录和任务日志;
- 事务提交后发送 Kafka 消息,启动后续异步处理流水线。
你可以把它理解为:用户上传文件后,系统先把这份文件“收进仓库”,再给它创建一张“身份证”和一张“工单”,最后通知后台流水线开始处理。
二、这块在整体架构中的位置
项目整体链路可以简化成这样:
现在我们只学到这里:
这部分是整个知识库生命周期的第一环,质量非常关键。因为后面的解析、切块、向量化、检索、问答,都依赖这里创建出来的 documentId 和任务状态。
三、先看接口入口:Controller 只负责接收请求
上传接口在 DocumentManageController 里,大概是这样的:
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<DocumentUploadVo> upload(
@RequestPart("file") MultipartFile file,
@Valid @RequestPart(value = "meta", required = false) DocumentUploadDto dto) {
return ApiResponse.ok(
documentManageService.upload(file, dto == null ? new DocumentUploadDto() : dto)
);
}
这里要重点理解三个点。
第一,接口使用的是 multipart/form-data,不是普通 JSON。因为它同时要上传二进制文件和结构化元信息。
其中:
@RequestPart("file") MultipartFile file
负责接收真实文件,例如 PDF、Word、PPT、Excel、Markdown 等。
而:
@RequestPart(value = "meta", required = false) DocumentUploadDto dto
负责接收文档的业务元信息,例如文档名称、知识范围、业务分类、标签、操作人 ID 等。
第二,meta 是可选的。也就是说,前端可以只传文件,不传元信息。Controller 里做了一个兜底:
dto == null ? new DocumentUploadDto() : dto
这样 Service 层就不用反复判断 dto == null,代码会更干净。
第三,Controller 不做业务逻辑。它只做参数绑定,然后把请求交给 Service。这个设计是很标准的分层思想:
Controller:接收请求、绑定参数、返回响应
Service:编排业务流程
StorageService:处理对象存储
Mapper:处理数据库持久化
KafkaProducer:处理消息投递
上传接口采用 multipart/form-data,同时接收文件二进制和可选元信息。Controller 层只负责参数绑定和空 DTO 兜底,实际的文件校验、对象存储、文档入库、任务创建和 Kafka 投递都下沉到 Service 层完成,保证控制器职责单一。
四、Service 第一阶段:文件校验
Service 进入后,第一步一定是校验文件。
核心代码逻辑是:
if (file == null || file.isEmpty()) {
throw new SuperAgentFrameException(...);
}
String originalFileName = file.getOriginalFilename();
if (StrUtil.isBlank(originalFileName)) {
throw new SuperAgentFrameException(...);
}
DocumentFileTypeEnum fileType = DocumentFileTypeEnum.fromFileName(originalFileName);
if (fileType == null) {
throw new SuperAgentFrameException(...);
}
这里做了三层校验。
第一层是文件内容不能为空:
file == null || file.isEmpty()
这可以拦住两类问题:一种是请求里根本没传文件,另一种是 multipart 字段存在,但文件内容为空。
第二层是原始文件名不能为空。
原始文件名不只是展示用,它还用于后续识别文件类型。例如系统会根据文件名后缀判断是 PDF、DOCX、PPTX、XLSX 还是 Markdown。
第三层是文件类型必须受支持。
DocumentFileTypeEnum.fromFileName(originalFileName)
这里一般会根据后缀名匹配枚举。如果匹配不上,就说明系统暂时不支持这种文件,直接拒绝上传。
这一块的核心思想是:越靠前发现非法输入,系统成本越低。
如果不校验,后面可能已经上传到 MinIO、写入数据库,甚至投递 Kafka 了,才发现文件无法解析,这样清理成本就很高。
五、读取文件字节并生成 documentId
通过校验后,系统会把 MultipartFile 一次性读成字节数组:
byte[] fileBytes = getFileBytes(file);
Long documentId = uidGenerator.getUid();
getFileBytes() 里面本质上是:
return file.getBytes();
这样做有两个目的。
第一,后续上传 MinIO 需要文件内容。
第二,文档主记录里要保存文件大小:
document.setFileSize((long) fileBytes.length);
所以先读成 byte[],后面可以复用。
然后生成 documentId。这个 ID 非常重要,它会贯穿整个文档生命周期:
MinIO 对象路径里有 documentId
文档主表主键是 documentId
任务表关联 documentId
任务日志关联 documentId
Kafka 消息携带 documentId
后续解析、切块、索引都靠 documentId 串联
所以这里不是等数据库自增生成 ID,而是服务端提前通过分布式 ID 生成器生成。
在项目描述里,分布式 ID 生成器是基于雪花算法、Lua 和 Redis 实现的。这里文档里提到用 uidGenerator.getUid(),本质目标都是一样的:在上传入口阶段提前生成全局唯一 ID,方便后续跨系统、跨表、跨消息链路追踪。
六、上传原始文件到 MinIO
接下来系统会先把原始文件上传到对象存储:
StoredObjectInfo storedObjectInfo = storageService.uploadOriginalFile(
documentId,
originalFileName,
fileBytes,
file.getContentType()
);
这里调用的是 MinioDocumentStorageService.uploadOriginalFile()。
它会拼接一个对象路径:
String objectName = properties.getMinio().getObjectPrefix() + "/"
+ documentId + "/"
+ System.currentTimeMillis() + "-"
+ originalFileName;
路径大概长这样:
doc-original/123456789/1714000000000-企业制度.pdf
这个路径设计有几个好处。
第一,按 documentId 分目录,方便定位某一份文档的所有相关文件。
第二,前面加时间戳,避免同名文件覆盖。
第三,保留原始文件名,排查问题时更直观。
然后调用真正的上传方法:
minioClient.putObject(...)
上传之前还会检查 bucket 是否存在:
if (!bucketExists()) {
minioClient.makeBucket(...);
}
这个设计适合新环境部署。比如测试环境第一次启动时,MinIO 里可能还没有对应的 bucket,系统会自动创建,减少人工初始化步骤。
上传成功后,会返回一个 StoredObjectInfo,里面有:
bucketName:桶名
objectName:对象路径
objectUrl:完整访问地址
这三个值后面会保存到文档主表。
这里有一个设计点需要注意:为什么先上传 MinIO,再写数据库?
因为文档主表需要保存文件的存储位置。如果不先上传,就拿不到 bucketName、objectName、objectUrl。所以流程上必须先拿到对象存储信息,再构建文档主记录。
不过这样也会带来一个边界问题:如果 MinIO 上传成功,但后面数据库入库失败,MinIO 里可能会留下一个“孤儿文件”。生产项目中一般会通过补偿任务、定时清理或失败回滚清理来处理。
七、构建文档主记录 SuperAgentDocument
文件上传成功后,系统开始构建文档主记录:
SuperAgentDocument document = new SuperAgentDocument();
document.setId(documentId);
document.setDocumentName(...);
document.setOriginalFileName(originalFileName);
document.setFileType(fileType.getCode());
document.setMimeType(file.getContentType());
document.setFileSize((long) fileBytes.length);
document.setStorageType(DocumentStorageTypeEnum.MINIO.getCode());
document.setBucketName(storedObjectInfo.getBucketName());
document.setObjectName(storedObjectInfo.getObjectName());
document.setObjectUrl(storedObjectInfo.getObjectUrl());
这一步是在内存中组装对象,还没有真正写数据库。
文档主记录可以理解为这份文档的“资产档案”。它记录了:
这份文档叫什么?
原始文件名是什么?
文件类型是什么?
MIME 类型是什么?
文件大小是多少?
存储在哪里?
当前解析状态是什么?
后续策略状态是什么?
索引状态是什么?
属于哪个知识范围?
属于哪个业务分类?
有哪些标签?
其中,文档名称的取值逻辑很重要:
document.setDocumentName(
StrUtil.isNotBlank(dto.getDocumentName())
? dto.getDocumentName()
: originalFileName
);
也就是说,如果前端传了展示名称,就用前端传的;否则用原始文件名。
比如用户上传的文件叫:
report_final_v3_2024_04_01.pdf
但前端传了:
2024 年一季度经营分析报告
那么系统展示时就用后者。这样对用户更友好。
八、三个核心状态:parseStatus、strategyStatus、indexStatus
文档主记录里最关键的是三个状态字段:
document.setParseStatus(DocumentParseStatusEnum.PARSING.getCode());
document.setStrategyStatus(DocumentStrategyStatusEnum.WAIT_RECOMMEND.getCode());
document.setIndexStatus(DocumentIndexStatusEnum.WAIT_BUILD.getCode());
这三个字段代表文档后续处理链路的三个阶段。
1. parseStatus:解析状态
上传成功后设置为:
PARSING
它表示:文档已经进入解析链路,等待异步任务处理。
注意,这里虽然叫 PARSING,但不代表当前接口已经完成了解析。它更准确的含义是:已进入解析中或待解析队列。
2. strategyStatus:切块策略状态
初始值是:
WAIT_RECOMMEND
表示系统还没有推荐切块策略。
后续 Tika 解析完文档文本和结构后,系统才会根据文档类型、结构质量、内容特征推荐切块策略,例如结构化切块、递归切块、语义切块、LLM 智能切块等。
3. indexStatus:索引状态
初始值是:
WAIT_BUILD
表示还没有构建检索索引。
后续用户确认切块策略后,系统才会进行父子块切分、向量化,然后写入 PGVector、Milvus、Elasticsearch、Neo4j 等存储系统。
所以这三个状态其实对应你项目里的异步处理主线:
这几个状态对于前端非常重要。前端可以根据这些状态展示:
上传成功
解析中
等待确认切块策略
索引构建中
处理完成
处理失败
九、业务元信息:知识范围、业务分类、标签
文档主记录还会保存上传时传入的业务信息:
document.setKnowledgeScopeCode(StrUtil.trimToNull(dto.getKnowledgeScopeCode()));
document.setKnowledgeScopeName(StrUtil.trimToNull(dto.getKnowledgeScopeName()));
document.setBusinessCategory(StrUtil.trimToNull(dto.getBusinessCategory()));
document.setDocumentTags(StrUtil.trimToNull(dto.getDocumentTags()));
document.setStatus(BusinessStatus.YES.getCode());
这些字段看起来只是普通元数据,但在项目里很重要。
因为系统设计了三级知识路由漏斗:
领域 → 主题 → 文档
那么文档上传时保存的知识范围、业务分类、标签,就会成为后续路由、筛选、召回和后台管理的重要依据。
比如一个文档属于:
知识范围:人力资源
业务分类:员工制度
标签:考勤、假期、薪酬
后面用户问:
年假怎么计算?
系统就可以更容易把问题路由到“人力资源 / 员工制度”相关文档,而不是全库盲搜。
十、构建任务记录 SuperAgentDocumentTask
文档主记录构建完成后,系统还会创建一条任务记录:
Long taskId = uidGenerator.getUid();
SuperAgentDocumentTask task = new SuperAgentDocumentTask();
task.setId(taskId);
task.setDocumentId(documentId);
task.setTaskType(DocumentTaskTypeEnum.PARSE_ROUTE.getCode());
task.setTaskStatus(DocumentTaskStatusEnum.NEW.getCode());
task.setCurrentStage(DocumentTaskStageEnum.FILE_UPLOAD.getCode());
task.setTriggerSource(resolveTriggerSource(operatorId));
task.setRetryCount(0);
task.setStatus(BusinessStatus.YES.getCode());
这里的任务可以理解为一张“工单”。
文档主记录表示:
这份文档是什么?
任务记录表示:
接下来要对这份文档做什么?
上传完成后创建的第一类任务是:
PARSE_ROUTE
也就是“解析路由任务”。
它负责把文档交给后续异步链路,让消费方决定下一步怎么解析、怎么推荐切块策略、怎么更新状态。
任务初始状态是:
NEW
表示刚创建,还未被消费。
当前阶段是:
FILE_UPLOAD
表示它是从文件上传阶段产生的任务。
触发来源通过 operatorId 判断:
private Integer resolveTriggerSource(Long operatorId) {
return operatorId == null
? DocumentTriggerSourceEnum.SYSTEM.getCode()
: DocumentTriggerSourceEnum.USER.getCode();
}
如果有操作人 ID,说明是用户主动上传;如果没有,则认为是系统触发。
这里还有一个小设计:
private Long parseOptionalLong(String rawValue) {
if (StrUtil.isBlank(rawValue)) {
return null;
}
try {
Long value = Long.valueOf(rawValue.trim());
return value > 0 ? value : null;
} catch (NumberFormatException exception) {
return null;
}
}
operatorId 是宽松解析的。空字符串、非数字、0、负数都当成 null,不会直接让上传失败。
这个设计体现了一个原则:可选字段不要轻易阻断主流程。
十一、最关键的设计:TransactionTemplate 手动控制事务
接下来是这块最值得学习的地方。
系统没有直接在 upload() 方法上加:
@Transactional
而是用了:
DocumentUploadVo uploadVo = transactionTemplate.execute(status -> {
documentMapper.insert(document);
taskMapper.insert(task);
taskLogService.saveLog(...);
return new DocumentUploadVo(...);
});
也就是说,只有这几步放在数据库事务里:
插入文档主表
插入任务表
插入任务日志
组装返回结果
Kafka 发送不在事务里,而是在事务提交之后执行。
为什么要这样设计?
因为如果整个 upload() 方法都加 @Transactional,然后在方法最后发送 Kafka,可能出现一个问题:
数据库事务还没提交
Kafka 消息已经发出
消费方马上消费消息
消费方根据 documentId 查询数据库
结果查不到文档记录
这就是典型的“消息先于事务可见”的问题。
所以这里用 TransactionTemplate 精确控制事务边界:
transactionTemplate.execute() 内部:只做数据库写入
execute() 正常返回:说明数据库事务已经提交
事务提交之后:再发送 Kafka
这个设计非常重要,可以作为项目亮点来讲。
上传链路中,没有简单使用方法级
@Transactional,而是通过TransactionTemplate手动收窄事务边界。文档主表、任务表和任务日志在同一个事务内提交,事务提交成功后再发送 Kafka 消息,避免消费者收到消息后查询不到数据库记录的问题。
十二、任务日志:为什么上传阶段也要记日志?
在事务里除了插入文档和任务,还会记录一条任务日志:
taskLogService.saveLog(
taskId,
documentId,
DocumentTaskStageEnum.FILE_UPLOAD.getCode(),
DocumentTaskEventTypeEnum.COMPLETE.getCode(),
DocumentLogLevelEnum.INFO.getCode(),
resolveOperatorType(operatorId),
operatorId,
"文件上传完成,已进入解析与策略推荐队列。",
Map.of("originalFileName", originalFileName, "fileSize", fileBytes.length)
);
这条日志记录的是:
哪个任务?
哪个文档?
当前处于哪个阶段?
发生了什么事件?
日志级别是什么?
谁触发的?
附带了哪些上下文?
这对你的项目“全链路可观测”非常重要。
因为文档处理是异步的,用户上传后并不会马上得到最终结果。如果后续解析失败、切块失败、索引失败,排查问题时就需要知道:
文件是否上传成功?
任务是否创建成功?
Kafka 是否投递成功?
消费方是否开始处理?
Tika 是否解析失败?
切块策略是否推荐成功?
索引是否写入成功?
所以从上传阶段就记录任务日志,可以形成完整时间线。
十三、事务提交后发送 Kafka 消息
数据库事务提交成功之后,系统发送 Kafka 消息:
kafkaProducer.sendParseRoute(
new DocumentParseRouteMessage(documentId, taskId)
);
消息体很简单:
public class DocumentParseRouteMessage {
private Long documentId;
private Long taskId;
}
为什么只发 documentId 和 taskId?
因为详细信息已经保存在数据库了。Kafka 消息只需要告诉消费方:
哪份文档?
哪个任务?
消费方拿到这两个 ID 后,再去数据库查询完整上下文。
这样做有几个好处。
第一,消息体小,不容易因为字段变更导致兼容问题。
第二,数据库是事实来源,消费方读取的是最新状态。
第三,任务状态和日志都可以围绕 taskId 做追踪。
发送时,topic 名称会拼接环境前缀:
SpringUtil.getPrefixDistinctionName() + "-" + properties.getKafka().getParseTopic()
这样 dev、test、prod 不同环境的消息不会混在一起。
消息 key 使用:
String.valueOf(message.getDocumentId())
这样同一份文档相关的消息会尽量进入同一个 partition,有利于保证同一文档任务的顺序性。
底层发送方法中还用了:
kafkaTemplate.send(topic, key, payload).get();
也就是说,它不是“发出去就不管”,而是同步等待 broker 确认。
好处是:如果 Kafka 发送失败,上传接口会直接报错,而不是告诉用户成功但后台根本没启动处理。
不过这里也有一个边界问题:如果数据库事务已经提交,但 Kafka 发送失败,数据库里已经有文档和任务记录,但异步链路没有启动。
所以生产上通常要配合补偿机制,比如:
定时扫描 parseStatus = PARSING 但长时间没有任务进展的文档
重新投递 Kafka 消息
或者将任务状态标记为待重试
这个权衡比“消息发了但数据库还没提交”更容易处理。
十四、最后返回上传结果
接口最后返回:
return uploadVo;
DocumentUploadVo 包含:
private Long documentId;
private Long taskId;
private String documentName;
private Integer parseStatus;
private Integer strategyStatus;
private Integer indexStatus;
它没有返回 MinIO 的 bucketName、objectName、objectUrl。
这是一个好设计。因为对象存储路径属于服务端内部细节,不应该暴露给前端。前端真正需要的是:
documentId:后续查询文档详情和处理进度
taskId:后续查询任务进度和日志
documentName:展示名称
parseStatus:解析状态
strategyStatus:策略状态
indexStatus:索引状态
也就是说,上传接口返回的是“业务可感知状态”,不是“底层存储细节”。
十五、把完整流程串起来
这一块完整流程可以背成一句话:
上传接口接收 multipart 文件和可选元信息,先进行空文件、原始文件名和文件类型校验,然后读取文件字节并生成全局 documentId;系统将原始文件上传到 MinIO,获得 bucket、objectName 和 objectUrl 后构建文档主记录,同时创建解析路由任务和任务日志;文档主表、任务表和日志表通过 TransactionTemplate 在同一个事务内提交,事务提交成功后再发送 Kafka 消息,触发后续异步解析流水线,最后返回 documentId、taskId 和初始处理状态给前端。
十六、你要重点掌握的设计思想
1. 上传接口不是处理完成,而是异步处理入口
这个接口不会直接完成解析、切块、向量化和索引构建。它只是把文件接入系统,并创建后续任务。
这体现的是异步架构思想:
同步接口做轻量初始化
重任务交给 Kafka 异步消费
状态表记录进度
前端轮询或订阅进度
2. documentId 是全链路主线
documentId贯穿:
对象存储路径
文档主表
任务表
任务日志
Kafka 消息
解析结果
切块结果
向量记录
ES 索引
Neo4j 图谱节点
问答引用来源
所以它必须全局唯一、提前生成、稳定可靠。
3. 文档主表保存“资产状态”
文档主表不是只保存文件路径,它还保存处理状态和业务分类。
它是整个文档生命周期的状态中心。
4. 任务表保存“处理动作”
文档主表表示“文档是什么状态”,任务表表示“当前要执行什么处理”。
这两个表要分开,不要混在一起。
5. TransactionTemplate 的核心价值是控制事务边界
它解决的是:
数据库事务提交
Kafka 消息发送
消费方查询可见性
之间的顺序问题。
6. Kafka 消息只传 ID,不传大对象
详细上下文放数据库,消息只负责触发流程。
这是异步系统里很常见的设计。
7. 任务日志从第一步开始记录
这为后续全链路可观测、失败排查和状态追踪打基础。
十七、这一块可以怎么写进简历或项目介绍
可以这样提炼:
负责设计并实现文档上传与异步解析入口链路,支持 multipart 文件与元信息上传,完成文件合法性校验、MinIO 原始文件存储、文档主记录创建、解析任务创建和任务日志记录。通过 TransactionTemplate 精确控制事务边界,保证文档主表、任务表和日志表事务一致提交,并在事务提交后投递 Kafka 解析路由消息,避免消费方读取未提交数据的问题。上传结果返回 documentId、taskId 和初始处理状态,支撑前端进度追踪和后续异步解析流水线。
如果要突出亮点,可以再加一句:
针对“数据库提交”和“消息投递”之间的一致性问题,采用“先事务提交、后同步发送 Kafka、失败补偿兜底”的方案,提升异步处理链路的可靠性和可观测性。
十八、面试官可能会问的问题
问题 1:为什么不直接在上传接口里解析文档?
因为解析文档可能很慢,尤其是 PDF、PPT、Excel 或大文件。如果同步解析,会导致接口响应时间过长,甚至超时。上传接口只做文件接入和任务创建,后续通过 Kafka 异步处理,可以提升用户体验和系统吞吐量。
问题 2:为什么先上传 MinIO 再写数据库?
因为文档主表需要保存文件的存储位置,包括 bucket、objectName 和 objectUrl。只有文件上传成功后,才能拿到这些信息并写入数据库。
问题 3:如果 MinIO 上传成功,但数据库写入失败怎么办?
会产生孤儿文件。可以通过失败回滚清理、定时扫描无主对象、或者在数据库写入失败时主动删除 MinIO 文件来补偿。
问题 4:为什么不用方法级 @Transactional?
如果方法级事务包住整个上传流程,Kafka 可能在数据库事务提交前就发送出去。消费方收到消息后查询数据库,可能查不到刚插入的文档和任务。使用 TransactionTemplate 可以把事务范围限定在数据库写入部分,确保事务提交后再发 Kafka。
问题 5:如果事务提交成功,但 Kafka 发送失败怎么办?
这种情况数据库里已经有文档和任务,但异步处理没有启动。可以通过补偿任务扫描长时间停留在 PARSING 或 NEW 状态的记录,重新投递 Kafka 或标记为失败待重试。
问题 6:为什么 Kafka 消息只传 documentId 和 taskId?
因为详细上下文已经在数据库中。消息只负责触发异步流程,消费方通过 ID 查询数据库即可。这样消息更小,也降低了字段变更带来的兼容成本。
问题 7:parseStatus 设置为 PARSING 是否准确?
从严格语义看,它表示文档已经进入解析流程或解析队列,并不代表当前请求线程正在解析。实际项目中也可以命名为 WAIT_PARSE 或 PARSING,关键是状态定义要清晰,前后端和消费方要保持一致。
十九、这一节你需要真正记住的代码主线
你不用死背每一行代码,但要记住主线:
public DocumentUploadVo upload(MultipartFile file, DocumentUploadDto dto) {
// 1. 校验文件
checkFile(file);
// 2. 读取字节,生成 documentId
byte[] fileBytes = file.getBytes();
Long documentId = uidGenerator.getUid();
// 3. 上传 MinIO
StoredObjectInfo stored = storageService.uploadOriginalFile(
documentId, originalFileName, fileBytes, contentType
);
// 4. 构建文档主记录
SuperAgentDocument document = buildDocument(...);
// 5. 构建任务记录
SuperAgentDocumentTask task = buildTask(...);
// 6. 数据库事务:插入文档、任务、日志
DocumentUploadVo vo = transactionTemplate.execute(status -> {
documentMapper.insert(document);
taskMapper.insert(task);
taskLogService.saveLog(...);
return buildUploadVo(document, task);
});
// 7. 事务提交后发送 Kafka
kafkaProducer.sendParseRoute(new DocumentParseRouteMessage(documentId, taskId));
// 8. 返回结果
return vo;
}
你看到任何上传链路源码,都可以按这个模板去拆。
二十、这一块和后续模块的衔接
上传接口完成后,系统已经具备了后续异步处理所需的全部上下文:
| 数据 | 作用 |
|---|---|
documentId |
串联文档全生命周期 |
taskId |
串联异步任务执行过程 |
objectName |
定位 MinIO 原始文件 |
parseStatus |
标记解析进度 |
strategyStatus |
标记切块策略推荐进度 |
indexStatus |
标记索引构建进度 |
taskLog |
支持进度追踪和问题排查 |
| Kafka 消息 | 触发后续解析流水线 |
所以它后面自然会接到:
Kafka Consumer 消费解析路由消息
根据 documentId 查询文档
从 MinIO 下载原始文件
使用 Apache Tika 解析
更新 parseStatus
推荐切块策略
等待用户确认
触发切块和向量化
构建 ES、向量库、Neo4j 索引
也就是说,“上传接口与文档主记录创建”这一块是后面所有知识库能力的起点。没有它,后面的 RAG、ReAct Agent、知识路由、图谱导航、引用来源都没有稳定的数据基础。
这一节先把入口链路、状态设计、事务边界和 Kafka 投递顺序吃透,这就是这个模块的核心。
企业级项目导航:⬅️ 05-文本转文档结构树 | 06-知识库系统的入口工程 | ➡️ 07-结构节点提取的四阶段流水线
💬 评论