黄俪把飞翔科技的员工手册 PDF 拖进工程文件夹:"这个 200 页的 PDF,怎么让 AI 读懂?"小崔头也不抬:"E-T-L——Extract 读出来,Transform 切成块,Load 写进向量库。三部曲,我帮你写个 Pipeline。"
ETL 数据注入流水线
定义与作用
ETL Pipeline 是 RAG 系统的数据准备流水线,将原始文档(PDF / TXT / JSON / Markdown)处理为向量库中的可检索片段:
Extract(提取)→ Transform(转换)→ Load(加载)
原始文档 文本分块 向量库
| 阶段 | Spring AI 组件 | 职责 |
|---|---|---|
| Extract | DocumentReader | 读取各种格式,产出 List<Document> |
| Transform | DocumentTransformer | 文本分割、元数据增强 |
| Load | VectorStore | 向量化 + 存储 |
核心原理:完整的 ETL 数据注入流程
图释:200 页 PDF 被拆分为 375 个文本块存入向量库。每个 Block 约 500 Token,加上前后 50 Token 的重叠区保证语义连续性。
DocumentReader 选型表
| Reader | 输入格式 | 典型使用场景 |
|---|---|---|
TextReader | .txt | 纯文本文件 |
PagePdfDocumentReader | PDF 文档(按页分割) | |
JsonReader | .json | 结构化 JSON 数据 |
MarkdownDocumentReader | .md | Markdown 文档 |
TikaDocumentReader | 任意格式 | Tika 支持的格式(Word、HTML 等) |
TokenTextSplitter 参数详解
TokenTextSplitter splitter = new TokenTextSplitter();
// 核心参数
splitter.setDefaultChunkSize(500); // 每块 500 Token
splitter.setMinChunkSizeChars(200); // 最小 200 字符(防止过小的 Block)
splitter.setMaxNumChunks(10000); // 最大块数(防止超大文档 OOM)
splitter.setKeepSeparator(true); // 保留分隔符(换行/句号)
| 参数 | 推荐值 | 说明 |
|---|---|---|
| chunkSize | 500 | 太小丢失上下文,太大搜索不精确 |
| overlap | 50 | 相邻块重叠 Token 数,防止关键信息被截断 |
| minChunkSizeChars | 200 | 过滤过小的分段 |
完整示例一:将飞翔科技员工手册注入向量库
场景说明
大翔要求:"客服 AI 必须知道我们的请假流程、报销制度和会议室使用规则。"这正是 ETL Pipeline 的经典场景。
依赖引入
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-transformers</artifactId>
</dependency>
关键代码
@Service
public class EmployeeManualIngestion {
private final VectorStore vectorStore;
public EmployeeManualIngestion(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void ingest() {
// ==================== E 阶段:提取 ====================
Resource pdfResource = new FileSystemResource(
"C:/docs/飞翔科技员工手册.pdf"
);
PagePdfDocumentReader reader = new PagePdfDocumentReader(pdfResource);
List<Document> documents = reader.get();
System.out.println("提取完成:" + documents.size() + " 页");
// ==================== T 阶段:分割 ====================
TokenTextSplitter splitter = new TokenTextSplitter();
splitter.setDefaultChunkSize(500);
splitter.setMinChunkSizeChars(200);
List<Document> chunks = splitter.apply(documents);
System.out.println("分割完成:" + chunks.size() + " 个文本块");
// ==================== L 阶段:加载 ====================
vectorStore.add(chunks);
System.out.println("注入完成!共 " + chunks.size() + " 条记录写入向量库");
}
}
运行结果
提取完成:200 页
分割完成:375 个文本块
注入完成!共 375 条记录写入向量库
完整示例二:带元数据增强的高级 Pipeline
场景说明
高英要求搜索结果不仅能按内容匹配,还能按"部门"和"主题"过滤——例如只查技术部的请假流程。
关键代码
@Service
public class AdvancedIngestion {
private final VectorStore vectorStore;
public AdvancedIngestion(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void ingestWithMetadata() {
Resource pdfResource = new FileSystemResource(
"C:/docs/飞翔科技制度汇编.pdf"
);
// E: 提取
PagePdfDocumentReader reader = new PagePdfDocumentReader(pdfResource);
List<Document> docs = reader.get();
// 手动添加元数据(根据页码范围)
for (Document doc : docs) {
int pageNum = (int) doc.getMetadata().getOrDefault("page_number", 0);
if (pageNum <= 20) {
doc.getMetadata().put("department", "全公司");
doc.getMetadata().put("topic", "考勤与请假");
} else if (pageNum <= 50) {
doc.getMetadata().put("department", "技术部");
doc.getMetadata().put("topic", "报销与采购");
} else {
doc.getMetadata().put("department", "全公司");
doc.getMetadata().put("topic", "会议室使用");
}
}
// T: 分割
TokenTextSplitter splitter = new TokenTextSplitter();
splitter.setDefaultChunkSize(500);
List<Document> chunks = splitter.apply(docs);
// L: 加载
vectorStore.add(chunks);
System.out.println("带元数据的 ETL 完成,共 " + chunks.size() + " 块");
}
/**
* 按部门 + 主题精确检索
*/
public List<Document> searchByDepartment(String question, String department) {
return vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(5)
.similarityThreshold(0.7)
.filterExpression("department == '" + department + "'")
.build()
);
}
}
易错场景与面试考点
易错场景一:chunkSize 过大导致检索失败
// ❌ 错误:chunkSize=5000,一个 Block 包含 5 页内容
TokenTextSplitter splitter = new TokenTextSplitter();
splitter.setDefaultChunkSize(5000); // 太大!
// 问题:搜索"请假流程"可能匹配到包含"请假"Block,但 Block 中 95% 内容不相关
// 结果:AI 被大量无关上下文淹没,回答质量下降
问题分析
chunkSize 过大→一个 Block 包含太多无关信息→搜索结果的"信噪比"低→AI 回答不准确。
// ✅ 正确:chunkSize=500,overlap=50
splitter.setDefaultChunkSize(500); // 约 300 个汉字
splitter.setMinChunkSizeChars(200);
// 同时设置 overlap 防止关键段落在边界被截断
面试高频题
Q1:ETL Pipeline 的三个阶段各自承担什么职责?
Extract(DocumentReader):从 PDF/文本/JSON 中读取原始内容,产出 Document 列表。Transform(DocumentTransformer):把长文档切成小块(TokenTextSplitter),增强元数据(KeywordMetadataEnricher)。Load(VectorStore.add()):对每个 Document 调用 Embedding 模型生成向量,写入向量数据库。
Q2:为什么需要 overlap?
文本分割是盲切的——可能把一句完整的话切成两段,分属两个 Block。overlap 让相邻 Block 共享部分内容,确保没有关键信息在边界丢失。推荐的 overlap 值通常是 chunkSize 的 10%。
本章小结
- ETL Pipeline = Extract(DocumentReader) + Transform(DocumentTransformer) + Load(VectorStore)
- TokenTextSplitter 的核心参数:chunkSize(500 推荐)、overlap(50 推荐)
- 元数据增强(metadata)支持按部门、分类等条件过滤搜索
- chunkSize 的选择是精度和召回率的权衡:太小丢失上下文,太大影响搜索精度