在大模型应用落地过程中,知识库RAG(检索增强生成)是解决“知识时效性”和“事实准确性”的核心方案。Spring AI Alibaba作为阿里云官方推出的AI开发框架,能让Java开发者以极低的成本快速构建RAG应用。本文将带大家从零开始,基于Spring AI Alibaba + 通义千问 + Qdrant向量库,实现一个完整的企业级知识库RAG系统。

一、核心技术栈说明

  • 基础框架:JDK21 + Spring Boot 3.x + Spring AI 1.1.2
  • AI能力:阿里云DashScope(通义千问qwen-max、文本嵌入text-embedding-v3、重排序gte-rerank-v2)
  • 向量数据库:Qdrant(轻量级、高性能的向量存储)
  • 核心组件:Spring AI Alibaba ReactAgent(智能体框架)

二、项目初始化与配置

2.1 依赖配置(pom.xml)

首先在pom.xml中引入核心依赖,通过BOM统一管理版本,避免依赖冲突:

<properties>
    <java.version>21</java.version>
    <spring-ai.version>1.1.2</spring-ai.version>
    <spring-ai-alibaba.version>1.1.2.0</spring-ai-alibaba.version>
    <fastjson2.version>2.0.61</fastjson2.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-bom</artifactId>
            <version>${spring-ai-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-extensions-bom</artifactId>
            <version>${spring-ai-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI Alibaba DashScope Starter(核心AI能力) -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>

    <!-- Spring AI Alibaba Agent Framework(ReactAgent智能体) -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-agent-framework</artifactId>
    </dependency>

    <!-- Qdrant向量库依赖 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
    </dependency>

    <!-- 简化开发 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- JSON处理 -->
    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>${fastjson2.version}</version>
    </dependency>
</dependencies>

2.2 核心配置(application.yml)

配置文件主要包含DashScope API密钥、Qdrant向量库连接信息、RAG相关参数:

server:
  port: 8080

spring:
  application:
    name: yuncheng-ai-rag
  ai:
    dashscope:
      # 替换为你的阿里云DashScope API Key(https://dashscope.console.aliyun.com/)
      api-key: ${AI_DASHSCOPE_API_KEY:sk-your-api-key}
      chat:
        options:
          model: qwen-max # 对话模型
      embedding:
        options:
          model: text-embedding-v3 # 文本嵌入模型
    vectorstore:
      qdrant:
        host: 127.0.0.1 # Qdrant服务地址
        port: 6334 # Qdrant gRPC端口
        api-key: test@qdrant # Qdrant API密钥
        collection-name: yuncheng-knowledge-base # 向量集合名称
        initialize-schema: true # 自动初始化集合

rag:
  docs-path: classpath:data/docs/ # 知识库文档存放路径(MD文件)
  qdrant-rest-url: http://127.0.0.1:6333 # Qdrant REST API端口
  rerank:
    model: gte-rerank-v2 # 重排序模型
    top-k: 3 # 重排序后保留的顶级结果数
  search:
    top-k: 10 # 向量检索的顶级结果数

注意:需要将api-keyhost等信息替换为你自己的实际配置,DashScope API Key可在阿里云百炼控制台获取。

三、核心模块实现

3.1 项目结构概览

com.yuncheng.ai.rag
├── common/          # 通用组件(统一响应)
├── agent/           # AI智能体配置
├── controller/      # 接口控制器
├── service/         # 核心业务逻辑
│   ├── DocumentService.java  # 文档加载与向量存储
│   ├── RagService.java       # 检索与重排序
│   └── RerankService.java    # 重排序实现
├── tool/            # 智能体工具
└── YunchengAiRagApplication.java # 启动类

3.2 文档加载与向量存储(DocumentService)

该模块负责加载本地MD文档、文本分块、清空旧数据、将分块后的文本存入Qdrant向量库(自动完成文本嵌入):

package com.yuncheng.ai.rag.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class DocumentService {

    private static final Logger logger = LoggerFactory.getLogger(DocumentService.class);

    @Value("${rag.qdrant-rest-url}")
    private String qdrantRestUrl;

    @Value("${spring.ai.vectorstore.qdrant.api-key}")
    private String qdrantApiKey;

    @Value("${spring.ai.vectorstore.qdrant.collection-name}")
    private String collectionName;

    @Value("${rag.docs-path}")
    private String docsPath;

    private final VectorStore vectorStore;
    private final ResourcePatternResolver resourcePatternResolver;
    private final RestTemplate restTemplate;

    public DocumentService(VectorStore vectorStore,
                           ResourcePatternResolver resourcePatternResolver,
                           RestTemplate restTemplate) {
        this.vectorStore = vectorStore;
        this.resourcePatternResolver = resourcePatternResolver;
        this.restTemplate = restTemplate;
    }

    /**
     * 重新加载文档:清空旧数据 -> 加载 .md 文件 -> 分块 -> 存入向量库
     *
     * @return 存入向量库的文档块数量
     */
    public int reloadDocuments() throws Exception {
        // 1. 清空历史旧数据
        clearOldData();

        // 2. 加载 .md 文档
        List<Document> documents = loadDocuments();
        if (documents.isEmpty()) {
            logger.warn("No documents found in {}", docsPath);
            return 0;
        }

        // 3. 文本分块(避免单文档过长)
        List<Document> chunks = splitDocuments(documents);

        // 4. 存入向量库(内部会自动调用嵌入模型生成向量)
        if (!chunks.isEmpty()) {
            vectorStore.add(chunks);
            logger.info("Successfully stored {} chunks into vector store", chunks.size());
        }

        return chunks.size();
    }

    /**
     * 清空 Qdrant 集合中的所有数据
     */
    private void clearOldData() {
        try {
            String url = qdrantRestUrl + "/collections/" + collectionName + "/points/delete";
            HttpHeaders headers = new HttpHeaders();
            headers.set("api-key", qdrantApiKey);
            headers.setContentType(MediaType.APPLICATION_JSON);

            // 空 filter 匹配所有数据
            String body = "{\"filter\":{}}";
            HttpEntity<String> entity = new HttpEntity<>(body, headers);
            restTemplate.exchange(url, HttpMethod.POST, entity, String.class);

            logger.info("Cleared old data from collection: {}", collectionName);
        } catch (Exception e) {
            logger.warn("Failed to clear old data (collection might not exist yet): {}", e.getMessage());
        }
    }

    /**
     * 从 classpath:data/docs/ 加载所有 .md 文件
     */
    private List<Document> loadDocuments() throws Exception {
        String pattern = docsPath.endsWith("/") ? docsPath + "*.md" : docsPath + "/*.md";
        Resource[] resources = resourcePatternResolver.getResources(pattern);
        List<Document> documents = new ArrayList<>();

        for (Resource resource : resources) {
            try (InputStream is = resource.getInputStream()) {
                String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
                String filename = resource.getFilename();

                if (content.isBlank()) {
                    logger.warn("Skipping empty document: {}", filename);
                    continue;
                }

                Map<String, Object> metadata = new HashMap<>();
                metadata.put("source", filename); // 记录文档来源

                documents.add(new Document(content, metadata));
                logger.info("Loaded document: {} ({} chars)", filename, content.length());
            } catch (Exception e) {
                logger.error("Failed to load document: {}", resource.getFilename(), e);
            }
        }

        logger.info("Loaded {} documents total", documents.size());
        return documents;
    }

    /**
     * 将文档按 token 进行分块(TokenTextSplitter默认按512token分块)
     */
    private List<Document> splitDocuments(List<Document> documents) {
        TokenTextSplitter splitter = new TokenTextSplitter();
        List<Document> chunks = splitter.apply(documents);
        logger.info("Split {} documents into {} chunks", documents.size(), chunks.size());
        return chunks;
    }
}

3.3 检索与重排序(RagService + RerankService)

3.3.1 重排序服务(RerankService)

向量检索的结果可能存在“相关性偏差”,通过DashScope的重排序模型对结果二次筛选,提升回答准确性:

package com.yuncheng.ai.rag.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

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

@Service
public class RerankService {

    private static final Logger logger = LoggerFactory.getLogger(RerankService.class);

    private static final String RERANK_URL = "https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank";

    @Value("${spring.ai.dashscope.api-key}")
    private String apiKey;

    @Value("${rag.rerank.model:gte-rerank-v2}")
    private String rerankModel;

    @Value("${rag.rerank.top-k:3}")
    private int defaultTopK;

    private final RestTemplate restTemplate;

    public RerankService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    /**
     * 使用 DashScope Reranker 对检索结果进行重排序,仅保留高相关片段
     *
     * @param query     用户查询
     * @param documents 检索到的文档列表
     * @param topN      返回的最大文档数
     * @return 重排序后的文档列表
     */
    @SuppressWarnings("unchecked")
    public List<Document> rerank(String query, List<Document> documents, int topN) {
        if (documents == null || documents.isEmpty()) {
            return List.of();
        }

        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("Authorization", "Bearer " + apiKey);

            List<String> docTexts = documents.stream()
                    .map(Document::getText)
                    .toList();

            Map<String, Object> body = Map.of(
                    "model", rerankModel,
                    "input", Map.of(
                            "query", query,
                            "documents", docTexts
                    ),
                    "parameters", Map.of(
                            "top_n", topN,
                            "return_documents", true
                    )
            );

            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
            ResponseEntity<Map> response = restTemplate.exchange(
                    RERANK_URL, HttpMethod.POST, entity, Map.class);

            Map<String, Object> responseBody = response.getBody();
            if (responseBody == null) {
                logger.warn("Rerank API returned null response, falling back to original order");
                return fallback(documents, topN);
            }

            Map<String, Object> output = (Map<String, Object>) responseBody.get("output");
            if (output == null) {
                logger.warn("Rerank API response missing 'output', falling back");
                return fallback(documents, topN);
            }

            List<Map<String, Object>> results = (List<Map<String, Object>>) output.get("results");
            if (results == null || results.isEmpty()) {
                logger.warn("Rerank API returned empty results, falling back");
                return fallback(documents, topN);
            }

            List<Document> reranked = new ArrayList<>();
            for (Map<String, Object> result : results) {
                int index = ((Number) result.get("index")).intValue();
                double score = ((Number) result.get("relevance_score")).doubleValue();

                // 仅保留相关性分数大于阈值的文档(过滤低相关结果)
                if (score > 0.1 && index < documents.size()) {
                    Document original = documents.get(index);
                    original.getMetadata().put("rerank_score", score); // 记录重排序分数
                    reranked.add(original);
                    logger.debug("Rerank result: index={}, score={}", index, score);
                }
            }

            if (reranked.isEmpty()) {
                logger.warn("No documents passed rerank threshold, falling back");
                return fallback(documents, topN);
            }

            logger.info("Reranked {} documents, kept {} high-relevance results",
                    documents.size(), reranked.size());
            return reranked;

        } catch (Exception e) {
            logger.error("Rerank API call failed, falling back to original order: {}", e.getMessage());
            return fallback(documents, topN);
        }
    }

    /**
     * 使用默认 topK 进行重排序
     */
    public List<Document> rerank(String query, List<Document> documents) {
        return rerank(query, documents, defaultTopK);
    }

    /**
     * 回退策略:返回前 topN 个原始文档(保证服务可用性)
     */
    private List<Document> fallback(List<Document> documents, int topN) {
        return documents.subList(0, Math.min(topN, documents.size()));
    }
}
3.3.2 核心检索服务(RagService)

整合向量检索和重排序,返回格式化的知识库上下文,供智能体调用:

package com.yuncheng.ai.rag.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class RagService {

    private static final Logger logger = LoggerFactory.getLogger(RagService.class);

    private static final String NO_RESULT_MESSAGE = "未找到相关信息";

    @Value("${rag.search.top-k:10}")
    private int searchTopK;

    @Value("${rag.rerank.top-k:3}")
    private int rerankTopK;

    private final VectorStore vectorStore;
    private final RerankService rerankService;

    public RagService(VectorStore vectorStore, RerankService rerankService) {
        this.vectorStore = vectorStore;
        this.rerankService = rerankService;
    }

    /**
     * 检索 + 重排序,返回格式化的知识上下文
     *
     * @param query 用户查询
     * @return 格式化的知识库上下文,或 "未找到相关信息"
     */
    public String searchAndRerank(String query) {
        try {
            // 1. 向量检索(从Qdrant获取TopK相似文档)
            List<Document> searchResults = vectorStore.similaritySearch(
                    SearchRequest.builder()
                            .query(query)
                            .topK(searchTopK)
                            .build()
            );

            if (searchResults == null || searchResults.isEmpty()) {
                logger.info("No search results found for query: {}", query);
                return NO_RESULT_MESSAGE;
            }
            logger.info("Vector search returned {} results for query: {}", searchResults.size(), query);

            // 2. 重排序,仅保留高相关片段
            List<Document> rerankedResults = rerankService.rerank(query, searchResults, rerankTopK);

            if (rerankedResults.isEmpty()) {
                logger.info("No relevant results after reranking for query: {}", query);
                return NO_RESULT_MESSAGE;
            }

            // 3. 格式化为上下文(带来源信息,方便追溯)
            StringBuilder context = new StringBuilder();
            for (int i = 0; i < rerankedResults.size(); i++) {
                Document doc = rerankedResults.get(i);
                String source = doc.getMetadata().getOrDefault("source", "unknown").toString();
                context.append("--- 知识片段 ").append(i + 1)
                        .append(" (来源: ").append(source).append(") ---\n");
                context.append(doc.getText()).append("\n\n");
            }

            logger.info("Returning {} reranked context snippets", rerankedResults.size());
            return context.toString().trim();

        } catch (Exception e) {
            logger.error("Search and rerank failed for query: {}", query, e);
            return NO_RESULT_MESSAGE;
        }
    }
}

3.4 知识库工具定义(KnowledgeBaseSearchTool)

定义供ReactAgent调用的工具类,封装检索逻辑:

package com.yuncheng.ai.rag.tool;

import com.yuncheng.ai.rag.service.RagService;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.ToolParam;

import java.util.function.BiFunction;

/**
 * 知识库搜索工具,供 ReactAgent 调用
 * 执行:向量检索 -> 重排序 -> 返回高相关知识片段
 */
public class KnowledgeBaseSearchTool implements BiFunction<KnowledgeBaseSearchTool.Request, ToolContext, String> {

    private final RagService ragService;

    public KnowledgeBaseSearchTool(RagService ragService) {
        this.ragService = ragService;
    }

    @Override
    public String apply(Request request, ToolContext toolContext) {
        try {
            return ragService.searchAndRerank(request.query());
        } catch (Exception e) {
            return "知识库检索失败: " + e.getMessage();
        }
    }

    /**
     * 工具输入参数,LLM 会以 JSON 对象形式传入
     */
    public record Request(
            @ToolParam(description = "用户提出的问题或查询关键词") String query
    ) {}
}

3.5 ReactAgent智能体配置(ReactAgentConfig)

配置AI智能体,定义系统提示词(约束智能体行为),绑定知识库搜索工具:

package com.yuncheng.ai.rag.agent;

import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.yuncheng.ai.rag.service.RagService;
import com.yuncheng.ai.rag.tool.KnowledgeBaseSearchTool;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class ReactAgentConfig {

    private static final String SYSTEM_PROMPT = """
            你是一个智能知识库问答助手。当用户提问时,你必须使用 knowledge_base_search 工具从知识库中检索相关信息,然后基于检索结果回答问题。

            严格规则:
            1. 收到用户问题后,必须先调用 knowledge_base_search 工具获取相关信息。
            2. 只能基于工具返回的知识库内容回答问题,禁止编造信息或使用你自身的知识。
            3. 如果工具返回"未找到相关信息",你必须明确回复"无法回答"。
            4. 回答时保持准确、简洁,可以适当引用知识片段的来源信息。
            5. 不要在回答中暴露工具调用细节,直接给出最终答案。
            """;

    @Bean
    @Primary
    public ReactAgent ragAgent(ChatModel chatModel, RagService ragService) {
        // 构建知识库搜索工具
        ToolCallback knowledgeBaseTool = FunctionToolCallback
                .builder("knowledge_base_search", new KnowledgeBaseSearchTool(ragService))
                .description("从知识库中搜索与用户问题相关的信息。输入用户的问题或查询关键词,返回知识库中最相关的知识片段。当用户提出任何问题时,必须调用此工具。")
                .inputType(KnowledgeBaseSearchTool.Request.class)
                .build();

        return ReactAgent.builder()
                .name("rag_agent")
                .model(chatModel) // 绑定通义千问模型
                .systemPrompt(SYSTEM_PROMPT) // 约束智能体行为
                .tools(knowledgeBaseTool) // 绑定知识库搜索工具
                .saver(new MemorySaver()) // 内存级对话记忆
                .build();
    }
}

3.6 接口控制器

3.6.1 文档加载控制器(DocumentController)

提供手动触发文档重新加载的接口:

package com.yuncheng.ai.rag.controller;

import com.yuncheng.ai.rag.common.ToolResponse;
import com.yuncheng.ai.rag.service.DocumentService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/rag/docs")
public class DocumentController {

    private static final Logger logger = LoggerFactory.getLogger(DocumentController.class);

    private final DocumentService documentService;

    public DocumentController(DocumentService documentService) {
        this.documentService = documentService;
    }

    /**
     * 手动触发文档重新加载
     * 清空历史旧数据后,从 classpath:data/docs/ 重新加载所有 .md 文件
     */
    @GetMapping("/reload")
    public ToolResponse<Integer> reloadDocuments() {
        try {
            logger.info("Received document reload request");
            int chunkCount = documentService.reloadDocuments();
            String msg = String.format("文档加载完成,共生成 %d 个知识片段", chunkCount);
            logger.info(msg);
            return ToolResponse.success(msg, chunkCount);
        } catch (Exception e) {
            logger.error("Document reload failed", e);
            return new ToolResponse<>(false, "文档加载失败: " + e.getMessage(), null);
        }
    }
}
3.6.2 RAG问答控制器(RagController)

提供流式问答接口,返回基于知识库的回答:

package com.yuncheng.ai.rag.controller;

import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.exception.GraphRunnerException;
import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
import com.alibaba.cloud.ai.graph.streaming.OutputType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import reactor.core.publisher.Flux;

import java.util.UUID;

@RestController
@RequestMapping("/api/rag")
public class RagController {

    private static final Logger logger = LoggerFactory.getLogger(RagController.class);

    private final ReactAgent ragAgent;

    @Autowired
    public RagController(ReactAgent ragAgent) {
        this.ragAgent = ragAgent;
    }

    /**
     * 流式问答接口
     * ReactAgent 内部会自动调用 knowledge_base_search 工具检索知识库,
     * 然后基于检索结果生成答案并流式返回。
     */
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamChat(@RequestParam String message,
                                   @RequestParam(required = false) String threadId)
            throws GraphRunnerException {
        final String tid = (threadId == null || threadId.isBlank())
                ? UUID.randomUUID().toString() : threadId;

        logger.info("Received stream request: message={}, threadId={}", message, tid);

        RunnableConfig config = RunnableConfig.builder()
                .threadId(tid) // 对话线程ID,用于维护对话上下文
                .build();

        return ragAgent.stream(message, config)
                .filter(StreamingOutput.class::isInstance)
                .cast(StreamingOutput.class)
                .filter(so -> so.getOutputType() == OutputType.AGENT_MODEL_STREAMING)
                .map(StreamingOutput::message)
                .filter(AssistantMessage.class::isInstance)
                .cast(AssistantMessage.class)
                .filter(am -> !am.hasToolCalls()) // 过滤工具调用信息,只返回最终回答
                .map(AssistantMessage::getText)
                .filter(text -> text != null && !text.isEmpty())
                .concatWith(Flux.just("[DONE]")) // 流式结束标识
                .onErrorResume(e -> {
                    logger.error("Stream error for threadId={}", tid, e);
                    return Flux.just("Error: " + e.getMessage(), "[DONE]");
                });
    }
}

3.7 通用响应封装(ToolResponse)

统一接口返回格式,提升前端对接体验:

package com.yuncheng.ai.rag.common;

import com.alibaba.fastjson2.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 统一响应结果封装类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ToolResponse<T> {
    /**
     * 是否成功
     */
    private boolean success;

    /**
     * 响应消息
     */
    private String message;

    /**
     * 响应数据
     */
    private T data;

    /**
     * 成功响应 - 无数据
     *
     * @param message 响应消息
     * @return 成功响应对象
     */
    public static ToolResponse<Void> success(String message) {
        return new ToolResponse<>(true, message, null);
    }

    /**
     * 成功响应 - 有数据
     *
     * @param message 响应消息
     * @param data    响应数据
     * @param <T>     数据类型
     * @return 成功响应对象
     */
    public static <T> ToolResponse<T> success(String message, T data) {
        return new ToolResponse<>(true, message, data);
    }

    /**
     * 失败响应
     *
     * @param message 错误消息
     * @return 失败响应对象
     */
    public static ToolResponse<Void> error(String message) {
        return new ToolResponse<>(false, message, null);
    }

    /**
     * 结构化输出JSON字符串
     *
     * @return JSON字符串
     */
    public String toJSONString() {
        return JSON.toJSONString(this);
    }
}

3.8 启动类

package com.yuncheng.ai.rag;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class YunchengAiRagApplication {

    /**
     * 注册RestTemplate Bean(用于调用Qdrant REST API和DashScope重排序接口)
     */
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * 应用程序入口点
     *
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        SpringApplication.run(YunchengAiRagApplication.class, args);
    }
}

