找回密码
 立即注册
首页 业界区 业界 Spring AI 代码分析(七)--文档的处理

Spring AI 代码分析(七)--文档的处理

郜庄静 2025-11-25 01:15:02
文档处理能力分析

请关注微信公众号:阿呆-bot
1. 工程结构概览

Spring AI 提供了完整的文档处理能力,包括文档读取、文本分块和预处理。这些能力是 RAG 应用的基础。
  1. document-readers/                 # 文档读取器
  2. ├── pdf-reader/                   # PDF 读取器
  3. │   ├── PagePdfDocumentReader.java      # 按页读取
  4. │   └── ParagraphPdfDocumentReader.java  # 按段落读取
  5. ├── markdown-reader/              # Markdown 读取器
  6. │   └── MarkdownDocumentReader.java
  7. ├── tika-reader/                  # 通用文档读取器(Tika)
  8. │   └── TikaDocumentReader.java
  9. └── jsoup-reader/                 # HTML 读取器
  10.     └── JsoupDocumentReader.java
  11. spring-ai-commons/                # 核心处理能力
  12. ├── document/
  13. │   └── Document.java        # 文档对象
  14. └── transformer/
  15.     └── splitter/                # 文本分块
  16.         ├── TextSplitter.java
  17.         ├── TokenTextSplitter.java
  18.         └── CharacterTextSplitter.java
复制代码
2. 技术体系与模块关系

文档处理流程:读取 → 分块 → 嵌入 → 存储
1.png

3. 关键场景示例代码

3.1 PDF 文档读取

PDF 读取支持按页和按段落两种方式:
  1. // 按页读取
  2. Resource pdfResource = new ClassPathResource("document.pdf");
  3. PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(pdfResource);
  4. List<Document> documents = pdfReader.get();
  5. // 按段落读取(更智能)
  6. ParagraphPdfDocumentReader paragraphReader =
  7.     new ParagraphPdfDocumentReader(pdfResource, config);
  8. List<Document> documents = paragraphReader.get();
复制代码
3.2 Markdown 文档读取

Markdown 读取器可以按标题、段落或水平线分组:
  1. MarkdownDocumentReader markdownReader =
  2.     new MarkdownDocumentReader("classpath:docs/*.md", config);
  3. List<Document> documents = markdownReader.get();
复制代码
3.3 Tika 通用读取

Tika 可以读取多种格式(PDF、Word、PPT 等):
  1. TikaDocumentReader tikaReader =
  2.     new TikaDocumentReader("classpath:document.docx");
  3. List<Document> documents = tikaReader.get();
复制代码
3.4 文档分块

将长文档分割成适合嵌入的小块:
  1. // Token 分块(推荐)
  2. TokenTextSplitter splitter = TokenTextSplitter.builder()
  3.     .chunkSize(800)           // 目标 token 数
  4.     .minChunkSizeChars(350)  // 最小字符数
  5.     .build();
  6. List<Document> chunks = splitter.split(documents);
  7. // 字符分块
  8. CharacterTextSplitter charSplitter = new CharacterTextSplitter(1000, 200);
  9. List<Document> chunks = charSplitter.split(documents);
复制代码
3.5 完整流程

文档处理的完整流程:
  1. // 1. 读取文档
  2. TikaDocumentReader reader = new TikaDocumentReader("document.pdf");
  3. List<Document> documents = reader.get();
  4. // 2. 分块
  5. TokenTextSplitter splitter = new TokenTextSplitter();
  6. List<Document> chunks = splitter.split(documents);
  7. // 3. 嵌入并存储
  8. vectorStore.add(chunks);
复制代码
4. 核心实现图

4.1 文档处理流程

2.png

5. 入口类与关键类关系

3.png

6. 关键实现逻辑分析

6.1 PDF 读取实现

PDF 读取有两种方式:
方式一:按页读取
  1. public class PagePdfDocumentReader implements DocumentReader {
  2.     @Override
  3.     public List<Document> get() {
  4.         List<Document> documents = new ArrayList<>();
  5.         int pageCount = document.getNumberOfPages();
  6.         
  7.         for (int i = 0; i < pageCount; i++) {
  8.             String pageText = extractTextFromPage(i);
  9.             Document doc = new Document(pageText);
  10.             doc.getMetadata().put("page", i);
  11.             documents.add(doc);
  12.         }
  13.         
  14.         return documents;
  15.     }
  16. }
