告别大模型"幻觉"!Spring AI + RAG 实战,打造导游知识库智能问答系统

文章目录


前言

在前面的文章中,我们分别学习了 Spring AI 的基本使用、向量数据库 Milvus 的安装与操作,以及 Spring AI 中 RAG 相关 Advisor 的原理。本文将通过一个完整的实战案例——导游知识库智能问答系统,把前面学到的知识串联起来,演示如何使用 Spring AI 从零到一搭建一个 RAG 应用。

什么是 RAG?

RAG(Retrieval-Augmented Generation,检索增强生成) 是目前大模型应用中最主流的技术方案之一。它的核心思想是:

先检索,再生成 —— 在用户提问时,先从知识库中检索出与问题相关的文档片段,再将这些片段作为上下文注入到大模型的 Prompt 中,让大模型基于这些"参考资料"来生成回答。

RAG 解决了大模型的两个核心痛点:

  • 知识时效性:大模型的训练数据有截止日期,无法回答最新信息;而 RAG 的知识库可以随时更新。
  • 领域专业性:大模型对特定领域(如企业内部文档、专业考试题库)知之甚少;RAG 通过将领域知识注入上下文,让大模型"临时学习"后作答。

本案例要做什么?

本案例模拟了一个导游考试智能问答系统:

  1. 将一份"导游面试问答.doc"文档作为知识库来源
  2. 将文档内容切分后,通过 Embedding 模型转成向量,存入 Milvus 向量数据库
  3. 用户提问时,系统从 Milvus 中检索出最相关的文档片段
  4. 将检索到的片段作为上下文,交给 DeepSeek 大模型生成最终回答

整个流程如下图所示:

用户提问

Spring AI ChatClient

Embedding 模型
智谱AI embedding-2

问题向量

Milvus 向量数据库
guide_qa_store

检索相关文档片段

Advisor 组装 Prompt
问题 + 上下文

DeepSeek 大模型
deepseek-chat

生成最终回答

两种 Advisor 对比

本案例同时使用了 Spring AI 提供的两种 RAG Advisor,并在后续测试中对比它们的效果:

对比项QuestionAnswerAdvisorRetrievalAugmentationAdvisor
定位简单直接的 RAG 实现模块化、可扩展的 RAG 实现
工作流程检索文档 → 直接拼接到 Prompt检索文档 → 查询增强 → 上下文注入 → 生成回答
查询增强不支持支持(通过 QueryAugmenter)
可扩展性较低,开箱即用高,各模块可自定义替换
适用场景快速原型、简单问答生产级应用、复杂 RAG 流程

项目说明

项目说明
项目名称SpringAIRAGDemo
项目简介基于 Spring AI 实现的导游知识库智能问答系统,演示 RAG(检索增强生成)从零到一的综合开发流程
技术栈Spring Boot 3.5.3 + Spring AI 1.0.0-SNAPSHOT + Milvus 向量数据库 + DeepSeek(对话模型)+ 智谱AI(Embedding 模型)
JDK 版本17
核心功能1. 从本地 DOC 文档构建知识库,将内容切片后存入 Milvus 向量数据库
2. 使用 QuestionAnswerAdvisor 实现基于向量检索的问答
3. 使用 RetrievalAugmentationAdvisor 实现增强检索问答
4. 两种 Advisor 对比测试
核心依赖spring-ai-starter-model-deepseek(DeepSeek 对话模型)
spring-ai-starter-model-zhipuai(智谱AI Embedding 模型)
spring-ai-starter-vector-store-milvus(Milvus 向量存储)
spring-ai-advisors-vector-store(QuestionAnswerAdvisor)
spring-ai-rag(RetrievalAugmentationAdvisor)
spring-ai-tika-document-reader(DOC 文档解析)
外部服务Milvus 向量数据库(Docker 部署,默认端口 19530)
项目结构config/AIConfig.java — 配置 Advisor、ChatClient,启动时初始化向量数据
controller/RagController.java — 提供 /ai/chat1 和 /ai/chat2 两个问答接口
API 接口GET /ai/chat1?message=xxx — 使用 QuestionAnswerAdvisor 问答
GET /ai/chat2?message=xxx — 使用 RetrievalAugmentationAdvisor 问答

