know-engine

文档索引构建流程——文档切分

前面我们通过文档的上传,以及文档的转换处理,已经可以拿到一份markdown文档了,接着,我们就要针对这份文档做切分了,切片成多个chunk,用于embedding。 markdown标题切分 我们这里选择的切块方式,针对markd…

TL;DR

前面我们通过文档的上传,以及文档的转换处理,已经可以拿到一份markdown文档了,接着,我们就要针对这份文档做切分了,切片成多个chunk,用于embedding。 markdown标题切分 我们这里选择的切块方式,针对markd…

前面我们通过文档的上传,以及文档的转换处理,已经可以拿到一份markdown文档了,接着,我们就要针对这份文档做切分了,切片成多个chunk,用于embedding。

markdown标题切分

我们这里选择的切块方式,针对markdown的话,选择的是基于标题的切分方式。因为前面我们讲过,mineru默认会把所有标题都解析成1级标题,但是其实实测下来大多数影响不太大。 如以下文档,会被切分成3段。 上面的第一个分段,因为只有标题,其实这个分段并没什么太大的用,我们可以在工程中把他过滤掉,当然,最好的办法就是像我们之前在RAG章节讲的那样,能够做到分块的合并,但是需要有标题的层级关系才行。如果没有层级关系,只有这种一级标题的话,是没办法做合并分块的。 所以可以根据文档的实际情况来判断下,如果这个标题真的很重要,重要到影响检索了,那么就需要在转换的时候指定llm_aided_title,让他基于大模型来做多级标题的识别。 但是实测下来,这种其实影响并不大。因为一般标题中的内容和正文内容都是有关联的,而只要有关联,语义相似度就可以做比较好的检索。如果效果不好,那就上其他优化手段,比如重写、多路检索、重排序等方式做优化。

切分代码逻辑

因为我们之前在上传后,已经在KnowledgeDocument表中存了一条记录了,那么就可以针对这个Document做切分,切分后的多个分段,分别保存在KnowledgeSegment中,并且记录顺序,方便我们在后台能够看到一个文档对应的分段的具体情况。就像很多智能体平台上的分段预览一样。

@PostMapping("/split/{documentId}")
@Transactional
public Integer splitDocument(@PathVariable Long documentId) {
    // 1. 查询文档
    KnowledgeDocument document = knowledgeDocumentService.getById(documentId);
    Assert.notNull(document, "文档不存在");
    Assert.notNull(document.getConvertedDocUrl(), "文档未转换完成");

    if (document.getStatus() == DocumentStatus.CHUNKED) {
        // 返回已切分的分段数量
        Long chunkedCount = knowledgeSegmentService.count(new QueryWrapper<KnowledgeSegment>()
                .eq("document_id", documentId)
                .eq("skipEmbedding", 0));

        return chunkedCount.intValue();
    }

    if(document.getStatus() != DocumentStatus.CONVERTED){
        throw new RuntimeException("文档状态不为CONVERTED,无法完成切分");
    }

    // 2. 从MinIO下载文件内容
    String convertedDocUrl = document.getConvertedDocUrl();
    String objectName = extractObjectNameFromUrl(convertedDocUrl);
    Assert.notNull(objectName, "无法解析文档URL");

    String content;
    try (InputStream inputStream = fileStorageService.downloadFile(objectName)) {
        content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
    } catch (Exception e) {
        throw new RuntimeException("下载文档失败: " + e.getMessage(), e);
    }

    // 3. 使用 MarkdownHeaderParentTextSplitter 进行切分
    MarkdownHeaderParentTextSplitter splitter = new MarkdownHeaderParentTextSplitter(1000, 100);
    Document doc = Document.from(content);
    List<TextSegment> segments = splitter.split(doc);

    // 4. 转换为 KnowledgeSegment 并保存
    List<KnowledgeSegment> knowledgeSegments = new ArrayList<>();
    for (int i = 0; i < segments.size(); i++) {
        TextSegment segment = segments.get(i);
        KnowledgeSegment knowledgeSegment = new KnowledgeSegment();
        knowledgeSegment.setText(segment.text());
        knowledgeSegment.setChunkId(segment.metadata().getString("chunkId"));
        knowledgeSegment.setMetadata(JSON.toJSONString(segment.metadata().toMap()));
        knowledgeSegment.setDocumentId(documentId);
        knowledgeSegment.setChunkOrder(i);
        knowledgeSegment.setStatus(SegmentStatus.INIT);

        // 检查是否需要跳过嵌入
        Integer skipEmbedding = segment.metadata().getInteger("skipEmbedding");
        if (skipEmbedding != null && skipEmbedding == 1) {
            knowledgeSegment.setSkipEmbedding(1);
        } else {
            knowledgeSegment.setSkipEmbedding(0);
        }

        knowledgeSegments.add(knowledgeSegment);
    }

    // 5. 批量保存片段
    boolean saveResult = knowledgeSegmentService.saveBatch(knowledgeSegments);
    Assert.isTrue(saveResult, "保存知识片段失败");

    // 6. 更新文档状态为 CHUNKED
    document.setStatus(DocumentStatus.CHUNKED);
    boolean updateResult = knowledgeDocumentService.updateById(document);
    Assert.isTrue(updateResult, "更新文档状态失败");

    return knowledgeSegments.size();
}

这里面用到了一个MarkdownHeaderParentTextSplitter,是我们给大家实现的一个父子分段的分段器。我们在下面的文档中专门讲:

✅表格&图片被拆分到多个分块如何解决?

在RAG智能客服中,比较常见的问题就是如果文档中包含了表格、或者图片,如何在分段的时候避免同一个图片、或者表格被切分到不同的分块中。 一旦表格被切分到不同分块,那么后面的分段就会丢失表头的信息,不仅会影响检索的效果,还会影响输出的结果,因为 LLMentor 这个方法执行完之后,knowledgeDocument的状态会推进到CHUNKED,并且在knowledgeSocument表中保存下来各个分段的内容和元数据的信息。

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。