Spring AI Alibaba RAG实战:基于向量存储的检索增强生成

Spring AI Alibaba RAG实战:基于向量存储的检索增强生成

导读:RAG(Retrieval-Augmented Generation,检索增强生成)是目前企业 AI 落地最成熟的技术路线。它解决了大模型最核心的两个痛点:知识截止日期和企业私有数据无法访问。本文从零构建完整 RAG 流水线,深入讲解检索策略、Prompt 注入机制与效果评估方法。


一、为什么 RAG 是企业 AI 的标配

直接和大模型对话有两个根本性的局限:

局限一:模型不知道你的业务知识

你的公司有几千份产品文档、几万条历史工单、一套独特的业务规范------这些内容不在模型的训练集里,模型无从回答。就算强行"喂"进去,也会遭遇上下文窗口限制和高昂的 Token 费用。

局限二:模型会产生幻觉

当模型不知道答案时,它倾向于"编造"一个听起来合理的回答,这在企业场景下是不可接受的风险。

RAG 的解法直白有效:先搜索,再回答。每次用户提问时,先从知识库里检索相关内容,把检索结果和问题一起送给模型,让模型"基于材料作答",大幅降低幻觉概率。


二、RAG 流水线全貌

复制代码
【离线阶段:知识入库】

原始文档(PDF/Word/HTML)
        |
   Document Loader  ← PdfPageReader / MarkdownPageReader
        |
   Text Splitter    ← 分块(固定长度 / 语义分割)
        |
   EmbeddingModel   ← text-embedding-v3(DashScope)
        |
   VectorStore      ← Milvus / Redis / Elasticsearch
        |
   向量数据库(存储 Document + Embedding)


【在线阶段:检索回答】

用户提问
    |
 EmbeddingModel(将问题向量化)
    |
 VectorStore.similaritySearch(Top-K 相似文档)
    |
 QuestionAnswerAdvisor(拼装 Prompt)
    |
 ChatModel(基于检索内容生成回答)
    |
 最终回答返回给用户

三、环境准备与依赖

3.1 Maven 依赖

xml 复制代码
<dependencies>
    <!-- Spring AI Alibaba DashScope(含 Embedding 支持) -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>

    <!-- 向量存储:Redis Vector(本文示例使用 Redis) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-redis-store-spring-boot-autoconfigure</artifactId>
    </dependency>

    <!-- PDF 文档加载 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pdf-document-reader</artifactId>
    </dependency>

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

    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

3.2 YAML 配置

yaml 复制代码
spring:
  ai:
    dashscope:
      api-key: ${AI_DASHSCOPE_API_KEY}
      chat:
        options:
          model: qwen-plus
          temperature: 0.3    # RAG 场景建议低温度,减少幻觉
          max-tokens: 2048
      # ① DashScope Embedding 模型配置
      embedding:
        options:
          model: text-embedding-v3  # 1.1版新增的高性能 Embedding 模型
          dimensions: 1024

    # Redis Vector Store 配置
    vectorstore:
      redis:
        uri: redis://localhost:6379
        index: rag-knowledge-index
        prefix: "doc:"
        initialize-schema: true  # 首次启动自动创建索引

  redis:
    host: localhost
    port: 6379

四、知识入库:Document 处理流水线

4.1 文档加载器