项目目录结构

SpringAIRAGDemo/
├── src/main/java/com/example/springairagdemo/
│   ├── config/
│   │   └── AIConfig.java              # 核心配置类:Advisor、ChatClient、向量数据初始化
│   ├── controller/
│   │   └── RagController.java         # REST 控制器:提供两个问答接口
│   └── SpringAiragDemoApplication.java # Spring Boot 启动类
├── src/main/resources/
│   ├── application.properties          # 配置文件:API Key、Milvus 连接等
│   └── 导游面试问答.doc                 # 知识库源文档
└── pom.xml                             # Maven 依赖配置

实战步骤

1. 创建SpringBoot项目

SpringBoot项目命名为SpringAIRAGDemo,设置使用的JDK为17版本。

2. 在项目中加入如下Maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>SpringAIRAGDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringAIRAGDemo</name>
<description>SpringAIRAGDemo</description>


   <properties>
      <java.version>17</java.version>
   </properties>

   <!-- 导入 Spring AI BOM,用于统一管理 Spring AI 依赖的版本,
    引用每个 Spring AI 模块时不用再写 <version>,只要依赖什么模块 Mavens 自动使用 BOM 推荐的版本 -->
   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>

   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>

      <!-- Deepseek AI 依赖 -->
      <dependency>
         <groupId>org.springframework.ai</groupId>
         <artifactId>spring-ai-starter-model-deepseek</artifactId>
      </dependency>

      <!-- 智普AI 依赖 -->
      <dependency>
         <groupId>org.springframework.ai</groupId>
         <artifactId>spring-ai-starter-model-zhipuai</artifactId>
      </dependency>

      <!-- QuestionAnswerAdvisor 依赖包-->
      <dependency>
         <groupId>org.springframework.ai</groupId>
         <artifactId>spring-ai-advisors-vector-store</artifactId>
      </dependency>

      <!--  RetrievalAugmentationAdvisor  依赖包-->
      <dependency>
         <groupId>org.springframework.ai</groupId>
         <artifactId>spring-ai-rag</artifactId>
      </dependency>

      <!--  Milvus VectorStore 依赖包-->
      <dependency>
         <groupId>org.springframework.ai</groupId>
         <artifactId>spring-ai-starter-vector-store-milvus</artifactId>
      </dependency>

      <!-- Tika DocumentReader 依赖包-->
      <dependency>
         <groupId>org.springframework.ai</groupId>
         <artifactId>spring-ai-tika-document-reader</artifactId>
      </dependency>


   </dependencies>

   <!-- 声明仓库, 用于获取 Spring AI 以及相关预发布版本-->
   <repositories>
      <repository>
         <id>spring-snapshots</id>
         <name>Spring Snapshots</name>
         <url>https://repo.spring.io/snapshot</url>
         <releases>
            <enabled>false</enabled>
         </releases>
      </repository>
      <repository>
         <name>Central Portal Snapshots</name>
         <id>central-portal-snapshots</id>
         <url>https://central.sonatype.com/repository/maven-snapshots/</url>
         <releases>
            <enabled>false</enabled>
         </releases>
         <snapshots>
            <enabled>true</enabled>
         </snapshots>
      </repository>
   </repositories>

</project>
依赖说明

下面对各个核心依赖的作用做一个梳理:

依赖作用在本案例中的角色
spring-ai-bom (1.0.0-SNAPSHOT)Spring AI 统一版本管理通过 BOM 管理所有 Spring AI 模块版本,避免版本冲突
spring-ai-starter-model-zhipuai智谱AI SDK 集成用作 Embedding 模型,将文档片段转换成向量
spring-ai-starter-model-deepseekDeepSeek SDK 集成用作 Chat 模型,负责最终的对话生成
spring-ai-advisors-vector-store基于向量存储的 Advisor提供 QuestionAnswerAdvisor,实现简单的 RAG 问答
spring-ai-ragRAG 框架提供 RetrievalAugmentationAdvisor,实现增强检索问答
spring-ai-starter-vector-store-milvusMilvus 向量存储集成连接 Milvus 向量数据库,存储和检索文档向量
spring-ai-tika-document-readerApache Tika 文档解析读取和解析 .doc 格式的 Word 文档

