【SpringAI教程】第二阶段:RAG 检索增强生成(企业落地版)

本文档用于记录 RAG(Retrieval Augmented Generation,检索增强生成)核心技术实现,覆盖企业 AI 落地 90% 场景所需的核心模块,是简历核心技术亮点的重要支撑。文档基于 Spring AI 框架实现,整合文本处理、向量嵌入、向量数据库及完整 RAG 流程,可直接用于本地学习、Demo 搭建及企业级基础落地。

一、技术概述

1.1 什么是 RAG

RAG 即检索增强生成,是结合「信息检索」与「生成式 AI」的核心技术,解决生成式 AI 幻觉、知识滞后、无法结合私有数据回答的核心痛点。其核心逻辑是:用户提问时,先从私有知识库(向量数据库)中检索出最相关的知识片段,再将知识片段与提问结合,交给大模型生成准确、有依据的回答。

核心价值:无需微调大模型,即可让 AI 结合企业私有数据(文档、手册、知识库等)回答问题,降低企业 AI 落地成本,提升回答准确性和实用性,是企业 AI 落地的首选方案。

1.2 技术栈说明

  • 核心框架:Spring AI 1.0.1(适配 Java 生态,企业级首选)
  • 文本处理:spring-ai-tika-document-reader、spring-ai-pdf-document-reader、自定义文本清洗/预处理工具
  • 向量嵌入:Ollama 本地模型(BGE-M3、nomic-embed-text 均可)
  • 向量数据库:Redis Stack(轻量、易部署、支持向量检索,适配中小规模企业场景)
  • 大模型:Ollama 本地大模型(ministral-3:8b)

二、核心技术模块实现

2.1 向量与嵌入基础

2.1.1 Embedding 定义与作用

Embedding(嵌入)是将非结构化数据(文本、图像、视频等)转换为结构化的浮点数数组(向量)的过程。其中,文本 Embedding 是 RAG 技术的核心,其核心作用是:

  • 将文本的语义信息转化为可计算的向量,实现「语义相似度匹配」(而非字面匹配);
  • 将文本向量存入向量数据库,实现高效的相似性检索,快速找到与用户提问最相关的知识片段;
  • 解决大模型无法直接处理长文本、私有文本的问题,为生成式 AI 提供精准的知识支撑。

2.1.2 Embedding 模型使用(本地 Ollama 版)

本方案采用 Ollama 本地运行 Embedding 模型,无需联网,适配本地学习和企业内网部署:

  1. nomic-embed-text:向量维度 768,轻量(60MB),CPU 运行速度快(比 BGE-M3 快 20 倍),适合本地学习、CPU 环境及中小规模知识库场景。

Spring AI 自动对接 Ollama Embedding 模型,无需手动实现向量生成逻辑,只需配置模型名称即可。

2.2 文本分块、清洗与预处理

文本预处理是 RAG 检索精度的核心保障——未经处理的文本(乱码、空格、无效信息)会导致向量生成失真,检索准确率下降。本方案实现完整的文本处理链路:PDF 解析 → 文本清洗 → 预处理 → 智能分块。

2.2.1 依赖引入(PDF 解析+文本处理)

2.2.2 核心工具类实现

(1)PDF 解析工具类(PdfParser)
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;

@Component
public class PdfParser {

    /**
     * PDF 文件字节数组 → 提取纯文本
     * @param pdfBytes PDF 文件字节流(前端上传后获取)
     * @return 提取的纯文本
     * @throws IOException 解析异常
     */
    public String extractText(byte[] pdfBytes) throws IOException {
        // 自动关闭文档流,避免资源泄露
        try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) {
            PDFTextStripper stripper = new PDFTextStripper();
            // 按 PDF 页码顺序提取所有文本
            return stripper.getText(document);
        }
    }
}
(2)文本清洗与预处理工具类(TextProcessor)
import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

@Component
public class TextProcessor {

    // 智能分块器(递归分块算法,Spring AI 默认最优算法)
    private static final TokenTextSplitter SPLITTER = new TokenTextSplitter(
            800,    // chunkSize:每块最大 token 数(1 token ≈ 0.7-0.8 中文汉字)
            200,    // minChunkSizeChars:每块最小字符数(过滤碎片)
            10,     // minChunkLengthToEmbed:允许向量化的最小字符数(过滤无效内容)
            10000,  // maxNumChunks:最大分块数(防止超大文本撑爆系统)
            true    // keepSeparator:保留分隔符(保持段落语义完整)
    );

