找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3886

积分

0

好友

534

主题
发表于 前天 03:25 | 查看: 49| 回复: 0

一、RAG 技术全景与架构设计

1.1 为什么需要 RAG?

┌─────────────────────────────────────────────────────────────────┐
│                            大模型的局限性                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ❌ 知识截止:训练数据有截止时间,无法获取最新信息                │
│  ❌ 幻觉问题:可能生成看似合理但实际错误的内容                   │
│  ❌ 私有数据:无法访问企业内部文档和数据                          │
│  ❌ 领域专业:缺乏特定领域的专业知识                              │
│  ❌ 不可追溯:无法提供答案的来源和依据                            │
│                                                                   │
│  RAG 解决方案:                                                   │
│  ✅ 检索外部知识库 → 增强上下文 → 生成准确答案                    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

RAG 的核心价值:

问题 传统 LLM RAG 增强
知识时效 训练数据截止 实时检索最新数据
准确性 可能产生幻觉 基于检索内容生成
私有数据 无法访问 可检索内部文档
可追溯性 无来源 可追溯文档来源
成本 需要微调 无需训练,成本低

1.2 RAG 工作原理

┌─────────────────────────────────────────────────────────────────┐
│                          RAG 完整工作流程                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  【离线流程】文档处理与向量化                                     │
│                                                                   │
│   原始文档          文本分块          向量化          存储        │
│  ┌────────┐      ┌────────┐      ┌────────┐      ┌────────┐   │
│  │  PDF   │      │ Chunk 1│      │ Vector │      │ 向量   │   │
│  │  Word  │ ───▶ │ Chunk 2│ ───▶ │ Embed  │ ───▶ │ 数据库 │   │
│  │  Markdown│    │ Chunk 3│      │  [768] │      │        │   │
│  └────────┘      └────────┘      └────────┘      └────────┘   │
│                                                                   │
│  【在线流程】检索增强生成                                          │
│                                                                   │
│   用户问题          语义检索          增强提示          生成答案  │
│  ┌────────┐      ┌────────┐      ┌────────┐      ┌────────┐   │
│  │  用户   │      │ 向量   │      │ LLM    │      │  带    │   │
│  │  提问   │ ───▶ │  检索  │ ───▶ │ Prompt │ ───▶ │  来源  │   │
│  └────────┘      └────────┘      └────────┘      └────────┘   │
│     │                │                │                │        │
│     │ "如何请假?"    │  Top-K 相似    │  问题 + 上下文   │ "根据...│   │
│     │                │  文档片段       │                │ 第 5 条  │   │
│     │                │                │                │ ..."   │   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

1.3 RAG 系统架构

┌─────────────────────────────────────────────────────────────────┐
│                      Spring AI RAG 系统架构                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                        API Gateway                        │   │
│  │                  (Spring Boot + REST API)                 │   │
│  └────────────────────┬────────────────────────────────────┘   │
│                       │                                           │
│          ┌─────────────┼─────────────┐                            │
│          │             │             │                            │
│          ▼             ▼             ▼                            │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐                 │
│  │  文档管理   │ │  检索服务   │ │  对话服务   │                 │
│  │  Service    │ │  Service    │ │  Service    │                 │
│  └──────┬──────┘ └──────┬──────┘ └──────┬──────┘                 │
│         │               │               │                        │
│         └───────────────┼───────────────┘                        │
│                         │                                        │
│          ┌───────────────┼───────────────┐                       │
│          │               │               │                       │
│          ▼               ▼               ▼                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐                 │
│  │  Vector     │ │  Embedding  │ │   LLM       │                 │
│  │  Database   │ │  Service    │ │   Client    │                 │
│  │  (pgvector) │ │  (本地/云)  │ │ (OpenAI 等)  │                 │
│  └─────────────┘ └─────────────┘ └─────────────┘                 │
│                                                                   │
│  核心组件:                                                       │
│  • DocumentReader - 文档加载器(PDF/Word/Markdown)              │
│  • TextSplitter - 文本分块器                                      │
│  • EmbeddingModel - 向量化模型                                  │
│  • VectorStore - 向量存储(pgvector/Milvus/Chroma)              │
│  • RetrievalTemplate - 检索模板                                 │
│  • ChatClient - 大模型客户端                                    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

1.4 技术选型

┌─────────────────────────────────────────────────────────────────┐
│                           技术栈选型                              │
├──────────────────┬──────────────────────────────────────────────┤
│ 组件类型          │ 推荐方案                                      │
├──────────────────┼──────────────────────────────────────────────┤
│ 开发框架          │ Spring Boot 3.x + Spring AI 1.x              │
├──────────────────┼──────────────────────────────────────────────┤
│ 向量数据库        │ PostgreSQL + pgvector(推荐)                │
│                  │ Milvus / Chroma / Weaviate(可选)            │
├──────────────────┼──────────────────────────────────────────────┤
│ Embedding 模型    │ OpenAI text-embedding-3-small(云端)        │
│                  │ BGE-M3 / m3e-base(本地部署)                 │
├──────────────────┼──────────────────────────────────────────────┤
│ 大语言模型        │ OpenAI GPT-4 / GPT-3.5(云端)                │
│                  │ Azure OpenAI / 通义千问(企业)                │
│                  │ Ollama + Llama3(本地部署)                   │
├──────────────────┼──────────────────────────────────────────────┤
│ 文档处理          │ Apache Tika / Spring AI DocumentReader       │
├──────────────────┼──────────────────────────────────────────────┤
│ 文本分块          │ RecursiveCharacterTextSplitter               │
├──────────────────┼──────────────────────────────────────────────┤
│ 缓存              │ Redis(缓存检索结果和对话历史)                │
├──────────────────┼──────────────────────────────────────────────┤
│ 消息队列          │ RabbitMQ / Kafka(异步文档处理)              │
└──────────────────┴──────────────────────────────────────────────┘

