文章 585
评论 5
浏览 226854
Spring AI 进阶:RAG(检索增强生成)在 Java 后端落地实战

Spring AI 进阶:RAG(检索增强生成)在 Java 后端落地实战

一、引言

LLM 大语言模型虽然强大,但有一个致命缺陷:不知道我们系统的内部设计

当你问"我们系统的订单流程是怎样的?"或者"用户登录接口的限流策略是什么?",LLM 只能回答通用知识,无法给出准确答案。

RAG(Retrieval-Augmented Generation,检索增强生成)正是解决这个问题的最佳方案。它的核心思想是:先从知识库中检索相关文档,再将这些文档作为上下文喂给 LLM,让它基于这些信息生成答案


二、RAG 架构原理

2.1 RAG 工作流程

┌─────────────────────────────────────────────────────────────────────────┐
│                        RAG 完整工作流程                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  【构建阶段】                                                           │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐              │
│  │ 原始文档      │───▶│ 文档切片      │───▶│ 生成向量      │              │
│  │ (PDF/Markdown)│    │ (Chunking)   │    │ (Embedding)  │              │
│  └──────────────┘    └──────────────┘    └──────────────┘              │
│                                                │                       │
│                                                ▼                       │
│                                       ┌──────────────┐                 │
│                                       │ 向量数据库    │                 │
│                                       │ (pgvector)   │                 │
│                                       └──────────────┘                 │
│                                                                         │
│  【查询阶段】                                                           │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐              │
│  │ 用户提问      │───▶│ 生成查询向量  │───▶│ 向量检索      │              │
│  │              │    │              │    │ (相似度匹配)  │              │
│  └──────────────┘    └──────────────┘    └──────────────┘              │
│                                                │                       │
│                                                ▼                       │
│                                       ┌──────────────┐                 │
│                                       │ 构建 Prompt  │                 │
│                                       │ (注入上下文)  │                 │
│                                       └──────────────┘                 │
│                                                │                       │
│                                                ▼                       │
│                                       ┌──────────────┐                 │
│                                       │ LLM 生成答案  │                 │
│                                       │              │                 │
│                                       └──────────────┘                 │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

2.2 核心组件

组件作用技术选型
文档切片器将长文档切分成适合 LLM 的小块Spring AI TokenTextSplitter
嵌入模型将文本转换为向量表示OpenAI text-embedding-3-small
向量数据库存储和检索向量PostgreSQL + pgvector
检索器根据查询向量找到最相关的文档pgvector 余弦相似度
LLM根据上下文生成答案OpenAI GPT-4o

三、环境准备

3.1 技术栈

技术版本说明
Java21Spring AI 3.x 要求 Java 17+
Spring Boot3.2.x最新稳定版
Spring AI1.0.xRAG 功能依赖
PostgreSQL16配合 pgvector 扩展
Docker24.x快速启动 PostgreSQL

3.2 Docker Compose 配置

创建 docker-compose.yml

version: '3.8'
services:
  postgres:
    image: pgvector/pgvector:pg16
    container_name: rag-postgres
    environment:
      POSTGRES_DB: rag_db
      POSTGRES_USER: rag_user
      POSTGRES_PASSWORD: rag_password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U rag_user -d rag_db"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

创建 init.sql

-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;

