RAG

文档预处理

文档预处理指的是将原始文档进行加载、解析,并转换为能够统一处理的标准化格式。 在这一步中,我们首先要将文档按照不同的类型加载,并转换成统一格式document,然后还需要去除文档中存在的大量无效内容,比如多余的空格、换行符、无意义的…

TL;DR

文档预处理指的是将原始文档进行加载、解析,并转换为能够统一处理的标准化格式。 在这一步中,我们首先要将文档按照不同的类型加载,并转换成统一格式document,然后还需要去除文档中存在的大量无效内容,比如多余的空格、换行符、无意义的…

文档预处理指的是将原始文档进行加载、解析,并转换为能够统一处理的标准化格式。 在这一步中,我们首先要将文档按照不同的类型加载,并转换成统一格式document,然后还需要去除文档中存在的大量无效内容,比如多余的空格、换行符、无意义的特殊符号、重复的内容等;最后还可能需要规范化文本格式,例如统一编码格式、统一大小写等。 文档预处理的目标,是让后续的“分片”和“向量化”能够在干净的数据上进行,从源头上保证知识库索引的质量。 文档预处理是构建高质量知识库的基础,它主要包含两个关键步骤:文档读取和数据清洗。 下面的代码基于SpringAI 1.1.0 版本。

文档读取

本文测试用到的文档: https://nfturbo-file.oss-cn-hangzhou.aliyuncs.com/llm/RAG%E6%9D%90%E6%96%99.zip 文档读取是将原始文档加载、解析,并转换为统一的结构化格式。在这一阶段,我们需要根据文档类型选择合适的解析工具,将不同格式的文档类型(如 TXT、PDF、Word、HTML、Markdown 等等)统一处理为可供后续处理的标准化格式。SpringAI 已经为我们提供了大量的读取器,拿来即用。这些读取器都来自——DocumentReader 在 Spring AI中,DocumentReader 是一个用于从各种格式的文档中提取文本内容并将其转换为 Document 对象的核心组件。这些 Document 对象随后可以被用于向量嵌入(embedding)、语义搜索、RAG(Retrieval-Augmented Generation)等 AI 应用场景。他的主要作用: - 统一读取接口:提供标准化方式从不同来源(如 PDF、Word、TXT、HTML、Markdown 等)加载原始文本。 - 结构化输出:将原始内容封装为 org.springframework.ai.document.Document 对象,包含: - content:文档的文本内容 - metadata:元数据(如文件名、页码、来源 URL、创建时间等) - 支持扩展:开发者可自定义实现特定格式的解析器。 目前主要的实现可以通过这个接口有哪些实现来看(https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/document/class-use/DocumentReader.html ): 不管是哪种服务器,只需要创建出来之后,调用read/get方法,就能得到一个Document的List。其实就是针对文档被加载后的对象。接着就可以基于这些Document做处理了。

策略接口

由于我们的文档格式是多变的,直接针对每种格式写单独的读取逻辑会有大量if-else的代码,导致代码重复、维护困难,同时也不利于扩展。为了解决这种问题,我们可以定义一个策略接口,用于动态路由到具体的文档读取器来解析各自的文档类型。

public interface DocumentReaderStrategy {
    /**
     * 判断是否支持该文件
     */
    boolean supports(File file);

    /**
     * 读取文件并返回 Document 列表
     */
    List<Document> read(File file) throws IOException;
}

文本读取器

直接读取文件内容,无需额外解析,适合简单文本,如txt文件。springai-commons包自带解析器。

@Component
public class TextReaderStrategy implements DocumentReaderStrategy {

    @Override
    public boolean supports(File file) {
        String name = file.getName().toLowerCase();
        return name.endsWith(".txt") || name.endsWith(".tex") || name.endsWith(".text");
    }

    @Override
    public List<Document> read(File file) throws IOException {
        Resource resource = new FileSystemResource(file);
        return new TextReader(resource).get();
    }
}

PDF读取器

专业的PDF文件解析器,支持按页提取文本,可删除页眉、页脚,支持分页生成多个 Document。

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-pdf-document-reader</artifactId>
  <version>1.1.0</version>

</dependency>