二、Spring AI 快速入门

2.1 什么是 Spring AI?

Spring AI 是 Spring 官方推出的 AI 应用开发框架,为 Java 开发者提供:

  • 🎯 统一的 API 抽象:屏蔽不同 AI 服务商的差异  
  • 🎯 开箱即用的集成:支持主流大模型和向量数据库  
  • 🎯 Spring 生态融合:与 Spring Boot、Spring Data 无缝集成  
  • 🎯 企业级特性:事务管理、监控、安全等  

2.2 项目初始化

Maven 依赖配置

<!-- 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>

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

    <groupId>com.example</groupId>
    <artifactId>spring-ai-rag-demo</artifactId>
    <version>1.0.0</version>
    <name>Spring AI RAG Demo</name>
    <description>Spring AI RAG 知识库系统</description>

    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.0-M1</spring-ai.version>
    </properties>

    <dependencies>
        <!-- Spring Boot 核心 -->
        <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-openai-spring-boot-starter</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>

        <!-- Spring AI pgvector -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>

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

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

        <!-- Spring AI 文档处理 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-tika-document-reader</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>

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

        <!-- 验证 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

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

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

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
</project>

应用配置

# application.yml
spring:
  application:
    name: spring-ai-rag-demo

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

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

  # Redis 配置
  data:
    redis:
      host: localhost
      port: 6379
      database: 0

# Spring AI 配置
spring:
  ai:
    # OpenAI 配置
    openai:
      api-key: ${OPENAI_API_KEY:sk-your-api-key}
      chat:
        options:
          model: gpt-3.5-turbo
          temperature: 0.7
          max-tokens: 1000
      embedding:
        options:
          model: text-embedding-3-small
          dimensions: 1536

    # pgvector 配置
    pgvector:
      store:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536

# 业务配置
rag:
  # 检索配置
  retrieval:
    # 检索文档数量
    top-k: 4
    # 相似度阈值
    similarity-threshold: 0.7
    # 是否启用元数据过滤
    enable-metadata-filter: true

  # 文本分块配置
  chunking:
    # 分块大小
    chunk-size: 500
    # 重叠大小
    chunk-overlap: 50

  # 文档处理配置
  document:
    # 上传目录
    upload-dir: ./uploads
    # 支持的扩展名
    allowed-extensions: pdf,doc,docx,txt,md

# 日志配置
logging:
  level:
    com.example.rag: DEBUG
    org.springframework.ai: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

2.3 核心配置类

// config/RagConfig.java
package com.example.rag.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.pgvector.PgVectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * RAG 核心配置类
 */
@Slf4j
@Configuration
public class RagConfig {

    /**
     * 聊天客户端(带 RAG 增强)
     */
    @Bean
    @Primary
    public ChatClient ragChatClient(ChatModel chatModel, VectorStore vectorStore) {
        return ChatClient.builder(chatModel)
                .defaultSystem("""
                        你是一个智能助手,基于知识库内容回答用户问题。
                        请遵循以下原则:
                        1. 优先使用检索到的上下文信息回答问题
                        2. 如果上下文不足以回答问题,请诚实告知
                        3. 引用上下文时请注明来源
                        4. 回答应简洁明了,避免冗长
                        """)
                .build();
    }

    /**
     * 普通聊天客户端(无 RAG)
     */
    @Bean
    public ChatClient defaultChatClient(ChatModel chatModel) {
        return ChatClient.builder(chatModel).build();
    }

    /**
     * 向量存储配置
     */
    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel, 
                                   PgVectorStore pgVectorStore) {
        // 使用 pgvector 作为向量存储
        return pgVectorStore;
    }
}

三、向量数据库搭建

3.1 PostgreSQL + pgvector 安装

Docker 快速部署

# 拉取带 pgvector 的 PostgreSQL 镜像
docker pull pgvector/pgvector:pg16

# 启动容器
docker run -d \
  --name postgres-pgvector \
  -e POSTGRES_PASSWORD=postgres123 \
  -e POSTGRES_DB=rag_db \
  -p 5432:5432 \
  -v pgvector_data:/var/lib/postgresql/data \
  pgvector/pgvector:pg16

# 验证安装
docker exec -it postgres-pgvector psql -U postgres -d rag_db -c "CREATE EXTENSION IF NOT EXISTS vector;"
docker exec -it postgres-pgvector psql -U postgres -d rag_db -c "\dx"

手动安装(Linux)

# 安装 PostgreSQL 16
sudo apt update
sudo apt install postgresql-16 postgresql-contrib-16

# 安装 pgvector
cd /tmp
git clone --branch v0.6.0 https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install

# 启用扩展
sudo -u postgres psql
CREATE EXTENSION vector;
\dx

3.2 数据库初始化

-- 创建数据库
CREATE DATABASE rag_db;
\c rag_db

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