-- 创建知识库表
CREATE TABLE IF NOT EXISTS document_embeddings (
    id SERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    metadata JSONB,
    embedding vector(1536),
    token_count INTEGER,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建向量索引(加速相似性搜索)
CREATE INDEX IF NOT EXISTS idx_embedding ON document_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

-- 创建全文索引(用于混合检索)
CREATE INDEX IF NOT EXISTS idx_content_fts ON document_embeddings USING GIN (to_tsvector('english', content));

启动服务:

docker-compose up -d

四、项目初始化

4.1 pom.xml 依赖

<?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 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>spring-ai-rag-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>21</java.version>
        <spring-ai.version>1.0.0</spring-ai.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>
        </dependencies>
    </dependencyManagement>

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

        <!-- Spring AI OpenAI -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>

        <!-- Spring AI pgvector -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
        </dependency>

        <!-- Spring Boot Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- PostgreSQL Driver -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 application.yml 配置

server:
  port: 8080

spring:
  application:
    name: spring-ai-rag-demo

  # PostgreSQL 数据源配置
  datasource:
    url: jdbc:postgresql://localhost:5432/rag_db
    username: rag_user
    password: rag_password
    driver-class-name: org.postgresql.Driver

  # JPA 配置
  jpa:
    hibernate:
      ddl-auto: none
    show-sql: true
    properties:
      hibernate:
        format_sql: true

  # Spring AI 配置
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o
          temperature: 0.1
      embedding:
        options:
          model: text-embedding-3-small
          dimensions: 1536

    # pgvector 配置
    vectorstore:
      pgvector:
        index-type: IVFFLAT
        distance-type: COSINE

五、核心代码实现

5.1 文档切片策略

策略一:按 Token 分割(推荐)

package com.example.rag.service;

import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

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

@Service
public class DocumentSplitterService {

    private final TokenTextSplitter tokenTextSplitter;

    public DocumentSplitterService(
            @Value("${app.rag.chunk-size:512}") int chunkSize,
            @Value("${app.rag.chunk-overlap:100}") int chunkOverlap) {
        this.tokenTextSplitter = new TokenTextSplitter(chunkSize, chunkOverlap);
    }

    public List<Document> splitByToken(Resource resource) {
        TextReader textReader = new TextReader(resource);
        List<Document> documents = textReader.get();
        return tokenTextSplitter.apply(documents);
    }

    public List<Document> splitByToken(String content, String source) {
        Document document = new Document(content, Map.of("source", source));
        return tokenTextSplitter.apply(List.of(document));
    }
}

策略二:按段落分割

package com.example.rag.service;

import org.springframework.ai.document.Document;
import org.springframework.stereotype.Service;

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

@Service
public class ParagraphSplitterService {

    @Value("${app.rag.max-paragraph-length:2000}")
    private int maxParagraphLength;

    public List<Document> splitByParagraph(String content, String source) {
        List<Document> documents = new ArrayList<>();
        String[] paragraphs = content.split("\\n\\s*\\n");
        
        for (int i = 0; i < paragraphs.length; i++) {
            String paragraph = paragraphs[i].trim();
            if (paragraph.isEmpty()) continue;

            // 如果段落太长,进一步分割
            if (paragraph.length() > maxParagraphLength) {
                List<String> chunks = splitLongParagraph(paragraph);
                for (int j = 0; j < chunks.size(); j++) {
                    documents.add(new Document(
                        chunks.get(j),
                        Map.of(
                            "source", source,
                            "paragraph", i + 1,
                            "chunk", j + 1
                        )
                    ));
                }
            } else {
                documents.add(new Document(
                    paragraph,
                    Map.of(
                        "source", source,
                        "paragraph", i + 1
                    )
                ));
            }
        }
        return documents;
    }

    private List<String> splitLongParagraph(String paragraph) {
        List<String> chunks = new ArrayList<>();
        int start = 0;
        int end = maxParagraphLength;

        while (start < paragraph.length()) {
            if (end >= paragraph.length()) {
                chunks.add(paragraph.substring(start));
                break;
            }

            int lastPeriod = paragraph.lastIndexOf('.', end);
            int lastNewline = paragraph.lastIndexOf('\n', end);
            int splitPoint = Math.max(lastPeriod, lastNewline);

            if (splitPoint <= start) {
                splitPoint = end;
            } else {
                splitPoint++;
            }

            chunks.add(paragraph.substring(start, splitPoint));
            start = splitPoint;
            end = start + maxParagraphLength;
        }
        return chunks;
    }
}

5.2 向量存储服务

package com.example.rag.service;

import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.PgVectorStore;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class VectorStoreService {

    private final VectorStore vectorStore;
    private final EmbeddingModel embeddingClient;

    @Autowired
    public VectorStoreService(PgVectorStore vectorStore, EmbeddingModel embeddingClient) {
        this.vectorStore = vectorStore;
        this.embeddingClient = embeddingClient;
    }

    public void addDocuments(List<Document> documents) {
        vectorStore.add(documents);
    }

    public List<Document> search(String query, int topK, double similarityThreshold) {
        SearchRequest searchRequest = SearchRequest
            .query(query)
            .topK(topK)
            .similarityThreshold(similarityThreshold);
        
        return vectorStore.similaritySearch(searchRequest);
    }

    public void deleteAll() {
        vectorStore.delete(List.of());
    }
}

5.3 RAG 问答服务

package com.example.rag.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.document.Document;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

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

@Service
public class RagService {

    private final ChatClient chatClient;
    private final VectorStoreService vectorStoreService;

    @Value("${app.rag.top-k:5}")
    private int topK;

    @Value("${app.rag.similarity-threshold:0.7}")
    private double similarityThreshold;

    @Autowired
    public RagService(ChatClient chatClient, VectorStoreService vectorStoreService) {
        this.chatClient = chatClient;
        this.vectorStoreService = vectorStoreService;
    }

    public String query(String question) {
        // 1. 从向量数据库检索相关文档
        List<Document> relevantDocs = vectorStoreService.search(
            question, topK, similarityThreshold
        );

        if (relevantDocs.isEmpty()) {
            return "抱歉,知识库中没有找到相关信息。";
        }

        // 2. 构建上下文
        String context = buildContext(relevantDocs);

        // 3. 构建 Prompt
        Prompt prompt = buildPrompt(question, context);

        // 4. 调用 LLM 生成答案
        return chatClient.call(prompt).getResult().getOutput().getContent();
    }

    private String buildContext(List<Document> documents) {
        StringBuilder context = new StringBuilder();
        for (int i = 0; i < documents.size(); i++) {
            Document doc = documents.get(i);
            context.append(String.format("【文档 %d】\n", i + 1));
            context.append(doc.getContent());
            context.append("\n\n");
        }
        return context.toString();
    }

    private Prompt buildPrompt(String question, String context) {
        String systemPrompt = """
            你是一个专业的技术助手,基于提供的上下文信息回答用户问题。
            
            规则:
            1. 只能基于提供的上下文信息回答,不要编造信息
            2. 如果上下文没有相关信息,明确说明"无法从知识库中找到相关答案"
            3. 回答要简洁明了,直接针对问题
            4. 如果有多个相关文档,综合所有信息给出完整答案
            """;

        String userPrompt = String.format("""
            上下文信息:
            %s
            
            用户问题:%s
            """, context, question);

        List<Message> messages = new ArrayList<>();
        messages.add(new SystemMessage(systemPrompt));
        messages.add(new UserMessage(userPrompt));

        return new Prompt(messages);
    }
}

5.4 使用 QuestionAnswerAdvisor 简化 RAG(推荐)

Spring AI 提供了 QuestionAnswerAdvisor,可以一键实现 RAG 问答,无需手动构建 Prompt 和检索上下文。

5.4.1 配置 ChatClient

package com.example.rag.config;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfig {

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

    @Value("${spring.ai.openai.chat.options.model}")
    private String model;

    @Value("${spring.ai.openai.chat.options.temperature}")
    private float temperature;

    @Bean
    public ChatClient ragChatClient(VectorStore vectorStore) {
        OpenAiChatOptions options = OpenAiChatOptions.builder()
            .withApiKey(apiKey)
            .withModel(model)
            .withTemperature(temperature)
            .build();

        return new OpenAiChatClient(options)
            .advisors(new QuestionAnswerAdvisor(vectorStore));
    }
}

5.4.2 简化版 RAG 服务

package com.example.rag.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class RagAdvisorService {

    private final ChatClient ragChatClient;

    public RagAdvisorService(@Qualifier("ragChatClient") ChatClient ragChatClient) {
        this.ragChatClient = ragChatClient;
    }

    public String query(String question) {
        return ragChatClient.call(question).getResult().getOutput().getContent();
    }
}

5.4.3 两种方式对比

维度手动构建方式QuestionAnswerAdvisor
代码量较多(需手动检索和构建 Prompt)极简(一行代码)
灵活性高(完全控制 Prompt 和检索逻辑)中等(默认行为,可自定义)
适用场景需要定制化的复杂场景快速开发、标准 RAG 场景
学习成本较高较低

💡 推荐策略:使用 QuestionAnswerAdvisor 进行快速开发,当需要更精细的控制时(如自定义 Prompt 模板、混合检索等),再切换到手动构建方式。

5.5 混合检索(向量 + 关键词)

package com.example.rag.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import com.example.rag.entity.DocumentEmbedding;

import java.util.List;

@Repository
public interface DocumentEmbeddingRepository extends JpaRepository<DocumentEmbedding, Long> {

    @Query(value = """
        SELECT *, 
               1 - (embedding <=> :queryVector) as similarity,
               ts_rank(to_tsvector('english', content), plainto_tsquery('english', :query)) as text_rank
        FROM document_embeddings
        WHERE embedding <=> :queryVector < :maxDistance
           OR to_tsvector('english', content) @@ plainto_tsquery('english', :query)
        ORDER BY (similarity * 0.7) + (text_rank * 0.3) DESC
        LIMIT :limit
        """, nativeQuery = true)
    List<DocumentEmbedding> hybridSearch(
        @Param("queryVector") float[] queryVector,
        @Param("query") String query,
        @Param("maxDistance") double maxDistance,
        @Param("limit") int limit
    );
}

5.5 文档实体类

package com.example.rag.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Table(name = "document_embeddings")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DocumentEmbedding {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(columnDefinition = "TEXT")
    private String content;

    @Column(columnDefinition = "JSONB")
    private String metadata;

    @Column(columnDefinition = "vector(1536)")
    private float[] embedding;

    @Column(name = "token_count")
    private Integer tokenCount;

    @Column(name = "created_at")
    private LocalDateTime createdAt;
}

5.6 REST API 控制器

package com.example.rag.controller;

import org.springframework.ai.document.Document;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import com.example.rag.service.DocumentSplitterService;
import com.example.rag.service.RagAdvisorService;
import com.example.rag.service.RagService;
import com.example.rag.service.VectorStoreService;

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

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

    private final RagService ragService;
    private final RagAdvisorService ragAdvisorService;
    private final VectorStoreService vectorStoreService;
    private final DocumentSplitterService documentSplitterService;

    public RagController(RagService ragService,
                        RagAdvisorService ragAdvisorService,
                        VectorStoreService vectorStoreService,
                        DocumentSplitterService documentSplitterService) {
        this.ragService = ragService;
        this.ragAdvisorService = ragAdvisorService;
        this.vectorStoreService = vectorStoreService;
        this.documentSplitterService = documentSplitterService;
    }

    @PostMapping("/upload")
    public ResponseEntity<Map<String, Object>> uploadDocument(
            @RequestParam("file") MultipartFile file) throws IOException {
        
        String content = new String(file.getBytes(), StandardCharsets.UTF_8);
        String filename = file.getOriginalFilename();
        
        List<Document> documents = documentSplitterService.splitByToken(content, filename);
        vectorStoreService.addDocuments(documents);

        Map<String, Object> response = new HashMap<>();
        response.put("status", "success");
        response.put("filename", filename);
        response.put("chunks", documents.size());
        
        return ResponseEntity.ok(response);
    }

    @PostMapping("/query")
    public ResponseEntity<Map<String, Object>> query(@RequestBody Map<String, String> request) {
        String question = request.get("question");
        String method = request.getOrDefault("method", "advisor");
        
        String answer;
        if ("manual".equals(method)) {
            answer = ragService.query(question);
        } else {
            answer = ragAdvisorService.query(question);
        }

        Map<String, Object> response = new HashMap<>();
        response.put("question", question);
        response.put("answer", answer);
        response.put("method", method);

        return ResponseEntity.ok(response);
    }

    @DeleteMapping("/documents")
    public ResponseEntity<Map<String, Object>> clearDocuments() {
        vectorStoreService.deleteAll();

        Map<String, Object> response = new HashMap<>();
        response.put("status", "success");
        response.put("message", "所有文档已删除");

        return ResponseEntity.ok(response);
    }
}