为什么需要两个 AI 模型?

在 RAG 流程中,Embedding 模型和Chat 模型各司其职:

  • 智谱AI(Embedding 模型):负责将文本转换成高维向量(本案例中为 1024 维),用于语义相似度计算和检索
  • DeepSeek(Chat 模型):负责理解用户问题并结合检索到的上下文,生成自然语言回答

3. 配置resources/application.properties

spring.application.name=SpringAIRAGDemo
server.port=8080

## 使用 智普AI Embedding 模型,需要在pom.xml中引入对应依赖
spring.ai.zhipuai.api-key=d...4
spring.ai.zhipuai.base-url=https://open.bigmodel.cn/api/paas
spring.ai.zhipuai.embedding.options.model=embedding-2

## 配置 Chat Model:Deepseek的基础URL、密钥和使用模型
spring.ai.deepseek.base-url=https://api.deepseek.com
spring.ai.deepseek.api-key=sk-8...1
spring.ai.deepseek.chat.options.model=deepseek-chat

## Milvus 配置
spring.ai.vectorstore.milvus.client.host=node2
spring.ai.vectorstore.milvus.client.port=19530
# 默认用户名和密码为 root 和 Milvus
spring.ai.vectorstore.milvus.client.token=root:Milvus
# 要使用的 Milvus 数据库的名称
spring.ai.vectorstore.milvus.database-name=default
# 用于存储 vector 的 Milvus 集合名称
spring.ai.vectorstore.milvus.collection-name=guide_qa_store
#是否自动初始化 schema,如:vector_store如果没有会自动创建
spring.ai.vectorstore.milvus.initialize-schema=true
# Milvus 集合中要存储的 vector 的维度,默认为1536,要与所使用的 embedding 模型的维度匹配(如:DeepSeek、ZhiPuAI 或其他,都是1024维)
spring.ai.vectorstore.milvus.embedding-dimension=1024
配置说明

配置文件中涉及三组关键配置:

① 智谱AI Embedding 配置

  • spring.ai.zhipuai.api-key:智谱AI 开放平台的 API Key
  • spring.ai.zhipuai.embedding.options.model=embedding-2:使用 embedding-2 模型,输出 1024 维向量

② DeepSeek Chat 配置

  • spring.ai.deepseek.api-key:DeepSeek 平台的 API Key
  • spring.ai.deepseek.chat.options.model=deepseek-chat:使用 deepseek-chat 模型进行对话

③ Milvus 向量数据库配置

  • host/port:Milvus 服务地址和端口
  • collection-name=guide_qa_store:存储导游知识库向量的集合名称
  • initialize-schema=true:自动创建集合和 Schema,无需手动建表
  • embedding-dimension=1024:向量维度必须与 Embedding 模型匹配(智谱AI embedding-2 输出 1024 维)

注意:embedding-dimension 必须与 Embedding 模型的输出维度一致,否则写入 Milvus 时会报错。

4. 准备知识库文档

将"导游面试问答.doc"文档放到项目resources资源目录下。

这份文档包含了导游考试中常见的面试问答内容,涵盖了景点知识、导游规范、应急处理等方面的问答对。这些内容将作为 RAG 系统的知识库来源。

5. 构建两个Rag Advisor和Chat Client

在项目中创建config/AIConfig.java ,该类标记为Configuration类。类中注入QuestionAnswerAdvisor、RetrievalAugmentationAdvisor以及ChatClient,并且每次运行SpringBoot项目时读取项目resources资源目录下的"导游面试问答.doc"内容,切分文档内容,并向Milvus中插入分片文档向量数据(如果已存在则不重复插入)。

package com.example.springairagdemo.config;