-- 创建向量表(Spring AI 会自动创建,这里展示手动创建)
CREATE TABLE IF NOT EXISTS vector_store (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    content TEXT,
    embedding vector(1536),
    metadata JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建向量索引(HNSW)
CREATE INDEX IF NOT EXISTS vector_index
ON vector_store
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- 创建文档元数据表
CREATE TABLE IF NOT EXISTS document_metadata (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    filename VARCHAR(255) NOT NULL,
    file_type VARCHAR(50),
    file_size BIGINT,
    chunk_count INTEGER,
    status VARCHAR(20) DEFAULT 'PENDING',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    processed_at TIMESTAMP,
    error_message TEXT
);

-- 创建索引
CREATE INDEX idx_document_status ON document_metadata(status);
CREATE INDEX idx_document_created ON document_metadata(created_at);

-- 创建检索历史表
CREATE TABLE IF NOT EXISTS retrieval_history (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    query_text TEXT NOT NULL,
    query_embedding vector(1536),
    results_count INTEGER,
    response_text TEXT,
    latency_ms INTEGER,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_retrieval_created ON retrieval_history(created_at);

3.3 向量数据库对比

┌─────────────────────────────────────────────────────────────────┐
│                         向量数据库选型对比                        │
├──────────────────┬──────────────────────────────────────────────┤
│ 数据库            │ 特点与适用场景                                │
├──────────────────┼──────────────────────────────────────────────┤
│ PostgreSQL       │ ✅ 成熟稳定,支持事务和复杂查询               │
│ + pgvector       │ ✅ 与现有 PostgreSQL 生态无缝集成              │
│                  │ ✅ 适合中小规模(百万级向量)                 │
│                  │ ⚠️ 超大规模性能不如专用向量数据库             │
├──────────────────┼──────────────────────────────────────────────┤
│ Milvus           │ ✅ 专为向量搜索设计,性能优秀                 │
│                  │ ✅ 支持十亿级向量                             │
│                  │ ✅ 丰富的索引类型和距离度量                     │
│                  │ ⚠️ 运维复杂度较高                             │
├──────────────────┼──────────────────────────────────────────────┤
│ Chroma           │ ✅ 轻量级,易于部署                           │
│                  │ ✅ 适合开发和原型                              │
│                  │ ⚠️ 生产环境成熟度待验证                        │
├──────────────────┼──────────────────────────────────────────────┤
│ Weaviate         │ ✅ 内置向量 + 图数据库                         │
│                  │ ✅ 支持混合搜索(向量 + 关键词)                 │
│                  │ ⚠️ 学习曲线较陡                               │
├──────────────────┼──────────────────────────────────────────────┤
│ Redis            │ ✅ 超低延迟,适合缓存层                        │
│ + RedisSearch    │ ✅ 支持向量搜索模块                            │
│                  │ ⚠️ 内存成本高,不适合大规模存储                │
└──────────────────┴──────────────────────────────────────────────┘

推荐:
• 初创/中小项目:PostgreSQL + pgvector  
• 大规模生产:Milvus  
• 快速原型:Chroma

四、文档处理与向量化

4.1 文档加载器

// service/DocumentLoaderService.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.tika.TikaDocumentReader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;

/**
 * 文档加载服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DocumentLoaderService {

    private final String uploadDir = "./uploads";

    /**
     * 上传并加载文档
     */
    public List<Document> loadDocument(MultipartFile file) throws IOException {
        // 1. 保存文件
        Path uploadPath = Paths.get(uploadDir);
        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }

        String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename();
        Path filePath = uploadPath.resolve(filename);
        Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);

        log.info("文件已保存:{}", filePath);

        // 2. 使用 Tika 加载文档
        Resource resource = new UrlResource(filePath.toUri());
        TikaDocumentReader reader = new TikaDocumentReader(resource);

        // 3. 读取文档内容
        List<Document> documents = reader.get();
        log.info("加载文档成功,文档数量:{}", documents.size());

        // 4. 添加元数据
        for (Document doc : documents) {
            doc.getMetadata().put("filename", filename);
            doc.getMetadata().put("original_filename", file.getOriginalFilename());
            doc.getMetadata().put("content_type", file.getContentType());
            doc.getMetadata().put("size", file.getSize());
            doc.getMetadata().put("upload_time", System.currentTimeMillis());
        }

        return documents;
    }

    /**
     * 从目录加载多个文档
     */
    public List<Document> loadDocumentsFromDirectory(String directoryPath) {
        List<Document> allDocuments = new ArrayList<>();
        Path directory = Paths.get(directoryPath);

        try {
            Files.walk(directory)
                .filter(Files::isRegularFile)
                .filter(path -> {
                    String name = path.getFileName().toString().toLowerCase();
                    return name.endsWith(".pdf") || name.endsWith(".doc") 
                        || name.endsWith(".docx") || name.endsWith(".txt")
                        || name.endsWith(".md");
                })
                .forEach(path -> {
                    try {
                        Resource resource = new UrlResource(path.toUri());
                        TikaDocumentReader reader = new TikaDocumentReader(resource);
                        List<Document> docs = reader.get();

                        for (Document doc : docs) {
                            doc.getMetadata().put("filename", path.getFileName().toString());
                            doc.getMetadata().put("filepath", path.toString());
                        }

                        allDocuments.addAll(docs);
                        log.info("加载文档:{}", path.getFileName());
                    } catch (Exception e) {
                        log.error("加载文档失败:{}", path, e);
                    }
                });
        } catch (IOException e) {
            log.error("遍历目录失败:{}", directoryPath, e);
        }

        log.info("总共加载文档:{} 个", allDocuments.size());
        return allDocuments;
    }
}