这个包里面提供了两个reader:ParagraphPdfDocumentReader、PagePdfDocumentReader 他俩的区别是,PagePdfDocumentReader 是“按页切分”,而 ParagraphPdfDocumentReader 是“按语义段落切分”。 在 rag 中,如果你需要实现 PDF 读取策略,通常建议: 建议优先考虑使用ParagraphPdfDocumentReader,因为它能够更好地保留信息的完整性。段落通常是一个完整的意思表达,这对于 LLM 理解上下文非常有帮助。但是他非常依赖PDF 本身的质量。如果 PDF 是扫描件或者没有良好的内部结构标记,它的效果可能不理想甚至回退到按行读取。

@Component
public class PdfReaderStrategy implements DocumentReaderStrategy {

    @Override
    public boolean supports(File file) {
        return file.getName().toLowerCase().endsWith(".pdf");
    }

    @Override
    public List<Document> read(File file) throws IOException {
        // 读取配置
        PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
                .withPageTopMargin(50)         // 忽略顶部50个单位的页眉
                .withPageBottomMargin(50)      // 忽略底部50个单位的页脚
                .withPagesPerDocument(1)       // 每一页作为一个 Document
                .withPageExtractedTextFormatter(new ExtractedTextFormatter.Builder()
                        .withNumberOfTopTextLinesToDelete(0) // 每页再额外删掉前0行
                        .build())
                .build();

        Resource resource = new FileSystemResource(file);
        return new PagePdfDocumentReader(resource, config).get();
    }
}

HTML读取器

基于Jsoup HTML解析器,可以使用selector选择器指定提取网页标签内容。

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-jsoup-document-reader</artifactId>
 <version>1.1.0</version>

</dependency>
@Component
public class HtmlReaderStrategy implements DocumentReaderStrategy {

    @Override
    public boolean supports(File file) {
        String name = file.getName().toLowerCase();
        return name.endsWith(".html") || name.endsWith(".htm");
    }

    @Override
    public List<Document> read(File file) throws IOException {
        // 读取配置
        JsoupDocumentReaderConfig config = JsoupDocumentReaderConfig.builder()
                // 只提取p标签段落
                .selector("p")
                // 文件编码
                .charset("UTF-8")
                // 包含超链接
                .includeLinkUrls(true)
                // 提取meta标签的元数据
                .metadataTags(List.of("author", "date"))
                // 添加自定义元数据
                .additionalMetadata("filename", file.getName())
                .build();
        Resource resource = new FileSystemResource(file);
        return new JsoupDocumentReader(resource, config).get();
    }
}

Markdown读取器

支持按标题或水平分割线分片,可以选择性的包含代码块或引用内容。

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-markdown-document-reader</artifactId>
  <version>1.1.0</version>

</dependency>
@Component
public class MarkdownReaderStrategy implements DocumentReaderStrategy {

    @Override
    public boolean supports(File file) {
        String name = file.getName().toLowerCase();
        return name.endsWith(".md");
    }

    @Override
    public List<Document> read(File file) throws IOException {
        // 读取配置
        MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
                // 水平线分割生成新文档
                .withHorizontalRuleCreateDocument(true)
                // 不包含代码块
                .withIncludeCodeBlock(false)
                // 不包含引用
                .withIncludeBlockquote(false)
                // 添加文件名元数据
                .withAdditionalMetadata("filename", file.getName())
                .build();
        Resource resource = new FileSystemResource(file);
        return new MarkdownDocumentReader(resource, config).get();
    }
}

JSON读取器

可处理JSON数组或对象,但不支持JSON嵌套字段,比较适合简单的JSON文件,如果文件本身比较复杂还是建议使用jackson、fastjson等专业的json工具来手动处理。

@Component
public class JsonReaderStrategy implements DocumentReaderStrategy {

    public boolean supports(File file) {
        String name = file.getName().toLowerCase();
        return name.endsWith(".json");
    }

    @Override
    public List<Document> read(File file) throws IOException {
        Resource resource = new FileSystemResource(file);
        // 假设目标提取json的两个字段description和content
        JsonReader jsonReader = new JsonReader(resource, "description", "content");
        return jsonReader.get();
    }
}

通用Tika读取器

一种通用文件处理器,可以处理大部分常见文档格式,如word、pdf、ppt等等,可以自动识别文档类型并提取文本和元数据。

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-tika-document-reader</artifactId>
  <version>1.1.0</version>

</dependency>
@Component
public class TikaReaderStrategy implements DocumentReaderStrategy {