    /**
     * 文本清洗:移除无效信息,标准化文本格式
     */
    public String cleanText(String text) {
        if (text == null) return "";

        // 1. 替换多余空格、制表符、全角空格
        text = text.replaceAll("\\s+", " ");
        text = text.replaceAll(" ", " ");
        // 2. 移除多余空行(连续换行保留1个)
        text = text.replaceAll("\\n+", "\n");
        // 3. 移除 PDF 常见垃圾信息(页码、页眉页脚)
        text = text.replaceAll("^\\d+$", "");
        text = text.replaceAll("第\\d+页", "");
        text = text.replaceAll("Page \\d+", "");
        // 4. 移除特殊符号、零宽字符、乱码
        text = text.replaceAll("[\\u200B\\u00A0]", "");
        text = text.replaceAll("[^\\p{Print}\\p{Han}]", "");
        // 5. 清理 URL、邮箱(可选,根据业务需求调整)
        text = text.replaceAll("https?://\\S+", "");
        text = text.replaceAll("\\S+@\\S+", "");

        return text.trim();
    }

    /**
     * 文本预处理:标准化格式,强化语义
     */
    public String preprocess(String text) {
        text = cleanText(text);

        // 中文标点标准化(避免中英文标点混用导致语义失真)
        text = text.replaceAll("\\. ", "。");
        text = text.replaceAll(", ", ",");
        text = text.replaceAll("\\? ", "?");
        text = text.replaceAll("! ", "!");

        // 标题强化(换行分隔,提升分块后检索精度)
        text = text.replaceAll("第[一二三四五六七八九十]+章", "\n$0");
        text = text.replaceAll("第\\d+章", "\n$0");

        return text.trim();
    }

    /**
     * 智能分块:清洗预处理后,生成可向量化的文档块
     * @param rawText 原始文本(PDF 解析后)
     * @param source 文档来源(如:纯休息休息吧.pdf,用于后续块管理)
     * @return 分块后的文档列表(每个块包含文本和元数据)
     */
    public List<Document> splitDocuments(String rawText, String source) {
        if (rawText == null || rawText.isBlank()) {
            return new ArrayList<>();
        }

        // 1. 文本清洗 + 预处理
        String processedText = preprocess(rawText);
        // 2. 构建原始文档,存入元数据(source 用于后续按文件管理块)
        Document originalDoc = new Document(processedText);
        originalDoc.getMetadata().put("source", source);
        originalDoc.getMetadata().put("textLength", processedText.length());
        // 3. 递归分块(按优先级切割:段落→换行→句号→逗号→硬切,保持语义完整)
        List<Document> chunks = SPLITTER.apply(List.of(originalDoc));
        // 4. 给每个分块打上 source 标签,便于后续按文件查询、删除
        for (Document chunk : chunks) {
            chunk.getMetadata().put("source", source);
        }

        return chunks;
    }
}

2.3 向量数据库(Redis Stack)

本方案选用 Redis Stack 作为向量数据库,其优势在于:轻量易部署、支持向量检索、与 Java 生态适配性好、适合中小规模知识库(企业 90% 基础场景够用),无需复杂的数据库配置,本地学习和企业部署均可快速上手。具体关于Redis Stack 的知识网上自行搜索。

2.3.1 application.yml配置

spring:
  ai:
    ollama:
      base-url: http://localhost:11434  # Ollama 本地服务地址
      embedding:
        model: nomic-embed-text  # 选用轻量 CPU 友好型 Embedding 模型
      chat:
        model: llama3  # 本地对话大模型(可替换为其他 Ollama 支持的模型)
  data:
    redis:
      vector-store:
        host: localhost  # Redis 地址(本地部署)
        port: 6379       # Redis 端口
        index-name: rag-index  # 向量索引名称(自定义)
        dimension: 768   # 向量维度(与 Embedding 模型一致:nomic-embed-text 768维)

2.3.2 向量数据库核心操作

Spring AI 封装了 Redis 向量库的所有操作,无需手动编写 Redis 命令,核心操作如下(集成在 RAG Service 中):

  • 向量入库:将分块后的文档向量存入 Redis;
  • 相似性检索:根据用户提问的向量,检索出最相关的文档块;
  • 全量查询:查询 Redis 中所有文档块(用于本地调试)。

2.3.3 向量数据效果

三、完整 RAG 流程实现

完整 RAG 流程分为「文档入库链路」和「用户提问链路」,两条链路相互独立,可单独扩展,以下是核心 Service 实现(整合所有模块)。

3.1 核心 Service 实现(RagService)

package org.example.testai.service.businessService;

import org.apache.commons.lang3.time.StopWatch;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
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.VectorStore;
import org.springframework.ai.vectorstore.filter.Filter;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import redis.clients.jedis.JedisPooled;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
public class RagService {

    private final OllamaChatModel chatModel;
    private final ChatClient chatClient;
    private final VectorStore vectorStore;
    private final TokenTextSplitter textSplitter;

//    private final EmbeddingModel embeddingModel;

    // 统一分块配置(生产标准:512token,100token重叠)
    public RagService(VectorStore vectorStore, OllamaChatModel chatModel, ChatClient chatClient) {
        this.vectorStore = vectorStore;
        this.chatModel = chatModel;
        this.chatClient = chatClient;
        this.textSplitter = new TokenTextSplitter();
    }