4.2 文本分块器

// config/TextSplitterConfig.java
package com.example.rag.config;

import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 文本分块配置
 */
@Configuration
public class TextSplitterConfig {

    /**
     * 基于 Token 的文本分块器
     */
    @Bean
    public TextSplitter tokenTextSplitter() {
        return new TokenTextSplitter(
            500,    // chunkSize: 每块最大 token 数
            50,     // chunkOverlap: 块之间重叠的 token 数
            1000,   // maxChunkSize: 单个块的最大 token 数
            250,    // minChunkSize: 单个块的最小 token 数
            10000   // maxDocumentLength: 单个文档的最大 token 数
        );
    }

    /**
     * 基于字符的文本分块器(备选)
     */
    @Bean
    public TextSplitter characterTextSplitter() {
        return new org.springframework.ai.transformer.splitter.RecursiveCharacterTextSplitter(
            500,    // chunkSize: 每块最大字符数
            50      // chunkOverlap: 块之间重叠的字符数
        );
    }
}
// service/TextProcessingService.java
package com.example.rag.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 文本处理服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class TextProcessingService {

    private final TextSplitter textSplitter;

    /**
     * 对文档进行分块
     */
    public List<Document> splitDocuments(List<Document> documents) {
        log.info("开始文本分块,文档数量:{}", documents.size());

        List<Document> chunks = textSplitter.apply(documents);

        log.info("分块完成,块数量:{}", chunks.size());

        // 为每个块添加序号
        for (int i = 0; i < chunks.size(); i++) {
            chunks.get(i).getMetadata().put("chunk_index", i);
            chunks.get(i).getMetadata().put("total_chunks", chunks.size());
        }

        return chunks;
    }

    /**
     * 对单个文档进行分块
     */
    public List<Document> splitDocument(Document document) {
        return splitDocuments(List.of(document));
    }
}

4.3 向量化与存储

// service/VectorStoreService.java
package com.example.rag.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingResponse;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 向量存储服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class VectorStoreService {

    private final VectorStore vectorStore;
    private final EmbeddingModel embeddingModel;

    /**
     * 存储文档(自动向量化)
     */
    @Transactional
    public void addDocuments(List<Document> documents) {
        log.info("开始存储文档,数量:{}", documents.size());

        // Spring AI 会自动处理向量化和存储
        vectorStore.add(documents);

        log.info("文档存储完成");
    }

    /**
     * 存储单个文档
     */
    @Transactional
    public void addDocument(Document document) {
        addDocuments(List.of(document));
    }

    /**
     * 语义搜索
     */
    public List<Document> search(String query, int topK) {
        log.info("执行语义搜索,query: {}, topK: {}", query, topK);

        SearchRequest searchRequest = SearchRequest.query(query)
                .withTopK(topK);

        List<Document> results = vectorStore.similaritySearch(searchRequest);

        log.info("搜索完成,返回 {} 条结果", results.size());

        return results;
    }

    /**
     * 带过滤的语义搜索
     */
    public List<Document> searchWithFilter(String query, int topK, Map<String, Object> filters) {
        log.info("执行带过滤的语义搜索,query: {}, filters: {}", query, filters);

        SearchRequest searchRequest = SearchRequest.query(query)
                .withTopK(topK);

        // 构建过滤表达式
        if (filters != null && !filters.isEmpty()) {
            Filter.Expression filterExpression = buildFilterExpression(filters);
            searchRequest = searchRequest.withFilterExpression(filterExpression);
        }

        List<Document> results = vectorStore.similaritySearch(searchRequest);

        log.info("搜索完成,返回 {} 条结果", results.size());

        return results;
    }

    /**
     * 删除文档
     */
    @Transactional
    public void deleteDocuments(List<String> documentIds) {
        log.info("删除文档,IDs: {}", documentIds);
        vectorStore.delete(documentIds);
    }

    /**
     * 构建过滤表达式
     */
    private Filter.Expression buildFilterExpression(Map<String, Object> filters) {
        FilterExpressionBuilder builder = new FilterExpressionBuilder();

        for (Map.Entry<String, Object> entry : filters.entrySet()) {
            builder.eq(entry.getKey(), entry.getValue().toString());
        }

        return builder.build();
    }

    /**
     * 获取向量维度
     */
    public int getEmbeddingDimensions() {
        // 测试获取向量维度
        EmbeddingResponse response = embeddingModel.embedForResponse(List.of("test"));
        return response.getResult().getOutput().length;
    }
}

五、RAG 检索与生成

5.1 检索服务

// service/RetrievalService.java
package com.example.rag.service;

import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

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

