文档处理能力分析
请关注微信公众号:阿呆-bot
1. 工程结构概览
Spring AI 提供了完整的文档处理能力,包括文档读取、文本分块和预处理。这些能力是 RAG 应用的基础。
document-readers/ # 文档读取器 ├── pdf-reader/ # PDF 读取器 │ ├── PagePdfDocumentReader.java # 按页读取 │ └── ParagraphPdfDocumentReader.java # 按段落读取 ├── markdown-reader/ # Markdown 读取器 │ └── MarkdownDocumentReader.java ├── tika-reader/ # 通用文档读取器(Tika) │ └── TikaDocumentReader.java └── jsoup-reader/ # HTML 读取器 └── JsoupDocumentReader.java spring-ai-commons/ # 核心处理能力 ├── document/ │ └── Document.java # 文档对象 └── transformer/ └── splitter/ # 文本分块 ├── TextSplitter.java ├── TokenTextSplitter.java └── CharacterTextSplitter.java
2. 技术体系与模块关系
文档处理流程:读取 → 分块 → 嵌入 → 存储

3. 关键场景示例代码
3.1 PDF 文档读取
PDF 读取支持按页和按段落两种方式:
// 按页读取 Resource pdfResource = new ClassPathResource("document.pdf"); PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(pdfResource); List<Document> documents = pdfReader.get(); // 按段落读取(更智能) ParagraphPdfDocumentReader paragraphReader = new ParagraphPdfDocumentReader(pdfResource, config); List<Document> documents = paragraphReader.get();
3.2 Markdown 文档读取
Markdown 读取器可以按标题、段落或水平线分组:
MarkdownDocumentReader markdownReader = new MarkdownDocumentReader("classpath:docs/*.md", config); List<Document> documents = markdownReader.get();
3.3 Tika 通用读取
Tika 可以读取多种格式(PDF、Word、PPT 等):
TikaDocumentReader tikaReader = new TikaDocumentReader("classpath:document.docx"); List<Document> documents = tikaReader.get();
3.4 文档分块
将长文档分割成适合嵌入的小块:
// Token 分块(推荐) TokenTextSplitter splitter = TokenTextSplitter.builder() .chunkSize(800) // 目标 token 数 .minChunkSizeChars(350) // 最小字符数 .build(); List<Document> chunks = splitter.split(documents); // 字符分块 CharacterTextSplitter charSplitter = new CharacterTextSplitter(1000, 200); List<Document> chunks = charSplitter.split(documents);
3.5 完整流程
文档处理的完整流程:
// 1. 读取文档 TikaDocumentReader reader = new TikaDocumentReader("document.pdf"); List<Document> documents = reader.get(); // 2. 分块 TokenTextSplitter splitter = new TokenTextSplitter(); List<Document> chunks = splitter.split(documents); // 3. 嵌入并存储 vectorStore.add(chunks);
4. 核心实现图
4.1 文档处理流程

5. 入口类与关键类关系