六、文档切片策略对比

6.1 策略对比

维度Token 分割段落分割
粒度控制精确(按 Token 数)粗略(按段落)
语义完整性可能切断语义保持段落语义完整
实现复杂度简单(Spring AI 内置)中等(需要自定义)
适用场景技术文档、代码博客文章、报告
Token 浪费较少可能较多

6.2 切片效果对比

原始文档:
"Spring Boot 是一个用于构建生产级应用的框架。它简化了 Spring 应用的初始搭建和开发过程。
Spring Boot 自动配置了大量的 Spring 组件,让开发者可以快速上手。"

【Token 分割(chunkSize=30)】
Chunk 1: "Spring Boot 是一个用于构建生产级应用的框架。它简化了 Spring 应用的"
Chunk 2: "应用的初始搭建和开发过程。Spring Boot 自动配置了大量的 Spring 组件,让"
Chunk 3: "让开发者可以快速上手。"

【段落分割】
Chunk 1: "Spring Boot 是一个用于构建生产级应用的框架。它简化了 Spring 应用的初始搭建和开发过程。"
Chunk 2: "Spring Boot 自动配置了大量的 Spring 组件,让开发者可以快速上手。"

6.3 推荐配置

// TokenTextSplitter 推荐配置
// chunkSize: 512-1024(根据 embedding 模型和 LLM 上下文窗口调整)
// chunkOverlap: 100-200(确保上下文连贯性)