    // ====================== 生产级:文件加载 ======================
    public void loadDocumentFromFile(MultipartFile file) throws Exception {
        String fileName = file.getOriginalFilename();
        // 1. 格式校验(生产必做)
        if (!isValidFile(fileName)) {
            throw new IllegalArgumentException("不支持的文件格式,仅支持:pdf、docx、txt、md");
        }

        // 2. 多格式解析(Spring AI 原生支持)
        List<Document> documents;
        if (fileName.endsWith(".pdf")) {
            // PDF 专用解析(支持分页、OCR)
            PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
                    .withPageExtractedTextFormatter(new ExtractedTextFormatter.Builder()
                            .withNumberOfBottomTextLinesToDelete(3)
                            .build())
                    .build();
            PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(file.getResource(), config);
            documents = pdfReader.read();
        } else {
            // Tika 通用解析(支持Word/Excel/Markdown等所有格式)
            TikaDocumentReader tikaReader = new TikaDocumentReader(file.getResource());
            documents = tikaReader.read();
        }

        // 3. 文本清洗 + 分块 + 入库
        processAndStoreDocuments(documents, fileName);
    }

    // ====================== 测试用:文本加载(生产可关闭) ======================
    public void loadDocumentFromText(String content) {
        Document doc = new Document(content);
        processAndStoreDocuments(List.of(doc), "测试文本");
    }

    // ====================== 统一处理逻辑 ======================
    private void processAndStoreDocuments(List<Document> rawDocs, String source) {
        // 1. 文本清洗(生产必做:去噪、去乱码、归一化),可以添加更多操作
        List<Document> cleanedDocs = rawDocs.stream()
                .peek(doc -> doc.getMetadata().put("source", source)) // 记录来源,生产必加
                .toList();

        // 2. 语义分块
        List<Document> chunks = textSplitter.apply(cleanedDocs);

        // 3. 向量化入库
        vectorStore.add(chunks);
        System.out.printf("✅ 文档「%s」处理完成,分块数:%d%n", source, chunks.size());
    }

    // ====================== 格式校验 ======================

    /*
    * 此处省略更多格式,只做演示
    * */
    private boolean isValidFile(String fileName) {
        if (fileName == null) return false;
        String lowerName = fileName.toLowerCase();
        return lowerName.endsWith(".pdf") || lowerName.endsWith(".docx")
                || lowerName.endsWith(".txt") || lowerName.endsWith(".md");
    }

    public String ask(String question) {
        StopWatch sw = new StopWatch();
        sw.start();
        List<Document> docs = vectorStore.similaritySearch(question);
        sw.split();
        System.out.printf("相似度搜索耗时:%d 秒%n%n", sw.getSplitTime()/1000);
        StringBuilder context = new StringBuilder();
        docs.forEach(doc -> context.append(doc.getText()).append("\n\n"));

        String prompt = """
                你是企业智能助手,严格参考以下资料回答,禁止编造。
                资料来源:%s
                问题:%s
                """.formatted(context, question);

//        String call = chatModel.call(prompt);
        String call = chatClient.prompt(prompt).call().content();
        sw.stop();
        System.out.printf("总耗时:%d 秒%n", sw.getTime(TimeUnit.SECONDS));
        return call;
    }


    /*
    * 想要自定义条数,默认用的是4条
    * */
    public List getAll(String msg) {
        SearchRequest request = SearchRequest.builder()
                .query(msg)
                .topK(5)
                .build();
        List<Document> documents = vectorStore.similaritySearch(request);
        System.out.println(documents);
        return documents;
    }
 

}

3.2 流程说明

3.2.1 文档入库流程(核心链路)

  1. 用户接口上传文档文件(字节流);
  2. 工具列解析提取文件中的文本;
  3. 通过 TextProcessor 对文本进行清洗、预处理(去垃圾信息、标准化格式);
  4. 通过 TokenTextSplitter 进行智能递归分块,生成多个语义完整的文档块;
  5. Spring AI 自动调用 Embedding 模型,将每个文档块转换为向量;
  6. 将向量及文档块元数据(source 等)存入 Redis 向量库。

3.2.2 用户提问流程(核心链路)

  1. 用户输入提问(如:“某某某.pdf 中提到的 xxx 有哪些?”);
  2. Spring AI 自动调用 Embedding 模型,将提问转换为向量;
  3. 向量数据库(Redis)根据提问向量,检索出最相关的 N 个文档块(topK 可配置,默认是 4);
  4. 拼接检索到的文档块,构造提示词(限制大模型仅参考该内容);
  5. 调用本地大模型(如 llama3),生成有依据、无幻觉的回答;
  6. 将回答返回给用户。

https://gitee.com/an-ape-hou/spring-aihttps://gitee.com/an-ape-hou/spring-ai     具体实现代码查看代码库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值