6. 关键实现逻辑分析
6.1 PDF 读取实现
PDF 读取有两种方式:
方式一:按页读取
public class PagePdfDocumentReader implements DocumentReader { @Override public List<Document> get() { List<Document> documents = new ArrayList<>(); int pageCount = document.getNumberOfPages(); for (int i = 0; i < pageCount; i++) { String pageText = extractTextFromPage(i); Document doc = new Document(pageText); doc.getMetadata().put("page", i); documents.add(doc); } return documents; } }
方式二:按段落读取(更智能)
public class ParagraphPdfDocumentReader implements DocumentReader { @Override public List<Document> get() { // 1. 提取段落 List<Paragraph> paragraphs = paragraphManager.flatten(); // 2. 将相邻段落合并为文档 List<Document> documents = new ArrayList<>(); for (int i = 0; i < paragraphs.size(); i++) { Paragraph from = paragraphs.get(i); Paragraph to = (i + 1 < paragraphs.size()) ? paragraphs.get(i + 1) : from; String text = getTextBetweenParagraphs(from, to); Document doc = new Document(text); addMetadata(from, to, doc); documents.add(doc); } return documents; } }
按段落读取的优势:
- 保持语义完整性:段落是自然的语义单元
- 更好的检索效果:段落级别的文档更适合向量搜索
- 保留布局信息:可以保留 PDF 的布局结构
6.2 Markdown 读取实现
Markdown 读取器使用 CommonMark 解析器:
public class MarkdownDocumentReader implements DocumentReader { @Override public List<Document> get() { List<Document> documents = new ArrayList<>(); for (Resource resource : markdownResources) { // 1. 解析 Markdown Node document = parser.parse(loadContent(resource)); // 2. 访问文档节点 DocumentVisitor visitor = new DocumentVisitor(config); document.accept(visitor); // 3. 收集文档 documents.addAll(visitor.getDocuments()); } return documents; } }
Markdown 读取器可以按以下方式分组:
- 按标题分组:每个标题及其内容成为一个文档
- 按段落分组:每个段落成为一个文档
- 按水平线分组:水平线分隔的内容成为独立文档
6.3 Tika 通用读取实现
Tika 使用自动检测解析器:
public class TikaDocumentReader implements DocumentReader { @Override public List<Document> get() { try (InputStream stream = resource.getInputStream()) { // 1. 自动检测文档类型并解析 parser.parse(stream, handler, metadata, context); // 2. 提取文本 String text = handler.toString(); // 3. 格式化文本 text = textFormatter.format(text); // 4. 创建文档 Document doc = new Document(text); doc.getMetadata().put(METADATA_SOURCE, resourceName()); return List.of(doc); } } }
Tika 的优势:
- 支持多种格式:PDF、Word、PPT、Excel、HTML 等
- 自动检测:无需指定文档类型
- 提取元数据:自动提取文档的元数据
6.4 文本分块实现
文本分块是 RAG 应用的关键步骤:
public abstract class TextSplitter implements DocumentTransformer { @Override public List<Document> apply(List<Document> documents) { List<Document> chunks = new ArrayList<>(); for (Document doc : documents) { // 1. 分割文本 List<String> textChunks = splitText(doc.getText()); // 2. 为每个分块创建文档 for (int i = 0; i < textChunks.size(); i++) { Map<String, Object> metadata = new HashMap<>(doc.getMetadata()); // 3. 添加分块元数据 metadata.put("parent_document_id", doc.getId()); metadata.put("chunk_index", i); metadata.put("total_chunks", textChunks.size()); Document chunk = Document.builder() .text(textChunks.get(i)) .metadata(metadata) .score(doc.getScore()) .build(); chunks.add(chunk); } } return chunks; } protected abstract List<String> splitText(String text); }
6.5 Token 分块实现
Token 分块使用编码器计算 token 数:
public class TokenTextSplitter extends TextSplitter { @Override protected List<String> splitText(String text) { // 1. 编码为 tokens List<Integer> tokens = encoding.encode(text).boxed(); List<String> chunks = new ArrayList<>(); while (!tokens.isEmpty() && chunks.size() < maxNumChunks) { // 2. 取目标大小的 tokens List<Integer> chunk = tokens.subList(0, Math.min(chunkSize, tokens.size())); String chunkText = decodeTokens(chunk); // 3. 在标点符号处截断(保持语义) int lastPunctuation = findLastPunctuation(chunkText); if (lastPunctuation > minChunkSizeChars) { chunkText = chunkText.substring(0, lastPunctuation + 1); } // 4. 过滤太短的分块 if (chunkText.length() > minChunkLengthToEmbed) { chunks.add(chunkText.trim()); } // 5. 移除已处理的 tokens tokens = tokens.subList(getEncodedTokens(chunkText).size(), tokens.size()); } return chunks; } }
Token 分块的优势:
- 精确控制大小:按 token 数分割,而不是字符数
- 保持语义:在标点符号处截断
- 适合嵌入模型:token 数是嵌入模型的输入单位
7. 文档分块策略
7.1 Token 分块(推荐)
适合大多数场景,特别是使用 OpenAI 等基于 token 的模型:
TokenTextSplitter splitter = TokenTextSplitter.builder() .chunkSize(800) // 目标 token 数 .minChunkSizeChars(350) // 最小字符数(避免过小) .minChunkLengthToEmbed(5) // 最小嵌入长度 .maxNumChunks(10000) // 最大分块数 .keepSeparator(true) // 保留分隔符 .build();
7.2 字符分块
适合固定大小的分块需求:
CharacterTextSplitter splitter = new CharacterTextSplitter( 1000, // chunkSize 200 // chunkOverlap(重叠部分,保持上下文) );
7.3 自定义分块
可以实现自己的分块策略:
public class CustomTextSplitter extends TextSplitter { @Override protected List<String> splitText(String text) { // 自定义分块逻辑 // 例如:按句子、按段落、按章节等 return customSplit(text); } }
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 应用。