TokenTextSplitter splitter = new TokenTextSplitter(512, 100);

七、相似度阈值调优

7.1 阈值影响

阈值效果适用场景
0.9+仅返回高度相似的文档精确匹配,如 API 文档查询
0.8-0.9返回较相似的文档一般问答场景
0.7-0.8返回相似文档(推荐)综合问答,平衡召回率和准确率
0.6-0.7返回较多相关文档探索性查询,需要更多上下文
< 0.6返回大量文档,包含不相关内容不推荐使用

7.2 调优方法

// 逐步调整阈值,观察效果
public class ThresholdTuner {

    public static void tuneThreshold(VectorStoreService vectorStoreService, 
                                     List<String> testQueries,
                                     List<String> expectedAnswers) {
        double[] thresholds = {0.9, 0.85, 0.8, 0.75, 0.7, 0.65};
        
        for (double threshold : thresholds) {
            int correct = 0;
            for (int i = 0; i < testQueries.size(); i++) {
                List<Document> results = vectorStoreService.search(
                    testQueries.get(i), 5, threshold
                );
                // 检查检索结果是否包含预期答案
                if (containsAnswer(results, expectedAnswers.get(i))) {
                    correct++;
                }
            }
            System.out.printf("Threshold %.2f: %d/%d correct%n", 
                threshold, correct, testQueries.size());
        }
    }

