SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档

SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档

目标:构建一个真正可用、具备上下文感知能力的知识库问答系统,让大模型基于你提供的私有文档生成准确、可靠的回答。

在上一章《嵌入模型与向量化存储》中,我们深入学习了如何将文本转化为向量、如何计算语义类似度,以及如何使用 VectorStore 存储和检索这些向量。这些技术不仅是独立的能力,更是本章核心——RAG(Retrieval-Augmented Generation,检索增强生成) 的基石。

通用大模型虽然强劲,但其知识是“静态”且“通用”的。它无法回答诸如“我们公司2025年差旅报销标准是多少?”这类依赖私有、动态知识的问题。而 RAG 正是解决这一痛点的关键技术。

通过本章,你将掌握如何将文档知识注入大模型的推理过程,构建一个真正可用的企业级智能问答系统

8.1 RAG 原理详解:检索 + 增强 + 生成

RAG 的核心思想是:将大模型的生成能力与外部知识库的检索能力相结合,形成“先查资料,再作答”的智能行为。

整个流程分为三步:

8.1.1 检索(Retrieval)

当用户提出问题时,系统使用上一章学习的 Embedding 模型 将问题转化为向量,然后在 VectorStore 中进行类似度搜索,找出与问题最相关的文档片段。

8.1.2 增强(Augmentation

将检索到的相关文档内容(原文)作为上下文,拼接到 Prompt 中,形成一个“增强版”的输入。这相当于告知模型:“请参考以下信息来回答问题。”

8.1.3 生成(Generation

大模型基于增强后的 Prompt(包含问题 + 上下文)生成最终回答。由于模型有了明确的信息依据,生成的答案更准确、更可信,大幅降低“幻觉”风险。

✅ RAG 的本质:
它不是让模型“记住”所有知识,而是让它“会查资料”。这正是人类专家的工作方式。

SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档

8.2 ETL 管道(Pipeline):数据处理的核心流程

从文档到向量数据库是RAG的前置条件,而文档到数据库有几个步骤超级重大:

  1. 文档读取:文档可能包括pdf、Excel、图片等等各种各样的内容,而这些内容中包括许多不同的格式,列如pdf中可能还包括表格和图片,因此选择哪些文档读取的工具超级重大
  2. 文档分块:由于大模型有token长度限制同时过长的token实则对于大模型的理解和推理会变慢或者不准确,因此需要将文档分块。对于如何分块,如何不会将原本有关联的语句分隔开也是文档向量化的重大步骤。
  3. 文档embedding:对于不同语言,不同的embedding模型有着不同效果.根据实际使用场景,选择适合自己的embedding模型
  4. 向量数据库:向量数据库的存储和类似度查询的准确性也可能会影响RAG最终的结果。

那么在这一套流程中,Spring AI称为ETL(Extract-Transform-Load) Pipeline,流程如下图(官方提供):

ETL 管道协调了从原始数据源到结构化向量存储的完整流程,确保数据最终转换为 AI 模型检索所需的最优格式。其三大核心阶段为:

  1. 提取(Extract):从 PDF、TXT、Word、HTML 等多种源中读取原始内容。
  2. 转换(Transform):对提取的文本进行清洗、分割、元数据增强等操作。
  3. 加载(Load):将处理后的文档写入向量数据库(VectorStore)或其他存储。

SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档

Spring AI 为 ETL 流程定义了三个核心接口,体现了清晰的职责分离:

接口

功能

对应阶段

DocumentReader

实现 Supplier<List<Document>>
,负责从数据源读取内容并构建 Document对象。

Extract

DocumentTransformer

实现 Function<List<Document>, List<Document>>
,负责对文档进行转换处理(如分割、增强)。

Transform

DocumentWriter

实现 Consumer<List<Document>>
,负责将最终文档写入目标存储(如 VectorStore)。

Load

下面是三个接口的定义:

public interface DocumentReader extends Supplier<List<Document>> {
    default List<Document> read() {
        return (List)this.get();
    }
}

public interface DocumentTransformer extends Function<List<Document>, List<Document>> {
    default List<Document> transform(List<Document> transform) {
        return (List)this.apply(transform);
    }
}

public interface DocumentWriter extends Consumer<List<Document>> {
    default void write(List<Document> documents) {
        this.accept(documents);
    }
}

关于Document,在上一章中已经学习过了。

8.3 Document Reader接口

Spring AI,也集成了许多不同文档解析工具,以下是已经支持的文档读取器:

DocumentReaders

描述

需要引入依赖

JsonReader

用于读取Json格式的数据,并将其转换为一系列Document对象。

内置

TextReader

处理纯文本文档,并将其转换为一系列Document对象。

内置

JsoupDocumentReader

用于处理 HTML 文档,并利用 JSoup 库将这些文档转换为一系列的 Document 对象。

spring-ai-jsoup-document-reader

MarkdownDocumentReader

会处理 Markdown格式文档,并将其转换为一系列的 Document 对象。

spring-ai-markdown-document-reader

PagePdfDocumentReader

使用 Apache PdfBox 库来解析 PDF 文档。

spring-ai-pdf-document-reader

ParagraphPdfDocumentReader

同样也是基于 Apache PdfBox 库,但是不同的是
ParagraphPdfDocumentReader 会利用 PDF 目录(例如目录)中的信息将输入的 PDF 文件拆分成文本段落,并为每个段落生成一个单独的文档(注意:并非所有 PDF 文件都包含 PDF 目录)。

spring-ai-pdf-document-reader

TikaDocumentReader

通过使用 Apache Tika 技术从各种文档格式(如 PDF、DOC/DOCX、PPT/PPTX 和 HTML)中提取文本。

spring-ai-tika-document-reader

8.4 DocumentTransformer接口

DocumentTransformer 可以对一批文档进行转换处理,它是一个接口,主要实现其transform方法,将一组Document转换为另外一组Document。实则现类有:

SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档

  • TextSplitter:是一个抽象基类,它有助于将文档进行分割,以适应人工智能模型的上下文窗口。
  • TokenTextSplitter:是“TextSplitter”的一种实现方式,它根据词的数量将文本分割成多个部分,并使用“CL100K_BASE”编码进行处理。
  • ContentFormatTransformer:可以将文档中的元数据内容变成键值对字符串,从而确保所有文档中的内容格式保持统一。
  • KeywordMetadataEnricher:它调用大模型从文档内容中提取关键词,并向Metadata中添加一个keywords字段,该字段包含了文档的关键词信息。
  • SummaryMetadataEnricher:它调用大模型为文档生成摘要,并将其作为Metadata添加到文档中。它能够为当前文档生成摘要,也能为相邻文档(即前文和后文的文档)生成摘要。

8.5 DocumentWriter接口

DocumentWriter Spring的Writers支持Flie以及向量数据库。实则现有:

SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档

可以看到 VectorStore实际上是 DocumentWriter的子接口。

8.6 实战:对PDF文件进行ETL处理

第一找一个样例 pdf文件,
chapter08/src/main/resources/Embedding-Models.pdf

  1. 引入依赖
chapter08/pom.xml
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
  1. 配置 Milvus VectorStore
// chapter08/src/main/java/com/kaifamiao/chapter08/configuration/VectorStoreConfig.java
@Configuration
public class VectorStoreConfig {
    @Bean
    public VectorStore vectorStore(MilvusServiceClient milvusClient, EmbeddingModel embeddingModel) {
        return MilvusVectorStore.builder(milvusClient, embeddingModel)
                .collectionName("default")
                .databaseName("default")
                .indexType(IndexType.IVF_FLAT)
                .metricType(MetricType.COSINE)
                .initializeSchema(true)
                .build();
    }

    @Bean
    public MilvusServiceClient milvusClient() {
        return new MilvusServiceClient(ConnectParam.newBuilder()
                .withHost("192.168.31.254")
                .withPort(19530)
                .build());
    }
}
  1. 编写一个Service类,这个类中一次完成 pdf文件读取, 文本分割,最后保存到 Milvus 向量数据库中。
//chapter08/src/main/java/com/kaifamiao/chapter08/service/PDFEmbeddingService.java
@Service
@Slf4j
public class PDFEmbeddingService {
    private final VectorStore vectorStore;

    public PDFEmbeddingService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public void processPDFAndStoreToMilvus(Resource pdfResource) {
        try {
            // 1. 读取PDF文档
            PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
                    .withPageExtractedTextFormatter(ExtractedTextFormatter.builder().withNumberOfTopTextLinesToDelete(0)
                            .build())
                    .withPagesPerDocument(1)//如果设置为0,则表明所有页都变成一个文档
                    .build();

            PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(
                    pdfResource, config);

            List<Document> documents = pdfReader.get();
            // 处理文档内容中的非UTF-8字符
            List<Document> cleanedDocuments = documents.stream()
                    .map(this::cleanNonUTF8Characters)
                    .collect(Collectors.toList());

            // 2. 文本分割
            TextSplitter   textSplitter   = new TokenTextSplitter();
            List<Document> splitDocuments = textSplitter.apply(cleanedDocuments);
//            for(Document doc:splitDocuments){
//                log.info("media:{}, metadata:{}  content:{}",doc.getMedia(),doc.getMetadata().keySet(),doc.getText());
//            }
            // 分批处理,每批不超过10个文档,避免超出嵌入模型的批量大小限制
            int batchSize = 10;
            for (int i = 0; i < splitDocuments.size(); i += batchSize) {
                int endIndex = Math.min(i + batchSize, splitDocuments.size());
                List<Document> batch = splitDocuments.subList(i, endIndex);
                // 3. 存储到Milvus向量数据库(分批处理)
                vectorStore.add(batch);
                log.info("Processed batch {} to {}", i / batchSize + 1, (endIndex - 1) / batchSize + 1);
            }

        } catch (Exception e) {
            throw new RuntimeException("Failed to process PDF and store to Milvus", e);
        }
    }

    // 修改清理非UTF-8字符的方法以适应新的API
    private Document cleanNonUTF8Characters(Document document) {
        String text = document.getText();
        // 清理非UTF-8字符
        String cleanedText = text.replaceAll("[^p{Print}s]", "?");
        // 创建新的Document对象
        return new Document(cleanedText, document.getMetadata());
    }

}
  1. 测试用例
//chapter08/src/test/java/com/kaifamiao/chapter08/PDFEmbeddingServiceTest.java
@SpringBootTest
@Slf4j
public class PDFEmbeddingServiceTest {
    @Autowired
    private PDFEmbeddingService pdfEmbeddingService;

    @Test
    public void testProcessPDFAndStoreToMilvus() {
        Resource resource = new ClassPathResource("Embedding-Models.pdf");
        pdfEmbeddingService.processPDFAndStoreToMilvus(resource);
    }

}

8.7 Advisor 机制与 QuestionAnswerAdvisor

在 Spring AI 中,Advisor 是一种强劲的拦截器机制,用于在请求发送给 AI 模型之前或之后修改请求或响应。它在 RAG 场景中扮演着核心角色。

Advisor 允许你在不修改核心业务逻辑的情况下,动态地:

  • 修改 Prompt
  • 注入上下文
  • 记录日志
  • 添加安全检查
  • 处理异常

QuestionAnswerAdvisor 是 Spring AI 提供的专门用于 RAG 的 Advisor。它自动完成了“检索 + 增强”的过程。使用它,必须要引入依赖:

<!-- chapter08/pom.xml -->
<!-- 引入向量数据库插件 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>

QuestionAnswerAdvisor类实现了BaseAdvisor接口,该BaseAdvisor接口主要有2个方法before和after,这个两个方法分别是调用Advisor的核心方法之前和之后执行的方法

// QuestionAnswerAdvisor 类部分源码
@Override
    public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
        // 1. Search for similar documents in the vector store.
        // 组装查询条件,并从向量数据库查询文档
        var searchRequestToUse = SearchRequest.from(this.searchRequest)
            .query(chatClientRequest.prompt().getUserMessage().getText())
            .filterExpression(doGetFilterExpression(chatClientRequest.context()))
            .build();

        List<Document> documents = this.vectorStore.similaritySearch(searchRequestToUse);

        // 2. Create the context from the documents.
        // 将文档作为上下文,传递到其它 advisor或者after 方法中使用
        Map<String, Object> context = new HashMap<>(chatClientRequest.context());
        context.put(RETRIEVED_DOCUMENTS, documents);

        String documentContext = documents == null ? ""
                : documents.stream().map(Document::getText).collect(Collectors.joining(System.lineSeparator()));

        // 3. Augment the user prompt with the document context.
        // 将文档使用promptTemplate组装为提示语
        UserMessage userMessage = chatClientRequest.prompt().getUserMessage();
        String augmentedUserText = this.promptTemplate
            .render(Map.of("query", userMessage.getText(), "question_answer_context", documentContext));

        // 4. Update ChatClientRequest with augmented prompt.
        // 访问大模型
        return chatClientRequest.mutate()
            .prompt(chatClientRequest.prompt().augmentUserMessage(augmentedUserText))
            .context(context)
            .build();
    }

8.7.1 QuestionAnswerAdvisor工作原理

  1. 拦截用户的提问。
  2. 使用配置的 Retriever 在 VectorStore 中检索相关文档。
  3. 将检索到的文档内容作为上下文,注入到原始 Prompt 中。
  4. 将增强后的 Prompt 发送给大模型。
  5. 返回生成的答案。

8.7.2 SearchRequest是一个封装的查询

public class SearchRequest {

    /**
     * Similarity threshold that accepts all search scores. A threshold value of 0.0 means
     * any similarity is accepted or disable the similarity threshold filtering. A
     * threshold value of 1.0 means an exact match is required.
     */
    public static final double SIMILARITY_THRESHOLD_ACCEPT_ALL = 0.0;

    /**
     * Default value for the top 'k' similar results to return.
     */
    public static final int DEFAULT_TOP_K = 4;

    /**
     * Default value is empty string.
     */
    private String query = "";

    private int topK = DEFAULT_TOP_K;

    private double similarityThreshold = SIMILARITY_THRESHOLD_ACCEPT_ALL;

    @Nullable
    private Filter.Expression filterExpression;

...
}
  1. query:查询的问题
  2. topK:返回前几个文档
  3. similarityThreshold:类似度,大于等于这个值之上的类似度才会匹配
  4. filterExpression:元数据的过滤条件,主要是过滤Document的metadata元数据属性

8.7.3 实战RAG

只需要为 ChatClient配置一个 QuestionAnswerAdvisor即可,而构建 QuestionAnswerAdvisor的时候,是需要和 VectorStore绑定的。

// chapter08/src/main/java/com/kaifamiao/chapter08/controller/RAGController.java
@RestController
public class RAGController {
    private final ChatClient chatClient;

    @Autowired
    public RAGController(ChatClient.Builder chatClientBuilder,
                         VectorStore vectorStore) {
        //通过Advisors方式,对向量数据库进行封装
        Advisor questionAdvisor =QuestionAnswerAdvisor
                .builder(vectorStore)
                .build();
        this.chatClient = chatClientBuilder
                .defaultAdvisors(questionAdvisor)
                .build();
    }

    @GetMapping(value="/chat/rag"
            , produces = "text/html;charset=UTF-8"
    )
    public String generate(@RequestParam(value = "question") String question) {
        return this.chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

这样,当用户提问的时候,就会第一到 向量数据库中对问题进行类似度搜索,然后将搜索到的相关提示词再组装后发送给大模型,最终大模型生成答案。

上面只是简单使用了QuestionAnswerAdvisor实际上,还可以对 QuestionAnswerAdvisor进行更详细的配置,列如:

 @Autowired
    public RAGController(ChatClient.Builder chatClientBuilder,
                         VectorStore vectorStore) {


        String promptTmp = """
                {query}
                参考信息如下,使用“---------------------”标识包裹在里面的为参考信息。
                ---------------------
                {question_answer_context}
                ---------------------
                鉴于当前的参考信息以及所提供的历史信息(而非任何先入为主的了解),请回复用户评论。如果答案不在上述背景信息中,告知用户您无法回答该问题。
                """;
        PromptTemplate promptTemplate = PromptTemplate.builder()
                .template(promptTmp)
                .build();

        var b = new FilterExpressionBuilder().eq("source", "官方网站").build();
        QuestionAnswerAdvisor questionAdvisor =QuestionAnswerAdvisor
                .builder(vectorStore)
                .promptTemplate(promptTemplate)
                .searchRequest(SearchRequest.builder()
                        .similarityThreshold(0.2) //类似度阈值,只有大于等于该值才会被返回(取值范围:0-1),默认是0(没有类似度排除)
                        .topK(2) //返回类似度排名前2的文档,默认是4
                        .filterExpression(b) // 通过文档的元数据过滤
                        .build())
                .build();
        this.chatClient = chatClientBuilder
                .defaultAdvisors(questionAdvisor)
                .build();
    }
© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容