/**
 * 检索服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class RetrievalService {

    private final VectorStore vectorStore;
    private final int topK = 4;
    private final double similarityThreshold = 0.7;

    /**
     * 检索相关文档
     */
    public RetrievalResult retrieve(String query) {
        log.info("执行检索,query: {}", query);

        long startTime = System.currentTimeMillis();

        // 1. 执行相似度搜索
        SearchRequest searchRequest = SearchRequest.query(query)
                .withTopK(topK);

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

        // 2. 过滤低相似度结果
        List<RetrievedDocument> filteredDocs = documents.stream()
                .map(doc -> {
                    double similarity = calculateSimilarity(doc, query);
                    return new RetrievedDocument(
                        doc.getId(),
                        doc.getContent(),
                        doc.getMetadata(),
                        similarity
                    );
                })
                .filter(doc -> doc.getSimilarity() >= similarityThreshold)
                .collect(Collectors.toList());

        long latency = System.currentTimeMillis() - startTime;

        log.info("检索完成,耗时:{}ms, 返回:{} 条结果", latency, filteredDocs.size());

        return new RetrievalResult(query, filteredDocs, latency);
    }

    /**
     * 计算相似度(简化版本)
     */
    private double calculateSimilarity(Document doc, String query) {
        // 实际应用中应该计算向量相似度
        // 这里简化处理
        return 0.85;
    }

    /**
     * 构建增强 Prompt
     */
    public String buildEnhancedPrompt(String query, List<RetrievedDocument> documents) {
        StringBuilder context = new StringBuilder();
        context.append("以下是相关的知识库内容:\n\n");

        for (int i = 0; i < documents.size(); i++) {
            RetrievedDocument doc = documents.get(i);
            context.append(String.format("[来源 %d]\n", i + 1));
            context.append(doc.getContent()).append("\n\n");

            if (doc.getMetadata().containsKey("filename")) {
                context.append(String.format("来源文件:%s\n\n", doc.getMetadata().get("filename")));
            }
        }

        context.append("请根据以上信息回答用户的问题:\n");
        context.append("用户问题:").append(query);

        return context.toString();
    }

    @Data
    @RequiredArgsConstructor
    public static class RetrievalResult {
        private final String query;
        private final List<RetrievedDocument> documents;
        private final long latencyMs;
    }

    @Data
    @RequiredArgsConstructor
    public static class RetrievedDocument {
        private final String id;
        private final String content;
        private final Map<String, Object> metadata;
        private final double similarity;
    }
}

5.2 RAG 聊天服务

// service/RagChatService.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.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Service;

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

/**
 * RAG 聊天服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class RagChatService {

    private final ChatClient ragChatClient;
    private final RetrievalService retrievalService;

    /**
     * 基于 RAG 的聊天
     */
    public ChatResponse chat(String query, String conversationId) {
        log.info("RAG 聊天请求,conversationId: {}, query: {}", conversationId, query);

        // 1. 检索相关文档
        RetrievalService.RetrievalResult retrievalResult = retrievalService.retrieve(query);

        // 2. 构建增强 Prompt
        String enhancedPrompt = buildRagPrompt(query, retrievalResult);

        // 3. 调用大模型生成回答
        String response = ragChatClient.prompt()
                .user(enhancedPrompt)
                .call()
                .content();

        log.info("RAG 聊天完成,response length: {}", response.length());

        return new ChatResponse(
            query,
            response,
            retrievalResult.getDocuments(),
            retrievalResult.getLatencyMs()
        );
    }

    /**
     * 带对话历史的 RAG 聊天
     */
    public ChatResponse chatWithHistory(String query, List<Message> history, String conversationId) {
        log.info("带历史的 RAG 聊天,conversationId: {}, history size: {}", conversationId, history.size());

        // 1. 检索相关文档
        RetrievalService.RetrievalResult retrievalResult = retrievalService.retrieve(query);

        // 2. 构建消息列表
        List<Message> messages = new ArrayList<>(history);

        // 3. 添加系统提示(包含检索到的上下文)
        String systemPrompt = buildSystemPrompt(retrievalResult);
        messages.add(0, new Message() {
            @Override
            public org.springframework.ai.chat.messages.MessageType getMessageType() {
                return org.springframework.ai.chat.messages.MessageType.SYSTEM;
            }

            @Override
            public String getContent() {
                return systemPrompt;
            }
        });

        // 4. 添加当前用户问题
        messages.add(new UserMessage(query));

        // 5. 调用大模型
        Prompt prompt = new Prompt(messages);
        String response = ragChatClient.prompt(prompt)
                .call()
                .content();

        return new ChatResponse(
            query,
            response,
            retrievalResult.getDocuments(),
            retrievalResult.getLatencyMs()
        );
    }

    /**
     * 构建 RAG Prompt
     */
    private String buildRagPrompt(String query, RetrievalService.RetrievalResult retrievalResult) {
        PromptTemplate template = new PromptTemplate("""
            你是一个智能助手,请基于以下知识库内容回答用户问题。

            【知识库内容】
            {context}

            【回答要求】
            1. 优先使用知识库内容回答问题
            2. 如果知识库内容不足以回答问题,请诚实告知
            3. 引用知识库内容时请注明来源
            4. 回答应简洁明了

            【用户问题】
            {question}

            【你的回答】
            """);

        Map<String, Object> params = new HashMap<>();
        params.put("question", query);
        params.put("context", buildContext(retrievalResult));

        return template.render(params);
    }

    /**
     * 构建系统 Prompt
     */
    private String buildSystemPrompt(RetrievalService.RetrievalResult retrievalResult) {
        return """
            你是一个智能助手,基于知识库内容回答用户问题。

            可用信息:
            """ + buildContext(retrievalResult);
    }

    /**
     * 构建上下文字符串
     */
    private String buildContext(RetrievalService.RetrievalResult retrievalResult) {
        StringBuilder context = new StringBuilder();

        for (int i = 0; i < retrievalResult.getDocuments().size(); i++) {
            RetrievalService.RetrievedDocument doc = retrievalResult.getDocuments().get(i);
            context.append(String.format("\n[文档 %d]\n", i + 1));
            context.append(doc.getContent());

            if (doc.getMetadata().containsKey("filename")) {
                context.append(String.format("\n来源:%s", doc.getMetadata().get("filename")));
            }
            context.append(String.format("\n相似度:%.2f", doc.getSimilarity()));
        }

        return context.toString();
    }

    /**
     * 聊天响应
     */
    public record ChatResponse(
        String question,
        String answer,
        List<RetrievalService.RetrievedDocument> sourceDocuments,
        long retrievalLatencyMs
    ) {}
}