    private static boolean containsAnswer(List<Document> documents, String expected) {
        return documents.stream()
            .anyMatch(doc -> doc.getContent().toLowerCase()
                .contains(expected.toLowerCase()));
    }
}

八、混合检索实现

8.1 混合检索策略

package com.example.rag.service;

import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.rag.entity.DocumentEmbedding;
import com.example.rag.repository.DocumentEmbeddingRepository;

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

@Service
public class HybridSearchService {

    private final DocumentEmbeddingRepository repository;
    private final EmbeddingModel embeddingClient;

    @Autowired
    public HybridSearchService(DocumentEmbeddingRepository repository, 
                               EmbeddingModel embeddingClient) {
        this.repository = repository;
        this.embeddingClient = embeddingClient;
    }

    public List<Document> hybridSearch(String query, int limit, double maxDistance) {
        // 生成查询向量
        float[] queryVector = embeddingClient.embed(query).toArray(new float[0]);

        // 混合检索
        List<DocumentEmbedding> results = repository.hybridSearch(
            queryVector, query, maxDistance, limit
        );

        // 转换为 Document 对象
        List<Document> documents = new ArrayList<>();
        for (DocumentEmbedding entity : results) {
            documents.add(new Document(
                entity.getContent(),
                Map.of(
                    "source", entity.getMetadata(),
                    "similarity", 1 - cosineDistance(entity.getEmbedding(), queryVector)
                )
            ));
        }
        return documents;
    }

    private double cosineDistance(float[] a, float[] b) {
        double dotProduct = 0;
        double normA = 0;
        double 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 1 - (dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)));
    }
}

8.2 混合检索效果对比

查询:"订单流程设计"

【纯向量检索】
返回:"用户登录后可以查看订单列表..."
      "订单状态包括:待付款、待发货、已完成..."
      
【混合检索(向量 + 关键词)】
返回:"订单创建流程:用户选择商品 → 确认订单 → 支付 → 发货..."
      "订单状态机设计:待付款 → 已付款 → 已发货 → 已完成..."
      "订单表结构设计:order_id, user_id, total_amount..."
      "订单消息通知机制:支付成功后发送短信通知..."

九、完整工作流程测试

9.1 上传文档

curl -X POST http://localhost:8080/api/rag/upload \
  -F "file=@system-design.md"

响应:

{
  "status": "success",
  "filename": "system-design.md",
  "chunks": 15
}

9.2 提问

curl -X POST http://localhost:8080/api/rag/query \
  -H "Content-Type: application/json" \
  -d '{"question": "我们系统的订单流程是怎样设计的?"}'

响应:

