欢迎光临
我们一直在努力

Spring-AI-RAG-入门与实战

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 的灵魂:

  • similaritySearch() 把用户问题向量化,去向量库找最相似的文档片段。
  • 把这些片段和用户问题一起塞进 Prompt。
  • 大模型基于“参考信息”作答,而不是瞎编。
  • 手动拼 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 内部自动完成了:

  • 将用户问题向量化。
  • 去 VectorStore 检索相关内容。
  • 把检索结果自动拼进 System Prompt。
  • 发送给大模型。
  • 你不再需要手写 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 差异速查表

    环节V1.0.0 写法最新版写法
    向量库检索 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
    • 阿里云百炼 – 通用文本向量

    全文完,如果这篇文章对你有帮助,欢迎点赞收藏,有问题也欢迎留言交流

    赞(0)
    未经允许不得转载:171主机测评 » Spring-AI-RAG-入门与实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址