5.3 REST API 控制器

// controller/RagController.java
package com.example.rag.controller;

import com.example.rag.service.RagChatService;
import com.example.rag.service.DocumentLoaderService;
import com.example.rag.service.TextProcessingService;
import com.example.rag.service.VectorStoreService;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

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

/**
 * RAG API 控制器
 */
@Slf4j
@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagController {

    private final RagChatService ragChatService;
    private final DocumentLoaderService documentLoaderService;
    private final TextProcessingService textProcessingService;
    private final VectorStoreService vectorStoreService;

    /**
     * 聊天接口
     */
    @PostMapping("/chat")
    public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) {
        log.info("收到聊天请求,query: {}", request.getQuery());

        RagChatService.ChatResponse response = ragChatService.chat(
            request.getQuery(), 
            request.getConversationId()
        );

        return ResponseEntity.ok(new ChatResponse(
            response.question(),
            response.answer(),
            response.sourceDocuments().stream()
                .map(doc -> new SourceDocument(
                    doc.getId(),
                    doc.getContent(),
                    doc.getMetadata(),
                    doc.getSimilarity()
                ))
                .toList(),
            response.retrievalLatencyMs()
        ));
    }

    /**
     * 上传文档
     */
    @PostMapping("/documents/upload")
    public ResponseEntity<Map<String, Object>> uploadDocument(
            @RequestParam("file") MultipartFile file) throws IOException {
        log.info("收到文档上传请求,filename: {}", file.getOriginalFilename());

        // 1. 加载文档
        List<Document> documents = documentLoaderService.loadDocument(file);

        // 2. 文本分块
        List<Document> chunks = textProcessingService.splitDocuments(documents);

        // 3. 向量化并存储
        vectorStoreService.addDocuments(chunks);

        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "文档处理完成");
        result.put("chunks", chunks.size());

        return ResponseEntity.ok(result);
    }

    /**
     * 批量导入文档
     */
    @PostMapping("/documents/import")
    public ResponseEntity<Map<String, Object>> importDocuments(
            @RequestParam("directory") String directory) {
        log.info("收到批量导入请求,directory: {}", directory);

        // 1. 加载文档
        List<Document> documents = documentLoaderService.loadDocumentsFromDirectory(directory);

        // 2. 文本分块
        List<Document> chunks = textProcessingService.splitDocuments(documents);

        // 3. 向量化并存储
        vectorStoreService.addDocuments(chunks);

        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "批量导入完成");
        result.put("documents", documents.size());
        result.put("chunks", chunks.size());

        return ResponseEntity.ok(result);
    }

    /**
     * 搜索接口
     */
    @GetMapping("/search")
    public ResponseEntity<SearchResponse> search(
            @RequestParam("query") String query,
            @RequestParam(value = "topK", defaultValue = "4") int topK) {
        log.info("收到搜索请求,query: {}", query);

        // 执行搜索(这里简化处理,实际应该调用检索服务)
        List<Document> results = vectorStoreService.search(query, topK);

        List<SearchResult> searchResults = results.stream()
            .map(doc -> new SearchResult(
                doc.getId(),
                doc.getContent(),
                doc.getMetadata()
            ))
            .toList();

        return ResponseEntity.ok(new SearchResponse(query, searchResults));
    }

    // ==================== DTO 类 ====================

    @Data
    public static class ChatRequest {
        @NotBlank(message = "问题不能为空")
        private String query;
        private String conversationId;
    }

    @Data
    public static class ChatResponse {
        private final String question;
        private final String answer;
        private final List<SourceDocument> sourceDocuments;
        private final long retrievalLatencyMs;
    }

    @Data
    public static class SourceDocument {
        private final String id;
        private final String content;
        private final Map<String, Object> metadata;
        private final double similarity;
    }

    @Data
    public static class SearchResponse {
        private final String query;
        private final List<SearchResult> results;
    }

    @Data
    public static class SearchResult {
        private final String id;
        private final String content;
        private final Map<String, Object> metadata;
    }
}

六、完整实战案例:企业知识库系统

6.1 项目结构

spring-ai-rag-demo/
├── src/main/java/com/example/rag/
│   ├── RagApplication.java           # 启动类
│   ├── config/
│   │   ├── RagConfig.java            # RAG 配置
│   │   ├── TextSplitterConfig.java   # 分块配置
│   │   └── RedisConfig.java          # Redis 配置
│   ├── controller/
│   │   ├── RagController.java        # RAG API
│   │   ├── DocumentController.java   # 文档管理 API
│   │   └── ConversationController.java # 对话管理 API
│   ├── service/
│   │   ├── RagChatService.java       # RAG 聊天服务
│   │   ├── RetrievalService.java     # 检索服务
│   │   ├── VectorStoreService.java   # 向量存储服务
│   │   ├── DocumentLoaderService.java # 文档加载服务
│   │   ├── TextProcessingService.java # 文本处理服务
│   │   └── ConversationService.java  # 对话历史服务
│   ├── entity/
│   │   ├── DocumentMetadata.java     # 文档元数据实体
│   │   └── Conversation.java         # 对话实体
│   ├── repository/
│   │   ├── DocumentMetadataRepository.java
│   │   └── ConversationRepository.java
│   ├── dto/
│   │   ├── request/
│   │   └── response/
│   └── util/
│       └── EmbeddingUtils.java       # 向量化辅助工具
├── src/main/resources/
│   ├── application.yml
│   ├── application-dev.yml
│   └── application-prod.yml
├── src/test/java/
├── docker/
│   ├── docker-compose.yml
│   └── pgvector/
│       └── init.sql
├── uploads/                          # 文档上传目录
├── pom.xml
└── README.md