四、运行与测试

4.1 准备工作

  1. 部署Qdrant:可以通过Docker快速部署(docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant)。
  2. 准备知识库文档:在src/main/resources/data/docs/目录下放入MD格式的知识库文档。
  3. 配置API Key:替换application.yml中的AI_DASHSCOPE_API_KEY为你的阿里云DashScope密钥。

4.2 启动项目

运行YunchengAiRagApplication,启动成功后,先调用文档加载接口:

# 加载知识库文档
curl http://localhost:8080/api/rag/docs/reload

返回示例:

{
  "success": true,
  "message": "文档加载完成,共生成 25 个知识片段",
  "data": 25
}

4.3 测试流式问答

调用流式问答接口,测试知识库回答能力:

# 流式问答(替换为你的问题)
curl http://localhost:8080/api/rag/stream?message=Spring AI Alibaba如何集成Qdrant向量库

接口会流式返回基于知识库的回答,结束时返回[DONE]标识。

五、核心亮点

  1. 标准化框架:基于Spring AI Alibaba,符合Spring生态开发习惯,降低Java开发者学习成本。
  2. 双层筛选:向量检索 + 重排序,大幅提升回答准确性。
  3. 智能体约束:通过系统提示词严格约束智能体行为,避免“幻觉”。
  4. 流式响应:支持流式输出,提升用户体验。
  5. 高可用性:重排序服务提供回退策略,保证服务稳定性。

总结

本文基于Spring AI Alibaba实现了一套完整的企业级知识库RAG系统,涵盖了文档加载、文本分块、向量存储、检索重排序、智能体问答等核心环节。整套方案符合Spring生态开发规范,代码可直接落地使用,同时预留了丰富的扩展空间,满足不同企业的个性化需求。

对于Java开发者而言,Spring AI Alibaba极大降低了AI应用的开发门槛,让我们可以聚焦业务逻辑,快速实现大模型应用的落地。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