Spring AI 实战系列|第 5 篇 RAG 检索增强生成(原理剖析 + 管线搭建 + 企业知识库实战)
📊 本文约 12000 字,阅读时间约 40 分钟。
系列说明:本文是《Spring AI 实战系列》第 5 篇,深入讲解 RAG(检索增强生成)的原理、Spring AI RAG 管线搭建、Advisor 高级用法,以及企业知识库实战。 前置知识:已完成第 4 篇,掌握 Embedding 和向量数据库基础。
文章目录
- Spring AI 实战系列|第 5 篇 RAG 检索增强生成(原理剖析 + 管线搭建 + 企业知识库实战)
-
- 前言
- 一、RAG 核心原理
-
- 1.1 传统 AI 对话的问题
- 1.2 RAG 的解决方案
- 1.3 RAG 的完整流程
- 二、手动搭建 RAG 管线
-
- 2.1 项目结构
- 2.2 依赖配置
- 2.3 配置文件
- 2.4 文档索引服务
- 2.5 RAG 查询服务
- 2.6 Controller 层
- 2.7 启动时自动加载文档
- 三、QuestionAnswerAdvisor — 声明式 RAG
-
- 3.1 基础用法
- 3.2 自定义搜索参数
- 3.3 自定义 Prompt 模板
- 3.4 手动 RAG vs Advisor 对比
- 四、自定义 Advisor 与高级 RAG
-
- 4.1 Advisor 接口理解
- 4.2 自定义 RAG Advisor(带摘要压缩)
- 4.3 多个 Advisor 组合
- 4.4 检索结果重排序(RRF)
- 五、企业知识库实战
-
- 5.1 系统架构
- 5.2 文档上传处理
- 5.3 带对话记忆的智能问答
- 5.4 Docker Compose 部署
- 六、RAG 效果评估
-
- 6.1 评估维度
- 6.2 忠实度自动评估
- 6.3 RAG 的局限性
- 七、常见问题
- 参考资源
- 写在最后
前言
去年有客户反馈,其 AI 客服经常\”胡说八道\”——向用户推荐根本不存在的功能,并编造产品参数。
此即典型的 AI 幻觉问题。大模型的知识来源于训练数据,存在截止日期,且倾向于\”脑补\”。
RAG(Retrieval-Augmented Generation,检索增强生成)正是解决此问题的核心方案。它使 AI 在回答之前,先从企业知识库中检索相关信息,基于事实进行回答。
本文从原理到实战,将 RAG 论述透彻。
一、RAG 核心原理
1.1 传统 AI 对话的问题
用户问:你们产品的退款政策是什么?
AI 回答(基于训练数据\”猜\”的):
\”根据一般电商平台的规则,7 天无理由退款…\”
❌ 问题:这不是你们公司的实际政策!
大模型存在三个固有缺陷:
| 幻觉 | 编造不存在的信息 | 给出错误答案 |
| 知识截止 | 训练数据有日期限制 | 无法获知最新信息 |
| 无私有知识 | 无法访问企业内部文档 | 无法回答业务问题 |
| 不可溯源 | 无法追溯答案来源 | 出现问题难以排查 |
1.2 RAG 的解决方案
用户问:你们产品的退款政策是什么?
Step 1: 检索 → 从知识库找到《退款政策 v2.3.pdf》
\”我司支持 30 天无理由退款,需保持商品完好…\”
Step 2: 增强 → 将检索到的内容连同用户问题一并提交给 AI
Step 3: 生成 → AI 基于检索到的内容回答
\”根据我司退款政策(v2.3),支持 30 天无理由退款…\” ✅
AI 的回答基于企业真实文档,可溯源、不编造。
1.3 RAG 的完整流程

RAG 分为两个阶段:
离线阶段(索引):
原始文档 → 读取解析 → 文本清洗 → 分块切分
→ Embedding 向量化 → 存入向量数据库
在线阶段(查询):
用户提问 → 问题 Embedding → 向量相似度搜索
→ 获取 Top-K 相关文档 → 构建 Prompt(检索结果 + 用户问题)
→ LLM 生成回答 → 返回给用户
二、手动搭建 RAG 管线
先以手动方式实现一遍,以理解每个环节的作用。
2.1 项目结构
rag-demo/
├── src/main/java/com/example/rag/
│ ├── RagApplication.java
│ ├── config/RagConfig.java
│ ├── service/
│ │ ├── DocumentIndexService.java # 文档索引(离线)
│ │ └── RagQueryService.java # RAG 查询(在线)
│ └── controller/RagController.java
├── src/main/resources/
│ ├── application.yml
│ └── documents/ # 知识库文档目录
└── pom.xml
2.2 依赖配置
<dependencies>
<!– Spring Boot Web –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!– Spring AI OpenAI(含 Chat + Embedding) –>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!– PGvector 向量数据库 –>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<!– PDF 文档读取 –>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
</dependencies>
2.3 配置文件
spring:
ai:
openai:
api-key: ${
OPENAI_API_KEY}
chat:
options:
model: gpt–4o
temperature: 0.3 # RAG 场景建议低温度,减少发散
embedding:
options:
model: text–embedding–3–small
vectorstore:
pgvector:
index-type: hnsw
distance-type: cosine
dimensions: 1536 # 必须与 Embedding 模型维度一致
initialize-schema: true # 自动建表
datasource:
url: jdbc:postgresql://localhost:5432/vectordb
username: postgres
password: password
2.4 文档索引服务
负责将各种格式的文档读入、分块、存入向量库:
@Service
public class DocumentIndexService {
private final VectorStore vectorStore;
public DocumentIndexService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/** 索引 PDF 文档 */
public void indexPdf(Resource pdfResource) {
PagePdfDocumentReader reader = new PagePdfDocumentReader(pdfResource);
List<Document> documents = reader.read();
// 附加元数据
documents.forEach(doc -> doc.getMetadata().put(\”source\”, pdfResource.getFilename()));
vectorStore.add(documents);
System.out.println(\”PDF 索引完成: \” + pdfResource.getFilename() + \”, 共 \” + documents.size() + \” 页\”);
}
/** 索引 Markdown 文档(按标题分块) */
public void indexMarkdown(Resource mdResource) throws IOException {
String content = new String(mdResource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
// 按二级标题分块
List<String> chunks = Arrays.stream(content.split(\”(?=^##\\\\s)\”, Pattern.MULTILINE))
.filter(s -> !s.trim().isEmpty())
.collect(Collectors.toList());
List<Document> documents = new ArrayList<>();
for (int i = 0; i < chunks.size(); i++) {
documents.add(new Document(chunks.get(i), Map.of(
\”source\”, mdResource.getFilename(),
\”type\”, \”markdown\”,
\”chunkIndex\”, String.valueOf(i)
)));
}
vectorStore.add(documents);
System.out.println(\”Markdown 索引完成: \” + mdResource.getFilename());
}
/** 索引纯文本(按 Token 分块) */
public void indexText(String content, String sourceName) {
TokenTextSplitter splitter = new TokenTextSplitter(500, 100, 50, true);
List<String> chunks = splitter.split(content);
List<Document> documents = chunks.stream()
.map(chunk -> new Document(chunk, Map.of(\”source\”, sourceName, \”type\”, \”text\”)))
.collect(Collectors.toList());
vectorStore.add(documents);
}
}
2.5 RAG 查询服务
核心逻辑:检索 → 组装 Prompt → 生成回答:
@Service
public<