6.2 Docker Compose 配置

# docker/docker-compose.yml
version: '3.8'

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

  # Redis
  redis:
    image: redis:7-alpine
    container_name: rag-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Spring Boot 应用
  app:
    build: ..
    container_name: rag-app
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: docker
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/rag_db
      SPRING_DATASOURCE_USERNAME: postgres
      SPRING_DATASOURCE_PASSWORD: postgres123
      SPRING_DATA_REDIS_HOST: redis
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - ./uploads:/app/uploads

volumes:
  postgres_data:
  redis_data:

6.3 对话历史管理

// service/ConversationService.java
package com.example.rag.service;

import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

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

/**
 * 对话历史服务(使用 Redis 存储)
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ConversationService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final long ttlHours = 24; // 对话历史保留 24 小时

    /**
     * 添加对话消息
     */
    public void addMessage(String conversationId, Message message) {
        String key = buildKey(conversationId);

        List<Message> messages = getHistory(conversationId);
        messages.add(message);

        // 限制历史消息数量
        if (messages.size() > 20) {
            messages = messages.subList(messages.size() - 20, messages.size());
        }

        redisTemplate.opsForList().setRange(key, 0, messages);
        redisTemplate.expire(key, ttlHours, TimeUnit.HOURS);

        log.debug("添加对话消息,conversationId: {}, total: {}", conversationId, messages.size());
    }

    /**
     * 获取对话历史
     */
    public List<Message> getHistory(String conversationId) {
        String key = buildKey(conversationId);

        List<Object> messages = redisTemplate.opsForList().range(key, 0, -1);

        if (messages == null || messages.isEmpty()) {
            return new ArrayList<>();
        }

        return messages.stream()
            .map(msg -> (Message) msg)
            .toList();
    }

    /**
     * 清除对话历史
     */
    public void clearHistory(String conversationId) {
        String key = buildKey(conversationId);
        redisTemplate.delete(key);
        log.info("清除对话历史,conversationId: {}", conversationId);
    }

    /**
     * 构建用户对话消息
     */
    public Message createUserMessage(String content) {
        return new UserMessage(content);
    }

    /**
     * 构建助手对话消息
     */
    public Message createAssistantMessage(String content) {
        return new AssistantMessage(content);
    }

    private String buildKey(String conversationId) {
        return "conversation:" + conversationId;
    }

    @Data
    public static class ConversationMessage {
        private String role;
        private String content;
        private long timestamp;
    }
}

6.4 文档管理 API

// controller/DocumentController.java
package com.example.rag.controller;

import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

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

/**
 * 文档管理控制器
 */
@Slf4j
@RestController
@RequestMapping("/api/documents")
@RequiredArgsConstructor
public class DocumentController {

    private final VectorStore vectorStore;

    /**
     * 上传单个文档
     */
    @PostMapping("/upload")
    public ResponseEntity<Map<String, Object>> uploadDocument(
            @RequestParam("file") MultipartFile file) {
        log.info("上传文档:{}", file.getOriginalFilename());

        // 处理文档上传逻辑
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("filename", file.getOriginalFilename());
        result.put("size", file.getSize());

        return ResponseEntity.ok(result);
    }

    /**
     * 删除文档
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Map<String, Object>> deleteDocument(
            @PathVariable String id) {
        log.info("删除文档:{}", id);

        vectorStore.delete(List.of(id));

        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "文档已删除");

        return ResponseEntity.ok(result);
    }

    /**
     * 列出所有文档
     */
    @GetMapping("/list")
    public ResponseEntity<List<DocumentInfo>> listDocuments() {
        // 实现文档列表查询
        return ResponseEntity.ok(List.of());
    }

    @Data
    public static class DocumentInfo {
        private String id;
        private String filename;
        private long size;
        private String contentType;
        private long createdAt;
    }
}

七、性能优化与最佳实践

7.1 检索性能优化

// config/RetrievalOptimizationConfig.java
package com.example.rag.config;

import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 检索优化配置
 */
@Configuration
public class RetrievalOptimizationConfig {

    /**
     * 优化策略 1:混合搜索(向量 + 关键词)
     */
    public SearchRequest buildHybridSearch(String query, int topK) {
        return SearchRequest.query(query)
                .withTopK(topK)
                .withSimilarityThreshold(0.7);
    }

    /**
     * 优化策略 2:元数据过滤
     */
    public SearchRequest buildFilteredSearch(String query, String docType) {
        FilterExpressionBuilder builder = new FilterExpressionBuilder();
        return SearchRequest.query(query)
                .withTopK(10)
                .withFilterExpression(builder.eq("doc_type", docType).build());
    }

    /**
     * 优化策略 3:多路召回
     */
    // 使用不同的检索策略,然后合并结果
}

7.2 缓存优化