java 复制代码
package com.example.rag.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.core.io.Resource;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 知识入库 Service
 * 负责文档加载、分块、Embedding、存储全流程
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class KnowledgeIngestionService {

    private final VectorStore vectorStore;

    /**
     * PDF 文档入库
     *
     * @param pdfResource PDF 文件资源
     * @param metadata    元数据(如来源、分类、版本等)
     */
    public void ingestPdf(Resource pdfResource, Map<String, Object> metadata) {
        log.info("开始处理 PDF:{}", pdfResource.getFilename());

        // 1. 加载 PDF(按页读取)
        PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
                .withPageExtractedTextFormatter(
                        // 配置文本提取格式:保留文档结构
                        PagePdfDocumentReader.PagesTextFormatter.of()
                )
                .withPagesPerDocument(1) // 每页作为一个 Document
                .build();

        PagePdfDocumentReader reader = new PagePdfDocumentReader(pdfResource, config);
        List<Document> rawDocuments = reader.get();
        log.info("PDF 共 {} 页,开始分块处理", rawDocuments.size());

        // 2. 文本分块(Token 级别,含重叠窗口)
        TokenTextSplitter splitter = new TokenTextSplitter(
                512,    // 每块最大 Token 数
                128,    // 重叠 Token 数(滑动窗口,保证上下文连续性)
                5,      // 最小块大小
                10000,  // 最大块大小
                true    // 保留分隔符
        );
        List<Document> chunks = splitter.apply(rawDocuments);

        // 3. 注入元数据(文件名、时间等)
        String fileName = pdfResource.getFilename();
        chunks.forEach(doc -> {
            Map<String, Object> docMetadata = new HashMap<>(metadata);
            docMetadata.put("source", fileName);
            docMetadata.put("ingestTime", System.currentTimeMillis());
            doc.getMetadata().putAll(docMetadata);
        });

        // 4. 向量化并存储(VectorStore 内部调用 EmbeddingModel)
        vectorStore.add(chunks);
        log.info("成功入库 {} 个文本块,文件:{}", chunks.size(), fileName);
    }

    /**
     * 纯文本入库(适合 FAQ、规则等结构化内容)
     */
    public void ingestText(String content, String title, Map<String, Object> metadata) {
        Document document = new Document(content, metadata);
        document.getMetadata().put("title", title);
        document.getMetadata().put("type", "text");
        document.getMetadata().put("ingestTime", System.currentTimeMillis());

        // 如果内容较长,同样需要分块
        if (content.length() > 1000) {
            TokenTextSplitter splitter = new TokenTextSplitter(512, 64, 5, 10000, true);
            List<Document> chunks = splitter.apply(List.of(document));
            vectorStore.add(chunks);
            log.info("长文本已分为 {} 块入库,标题:{}", chunks.size(), title);
        } else {
            vectorStore.add(List.of(document));
            log.info("短文本入库成功,标题:{}", title);
        }
    }

    /**
     * 批量删除(根据元数据条件)
     */
    public void deleteBySource(String source) {
        // 使用过滤表达式删除指定来源的所有文档
        vectorStore.delete(List.of(source));
        log.info("已删除来源为 '{}' 的所有文档", source);
    }
}

4.2 入库 REST 接口

java 复制代码
@RestController
@RequestMapping("/api/knowledge")
@RequiredArgsConstructor
public class KnowledgeController {

    private final KnowledgeIngestionService ingestionService;

    @PostMapping("/ingest/pdf")
    public ResponseEntity<Map<String, Object>> ingestPdf(
            @RequestParam("file") MultipartFile file,
            @RequestParam(defaultValue = "general") String category,
            @RequestParam(defaultValue = "") String tags) {

        try {
            Resource resource = file.getResource();
            Map<String, Object> metadata = Map.of(
                    "category", category,
                    "tags", tags,
                    "uploadUser", "system"
            );
            ingestionService.ingestPdf(resource, metadata);

            return ResponseEntity.ok(Map.of(
                    "status", "success",
                    "fileName", file.getOriginalFilename(),
                    "size", file.getSize()
            ));
        } catch (Exception e) {
            return ResponseEntity.internalServerError()
                    .body(Map.of("error", e.getMessage()));
        }
    }
}

五、检索问答:RAG 核心实现

5.1 使用 QuestionAnswerAdvisor 自动注入上下文

java 复制代码
package com.example.rag.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Service;

/**
 * RAG 检索问答 Service
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class RagQueryService {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    /**
     * 基础 RAG 问答
     * QuestionAnswerAdvisor 会自动:
     * 1. 将问题向量化
     * 2. 从 VectorStore 检索 TopK 相关文档
     * 3. 将检索结果拼入 Prompt
     */
    public String query(String question) {
        log.info("RAG 查询:{}", question);

        // 构建 QuestionAnswerAdvisor(核心 RAG 组件)
        QuestionAnswerAdvisor ragAdvisor = new QuestionAnswerAdvisor(
                vectorStore,
                SearchRequest.defaults()
                        .withTopK(5)             // 检索 Top-5 相关文档
                        .withSimilarityThreshold(0.7) // 相似度阈值,低于此值的不使用
        );

        return chatClient.prompt()
                .system("""
                        你是一个企业知识库问答助手。请严格基于提供的参考资料回答问题。
                        如果参考资料中没有相关信息,请明确告知用户"知识库中暂无相关信息",
                        不要凭空编造答案。
                        """)
                .user(question)
                .advisors(ragAdvisor)
                .call()
                .content();
    }

    /**
     * 带元数据过滤的 RAG 问答
     * 只在特定分类的文档中检索
     */
    public String queryWithFilter(String question, String category) {
        SearchRequest searchRequest = SearchRequest.defaults()
                .withTopK(5)
                .withSimilarityThreshold(0.65)
                // 元数据过滤:只检索指定分类的文档
                .withFilterExpression("category == '" + category + "'");

        QuestionAnswerAdvisor ragAdvisor = new QuestionAnswerAdvisor(
                vectorStore, searchRequest);

        return chatClient.prompt()
                .user(question)
                .advisors(ragAdvisor)
                .call()
                .content();
    }

    /**
     * 流式 RAG 问答
     */
    public Flux<String> streamQuery(String question) {
        QuestionAnswerAdvisor ragAdvisor = new QuestionAnswerAdvisor(
                vectorStore,
                SearchRequest.defaults().withTopK(3));

        return chatClient.prompt()
                .user(question)
                .advisors(ragAdvisor)
                .stream()
                .content();
    }
}