{
  "question": "我们系统的订单流程是怎样设计的?",
  "answer": "根据知识库,我们系统的订单流程设计如下:\n\n1. 用户选择商品加入购物车\n2. 点击结算,进入订单确认页面\n3. 确认订单信息后提交订单\n4. 系统调用支付服务完成支付\n5. 支付成功后扣减库存\n6. 通知仓库进行发货\n7. 用户可以查看订单状态\n\n关键设计点:\n- 使用分布式锁保证库存扣减的原子性\n- 支付超时时间设置为 30 分钟\n- 订单状态变更通过消息队列异步通知"
}

十、生产环境注意事项

10.1 性能优化

性能优化策略:
┌─────────────────────────────────────────────────────┐
│ 1. 向量索引优化                                      │
│    ├─ 使用 IVFFLAT 索引(适合大规模数据)              │
│    └─ 根据数据量调整 lists 参数(通常为数据量的平方根)│
│                                                     │
│ 2. 缓存优化                                          │
│    ├─ 缓存频繁查询的向量                              │
│    └─ 使用 Redis 缓存检索结果                         │
│                                                     │
│ 3. 异步处理                                          │
│    ├─ 文档上传异步处理                                │
│    └─ 使用消息队列解耦                                │
│                                                     │
│ 4. 批量操作                                          │
│    ├─ 批量生成向量                                    │
│    └─ 批量插入数据库                                  │
└─────────────────────────────────────────────────────┘

10.2 数据安全

安全注意事项:
┌─────────────────────────────────────────────────────┐
│ 1. API Key 管理                                      │
│    ├─ 使用环境变量存储 API Key                        │
│    └─ 不要硬编码到代码中                              │
│                                                     │
│ 2. 输入验证                                          │
│    ├─ 验证上传文件的类型和大小                         │
│    └─ 过滤敏感内容                                    │
│                                                     │
│ 3. 访问控制                                          │
│    ├─ 添加 API 认证(JWT/OAuth)                      │
│    └─ 限制上传和查询权限                              │
│                                                     │
│ 4. 数据脱敏                                          │
│    ├─ 对敏感信息进行脱敏处理                          │
│    └─ 不要在上下文中包含密码、密钥等敏感信息            │
└─────────────────────────────────────────────────────┘

10.3 监控与日志

监控指标:
┌─────────────────────────────────────────────────────┐
│ - 向量检索耗时                                        │
│ - LLM 调用耗时                                       │
│ - API 调用成功率                                     │
│ - 知识库文档数量                                      │
│ - 检索命中率                                         │
│ - Token 使用量                                       │
│ - 错误率                                             │
└─────────────────────────────────────────────────────┘

十一、总结

11.1 RAG 落地关键步骤

落地步骤:
1. 准备环境:PostgreSQL + pgvector + Docker
2. 项目初始化:Spring Boot 3.x + Spring AI 依赖
3. 文档处理:选择合适的切片策略(Token 分割推荐)
4. 向量生成:使用 EmbeddingModel 生成向量
5. 向量存储:使用 PgVectorStore 存储到数据库
6. 检索配置:调优相似度阈值和 topK 参数
7. RAG 问答:构建 Prompt,调用 LLM 生成答案
8. 混合检索:结合向量和关键词检索提升召回率
9. 监控优化:添加监控和日志,持续优化

11.2 常见问题与解决方案

问题原因解决方案
检索结果不相关相似度阈值太高降低阈值到 0.7-0.8
答案不准确上下文不足增加 topK 值或调整切片策略
响应慢向量检索耗时添加索引或使用缓存
Token 消耗大上下文太长优化切片大小或减少 topK
知识库过期文档没有更新建立文档更新机制

11.3 下一步建议

  1. 添加文档管理功能:支持文档版本管理、增量更新
  2. 优化 Prompt 工程:根据不同场景设计专用 Prompt
  3. 集成更多数据源:支持 PDF、Word、Confluence 等
  4. 添加反馈机制:让用户评价答案质量,持续优化
  5. 考虑私有化部署:使用开源模型(如 Llama 3)替代 OpenAI

💡 互动话题:你在实践 RAG 的过程中遇到过哪些挑战?有什么优化经验可以分享?欢迎在评论区讨论!


标题:Spring AI 进阶:RAG(检索增强生成)在 Java 后端落地实战
作者:jiangyi
地址:http://jiangyi.space/articles/2026/07/16/1783849459682.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消