复制代码
方式二:按段落读取(更智能)
  1. public class ParagraphPdfDocumentReader implements DocumentReader {
  2.     @Override
  3.     public List<Document> get() {
  4.         // 1. 提取段落
  5.         List<Paragraph> paragraphs = paragraphManager.flatten();
  6.         
  7.         // 2. 将相邻段落合并为文档
  8.         List<Document> documents = new ArrayList<>();
  9.         for (int i = 0; i < paragraphs.size(); i++) {
  10.             Paragraph from = paragraphs.get(i);
  11.             Paragraph to = (i + 1 < paragraphs.size())
  12.                 ? paragraphs.get(i + 1)
  13.                 : from;
  14.             
  15.             String text = getTextBetweenParagraphs(from, to);
  16.             Document doc = new Document(text);
  17.             addMetadata(from, to, doc);
  18.             documents.add(doc);
  19.         }
  20.         
  21.         return documents;
  22.     }
  23. }
复制代码
按段落读取的优势:

  • 保持语义完整性:段落是自然的语义单元
  • 更好的检索效果:段落级别的文档更适合向量搜索
  • 保留布局信息:可以保留 PDF 的布局结构
6.2 Markdown 读取实现

Markdown 读取器使用 CommonMark 解析器:
  1. public class MarkdownDocumentReader implements DocumentReader {
  2.     @Override
  3.     public List<Document> get() {
  4.         List<Document> documents = new ArrayList<>();
  5.         
  6.         for (Resource resource : markdownResources) {
  7.             // 1. 解析 Markdown
  8.             Node document = parser.parse(loadContent(resource));
  9.             
  10.             // 2. 访问文档节点
  11.             DocumentVisitor visitor = new DocumentVisitor(config);
  12.             document.accept(visitor);
  13.             
  14.             // 3. 收集文档
  15.             documents.addAll(visitor.getDocuments());
  16.         }
  17.         
  18.         return documents;
  19.     }
  20. }
复制代码
Markdown 读取器可以按以下方式分组:

  • 按标题分组:每个标题及其内容成为一个文档
  • 按段落分组:每个段落成为一个文档
  • 按水平线分组:水平线分隔的内容成为独立文档
6.3 Tika 通用读取实现

Tika 使用自动检测解析器:
  1. public class TikaDocumentReader implements DocumentReader {
  2.     @Override
  3.     public List<Document> get() {
  4.         try (InputStream stream = resource.getInputStream()) {
  5.             // 1. 自动检测文档类型并解析
  6.             parser.parse(stream, handler, metadata, context);
  7.             
  8.             // 2. 提取文本
  9.             String text = handler.toString();
  10.             
  11.             // 3. 格式化文本
  12.             text = textFormatter.format(text);
  13.             
  14.             // 4. 创建文档
  15.             Document doc = new Document(text);
  16.             doc.getMetadata().put(METADATA_SOURCE, resourceName());
  17.             
  18.             return List.of(doc);
  19.         }
  20.     }
  21. }
复制代码
Tika 的优势:

  • 支持多种格式:PDF、Word、PPT、Excel、HTML 等
  • 自动检测:无需指定文档类型
  • 提取元数据:自动提取文档的元数据
6.4 文本分块实现

文本分块是 RAG 应用的关键步骤:
  1. public abstract class TextSplitter implements DocumentTransformer {
  2.     @Override
  3.     public List<Document> apply(List<Document> documents) {
  4.         List<Document> chunks = new ArrayList<>();
  5.         
  6.         for (Document doc : documents) {
  7.             // 1. 分割文本
  8.             List<String> textChunks = splitText(doc.getText());
  9.             
  10.             // 2. 为每个分块创建文档
  11.             for (int i = 0; i < textChunks.size(); i++) {
  12.                 Map<String, Object> metadata = new HashMap<>(doc.getMetadata());
  13.                
  14.                 // 3. 添加分块元数据
  15.                 metadata.put("parent_document_id", doc.getId());
  16.                 metadata.put("chunk_index", i);
  17.                 metadata.put("total_chunks", textChunks.size());
  18.                
  19.                 Document chunk = Document.builder()
  20.                     .text(textChunks.get(i))
  21.                     .metadata(metadata)
  22.                     .score(doc.getScore())
  23.                     .build();
  24.                
  25.                 chunks.add(chunk);
  26.             }
  27.         }
  28.         
  29.         return chunks;
  30.     }
  31.    
  32.     protected abstract List<String> splitText(String text);
  33. }
复制代码
6.5 Token 分块实现