    @Override
    public boolean supports(File file) {
        String name = file.getName().toLowerCase();
        return name.endsWith(".doc") || name.endsWith(".docx");
    }

    @Override
    public List<Document> read(File file) throws IOException {
        Resource resource = new FileSystemResource(file);
        return new TikaDocumentReader(resource).get();
    }
}

策略选择器

@Service
public class DocumentReaderStrategySelector {

    private final List<DocumentReaderStrategy> strategies;

    public DocumentReaderStrategySelector(List<DocumentReaderStrategy> strategies) {
        this.strategies = strategies;
    }

    public List<Document> read(File file) throws IOException {
        for (DocumentReaderStrategy strategy : strategies) {
            if (strategy.supports(file)) {
                return strategy.read(file);
            }
        }
        throw new IllegalArgumentException("不支持的文件类型: " + file.getName());
    }
}

测试接口

@RestController
@RequestMapping("/rag")
public class RagController {

    private final DocumentReaderStrategySelector selector;

    @Autowired
    public RagController(DocumentReaderStrategySelector selector) {
        this.selector = selector;
    }

    /**
     * 读取文件
     */
    @GetMapping("/read")
    public List<Document> readDocument(@RequestParam("path") String path) {
        File file = new File(path);
        if (!file.exists() || !file.isFile()) {
            throw new IllegalArgumentException("文件不存在或不是有效文件: " + path);
        }
        try {
            return selector.read(file);
        } catch (IOException e) {
            throw new RuntimeException("读取文件失败: " + e.getMessage(), e);
        }
    }
}

调用示例

以一篇PDF文档为例,可以看到已经成功将文档解析了出来,包含了文档的内容和元数据。 根据选择器的路由,此PDF文档被交给了PDF读取器进行处理,我们之前在PDF读取器中设置了withPagesPerDocument参数,所以生成的结果是每页一个document。

数据清洗

在前面的步骤中,我们已成功实现文档读取功能。然而,读取结果往往包含大量无效干扰字符(下图就是调用示例的响应结果),因此需要进行数据清洗。数据清洗是对文本内容进行整理和优化的过程,包括去除多余空格、换行符、无意义的特殊符号和重复内容等。通过清洗,后续的文档分片和向量化操作可以在干净、一致的数据上进行,从源头提升知识库的质量与检索效果。

document清洗

这边列举了几种常见的清洗手段,实际上你可以根据自己的文档数据、业务需求,定制化的开发清洗策略。

@GetMapping("/read")
public List<Document> readDocument(@RequestParam("path") String path) {
    File file = new File(path);
    if (!file.exists() || !file.isFile()) {
        throw new IllegalArgumentException("文件不存在或不是有效文件: " + path);
    }
    try {

        List<Document> documents = selector.read(file);

        return cleanDocuments(documents);

    } catch (IOException e) {
        throw new RuntimeException("读取文件失败: " + e.getMessage(), e);
    }
}

/**
 * 文本清洗
 */
public List<Document> cleanDocuments(List<Document> documents) {
    if (CollectionUtils.isEmpty(documents)) {
        return documents;
    }

    return documents.stream()
    .map(doc -> {
        if (doc == null || doc.getText() == null) {
            return doc;
        }

        String text = doc.getText();

        // 1. 去掉多余空白字符(空格、制表符、换行等)
        text = text.replaceAll("\\s+", " ").trim();

        // 2. 去掉无意义的乱码或特殊符号
        text = text.replaceAll("[^\\p{L}\\p{N}\\p{P}\\p{Z}\\n]", "");

        // 3. 可选:统一大小写
        // text = text.toLowerCase();

        // 4. 按换行拆分段落,去除重复段落
        String[] paragraphs = text.split("\\n+");
        Set<String> seen = new LinkedHashSet<>();
        for (String para : paragraphs) {
            String trimmed = para.trim();
            if (!trimmed.isEmpty()) {
                seen.add(trimmed);
            }
        }

        text = String.join("\n", seen);

        return new Document(text);
    })
    .collect(Collectors.toList());
}

调用示例

通过文本清洗,原始数据中的噪声、冗余和格式不一致问题得到了有效处理,文本内容变得更加规范、整洁。为后续的文档分片、向量化以及检索生成等环节提供了更高质量的基础支撑。

版本提示

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

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

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