// service/CachedRetrievalService.java
package com.example.rag.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 带缓存的检索服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CachedRetrievalService {

    private final RetrievalService retrievalService;
    private final RedisTemplate<String, Object> redisTemplate;
    private final long cacheTtlMinutes = 30;

    /**
     * 带缓存的检索
     */
    public RetrievalService.RetrievalResult retrieveWithCache(String query) {
        String cacheKey = "retrieval:" + hashQuery(query);

        // 1. 尝试从缓存获取
        Object cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            log.info("缓存命中,query: {}", query);
            return (RetrievalService.RetrievalResult) cached;
        }

        // 2. 执行检索
        RetrievalService.RetrievalResult result = retrievalService.retrieve(query);

        // 3. 存入缓存
        redisTemplate.opsForValue().set(cacheKey, result, cacheTtlMinutes, TimeUnit.MINUTES);

        return result;
    }

    private String hashQuery(String query) {
        return Integer.toHexString(query.hashCode());
    }
}

7.3 监控与日志

// aspect/RetrievalMetricsAspect.java
package com.example.rag.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * 检索指标监控
 */
@Slf4j
@Aspect
@Component
public class RetrievalMetricsAspect {

    @Around("execution(* com.example.rag.service.RetrievalService.retrieve(..))")
    public Object measureRetrieval(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        try {
            Object result = joinPoint.proceed();
            long latency = System.currentTimeMillis() - startTime;

            log.info("检索指标 - 耗时:{}ms, 参数:{}", latency, joinPoint.getArgs()[0]);

            // 可以发送到监控系统(Prometheus、Micrometer 等)

            return result;
        } catch (Exception e) {
            log.error("检索异常", e);
            throw e;
        }
    }
}

7.4 最佳实践清单

┌─────────────────────────────────────────────────────────────────┐
│                         RAG 系统最佳实践                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  文档处理:                                                       │
│  □ 选择合适的分块大小(500-1000 tokens)                         │
│  □ 设置合理的重叠(10-20%)                                      │
│  □ 保留文档元数据(来源、时间、类型)                             │
│  □ 异步处理大文档上传                                             │
│                                                                   │
│  向量化:                                                         │
│  □ 选择适合中文的 Embedding 模型                                  │
│  □ 统一向量维度(1536 或 768)                                    │
│  □ 批量处理提高吞吐量                                             │
│                                                                   │
│  检索优化:                                                       │
│  □ 设置合理的 Top-K(3-5)                                         │
│  □ 使用相似度阈值过滤                                             │
│  □ 实现检索结果缓存                                               │
│  □ 考虑混合搜索(向量 + 关键词)                                   │
│                                                                   │
│  Prompt 工程:                                                    │
│  □ 清晰的系统提示                                                 │
│  □ 明确的回答要求                                                 │
│  □ 包含来源引用                                                     │
│  □ 处理知识不足的情况                                             │
│                                                                   │
│  性能优化:                                                       │
│  □ 向量数据库索引(HNSW)                                         │
│  □ Redis 缓存检索结果                                              │
│  □ 异步文档处理                                                     │
│  □ 连接池优化                                                       │
│                                                                   │
│  安全与监控:                                                     │
│  □ API 认证与授权                                                  │
│  □ 输入验证与过滤                                                   │
│  □ 检索日志记录                                                     │
│  □ 性能指标监控                                                     │
│  □ 错误告警机制                                                     │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

八、常见问题与解决方案

Q1: 检索结果不相关怎么办?

解决方案:

  1. 调整 Embedding 模型(尝试不同的模型)  
  2. 优化文本分块策略(调整 chunk size)  
  3. 增加元数据过滤  
  4. 使用混合搜索(向量 + 关键词)  
  5. 调整相似度阈值  

Q2: 回答质量不高?

解决方案:

  1. 优化 Prompt 模板  
  2. 增加检索文档数量(Top-K)  
  3. 改进文档质量  
  4. 调整温度参数(temperature)  
  5. 添加 Few-Shot 示例  

Q3: 响应速度慢?

解决方案:

  1. 启用检索结果缓存  
  2. 优化向量数据库索引  
  3. 减少 Top-K 数量  
  4. 使用更快的 Embedding 模型  
  5. 异步处理文档入库  

Q4: 如何处理多轮对话?

解决方案:

// 使用对话历史增强检索
public String buildQueryWithContext(String currentQuery, List<Message> history) {
    // 从历史中提取关键信息
    // 重写查询以包含上下文
    return enhancedQuery;
}

九、总结

9.1 核心要点回顾

  1. RAG 架构:检索 + 增强 + 生成,解决 LLM 局限性  
  2. Spring AI:统一的 API 抽象,简化 AI 应用开发  
  3. 向量数据库:pgvector 适合中小规模,Milvus 适合大规模  
  4. 文档处理:加载 → 分块 → 向量化 → 存储  
  5. 检索优化:缓存、索引、混合搜索  
  6. 生产实践:监控、安全、性能优化  

9.2 后续扩展方向

  • 📌 多模态 RAG(支持图片、表格)  
  • 📌 Graph RAG(知识图谱增强)  
  • 📌 Agentic RAG(Agent 自主检索)  
  • 📌 流式 RAG(实时数据检索)



上一篇:Kafka Producer 故障排查指南:消息丢失/重复/TPS低/卡死?一张图搞定
下一篇:蔚来2026年战略解析:大五座SUV成新增长点,直面存储涨价压力
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-18 16:55 , Processed in 0.502133 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表