Token 分块使用编码器计算 token 数:
  1. public class TokenTextSplitter extends TextSplitter {
  2.     @Override
  3.     protected List<String> splitText(String text) {
  4.         // 1. 编码为 tokens
  5.         List<Integer> tokens = encoding.encode(text).boxed();
  6.         List<String> chunks = new ArrayList<>();
  7.         
  8.         while (!tokens.isEmpty() && chunks.size() < maxNumChunks) {
  9.             // 2. 取目标大小的 tokens
  10.             List<Integer> chunk = tokens.subList(0,
  11.                 Math.min(chunkSize, tokens.size()));
  12.             String chunkText = decodeTokens(chunk);
  13.             
  14.             // 3. 在标点符号处截断(保持语义)
  15.             int lastPunctuation = findLastPunctuation(chunkText);
  16.             if (lastPunctuation > minChunkSizeChars) {
  17.                 chunkText = chunkText.substring(0, lastPunctuation + 1);
  18.             }
  19.             
  20.             // 4. 过滤太短的分块
  21.             if (chunkText.length() > minChunkLengthToEmbed) {
  22.                 chunks.add(chunkText.trim());
  23.             }
  24.             
  25.             // 5. 移除已处理的 tokens
  26.             tokens = tokens.subList(getEncodedTokens(chunkText).size(),
  27.                 tokens.size());
  28.         }
  29.         
  30.         return chunks;
  31.     }
  32. }
复制代码
Token 分块的优势:

  • 精确控制大小:按 token 数分割,而不是字符数
  • 保持语义:在标点符号处截断
  • 适合嵌入模型:token 数是嵌入模型的输入单位
7. 文档分块策略

7.1 Token 分块(推荐)

适合大多数场景,特别是使用 OpenAI 等基于 token 的模型:
  1. TokenTextSplitter splitter = TokenTextSplitter.builder()
  2.     .chunkSize(800)              // 目标 token 数
  3.     .minChunkSizeChars(350)     // 最小字符数(避免过小)
  4.     .minChunkLengthToEmbed(5)    // 最小嵌入长度
  5.     .maxNumChunks(10000)        // 最大分块数
  6.     .keepSeparator(true)        // 保留分隔符
  7.     .build();
复制代码
7.2 字符分块

适合固定大小的分块需求:
  1. CharacterTextSplitter splitter = new CharacterTextSplitter(
  2.     1000,  // chunkSize
  3.     200    // chunkOverlap(重叠部分,保持上下文)
  4. );
复制代码
7.3 自定义分块

可以实现自己的分块策略:
  1. public class CustomTextSplitter extends TextSplitter {
  2.     @Override
  3.     protected List<String> splitText(String text) {
  4.         // 自定义分块逻辑
  5.         // 例如:按句子、按段落、按章节等
  6.         return customSplit(text);
  7.     }
  8. }
复制代码
8. 外部依赖

不同读取器的依赖:
8.1 PDF Reader


  • PDFBox:Apache PDFBox,PDF 解析库
  • 无其他依赖
8.2 Markdown Reader


  • CommonMark:Markdown 解析库
  • 无其他依赖
8.3 Tika Reader


  • Apache Tika:通用文档解析库
  • 支持 100+ 种格式
8.4 Text Splitter


  • tiktoken:Token 编码库(用于 TokenTextSplitter)
  • 无其他依赖(CharacterTextSplitter)
9. 工程总结

Spring AI 的文档处理能力设计有几个亮点:
统一的 Document 抽象。所有读取器都返回 Document 对象,这让后续处理(分块、嵌入、存储)变得统一。不管是从 PDF 还是 Word 读取,出来的都是 Document,处理起来很方便。
灵活的读取策略。不同格式有不同的读取策略(按页、按段落、按标题),可以根据需求选择最合适的方式。PDF 可以按页读,也可以按段落读,看你的需求。
智能的分块机制。Token 分块不仅考虑大小,还考虑语义完整性(在标点符号处截断),这提高了检索效果。不会在句子中间截断,保持语义完整。
元数据保留。分块时会保留原始文档的元数据,并添加分块相关的元数据(parent_document_id、chunk_index 等),这有助于追踪和调试。想知道某个分块来自哪个文档?看元数据就行。
可扩展性。所有组件都通过接口定义,可以轻松实现自定义的读取器和分块器。想支持新的文档格式?实现 DocumentReader 接口就行。
总的来说,Spring AI 的文档处理能力既全面又灵活。它支持多种文档格式,提供了智能的分块策略,同时保持了高度的可扩展性。这种设计让开发者可以轻松构建基于文档的 RAG 应用。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册