【第17篇】玩转RAG之ETL Pipelin
一、背景: RAG ETL?
RAG(Retrieval-Augmented Generation,检索增强生成) 的核心思想很简单:在提问大模型之前,先从企业的私有知识库中检索出相关的上下文,把"问题 + 相关文档片段"一起喂给模型,让它基于事实作答。
但原始文档(PDF、Word、网页等)并不能直接被向量数据库使用,这就需要一个数据加工流水线——也就是 ETL(Extract-Transform-Load):
- Extract:从各种格式的源文档中提取原始文本
- Transform:将长文本切分成语义完整的块,并提取元数据
- Load:将处理后的文本块转换为向量,存入向量数据库
只有经过 ETL 处理的知识库,才能支撑起高质量的 RAG 问答。
二、项目概述
2.1 项目定位
本项目是 Spring AI Alibaba 生态中的 RAG ETL 流水线示例,完整演示了从"原始文档"到"可检索向量"的全流程。它基于 Spring Boot 3.x 构建,通过 Spring AI Alibaba 统一抽象,接入阿里云 DashScope 的大模型和 Embedding 能力。
2.2 核心技术栈
| 组件 | 技术 | 作用 |
|---|---|---|
| 应用框架 | Spring Boot 3.x | 快速构建 REST API 服务 |
| AI 抽象层 | Spring AI Alibaba | 统一封装 LLM、Embedding、Vector Store 能力 |
| 大语言模型 | 阿里云 DashScope (qwen-max) | 提供对话与文本生成能力 |
| 向量嵌入 | text-embedding-v1 | 将文本语义转换为高维向量 |
| 文档解析 | Tika、JSoup、PDF Reader | 解析 PDF、HTML、Markdown、Office 等格式 |
| 向量存储 | SimpleVectorStore(内存) | 轻量级存储,适合开发与演示 |
2.3 整体架构
架构设计哲学:三层之间通过标准的 Document 对象传递数据,每一层都是可插拔的。你可以只替换 Reader 来支持新的文档格式,或者替换 Writer 来接入生产级向量数据库,而无需改动其他层的代码。
三、核心组件详解
3.1 Reader(数据提取层)
Reader 的职责是消除文档格式的差异性。无论源文件是 PDF、网页还是 Word,Reader 都会将其统一转换为 Spring AI 标准的 Document 对象。
3.1.1 支持的文档格式
| Reader | 支持格式 | 适用场景 |
|---|---|---|
TextReader |
.txt |
简单文本处理 |
JsonReader |
.json |
结构化数据读取 |
PagePdfDocumentReader |
.pdf |
按页读取 PDF,保留页面结构 |
ParagraphPdfDocumentReader |
.pdf |
按段落/章节读取,适合有目录的学术 PDF |
MarkdownDocumentReader |
.md |
Markdown 文档,保留标题层级 |
JsoupDocumentReader |
.html |
网页内容提取,自动过滤标签 |
TikaDocumentReader |
多种格式 | 通用解析(PDF/Word/Excel/PPT 等),基于 Apache Tika |
3.1.2 数据流转原理
核心概念 Document:
id:文档唯一标识,可用于去重和更新content:纯文本内容,后续分块和向量化的原料metadata:键值对形式的元数据,如file_name、page_number、category,可用于检索过滤
3.1.3 代码示例
@GetMapping("/pdf-page")
public List<Document> readPdfPage() {
// Spring Resource 抽象,支持 classpath、file、http 等多种来源
Resource resource = new DefaultResourceLoader()
.getResource(Constant.PDF_FILE_PATH);
// 按页读取 PDF,每页生成一个 Document
PagePdfDocumentReader reader = new PagePdfDocumentReader(resource);
return reader.read();
}
3.1.4 常见问题与解决方案
| 问题 | 根因 | 解决方案 |
|---|---|---|
| PDF 读取乱码 | PDF 内嵌字体缺失或文件加密 | 换用 ParagraphPdfDocumentReader,或先用工具将 PDF 转为文本 |
| 大文件内存溢出 | Reader 一次性将整个文件载入内存 | 采用流式读取,或先对大文件做物理拆分 |
| HTML 内容夹杂标签 | 原始 HTML 包含脚本和样式 | 使用 JsoupDocumentReader,它会自动清洗 HTML 标签 |
3.2 Transformer(数据转换层)
如果说 Reader 解决了"读得到"的问题,Transformer 则解决"读得好"的问题。它是整个 RAG 流水线中对最终问答质量影响最大的环节。
3.2.1 为什么必须做文本分块?
想象你有一本 300 页的技术手册,用户问:“如何配置连接池超时时间?”
如果不分块,整本书的内容会被压缩成一个向量。这个向量虽然包含了"连接池"的语义,但也混杂了"部署指南"、"权限管理"等无关信息,导致检索时信噪比极低。
分块的本质是"语义聚焦":让每个向量只代表一个独立、完整的知识点,这样检索时才能精准命中。
3.2.2 TokenTextSplitter(智能分块)
这是 Transformer 中最核心的组件,负责将长文本切分成适合向量化的小块。
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(800) // 每个块的目标 token 数
.withMinChunkSizeChars(350) // 每个块的最小字符数
.withMinChunkLengthToEmbed(5) // 小于此长度的块直接丢弃
.withMaxNumChunks(10000) // 单文档最多生成的块数
.withKeepSeparator(true) // 是否保留换行等分隔符
.build();
参数调优指南:
| 参数 | 设置过小 | 设置过大 | 推荐实践 |
|---|---|---|---|
chunkSize |
上下文碎片化,丢失前后关联 | 向量语义混杂,检索精度下降 | 500-1000 tokens,根据文档类型调整 |
minChunkSizeChars |
产生大量无意义碎片 | 遗漏短但重要的段落(如配置项) | 300-500 字符 |
keepSeparator |
段落边界丢失 | 保留换行有助于后续格式恢复 | Markdown/代码文档建议 true |
分块策略的选择建议:
- 固定大小分块(本项目方式):实现简单,通用性强,适合大多数文档
- 递归字符分块:按段落 -> 句子 -> 单词的优先级递归切分,保留更多语义边界
- 语义分块:基于模型判断句子边界,效果最好但成本最高
3.2.3 ContentFormatTransformer(格式标准化)
文档从各处收集而来,格式往往参差不齐:有的用 \r\n 换行,有的夹杂大量空格,有的包含特殊控制字符。这个组件负责"清洗":
DefaultContentFormatter formatter = DefaultContentFormatter.defaultConfig();
ContentFormatTransformer transformer = new ContentFormatTransformer(formatter);
典型处理:
- 去除首尾空白字符
- 将多个连续空格合并为一个
- 统一换行符为
\n - 过滤不可见控制字符
3.2.4 MetadataEnricher(元数据增强)
这是 Transformer 中**最"智能"**的环节。它调用 LLM 为每个文本块自动生成附加信息,相当于给每个 Chunk 打上"标签"和"摘要"。
两种增强方式:
① 关键词增强:
KeywordMetadataEnricher enricher =
new KeywordMetadataEnricher(this.chatModel, 3); // 提取 3 个关键词
- 作用:提升基于关键词的精确匹配和过滤能力
- 适用:文档主题明确、需要快速分类的场景
② 摘要增强:
List<SummaryMetadataEnricher.SummaryType> types = List.of(
SummaryMetadataEnricher.SummaryType.PREVIOUS, // 前一段摘要
SummaryMetadataEnricher.SummaryType.CURRENT, // 当前段摘要
SummaryMetadataEnricher.SummaryType.NEXT // 后一段摘要
);
SummaryMetadataEnricher enricher =
new SummaryMetadataEnricher(this.chatModel, types);
- 作用:在检索时,即使只命中一个 Chunk,也能通过摘要了解其上下文,缓解"断章取义"问题
- 适用:长文档、强上下文依赖的技术文档
⚠️ 重要成本提醒:元数据增强需要调用 LLM(如 qwen-max),每次调用都会消耗 DashScope API 配额。建议:
- 仅对高价值文档启用,或抽样处理
- 使用本地缓存(如 Redis)避免重复处理相同文档
- 考虑用轻量级模型(如 qwen-turbo)替代 qwen-max 做元数据提取
3.3 Writer(数据加载层)
Writer 负责将处理好的 Document 持久化,是 ETL 流水线的终点,也是 RAG 查询的起点。
3.3.1 FileDocumentWriter(文件写入)
主要用于调试和备份,将处理后的文本块写入本地文件查看效果:
FileDocumentWriter writer = new FileDocumentWriter("output.txt", true);
writer.accept(documents); // true = 追加写入,false = 覆盖
3.3.2 SimpleVectorStore(向量存储)
SimpleVectorStore vectorStore = SimpleVectorStore.builder(embeddingModel).build();
vectorStore.add(documents); // 文档自动向量化并存储
关键特性:
| 特性 | 说明 |
|---|---|
| 存储介质 | JVM 内存(ConcurrentHashMap) |
| 持久化 | ❌ 应用重启数据丢失 |
| 适用场景 | 开发测试、快速原型验证 |
| 生产替代 | Milvus、PgVector、Elasticsearch、阿里云百炼向量检索 |
3.3.3 向量检索原理
"如何配置连接池?"] -->|Embed ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("如何配置连接池超时时间?")
.topK(5) // 返回最相似的 5 个 Chunk
.similarityThreshold(0.7) // 可选:相似度阈值过滤
.build()
);
检索过程解析:
- 用户的查询文本通过
text-embedding-v1转换为向量 - 向量库计算查询向量与所有文档向量的余弦相似度
- 按相似度排序,返回 Top-K 个最相关的 Chunk
- 这些 Chunk 作为上下文,与问题一起拼成 Prompt 发给 LLM
四、完整数据流:从文档到答案
为了更直观地理解整个系统如何工作,以下是用户发起一次 RAG 查询时的完整数据流:
五、API 接口清单
项目将 ETL 各阶段的能力封装为 REST 接口,方便独立测试和调试。
5.1 Reader 接口
| 接口路径 | 方法 | 功能描述 |
|---|---|---|
/reader/text |
GET | 读取纯文本文件 |
/reader/json |
GET | 读取 JSON 文件 |
/reader/pdf-page |
GET | 按页面读取 PDF |
/reader/pdf-paragraph |
GET | 按段落/章节读取 PDF |
/reader/markdown |
GET | 读取 Markdown 文档 |
/reader/html |
GET | 通过 JSoup 读取 HTML |
/reader/tika |
GET | Tika 通用文档解析 |
5.2 Transformer 接口
| 接口路径 | 方法 | 功能描述 |
|---|---|---|
/transformer/token-text-splitter |
GET | 智能分块演示 |
/transformer/content-format-transformer |
GET | 格式标准化演示 |
/transformer/keyword-metadata-enricher |
GET | 关键词元数据增强(⚠️ 消耗 API) |
/transformer/summary-metadata-enricher |
GET | 摘要元数据增强(⚠️ 消耗 API) |
5.3 Writer 接口
| 接口路径 | 方法 | 功能描述 |
|---|---|---|
/writer/file |
GET | 写入本地文件 |
/writer/vector |
GET | 写入向量数据库 |
/writer/search |
GET | 执行向量相似度检索 |
六、部署实操指南
6.1 环境准备
步骤 1:安装 Java 17+
# 验证 Java 版本
java -version
# 如未安装,推荐通过 SDKMAN 安装
curl -s "https://get.sdkman.io" | bash
source ~/.bashrc
sdk install java 17.0.9-tem
步骤 2:安装 Maven
# 验证 Maven
mvn -version
# Ubuntu/Debian
sudo apt install maven
# macOS
brew install maven
步骤 3:获取阿里云 DashScope API Key
- 访问 阿里云百炼控制台
- 创建应用并获取 API Key
- 务必确认已开通以下模型权限:
qwen-max(对话模型,用于元数据增强和最终问答)text-embedding-v1(向量嵌入模型)
6.2 项目构建与启动
方式一:直接构建子模块(推荐)
cd /home/tht/examples-main/spring-ai-alibaba-rag-example/rag-etl-pipeline-example
# 设置环境变量(Linux/macOS)
export AI_DASHSCOPE_API_KEY="your-api-key-here"
# 或写入 ~/.bashrc 永久生效
echo 'export AI_DASHSCOPE_API_KEY="your-api-key-here"' >> ~/.bashrc
source ~/.bashrc
# 构建并启动
mvn clean package -DskipTests
mvn spring-boot:run
方式二:从父项目构建
cd /home/tht/examples-main/spring-ai-alibaba-rag-example
# 安装父 POM 及依赖模块
mvn clean install -DskipTests -pl rag-etl-pipeline-example -am
cd rag-etl-pipeline-example
mvn spring-boot:run
常见问题排查
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
Could not resolve dependencies |
Maven 依赖下载失败 | 检查网络,配置国内镜像(阿里云 Maven) |
ApiException: invalid api key |
API Key 无效或权限不足 | 核对 Key 是否正确,确认已开通 qwen-max 和 text-embedding-v1 |
Port 8080 already in use |
端口冲突 | lsof -i :8080 查看占用进程,或在 application.yml 中修改 server.port |
6.3 测试验证
# ===== Reader 测试 =====
curl http://localhost:8080/reader/pdf-page
curl http://localhost:8080/reader/markdown
curl http://localhost:8080/reader/tika
# ===== Transformer 测试 =====
curl http://localhost:8080/transformer/token-text-splitter
# ⚠️ 以下接口消耗 API 配额
curl http://localhost:8080/transformer/keyword-metadata-enricher
# ===== Writer & 检索测试 =====
curl http://localhost:8080/writer/vector
curl http://localhost:8080/writer/search
七、生产环境建议
7.1 向量数据库选型
| 数据库 | 核心特点 | 最佳适用场景 |
|---|---|---|
| Milvus | 开源、高性能、支持亿级向量、分布式架构 | 大规模生产环境,需要高并发检索 |
| PgVector | PostgreSQL 插件,SQL 接口,事务支持 | 中小规模,已有 PostgreSQL 基础设施 |
| Elasticsearch | 全文检索 + 向量检索混合,生态成熟 | 已使用 ES 的团队,需要混合搜索 |
| 阿里云百炼向量检索 | 全托管云服务,免运维,与 DashScope 深度集成 | 快速上线,不愿投入运维资源 |
7.2 架构演进路线
一个 RAG 系统从原型到生产,通常经历以下演进:
7.3 性能优化 checklist
- 批量 Embedding:避免逐条调用 Embedding 接口,使用批量接口减少网络 RTT
- 异步处理:ETL 流水线中的非关键步骤(如元数据增强)放入异步队列
- 缓存策略:相同文本的 Embedding 结果缓存,避免重复计算
- 分块策略迭代:根据实际检索效果(准确率、召回率)持续调整
chunkSize和overlap - 混合检索:向量相似度检索 + 关键词过滤(BM25)结合,提升综合效果
八、完整示例:构建一个可用的 RAG 系统
以下是一个最小可用的自定义 RAG Controller,可直接集成到项目中:
步骤 1:准备知识库
将文档放入 src/main/resources/data/ 目录,支持 PDF、Markdown、TXT 等格式。
步骤 2:修改配置
public class Constant {
public static final String PREFIX = "classpath:data/";
public static final String PDF_FILE_PATH = PREFIX + "your-document.pdf";
}
步骤 3:编写 RAG 问答接口
@RestController
@RequestMapping("/rag")
public class CustomRagController {
private final VectorStore vectorStore;
private final ChatModel chatModel;
private final ChatMemory chatMemory;
public CustomRagController(VectorStore vectorStore, ChatModel chatModel) {
this.vectorStore = vectorStore;
this.chatModel = chatModel;
// 保留最近 20 轮对话上下文
this.chatMemory = new MessageWindowChatMemory(20);
}
@PostMapping("/ask")
public String ask(@RequestBody Map<String, String> request) {
String question = request.get("question");
// 1. 向量检索:将问题向量化,召回 Top-5 相关 Chunk
List<Document> results = vectorStore.similaritySearch(
SearchRequest.query(question).topK(5)
);
// 2. 构建上下文:将 Chunk 内容拼接
String context = results.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n---\n"));
// 3. 构造 Prompt:明确约束模型基于上下文作答
String prompt = """
你是一个专业的技术助手。请严格根据以下提供的上下文回答问题。
如果上下文中没有相关信息,请明确告知"根据现有资料无法回答",不要编造。
=== 上下文 ===
%s
=== 用户问题 ===
%s
""".formatted(context, question);
// 4. 调用 LLM 生成答案
return chatModel.call(prompt);
}
}
步骤 4:配置生产级 VectorStore(以 Milvus 为例)
# application.yml
spring:
ai:
vectorstore:
milvus:
url: ${MILVUS_URL:localhost:19530}
collection-name: my_rag_collection
dimension: 1536 # 与 embedding 模型维度一致
index-type: IVF_FLAT
metric-type: COSINE
九、总结
本项目完整展示了 RAG ETL 流水线的核心实现,其价值不仅在于代码本身,更在于提供了一套标准化的文档处理范式:
| 层级 | 核心职责 | 关键决策点 |
|---|---|---|
| Reader | 多格式文档解析 | 根据文档类型选择合适的 Reader,PDF 优先考虑 ParagraphPdfDocumentReader |
| Transformer | 分块 + 清洗 + 增强 | chunkSize 决定检索精度与上下文丰富度的平衡;元数据增强需评估成本 |
| Writer | 持久化与检索 | 开发用 SimpleVectorStore,生产务必替换为 Milvus/PgVector |
三个需要特别注意的坑:
- 向量存储不是永久的:
SimpleVectorStore存在内存里,重启即失,生产环境必须替换 - 元数据增强有成本:每次调用 LLM 都花钱,大批量文档处理前先算一笔账
- 分块没有银弹:
chunkSize=800不一定适合你的文档,需要根据实际问答效果反复调优
希望我这篇博文能帮助你快速理解并落地 RAG ETL Pipeline。如果有具体的调优问题或疑问,欢迎继续探讨。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)