一、背景: 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 整体架构

💾 Writer 加载层

🔧 Transformer 转换层

📥 Reader 提取层

用户上传文档 / API 请求

Reader
数据提取层

TextReader

PDF Reader

Markdown Reader

HTML/JSoup Reader

Tika Reader

Transformer
数据转换层

TokenTextSplitter
智能分块

ContentFormatTransformer
格式标准化

MetadataEnricher
元数据增强
关键词/摘要

Writer
数据加载层

FileDocumentWriter
写入文件

SimpleVectorStore
写入向量库

向量检索 & RAG 查询

LLM 生成答案

架构设计哲学:三层之间通过标准的 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 数据流转原理

Resource 加载

解析提取

源文件
PDF/HTML/TXT

Reader

Document 对象

id: 唯一标识

content: 文本内容

metadata: 文件名/页码/标题等

核心概念 Document

  • id:文档唯一标识,可用于去重和更新
  • content:纯文本内容,后续分块和向量化的原料
  • metadata:键值对形式的元数据,如 file_namepage_numbercategory,可用于检索过滤
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 页的技术手册,用户问:“如何配置连接池超时时间?”

如果不分块,整本书的内容会被压缩成一个向量。这个向量虽然包含了"连接池"的语义,但也混杂了"部署指南"、"权限管理"等无关信息,导致检索时信噪比极低。

分块的本质是"语义聚焦":让每个向量只代表一个独立、完整的知识点,这样检索时才能精准命中。

TokenTextSplitter

TokenTextSplitter

TokenTextSplitter

Embedding

Embedding

Embedding

原始长文档
300页技术手册

文本块 Chunk 1
连接池配置

文本块 Chunk 2
日志级别设置

文本块 Chunk 3
安全认证流程

向量 1

向量 2

向量 3

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 打上"标签"和"摘要"。

LLM 调用

LLM 调用

文本块 Chunk

MetadataEnricher

提取 3 个关键词

生成上下文摘要
前一段/当前段/后一段

增强后的 Document
metadata.keywords
metadata.summary

两种增强方式

① 关键词增强

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 配额。建议:

  1. 仅对高价值文档启用,或抽样处理
  2. 使用本地缓存(如 Redis)避免重复处理相同文档
  3. 考虑用轻量级模型(如 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 向量检索原理
渲染错误: Mermaid 渲染失败: Parse error on line 2: ... LR A[用户问题
"如何配置连接池?"] -->|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()
);

检索过程解析

  1. 用户的查询文本通过 text-embedding-v1 转换为向量
  2. 向量库计算查询向量与所有文档向量的余弦相似度
  3. 按相似度排序,返回 Top-K 个最相关的 Chunk
  4. 这些 Chunk 作为上下文,与问题一起拼成 Prompt 发给 LLM

四、完整数据流:从文档到答案

为了更直观地理解整个系统如何工作,以下是用户发起一次 RAG 查询时的完整数据流:

DashScope LLM Vector Store Embedding Model Transformer Reader REST API 用户 DashScope LLM Vector Store Embedding Model Transformer Reader REST API 用户 === ETL 阶段(文档入库,一次性的)=== === RAG 查询阶段(每次提问)=== 上传文档(PDF/Markdown等) 1 读取源文件 2 返回 List<Document> 3 分块 + 格式清洗 + 元数据增强 4 返回处理后的 List<Document> 5 对每个 Chunk 调用 Embedding 6 返回向量 7 存储(向量 + 原文 + 元数据) 8 提问:"如何配置连接池?" 9 对问题文本生成向量 10 返回问题向量 11 similaritySearch(queryVector, topK=5) 12 返回最相关的 5 个 Chunk 13 发送 Prompt(问题 + 5个Chunk作为上下文) 14 生成基于上下文的答案 15 返回答案 16

五、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
  1. 访问 阿里云百炼控制台
  2. 创建应用并获取 API Key
  3. 务必确认已开通以下模型权限:
    • 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 系统从原型到生产,通常经历以下演进:

数据量增长
需持久化

文档来源增多
需异步处理

数据持续更新
需增量同步

多源异构数据
需统一接入

阶段一:原型验证
SimpleVectorStore
单机内存存储

阶段二:基础生产
PgVector / Milvus
持久化向量存储

阶段三:异步流水线
Kafka/RabbitMQ
解耦 ETL 各环节

阶段四:增量更新
定时任务 + CDC
实时感知文档变更

阶段五:数据中台
OSS + 各类数据库
统一 ETL 网关

7.3 性能优化 checklist

  1. 批量 Embedding:避免逐条调用 Embedding 接口,使用批量接口减少网络 RTT
  2. 异步处理:ETL 流水线中的非关键步骤(如元数据增强)放入异步队列
  3. 缓存策略:相同文本的 Embedding 结果缓存,避免重复计算
  4. 分块策略迭代:根据实际检索效果(准确率、召回率)持续调整 chunkSizeoverlap
  5. 混合检索:向量相似度检索 + 关键词过滤(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

三个需要特别注意的坑

  1. 向量存储不是永久的SimpleVectorStore 存在内存里,重启即失,生产环境必须替换
  2. 元数据增强有成本:每次调用 LLM 都花钱,大批量文档处理前先算一笔账
  3. 分块没有银弹chunkSize=800 不一定适合你的文档,需要根据实际问答效果反复调优

希望我这篇博文能帮助你快速理解并落地 RAG ETL Pipeline。如果有具体的调优问题或疑问,欢迎继续探讨。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