5.2 检索策略对比

三种主要检索策略的适用场景:

复制代码
+------------------+--------------------------------------------+------------------+
| 检索策略          | 原理                                        | 适用场景         |
+------------------+--------------------------------------------+------------------+
| Top-K 相似度     | 返回向量距离最近的 K 个文档                   | 通用问答         |
| MMR 多样性算法   | 在相似度基础上,增加结果多样性(避免重复内容)  | 多角度综合分析   |
| 混合检索         | 向量检索 + 关键词(BM25)检索结果融合          | 专业术语精准查询 |
+------------------+--------------------------------------------+------------------+

Top-K 检索(默认,最简单):

java 复制代码
SearchRequest.defaults()
    .withTopK(5)
    .withSimilarityThreshold(0.7)

MMR 多样性检索(结果更丰富,避免检索到重复内容):

java 复制代码
// Spring AI 的 VectorStore 部分实现支持 MMR
SearchRequest.builder()
    .withTopK(10)           // 先取 10 个候选
    .withSimilarityThreshold(0.6)
    // 通过 filterExpression 配合实现 MMR(具体实现依赖 VectorStore)
    .build()

六、Prompt 组装与 Token 超限处理

6.1 QuestionAnswerAdvisor 的 Prompt 模板

QuestionAnswerAdvisor 使用的默认 Prompt 模板大致如下(可以自定义):

复制代码
系统提示:{你设置的 defaultSystem}

参考资料(从知识库检索到的相关内容):
------
{document_1_content}
------
{document_2_content}
------
...

用户问题:{user_question}

请基于以上参考资料回答问题。

自定义 Prompt 模板:

java 复制代码
// 自定义 QuestionAnswerAdvisor 的用户文本模板
String customUserTextAdvice = """
        以下是从知识库中检索到的相关参考资料:
        
        {question_answer_context}
        
        ---------------------
        基于以上资料,请回答:{user_input}
        
        注意事项:
        1. 如果资料中有明确答案,直接引用
        2. 如果资料不足以回答,请告知用户
        3. 不要捏造资料中没有的信息
        """;

QuestionAnswerAdvisor ragAdvisor = new QuestionAnswerAdvisor(
        vectorStore,
        SearchRequest.defaults().withTopK(5),
        customUserTextAdvice  // 传入自定义模板
);

6.2 Token 超限截断策略

当检索到的文档过多,拼接后的 Prompt 可能超过模型的上下文限制。处理策略:

java 复制代码
@Component
public class TokenLimitHandler {

    // qwen-plus 的上下文窗口为 131072 tokens,留出 2048 用于回答
    private static final int MAX_CONTEXT_TOKENS = 120000;

    /**
     * 截断过长的检索文档列表
     */
    public List<Document> truncateDocuments(List<Document> documents) {
        List<Document> result = new ArrayList<>();
        int totalTokens = 0;

        for (Document doc : documents) {
            // 粗估 Token 数(中文约 1.5字/token,英文约 4字/token)
            int docTokens = estimateTokens(doc.getContent());

            if (totalTokens + docTokens <= MAX_CONTEXT_TOKENS) {
                result.add(doc);
                totalTokens += docTokens;
            } else {
                log.warn("文档列表 Token 超限,已截断,保留 {} 个文档,约 {} tokens",
                        result.size(), totalTokens);
                break;
            }
        }
        return result;
    }

    private int estimateTokens(String text) {
        if (text == null) return 0;
        // 中英文混合粗估
        long chineseCount = text.chars()
                .filter(c -> c >= 0x4E00 && c <= 0x9FFF).count();
        long otherCount = text.length() - chineseCount;
        return (int) (chineseCount / 1.5 + otherCount / 4);
    }
}

七、效果评估指标

7.1 三个核心指标

复制代码
1. 检索准确率(Retrieval Precision)
   = 检索到的相关文档数 / 检索到的总文档数
   含义:检索结果中有多少是真正有用的

2. 答案相关性(Answer Relevancy)
   = 答案与问题的语义相似度
   评估方式:用 EmbeddingModel 计算相似度,或用 LLM 作为评判者

3. 幻觉检测(Faithfulness)
   = 答案中有多少内容有文档支撑
   评估方式:让 LLM 判断答案的每句话是否能在参考资料中找到依据

7.2 简单评估实现

java 复制代码
@Service
@RequiredArgsConstructor
public class RagEvaluationService {

    private final ChatClient evaluatorClient;
    private final EmbeddingModel embeddingModel;

