Spring AI RAG 入门的实战
一、为什么需要 RAG?
大模型有两个天然短板:
最粗暴的解决办法是把这些知识全塞进 Prompt。但知识库往往很大,而模型上下文有限(早期 GPT 只有 2k,现在主流也就 128k),不可能全塞进去。
RAG(Retrieval-Augmented Generation,检索增强生成) 的思路很朴素:
用户提问时,先从知识库里检索出最相关的一小段内容,再把这一小段内容和用户问题一起送给大模型回答。
这样,大模型既拥有了“实时检索”的能力,又不用背负整个知识库的重量。
二、RAG 核心原理速览
RAG 的整个流程可以拆成三个阶段:
┌───────────────┐ ┌───────────────┐ ┌──────────────┐
│ 1.存储知识库 │ –> │ 2. 检索相关内容│ –>│ 3. 对话大模型│
└───────────────┘ └───────────────┘ └──────────────┘
2.1 文本如何变成可检索的数据?
知识库通常是 PDF、Word、Markdown 等文档。我们要先把它们切成小段,再把每段文本转成向量(Embedding),最后存进向量数据库。
为什么用向量?因为向量可以计算距离。语义相近的句子,在向量空间里的距离也更近。这样检索时,就能把“字面不同、但意思相近”的内容找出来,而不是简单的关键字匹配。
常用的距离算法有两种:
- 欧氏距离(Euclidean):值越小越相似。
- 余弦距离(Cosine):值越小越相似(范围 [0, 2])。
在 Spring AI 中,我们可以写一个工具类来验证向量模型的效果:
package com.example.rag.util;
public class VectorDistanceUtils {
private VectorDistanceUtils() {}
private static final double EPSILON = 1e-12;
public static double euclideanDistance(float[] vectorA, float[] vectorB) {
validateVectors(vectorA, vectorB);
double sum = 0.0;
for (int i = 0; i < vectorA.length; i++) {
double diff = vectorA[i] – vectorB[i];
sum += diff * diff;
}
return Math.sqrt(sum);
}
public static double cosineDistance(float[] vectorA, float[] vectorB) {
validateVectors(vectorA, vectorB);
double dotProduct = 0.0;
double normA = 0.0;
double normB = 0.0;
for (int i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i];
normA += vectorA[i] * vectorA[i];
normB += vectorB[i] * vectorB[i];
}
normA = Math.sqrt(normA);
normB = Math.sqrt(normB);
if (normA < EPSILON || normB < EPSILON) {
throw new IllegalArgumentException("Vectors cannot be zero vectors");
}
double similarity = dotProduct / (normA * normB);
similarity = Math.max(Math.min(similarity, 1.0), –1.0);
return 1 – similarity;
}
private static void validateVectors(float[] a, float[] b) {
if (a == null || b == null) {
throw new IllegalArgumentException("Vectors cannot be null");
}
if (a.length != b.length) {
throw new IllegalArgumentException("Vectors must have same dimension");
}
if (a.length == 0) {
throw new IllegalArgumentException("Vectors cannot be empty");
}
}
}
2.2 向量数据库的作用
向量数据库只做两件事:
Spring AI 对主流向量数据库(Milvus、PgVector、Redis、Elasticsearch 等)做了统一抽象,都实现了 VectorStore 接口。这意味着:无论你用哪种数据库,业务代码都是一样的,切换成本几乎为零。
学习阶段推荐用 SimpleVectorStore,它是基于内存的实现,零配置,复制粘贴就能跑。生产环境再替换为持久化的向量库即可。
三、Spring AI V1.0.0 RAG 实战
下面我们用手写代码的方式,完整走一遍 RAG 的三个阶段。只有亲手拼过 Prompt,你才能真正理解 RAG 在做什么。
3.1 环境准备
pom.xml 核心依赖:
<dependencies>
<!– Spring Boot Starter –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!– Spring AI OpenAI(兼容阿里云百炼等 OpenAI 格式接口) –>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!– Spring AI Vector Store –>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<!– PDF 文档读取 –>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
<!– 测试 –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!– Spring AI BOM,统一管理版本 –>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.2 配置文件
这里以 阿里云百炼平台 的向量模型为例(兼容 OpenAI 接口),同时配置 DeepSeek 作为对话模型:
spring:
application:
name: spring–ai–rag–demo
ai:
deepseek:
api-key: ${DEEPSEEK_API_KEY}
chat:
options:
model: deepseek–chat
openai:
# 阿里云百炼兼容 OpenAI 接口
base-url: https://dashscope.aliyuncs.com/compatible–mode
api-key: ${QWEN_API_KEY}
embedding:
options:
model: text–embedding–v3
dimensions: 1024
3.3 配置 VectorStore
package com.example.rag.config;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RAGConfiguration {
@Bean
public VectorStore vectorStore(OpenAiEmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
}
3.4 文档读取与存储
Spring AI 提供了 PagePdfDocumentReader,可以按页读取 PDF 并拆分为 Document 列表:
package com.example.rag;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import java.util.List;
@SpringBootTest
class DocumentLoadTest {
@Autowired
private VectorStore vectorStore;
@Test
void loadPdfToVectorStore() {
// 1. 读取 PDF
Resource resource = new FileSystemResource("java手册.pdf");
PagePdfDocumentReader reader = new PagePdfDocumentReader(
resource,
PdfDocumentReaderConfig.builder()
.withPagesPerDocument(1) // 每 1 页作为一个 Document
.build()
);
List<Document> documents = reader.read();
// 2. (可选)进一步切分,防止单页内容过长
TokenTextSplitter splitter = new TokenTextSplitter(500, 100, 50, 1000, true);
documents = splitter.apply(documents);
// 3. 存入向量库
vectorStore.add(documents);
}
}
注意:PagePdfDocumentReader 按页拆分是最稳妥的起步方案。如果一页内容很长,可以再用 TokenTextSplitter 按 Token 数二次切分,避免超出模型上下文。
3.5 手动实现 RAG 对话
现在到了最关键的一步:检索 + 拼装 Prompt + 调用模型。
package com.example.rag;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@SpringBootTest
class RagApplicationTests {
@Autowired
private VectorStore vectorStore;
@Autowired
private ChatClient chatClient;
// RAG 提示词模板
private static final String RAG_PROMPT_TEMPLATE = """
你是一个专业的技术助手,请根据以下参考信息回答用户问题。
如果参考信息不足以回答问题,请直接说明"根据现有资料无法回答"。
——————–
参考信息:
{context}
——————–
用户问题:{query}
""";
@Test
public void testRagChat() {
String userQuery = "数据库索引规范有哪些?";
// ===== 第 1 步:检索相关知识 =====
SearchRequest request = SearchRequest.builder()
.query(userQuery)
.topK(3) // 取最相关的 3 条
.similarityThreshold(0.6) // 相似度阈值,过滤低质量结果
.build();
List<Document> relevantDocs = vectorStore.similaritySearch(request);
if (relevantDocs.isEmpty()) {
System.out.println("未检索到相关内容");
return;
}
// 将检索结果拼接为上下文
String context = relevantDocs.stream()
.map(Document::getText)
.collect(Collectors.joining("\\n—\\n"));
// ===== 第 2 步:拼装 Prompt =====
SystemPromptTemplate promptTemplate = new SystemPromptTemplate(RAG_PROMPT_TEMPLATE);
Prompt prompt = promptTemplate.create(Map.of(
"context", context,
"query", userQuery
));
// ===== 第 3 步:调用大模型 =====
String response = chatClient.call(prompt).getResult().getOutput().getText();
System.out.println("========== RAG 回答 ==========");
System.out.println(response);
System.out.println("========== 引用来源 ==========");
relevantDocs.forEach(doc -> {
System.out.println("[Score: " + doc.getScore() + "] " + doc.getText().substring(0, Math.min(50, doc.getText().length())) + "…");
});
}
}
这段代码就是 RAG 的灵魂:
手动拼 Prompt 虽然啰嗦,但它让你清晰地看到:RAG 本质上就是在调用大模型之前,做了一层“资料查找”。
四、新版本 Spring AI 的变化
Spring AI 在 V1.0.0 之后的版本中,引入了一个非常重要的概念 —— Advisor(顾问)。
4.1 为什么需要 Advisor?
上面的代码中,检索、拼 Prompt、调模型全是散装的。实际项目中,你可能希望在调用前后自动做很多事:
- 自动根据问题检索向量库并拼进 Prompt(RAG)
- 自动加载历史聊天记录(Memory)
- 自动记录日志、重试、限流
Advisor 就是用来拦截和增强 ChatClient 调用过程的机制。它把横切关注点抽离出来,让你的业务代码更干净。
4.2 用 QuestionAnswerAdvisor 重构 RAG
最新版中,实现同样的 RAG 功能,只需要几行代码:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class RagService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
@Autowired
public RagService(ChatClient chatClient, VectorStore vectorStore) {
this.chatClient = chatClient;
this.vectorStore = vectorStore;
}
/**
* 基于 RAG 的问答方法
* @param question 用户问题
* @return AI 回答
*/
public String ask(String question) {
// 定义 Advisor,绑定向量库和检索参数
QuestionAnswerAdvisor qaAdvisor = new QuestionAnswerAdvisor(
vectorStore,
SearchRequest.builder()
.topK(3)
.similarityThreshold(0.6)
.build()
);
// 一行代码完成 RAG
return chatClient.prompt()
.advisors(qaAdvisor) // 注入 RAG 顾问
.user(question)
.call()
.content();
}
}
QuestionAnswerAdvisor 内部自动完成了:
你不再需要手写 Prompt 模板,框架帮你做了。
4.3 另一个选择:RetrievalAugmentationAdvisor
如果你的需求更复杂,比如需要多轮对话 + RAG,可以用 RetrievalAugmentationAdvisor:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class RagService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
@Autowired
public RagService(ChatClient chatClient, VectorStore vectorStore) {
this.chatClient = chatClient;
this.vectorStore = vectorStore;
}
/**
* 基于 RAG 的问答方法(新版 API)
* @param question 用户问题
* @return AI 回答
*/
public String ask(String question) {
// 构建 DocumentRetriever
VectorStoreDocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.6)
.topK(3)
.build();
// 构建 RetrievalAugmentationAdvisor
RetrievalAugmentationAdvisor raAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(documentRetriever)
.build();
// 执行 RAG 问答
return chatClient.prompt()
.advisors(raAdvisor)
.user(question)
.call()
.content();
}
}
RetrievalAugmentationAdvisor 是更底层的 RAG 抽象,支持自定义检索器(Retriever)、查询转换器(Query Transformer)、结果重排序等扩展点,适合进阶使用。
4.4 API 差异速查表
| 向量库检索 | vectorStore.similaritySearch(request) | QuestionAnswerAdvisor 内部自动处理 |
| Prompt 拼装 | 手动写 SystemPromptTemplate | Advisor 自动注入上下文 |
| 对话调用 | chatClient.call(prompt) | chatClient.prompt().advisors(…).user(…).call() |
| 记忆管理 | 手动维护 ChatMemory | MessageChatMemoryAdvisor 一行注入 |
| 扩展性 | 自己写拦截逻辑 | Advisor 链式组合,插件化扩展 |
我的建议:先用 V1.0.0 手写一遍,搞懂 RAG 的每一步在干什么;再用新版的 Advisor 重构,感受框架设计带来的便利。
五、总结
RAG 并不是一个很复杂的概念。把它拆解开,核心就三件事:
Spring AI V1.0.0 提供了完整的底层能力,你可以手动控制每一步,深入理解原理。而新版本的 Advisor 机制,则把 RAG、Memory、日志等横切逻辑封装成了可插拔的组件,让代码更简洁、更易于扩展
参考与拓展阅读
- Spring AI 官方文档 – Vector Databases
- Spring AI 官方文档 – ETL Pipeline
- 阿里云百炼 – 通用文本向量
全文完,如果这篇文章对你有帮助,欢迎点赞收藏,有问题也欢迎留言交流