import io.milvus.client.MilvusServiceClient;
import io.milvus.grpc.GetCollectionStatisticsResponse;
import io.milvus.param.R;
import io.milvus.param.collection.FlushParam;
import io.milvus.param.collection.GetCollectionStatisticsParam;
import io.milvus.response.GetCollStatResponseWrapper;
import jakarta.annotation.PostConstruct;
import org.apache.tika.Tika;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class AIConfig {

    // 注入 MilvusVectorStore 实例
    @Autowired
    private MilvusVectorStore vectorStore;

    // 从配置文件中读取 Milvus 集合名称
    @Value("${spring.ai.vectorstore.milvus.collection-name}")
    private String collectionName;

    //配置 QuestionAnswerAdvisor,用于问答任务
    @Bean
    public QuestionAnswerAdvisor questionAnswerAdvisor() {
        return QuestionAnswerAdvisor.builder(vectorStore)
                // 配置向量搜索的参数,这里设置了相似度阈值为 0.5,返回前 6 个结果
                .searchRequest(SearchRequest.builder().similarityThreshold(0.1d).topK(6).build())
                .build();
    }


    //配置 RetrievalAugmentationAdvisor,用于增强检索任务
    @Bean
    public RetrievalAugmentationAdvisor retrievalAugmentationAdvisor() {

        //VectorStoreDocumentRetriever 负责在向量数据库中根据查询获取最相关的文档
        //支持根据相似度阈值、top-K(返回前K个最相关文档)和过滤表达式等方式进行精确检索
        VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore) // 设置向量存储对象
                .similarityThreshold(0.1)//设置相似度阈值
                .topK(6)//设置返回前K个最相关文档
                .build();

        //ContextualQueryAugmenter 用于增强查询,根据上下文信息生成更具体的查询
        //支持基于上下文的查询增强,例如根据用户输入的上下文信息生成更具体的查询,提高检索的准确性和效率
        ContextualQueryAugmenter cqa = ContextualQueryAugmenter.builder()
                .allowEmptyContext(true)//允许空上下文
                .build();

        //返回 RetrievalAugmentationAdvisor 对象,用于增强检索任务
        return RetrievalAugmentationAdvisor.builder()
                .documentRetriever(retriever) // 设置文档检索器
                .queryAugmenter(cqa) // 设置查询增强器
                .build();
    }


    //配置 ChatClient,初始化聊天客户端
    @Bean
    public ChatClient chatClient(DeepSeekChatModel chatModel,QuestionAnswerAdvisor qaAdvisor) {
        return ChatClient.builder(chatModel)
                .defaultSystem("你是一个助手,回答用户问题是不要提及回复是从上下文信息中获取的," +
                        "不要回答你不知道的问题,如果你不知道答案,就回答:抱歉,我不清楚这个问题。")
                .build();
    }


    /**
     * @PostConstruct 注解的方法会在 Spring 容器完成依赖注入后自动调用一次,常用于执行初始化逻辑。
     */
    @PostConstruct
    public void initVectorData() {
        System.out.println("初始化向量数据,写入到 Milvus 数据库中...");

        // 获取 Milvus 客户端实例
        MilvusServiceClient client = (MilvusServiceClient)vectorStore.getNativeClient().get();

        // 获取集合统计信息,查询集合的记录数量
        R<GetCollectionStatisticsResponse> resp = client.getCollectionStatistics(GetCollectionStatisticsParam.newBuilder()
                .withCollectionName(collectionName)
                .build());

        // 获取实体数量,即数据行数
        long rowCount = new GetCollStatResponseWrapper(resp.getData()).getRowCount();

        System.out.println("Milvus vector_store 中数据数量:" + rowCount);

        //如果集合没有数据就写入
        if(rowCount==0){
            System.out.println("Milvus vector_store 中没有数据,开始写入...");
            loadAndStoreDocumentData();
        }

        // 获取 Milvus 客户端并手动调用 flush,刷新索引,确保数据被持久化到 Milvus 数据库中
        System.out.println("Milvus vector_store 刷新索引...");
        client.flush(FlushParam.newBuilder()
                .withCollectionNames(List.of(collectionName)) // 设置刷新操作的集合名称
                .build());


    }


    /**
     * 读取 Word 文件内容并存入 Milvus 向量数据库
     */
    private void loadAndStoreDocumentData() {
        List<Document> documents = new ArrayList<>();

        try {
            // 从 resources 目录加载文档
            var resource = new ClassPathResource("导游面试问答.doc");
            Tika tika = new Tika();
            String text = tika.parseToString(resource.getFile());

            // 使用 TokenTextSplitter 拆分文本
            TokenTextSplitter splitter = TokenTextSplitter.builder()
                    .withChunkSize(800)           // 每段最多 800 个 token
                    .withMinChunkSizeChars(400)   // 每段最小字符数 400
                    .withKeepSeparator(true)      // 保留分隔符
                    .build();


            // 拆分文本
            List<Document> rawDocs = List.of(new Document(text));
            List<Document> chunks = splitter.apply(rawDocs);

            // 遍历每个 chunk,生成 embedding 并存储
            for (Document chunk : chunks) {
                String chunkText = chunk.getText().strip();
                //chunk:chunk: Document{id='10d79633-7d1c-4736-9423-3877d05f6cd6', text='内容。。。', media='null', metadata={}, score=null}
                System.out.println("chunk: " + chunk);
                System.out.println("chunkText: " + chunkText);
                if (!chunkText.isBlank()) {
                    // 将文档内容存入 Milvus 向量数据库
                    documents.add(chunk);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 将文档内容存入 Milvus 向量数据库,自动写入向量数据表 guide_qa_vectors 中
        vectorStore.add(documents);
    }
}
代码详解

这个配置类是整个项目的核心,包含以下几个关键部分:

① QuestionAnswerAdvisor 配置

@Bean
public QuestionAnswerAdvisor questionAnswerAdvisor() {
    return QuestionAnswerAdvisor.builder(vectorStore)
            .searchRequest(SearchRequest.builder()
                .similarityThreshold(0.1d)  // 相似度阈值
                .topK(6)                     // 返回前6个最相关结果
                .build())
            .build();
}

QuestionAnswerAdvisor 是 Spring AI 提供的"开箱即用"型 RAG Advisor。它的工作流程非常简单:

  1. 接收用户问题
  2. 从 VectorStore 中检索相似度 ≥ 0.1 的前 6 个文档片段
  3. 将检索到的片段拼接成上下文,附加到用户问题的 Prompt 中
  4. 交给 Chat 模型生成回答

参数说明:

  • similarityThreshold(0.1):相似度阈值,取值 0~1,值越低召回越多但可能引入噪音,值越高越精确但可能遗漏相关内容
  • topK(6):返回最相关的前 K 个文档片段

② RetrievalAugmentationAdvisor 配置

@Bean
public RetrievalAugmentationAdvisor retrievalAugmentationAdvisor() {
    // 文档检索器
    VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
            .vectorStore(vectorStore)
            .similarityThreshold(0.1)
            .topK(6)
            .build();

    // 查询增强器
    ContextualQueryAugmenter cqa = ContextualQueryAugmenter.builder()
            .allowEmptyContext(true)
            .build();

    return RetrievalAugmentationAdvisor.builder()
            .documentRetriever(retriever)
            .queryAugmenter(cqa)
            .build();
}

RetrievalAugmentationAdvisor 是 Spring AI 提供的模块化 RAG Advisor,相比 QuestionAnswerAdvisor 更加灵活:

  • VectorStoreDocumentRetriever:文档检索器,负责从向量数据库中检索与查询最相关的文档片段。它支持相似度阈值、Top-K、过滤表达式等检索策略。
  • ContextualQueryAugmenter:查询增强器,负责将检索到的文档片段与用户问题进行智能组合,生成增强后的 Prompt。allowEmptyContext(true) 表示当检索不到相关文档时,仍然允许大模型直接回答(而不是报错)。

③ ChatClient 配置

@Bean
public ChatClient chatClient(DeepSeekChatModel chatModel, QuestionAnswerAdvisor qaAdvisor) {
    return ChatClient.builder(chatModel)
            .defaultSystem("你是一个助手,回答用户问题是不要提及回复是从上下文信息中获取的," +
                    "不要回答你不知道的问题,如果你不知道答案,就回答:抱歉,我不清楚这个问题。")
            .build();
}

ChatClient 使用 DeepSeek 作为底层 Chat 模型,并设置了系统提示词(System Prompt),关键约束:

  • 不要暴露回答是基于上下文信息的(让回答更自然)
  • 不知道的问题要如实回答"不知道"(避免大模型"幻觉")

④ 向量数据初始化(@PostConstruct)

@PostConstruct
public void initVectorData() {
    // 1. 获取 Milvus 客户端
    // 2. 查询集合中已有的数据量
    // 3. 如果数据量为 0,则读取文档并写入
    // 4. 手动 flush 确保数据持久化
}

这段初始化逻辑在 Spring 容器启动时自动执行一次,核心流程:

  1. 检查数据量:通过 Milvus 原生客户端查询 guide_qa_store 集合中的记录数
  2. 按需写入:只有当集合为空时才执行文档读取和写入,避免重复导入
  3. 手动刷新:调用 flush 确保数据被持久化到磁盘,使后续查询能立即命中新数据

⑤ 文档读取与切分(loadAndStoreDocumentData)

private void loadAndStoreDocumentData() {
    // 1. 使用 Apache Tika 读取 .doc 文件为纯文本
    // 2. 使用 TokenTextSplitter 将文本切分为多个 chunk
    // 3. 过滤空白 chunk
    // 4. 批量写入 Milvus(自动调用 Embedding 模型生成向量)
}

文档处理流程详解:

步骤工具说明
文档读取Apache Tika将 .doc 格式的 Word 文档解析为纯文本
文本切分TokenTextSplitter按 Token 数量切分,chunkSize=800 表示每段最多 800 个 Token
最小长度minChunkSizeChars=400每个切分片段最少 400 个字符,避免过短的无意义片段
保留分隔符keepSeparator=true保留原文中的分隔符,维持文本结构
向量生成MilvusVectorStore.add()调用 add() 时,Spring AI 自动使用智谱AI Embedding 模型将文本转成向量并存入 Milvus

关于 TokenTextSplitter 的参数调优:

  • chunkSize 不宜过大,否则单个片段包含太多主题,检索精度下降
  • chunkSize 不宜过小,否则语义不完整,大模型难以理解上下文
  • 一般建议 500~1000 Token 之间,根据实际文档类型和效果进行调整

6. 创建RagController.java

package com.example.springairagdemo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/ai")
public class RagController {

    @Autowired
    private ChatClient chatClient;
    @Autowired
    private QuestionAnswerAdvisor qaAdvisor;
    @Autowired
    private RetrievalAugmentationAdvisor ragAdvisor;

    @GetMapping("/chat1")
    public String ask1(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                //设置使用 QuestionAnswerAdvisor
                .advisors(List.of(qaAdvisor))
                .call()
                .content();
    }

    @GetMapping("/chat2")
    public String ask2(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                //设置使用 RetrievalAugmentationAdvisor
                .advisors(List.of(ragAdvisor))
                .call()
                .content();
    }

}
接口说明

控制器提供了两个 GET 接口,使用同一个 ChatClient,但分别搭配不同的 Advisor:

接口Advisor说明
GET /ai/chat1?message=xxxQuestionAnswerAdvisor使用简单的 RAG 问答流程
GET /ai/chat2?message=xxxRetrievalAugmentationAdvisor使用增强检索问答流程

两个接口的调用方式完全一致,区别仅在于底层使用的 Advisor 不同,这样方便我们对比两种 Advisor 在实际问答中的效果差异。

.advisors() 的作用:ChatClient 的 .advisors() 方法用于指定本次请求要经过哪些 Advisor 处理。Advisor 会在用户请求发送给大模型之前,对 Prompt 进行增强处理(如注入检索到的上下文信息)。

7. 启动Milvus

在linux node2 节点启动Milvus:

#进入 milvus目录
cd /software/milvus

#启动 Milvus
docker compose up -d

确认 Milvus 启动成功后,可以通过以下命令查看容器状态:

docker compose ps

确保 Milvus 服务在 19530 端口正常监听后,再进行下一步。

8. 启动项目并测试

启动SpringAIRAGDemo项目,运行主应用类SpringAiragDemoApplication.java,启动该项目。

启动后控制台会输出向量数据初始化日志:

初始化向量数据,写入到 Milvus 数据库中...
Milvus vector_store 中数据数量:0
Milvus vector_store 中没有数据,开始写入...
chunk: Document{id='xxx', text='导游面试问答内容...', ...}
chunkText: 导游面试问答内容...
...(更多 chunk 输出)
Milvus vector_store 刷新索引...

首次启动时,系统会自动读取文档、切分、生成向量并写入 Milvus。再次启动时,由于集合中已有数据,不会重复写入。

测试用例与结果分析

在浏览器中分别测试两个接口:

测试一:知识库内的问题(导游知识)

请求:http://localhost:8080/ai/chat1?message=何为丹崖断墙

回答:丹崖断墙是一条自燕山运动以来多次活动的断层带,总断距600米以上。它是由紫红色石英砂岩
构成的断层面,长250米,高150米。该断墙下的一系列断阶完整地体现了造山运动的过程,展示了
地质运动的伟大力量。
请求:http://localhost:8080/ai/chat2?message=何为丹崖断墙

回答:丹崖断墙是一条自燕山运动以来多次活动的断层带,总断距600米以上。丹崖断墙是该断层最新
一次活动形成的断层面,由紫红色石英砂岩构成,长250米,高150米。断墙下的一系列断阶,完整地
体现了造山运动的过程,又展示了造山运动的伟大。

分析:两个接口都正确回答了知识库中的问题。对比来看,chat2(RetrievalAugmentationAdvisor)的回答在表述上更加流畅自然,这是因为 ContextualQueryAugmenter 对查询进行了增强处理。

测试二:知识库外的问题

请求:http://localhost:8080/ai/chat1?message=Spring AI是什么?

回答:抱歉,我不清楚这个问题。
请求:http://localhost:8080/ai/chat2?message=今天天气如何?

回答:抱歉,我不清楚这个问题。

分析:对于知识库中没有涉及的问题,两个接口都能正确地拒绝回答,而不是让大模型"胡编乱造"。这正是 RAG 的价值所在——让大模型的回答有据可依,有效减少"幻觉"。

两种 Advisor 效果对比总结
测试场景QuestionAnswerAdvisor (chat1)RetrievalAugmentationAdvisor (chat2)
知识库内问题能正确回答,表述较简洁能正确回答,表述更流畅自然
知识库外问题正确拒绝回答正确拒绝回答
回答准确性高高
回答流畅度中等较好
配置复杂度低(开箱即用)中等(需配置 Retriever 和 Augmenter)

总结

本文通过一个完整的导游知识库智能问答系统,演示了使用 Spring AI 搭建 RAG 应用的全过程。核心流程回顾:

文档准备 → 文档切分 → 向量化 → 存入Milvus → 用户提问 → 向量检索 → 上下文注入 → 大模型生成回答

关键要点:

  1. RAG 的本质是"先检索,再生成",通过注入外部知识来增强大模型的回答能力
  2. Embedding 模型(智谱AI)负责将文本转成向量,Chat 模型(DeepSeek)负责生成回答,两者各司其职
  3. Milvus 作为向量数据库,负责高效存储和检索文档向量
  4. QuestionAnswerAdvisor 适合快速搭建简单的 RAG 应用,RetrievalAugmentationAdvisor 更适合需要精细控制的生产级应用
  5. 文档切分策略(chunk 大小、最小字符数等)直接影响检索效果,需要根据实际场景调优

后续可以探索的方向:

  • 尝试不同的文档切分策略(如按段落切分、递归切分等)
  • 调整相似度阈值和 Top-K 参数,观察对回答质量的影响
  • 接入更多格式的知识库文档(PDF、Markdown 等)
  • 使用自定义的 QueryAugmenter 实现更复杂的查询增强逻辑

如果觉得有用,点个关注不迷路,后续更新第一时间收到~

更多推荐