    /**
     * 幻觉检测:让 LLM 判断答案是否忠于参考资料
     */
    public double evaluateFaithfulness(String answer,
                                        String context,
                                        String question) {
        String prompt = String.format("""
                请判断以下 AI 回答是否完全基于提供的参考资料,没有捏造信息。
                
                参考资料:
                %s
                
                问题:%s
                
                AI 回答:
                %s
                
                请以 0-1 的数值评分(1=完全忠于资料,0=严重捏造):
                只输出数字,不要解释。
                """, context, question, answer);

        String scoreStr = evaluatorClient.prompt(prompt).call().content().trim();
        try {
            return Double.parseDouble(scoreStr);
        } catch (NumberFormatException e) {
            log.warn("评分解析失败:{}", scoreStr);
            return 0.5;
        }
    }

    /**
     * 答案相关性评估:基于向量相似度
     */
    public double evaluateRelevancy(String question, String answer) {
        float[] questionEmbedding = embeddingModel.embed(question);
        float[] answerEmbedding = embeddingModel.embed(answer);
        return cosineSimilarity(questionEmbedding, answerEmbedding);
    }

    private double cosineSimilarity(float[] a, float[] b) {
        double dotProduct = 0, normA = 0, normB = 0;
        for (int i = 0; i < a.length; i++) {
            dotProduct += a[i] * b[i];
            normA += a[i] * a[i];
            normB += b[i] * b[i];
        }
        return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
    }
}

八、DashScope text-embedding-v3 特性说明

Spring AI Alibaba 1.1 版新增了 text-embedding-v3 的专项支持,这个模型相比旧版有几个显著提升:

复制代码
+------------------------+------------------+------------------+
| 特性                    | text-embedding-v1 | text-embedding-v3 |
+------------------------+------------------+------------------+
| 向量维度                 | 1536             | 1024(可调)      |
| 支持语言                 | 中英文            | 多语言(50+语种)  |
| 最大 Token               | 2048             | 8192              |
| 中文效果                 | 良好              | 优秀              |
| 批量请求                 | 支持             | 支持               |
+------------------------+------------------+------------------+

配置 text-embedding-v3:

yaml 复制代码
spring:
  ai:
    dashscope:
      embedding:
        options:
          model: text-embedding-v3
          # 可选:指定输出维度(512/768/1024,维度越小,存储和计算越快)
          dimensions: 1024

九、生产注意事项

1. 入库幂等性:同一文档多次入库会产生重复向量,需要基于文档 hash 做去重检查。

2. 增量更新:文档变更时,先删除旧向量(按元数据过滤),再重新入库。

3. 相似度阈值调优0.7 是经验值,实际需要根据业务语料测试。阈值过高则召回太少,过低则引入不相关内容。

4. 分块策略选择

  • 固定 512 Token 分块:简单通用,适合大多数场景;
  • 按段落分块:适合结构化文档(技术文档、规范等);
  • 语义分块:效果最好但实现复杂,适合对质量要求极高的场景。

十、总结

RAG 体系的核心四步:加载 → 分块 → 向量化 → 检索问答。本文的关键收获:

  1. QuestionAnswerAdvisor 是 Spring AI 中 RAG 的"一键开关",极大简化了实现复杂度;
  2. text-embedding-v3 是 DashScope 目前综合效果最好的 Embedding 模型,1.1 版已完整支持;
  3. 相似度阈值、Top-K 数量需要结合实际语料反复调优;
  4. 幻觉检测是 RAG 质量保障的关键一环,不能省略。

下一篇深入向量数据库的生产级配置:Milvus 集群部署、ES 向量字段优化,以及混合查询的实现细节。


参考资料

相关推荐
秋名RG9 小时前
2026/5/27 生产事故复盘与整改方案
java
m0_462605229 小时前
week05
人工智能
johnsong9 小时前
AI 前沿日报:2026年9月4日
人工智能
leonkay9 小时前
C# 锁机制——【2】深入原理
开发语言·后端·spring·c#·个人开发
lee_curry9 小时前
AI Agent 工程师完整学习路线(面向生产级项目与面试)
人工智能·学习·ai·面试·agent
大模型真好玩10 小时前
DeepSeek Harness 入门很简单(二)——DeepSeek Harness通用设置及Agent预设详解
人工智能·agent·deepseek
血小板要健康10 小时前
链表 阶段算法总结
java·数据结构·笔记·算法·leetcode·链表
坐吃山猪10 小时前
【多线程】CompletableFuture使用
java·开发语言·多线程
前端世界10 小时前
基于 Linux 的本地 AI 智能问答平台搭建实践 —— Ollama + FastAPI + Nginx 实现本地大模型 Web 服务
linux·运维·人工智能
小小小小钰儿10 小时前
1.2-AI核心理论与术语
人工智能·计算机·网络安全·信息安全·程序员·编程