Spring AI 实战系列 | 第 3 篇
VectorStore + RAG:构建私有知识库
系列说明:本文为《Spring AI 实战系列 入门篇》第 3 篇
前置知识:完成第 1-2 篇
预计阅读时间:15 分钟
📖 目录
- [为什么需要 RAG?](#为什么需要 RAG? "#%E4%B8%80-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-rag")
- [RAG 工作原理](#RAG 工作原理 "#%E4%BA%8C-rag-%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86")
- 向量数据库选型
- [Spring AI VectorStore API](#Spring AI VectorStore API "#%E5%9B%9B-spring-ai-vectorstore-api")
- 实战:构建文档问答系统
- 常见问题
- 系列预告
一、为什么需要 RAG?
1.1 AI 的知识局限性
大语言模型的知识有两个致命问题:
| 问题 | 说明 | 示例 |
|---|---|---|
| ❌ 知识截止 | 训练数据有时效性 | GPT-4 截止 2023 年 9 月 |
| ❌ 缺乏私有数据 | 无法访问企业内网文档 | 不知道公司内部规定 |
1.2 解决方案对比
| 方案 | 原理 | 成本 | 效果 |
|---|---|---|---|
| Fine-tuning | 重新训练模型 | 极高 | 好但昂贵 |
| RAG | 检索增强生成 | 中等 | ✅ 推荐 |
| Prompt Stuffing | 塞进上下文 | 低 | 受限 token 数 |
1.3 RAG 核心价值
┌─────────────────────────────────────────────────────────────┐
│ RAG 核心价值 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 📚 企业私有数据 ──────────────────┐ │
│ - 产品文档 │ │
│ - 客服FAQ │ ──→ AI 生成回答 │
│ - 技术手册 │ │
│ - 员工手册 │ │
│ │ │
│ ✅ 优点: │
│ • 无需训练模型 │
│ • 数据实时更新 │
│ • 保护隐私安全 │
│ • 成本可控 │
│ │
└─────────────────────────────────────────────────────────────┘
二、RAG 工作原理
2.1 完整流程
scss
┌────────────────────────────────────────────────────────────────────┐
│ RAG 工作流程 │
├────────────────────────────────────────────────────────────────────┤
│ │
│ 【阶段一:数据准备 - 离线】 │
│ │
│ ┌────────┐ ┌──────────┐ ┌─────────┐ ┌────────────┐ │
│ │ 文档库 │ → │ 文档读取 │ → │ 文档分割 │ → │ Embedding │ │
│ │PDF/Word│ │ Reader │ │ Splitter │ │ 转向量 │ │
│ └────────┘ └──────────┘ └─────────┘ └─────┬──────┘ │
│ ↓ │
│ ┌────────────┐ │
│ │ 向量数据库 │ │
│ │ 存储向量 │ │
│ └────────────┘ │
│ │
│ 【阶段二:查询回答 - 在线】 │
│ │
│ ┌────────┐ ┌──────────┐ ┌─────────┐ ┌────────────┐ │
│ │ 用户 │ → │ 问题转 │ → │ 向量 │ → │ 检索相似 │ │
│ │ 问题 │ │ 向量 │ │ 搜索 │ │ 文档 │ │
│ └────────┘ └──────────┘ └─────────┘ └─────┬──────┘ │
│ ↓ │
│ ┌──────────────────┐ │
│ │ 构建提示词 │ │
│ │ (问题+文档+指令) │ │
│ └────────┬─────────┘ │
│ ↓ │
│ ┌────────────┐ │
│ │ AI 生成 │ │
│ │ 最终回答 │ │
│ └────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
2.2 关键环节
| 环节 | 作用 | 常见工具 |
|---|---|---|
| 文档读取 | 解析 PDF/Word/HTML/Markdown | PDF Reader, Jsoup |
| 文档分割 | 长文本切分成小片段 | TokenTextSplitter |
| Embedding | 文本转向量 | OpenAI, DashScope |
| 向量存储 | 高效存储与检索 | PGVector, Redis, Pinecone |
| 相似度检索 | 找到最相关的文档 | VectorStore API |
三、向量数据库选型
3.1 Spring AI 支持的向量数据库
| 数据库 | 特点 | 适用场景 |
|---|---|---|
| PGVector (PostgreSQL) | 简单易用,生态好 | 中小型项目 |
| Redis | 性能极高 | 需要快速响应 |
| Pinecone | 云服务,无需运维 | 云上部署 |
| Milvus | 功能强大 | 大规模数据 |
| Neo4j | 图数据库+向量 | 知识图谱 |
| Weaviate | 开源,云原生 | 现代化架构 |
| Qdrant | 高性能 Rust 实现 | 高并发场景 |
| Elasticsearch | 全文搜索+向量 | 已有 ES 集群 |
3.2 快速选择指南
scss
数据量 < 10万条 → PGVector (PostgreSQL)
需要云服务 → Pinecone / Azure Vector
需要最高性能 → Redis / Qdrant
需要图关系 → Neo4j
已有 ES 集群 → Elasticsearch
需要本地部署 → Ollama + Chroma
四、Spring AI VectorStore API
4.1 核心接口
java
public interface VectorStore extends DocumentWriter, VectorStoreRetriever {
// 添加文档
void add(List<Document> documents);
// 删除文档
void delete(List<String> idList);
// 相似度搜索
List<Document> similaritySearch(SearchRequest request);
}
@FunctionalInterface
public interface VectorStoreRetriever {
List<Document> similaritySearch(SearchRequest request);
}
4.2 Document 结构
java
public class Document {
private String id; // 唯一ID
private String content; // 文本内容
private Map<String, Object> metadata; // 元数据
}
4.3 SearchRequest 构建
java
SearchRequest request = SearchRequest.builder()
.query("用户问题")
.topK(4) // 返回 4 条最相似结果
.similarityThreshold(0.75) // 相似度阈值 (0-1)
.filterExpression( // 元数据过滤
FilterExpressionBuilder
.and(country.eq("US"), year.gte(2020))
)
.build();
4.4 依赖配置示例
PGVector (PostgreSQL):
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vectorstore-pgvector</artifactId>
</dependency>
properties
spring.ai.vectorstore.pgvector.enabled=true
spring.datasource.url=jdbc:postgresql://localhost:5432/vectordb
Redis:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vectorstore-redis</artifactId>
</dependency>
properties
spring.ai.vectorstore.redis.enabled=true
spring.data.redis.host=localhost
spring.data.redis.port=6379
五、实战:构建文档问答系统
5.1 项目结构
bash
rag-demo/
├── pom.xml
└── src/main/
├── java/com/example/demo/
│ ├── DemoApplication.java
│ ├── controller/
│ │ └── RagController.java
│ └── service/
│ ├── DocumentService.java # 文档加载与存储
│ └── RagService.java # RAG 问答核心
└── resources/
└── application.properties
5.2 依赖配置 pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>rag-demo</artifactId>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Ollama 本地模型(Chat + Embedding) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<!-- 向量数据库:Chroma -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-chroma</artifactId>
</dependency>
<!-- 文档读取:PDF -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
<!-- 文档读取:Markdown -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
</dependency>
</dependencies>
</project>
5.3 配置文件 application.properties
properties
# Ollama 本地模型配置
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.options.model=qwen2.5:7b
spring.ai.ollama.embedding.options.model=qwen3-embedding:0.6b
# Chroma 向量数据库配置
spring.ai.vectorstore.chroma.client.host=http://localhost
spring.ai.vectorstore.chroma.client.port=8000
spring.ai.vectorstore.chroma.collection-name=TestCollection
spring.ai.vectorstore.chroma.initialize-schema=true
# 服务端口
server.port=8080
5.4 文档服务 DocumentService.java
java
package com.example.demo.service;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
/**
* 文档加载与向量化存储服务
*/
@Service
public class DocumentService {
private final VectorStore vectorStore;
private final ResourceLoader resourceLoader;
public DocumentService(VectorStore vectorStore, ResourceLoader resourceLoader) {
this.vectorStore = vectorStore;
this.resourceLoader = resourceLoader;
}
/**
* 从资源文件加载文档并存储到向量数据库
*/
public void loadDocument(String resourcePath) throws IOException {
Resource resource = resourceLoader.getResource(resourcePath);
// 根据文件类型选择 Reader
DocumentReader reader = getDocumentReader(resource);
// 读取文档
List<Document> documents = reader.read();
// 存储到向量数据库(自动转为 Embedding)
vectorStore.add(documents);
System.out.println("已加载 " + documents.size() + " 个文档片段到向量数据库");
}
private DocumentReader getDocumentReader(Resource resource) {
String filename = resource.getFilename();
if (filename == null) {
throw new IllegalArgumentException("文件名不能为空");
}
if (filename.endsWith(".pdf")) {
return new PagePdfDocumentReader(resource);
} else if (filename.endsWith(".md")) {
return new MarkdownDocumentReader(resource,
MarkdownDocumentReaderConfig.builder().build());
} else {
throw new IllegalArgumentException("不支持的文件类型: " + filename);
}
}
/**
* 添加单个文档到向量数据库
*/
public void addDocument(String content) {
Document document = new Document(content);
vectorStore.add(List.of(document));
}
/**
* 添加单个文档到向量数据库(带元数据)
*/
public void addDocument(String content, java.util.Map<String, Object> metadata) {
Document document = new Document(content, metadata);
vectorStore.add(List.of(document));
}
}
5.5 RAG 服务 RagService.java
java
package com.example.demo.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* RAG 问答服务
*/
@Service
public class RagService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public RagService(ChatClient.Builder builder, VectorStore vectorStore) {
this.chatClient = builder.build();
this.vectorStore = vectorStore;
}
/**
* 基于知识库回答问题(使用 RAG Advisor)
*/
public String answer(String question) {
// 第一步:在向量数据库中搜索相似文档
List<Document> similarDocuments = vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(3) // 返回最相似的 3 个文档
.build()
);
// 第二步:提取文档内容作为上下文
String context = similarDocuments.stream()
.map(Document::getText) // 使用 getText() 获取文档内容
.collect(Collectors.joining("\n\n"));
// 第三步:构建提示词,包含上下文和问题
String prompt = String.format(
"你是一个问答助手。请根据以下上下文来回答用户的问题。\n\n" +
"上下文信息:\n%s\n\n" +
"用户问题:%s\n\n" +
"请基于上下文信息来回答问题。如果上下文中没有相关信息,请说明。",
context,
question
);
// 第四步:调用 AI 模型获取回答
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
/**
* 手动实现 RAG(更灵活的控制)
*/
public String answerManual(String question) {
// 1. 检索相似文档
List<String> docs = vectorStore.similaritySearch(question).stream()
.map(d -> d.getText())
.toList();
// 2. 构建提示词
String context = String.join("\n\n", docs);
String prompt = """
请根据以下上下文来回答问题。如果上下文中没有相关信息,请如实说明。
上下文:
%s
问题:%s
""".formatted(context, question);
// 3. 调用 AI
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
5.6 控制器 RagController.java
java
package com.example.demo.controller;
import com.example.demo.service.DocumentService;
import com.example.demo.service.RagService;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* RAG 问答控制器
*/
@RestController
@RequestMapping("/rag")
public class RagController {
private final RagService ragService;
private final DocumentService documentService;
public RagController(RagService ragService, DocumentService documentService) {
this.ragService = ragService;
this.documentService = documentService;
}
/**
* 基于知识库回答问题
* GET /rag/ask?question=什么是Spring AI?
*/
@GetMapping("/ask")
public Map<String, Object> ask(@RequestParam String question) {
String answer = ragService.answer(question);
return Map.of(
"question", question,
"answer", answer,
"timestamp", System.currentTimeMillis()
);
}
/**
* 手动实现 RAG(更灵活的控制)
* GET /rag/ask-manual?question=什么是Spring AI?
*/
@GetMapping("/ask-manual")
public Map<String, Object> askManual(@RequestParam String question) {
String answer = ragService.answerManual(question);
return Map.of(
"question", question,
"answer", answer,
"timestamp", System.currentTimeMillis()
);
}
/**
* 添加文档到向量数据库
* POST /rag/add-doc
* Body: {"content": "Spring AI 是...", "metadata": {"source": "docs"}}
*/
@PostMapping("/add-doc")
public Map<String, String> addDocument(@RequestBody AddDocRequest request) {
try {
if (request.metadata() != null && !request.metadata().isEmpty()) {
documentService.addDocument(request.content(), request.metadata());
} else {
documentService.addDocument(request.content());
}
return Map.of("status", "success", "message", "文档已添加到向量数据库");
} catch (Exception e) {
return Map.of("status", "error", "message", "添加文档失败: " + e.getMessage());
}
}
/**
* 从资源文件加载文档
* POST /rag/load-docs
* Body: {"resourcePath": "classpath:docs/faq.md"}
*/
@PostMapping("/load-docs")
public Map<String, String> loadDocuments(@RequestBody LoadDocsRequest request) {
try {
documentService.loadDocument(request.resourcePath());
return Map.of("status", "success", "message", "文档加载完成");
} catch (Exception e) {
return Map.of("status", "error", "message", "加载文档失败: " + e.getMessage());
}
}
/**
* 获取知识库统计信息
* GET /rag/stats
*/
@GetMapping("/stats")
public Map<String, Object> getStats() {
return Map.of(
"documentCount", ragService.getDocumentCount(),
"timestamp", System.currentTimeMillis()
);
}
/**
* 健康检查
* GET /rag/health
*/
@GetMapping("/health")
public Map<String, String> health() {
try {
ragService.answer("test");
return Map.of("status", "✅ RAG 服务正常运行");
} catch (Exception e) {
return Map.of("status", "❌ RAG 服务异常: " + e.getMessage());
}
}
public record AddDocRequest(String content, java.util.Map<String, Object> metadata) {}
public record LoadDocsRequest(String resourcePath) {}
}
5.7 测试
bash
# 1. 健康检查
curl "http://localhost:8080/rag/health"
# 2. 添加文档
curl -X POST http://localhost:8080/rag/add-doc \
-H "Content-Type: application/json" \
-d '{"content": "Spring AI 是 Spring 官方推出的 AI 集成框架。", "metadata": {"source": "docs"}}'
# 3. 提问
curl "http://localhost:8080/rag/ask?question=什么是Spring AI?"
# 4. 手动 RAG
curl "http://localhost:8080/rag/ask-manual?question=Spring AI支持哪些模型?"
# 5. 获取统计
curl "http://localhost:8080/rag/stats"
六、常见问题
Q1: 文档太长怎么办?
使用 TokenTextSplitter 分割:
java
List<Document> chunks = new TokenTextSplitter()
.apply(documents);
Q2: 向量检索效果不好?
- 调整
topK数量 - 调整
similarityThreshold阈值 - 优化文档分割策略
Q3: 首次运行报错 schema?
确保设置:
properties
spring.ai.vectorstore.chroma.initialize-schema=true
七、系列预告
本篇小结
- ✅ 理解了 RAG 的价值与原理
- ✅ 掌握了 VectorStore API
- ✅ 完成了文档问答系统实战
完整系列
| 篇目 | 内容 | 状态 |
|---|---|---|
| 第 1 篇 | 核心概念 + 快速上手 | ✅ |
| 第 2 篇 | Tool Calling | ✅ |
| 第 3 篇 | VectorStore + RAG | ✅ 本文 |
| 第 4 篇 | 结构化输出 | 🔜 下篇 |
| 第 5 篇 | Advisors 中间件 | 🔜 |
| 第 6 篇 | 国产模型集成 | 🔜 |
下篇预告
第 4 篇:结构化输出 - AI 结果映射为 POJO
- BeanOutputConverter 用法
- Native Structured Output
- 泛型集合处理
📚 参考资料
-
Spring AI VectorStore 文档
-
Spring AI ETL Pipeline 文档
-
Spring AI RAG 文档
📌 引用说明 :本文核心概念与技术描述参考自 Spring AI 官方文档(docs.spring.io/spring-ai/r... 与 ETL Pipeline 内容来自官方对应章节。
关注公众号「AI日撰」,点击菜单「获取源码」获取完整代码(Gitee 仓库)。
系列:《Spring AI 实战系列 入门篇》第 3 篇(共 6 篇)