版本说明:基于 Spring AI Alibaba 最新稳定版本(0.8.x),兼容 Spring Boot 3.x,使用 PostgreSQL 16 + pgvector 扩展实现企业级向量检索。
开源镜像地址:rag-pgvector-example


一、概述

使用 PostgreSQL + pgvector 作为向量数据库,实现了文档导入、存储、检索和智能问答的完整流程。

📦 项目结构

rag-pgvector-example/
├── pom.xml                          # Maven 依赖配置
├── README.md                        # 项目说明文档
├── docker-compose.yml               # 数据库快速部署
├── init.sql                         # 数据库初始化脚本
├── src/main/resources/
│   ├── application.yml              # 应用配置(含环境变量)
│   ├── prompts/system-qa.st         # 系统提示词模板
│   └── data/spring_ai_alibaba_quickstart.pdf  # 示例文档
└── src/main/java/.../
    ├── controller/
    │   └── RagPgVectorController.java   # 核心控制器(6个API端点)
    ├── config/
    │   └── VectorStoreConfig.java       # 向量存储配置
    └── service/
        └── DocumentService.java         # 文档处理服务

二、详细解析

🔄 完整工作流程

用户上传文档

文档解析
Tika/PDF Reader

文本分块
TokenTextSplitter

向量化
DashScope Embedding
text-embedding-v2

存储到 Pgvector
1536维向量

用户提问

问题向量化

相似度检索
HNSW索引加速

Top-K 候选片段

Rerank重排序
text-reranker-v1

过滤低相似度
阈值0.1

构建上下文
System Prompt

生成回答
Qwen大模型
流式输出

步骤详解

步骤 技术组件 作用说明
1. 文档解析 TikaDocumentReader / PagePdfDocumentReader 读取 PDF、Word、Excel、PPT、TXT 等多种格式
2. 文本分块 TokenTextSplitter 将长文档切成 500 tokens 的小块,重叠 50 tokens 保持上下文连贯
3. 向量化 DashScope Embedding API (text-embedding-v2) 将文本转为 1536 维稠密向量
4. 存储 PostgreSQL + pgvector 存入 vector_store 表,使用 HNSW 索引加速检索
5. 检索 HNSW 近似最近邻搜索 用户提问 → 向量化 → 相似度搜索 → 取 top-K 相关片段
6. 重排序 Rerank 模型 (text-reranker-v1) 对检索结果二次精排,提升相关性
7. 生成 Qwen 大模型 (qwen-turbo/max) 将相关片段作为上下文,生成最终回答

三、依赖分析(pom.xml)

✅ 核心依赖清单

依赖 作用 版本说明
spring-boot-starter-web Web 服务 提供 REST API 基础能力
spring-ai-alibaba-starter-dashscope 阿里云 AI 服务 调用 Qwen 对话模型、Embedding 模型、Rerank 模型
spring-ai-pdf-document-reader PDF 解析 基于 Apache PDFBox 读取 PDF 文件内容
spring-ai-tika-document-reader 通用文档解析 基于 Apache Tika,支持 1000+ 种文件格式
spring-ai-pgvector-store 向量存储 集成 pgvector 扩展,提供 VectorStore 接口实现
postgresql PostgreSQL 驱动 JDBC 连接驱动

⚠️ 依赖配置注意事项

关于 Rerank 依赖

Spring AI Alibaba 的 Rerank 功能已集成在 spring-ai-alibaba-starter-dashscope 中,无需额外引入 spring-ai-rerank-store 依赖。但需要在配置中显式指定 Rerank 模型:

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    <version>0.8.1</version>
</dependency>

父 POM 版本管理

建议使用 Spring AI Alibaba BOM 统一管理版本,避免版本冲突:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-bom</artifactId>
            <version>0.8.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

四、配置文件详解(application.yml)

📋 生产级配置模板

spring:
  application:
    name: rag-pgvector-example

  # 数据库配置(使用环境变量,避免硬编码)
  datasource:
    url: jdbc:postgresql://${DB_HOST:127.0.0.1}:${DB_PORT:5432}/${DB_NAME:rag_db}
    username: ${DB_USERNAME:postgres}
    password: ${DB_PASSWORD:}
    driver-class-name: org.postgresql.Driver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

  jpa:
    hibernate:
      ddl-auto: none  # 关闭自动建表,使用手动 SQL 初始化
    show-sql: false

  # Spring AI 配置
  ai:
    dashscope:
      api-key: ${DASH_SCOPE_API_KEY}  # 必须:阿里云百炼 API Key
      chat:
        options:
          model: qwen-turbo           # 对话模型:qwen-turbo / qwen-max / qwen-plus
          temperature: 0.7            # 温度参数:0-1,越低越确定
          top-p: 0.8                  # 核采样参数
      embedding:
        options:
          model: text-embedding-v2    # Embedding 模型:固定 1536 维 
      rerank:
        options:
          model: text-reranker-v1     # Rerank 模型:精排优化

    vectorstore:
      pgvector:
        dimensions: 1536                # 必须:与 text-embedding-v2 匹配 
        index-type: hnsw              # 索引类型:HNSW(高性能近似搜索)
        distance-type: cosine_distance # 距离度量:余弦相似度(适合语义搜索)
        table-name: vector_store      # 向量表名
        initialize-schema: false      # 手动初始化表结构(生产环境推荐)

  # 文件上传配置
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 100MB
      enabled: true

# 日志配置
logging:
  level:
    org.springframework.ai: DEBUG
    com.alibaba.cloud.ai: DEBUG

🔧 关键配置项

1. 向量维度(dimensions)

  • text-embedding-v2 固定输出 1536 维 向量
  • text-embedding-v3 默认 1024 维,支持 768/512 可选
  • text-embedding-v4 默认 1024 维(最新推荐模型)
  • 必须确保 dimensions 配置与所选模型输出维度完全一致,否则会报错 “Vector dimension mismatch”

2. HNSW 索引参数

-- 建表时指定 HNSW 索引参数
CREATE INDEX vector_store_embedding_idx 
    ON vector_store USING HNSW (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);
参数 默认值 作用 调优建议
m 16 每层最大连接数 数据量 < 10万:16;10-100万:32;>100万:64
ef_construction 64 构建时候选列表大小 精度要求高:128-256;构建速度快:40-64

3. 距离度量方式

  • cosine_distance:余弦相似度(推荐,适合语义相似度)
  • l2_distance:欧氏距离(适合空间向量)
  • inner_product:内积(适合归一化向量)

五、数据库部署

🐳 方案 A:Docker Compose 快速部署(推荐)

创建 docker-compose.yml

version: '3.8'

services:
  postgres:
    image: pgvector/pgvector:pg16  # 官方镜像,已集成 pgvector 0.7.0+
    container_name: rag-postgres
    environment:
      POSTGRES_USER: ${DB_USERNAME:-postgres}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-mysecretpassword}
      POSTGRES_DB: ${DB_NAME:-rag_db}
    ports:
      - "${DB_PORT:-5432}:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro  # 自动初始化
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_NAME:-rag_db}"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped

volumes:
  postgres_data:
    driver: local

创建 init.sql(数据库初始化脚本):

-- 启用必要扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "vector";

-- 创建向量存储表(与 Spring AI PgVectorStore 兼容)
CREATE TABLE IF NOT EXISTS vector_store (
    id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
    content text NOT NULL,                    -- 原始文本内容
    metadata jsonb DEFAULT '{}',              -- 元数据(fileId、文件名等)
    embedding vector(1536) NOT NULL           -- 向量数据,维度与模型匹配
);

-- 创建 HNSW 索引(关键!大幅提升检索速度)
-- m=16, ef_construction=64 是平衡点,适合大多数场景
CREATE INDEX IF NOT EXISTS vector_store_embedding_idx 
    ON vector_store USING HNSW (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

-- 可选:创建 GIN 索引加速 metadata 过滤查询
CREATE INDEX IF NOT EXISTS vector_store_metadata_idx 
    ON vector_store USING GIN (metadata);

-- 可选:创建全文检索索引(混合检索场景)
CREATE INDEX IF NOT EXISTS vector_store_content_idx 
    ON vector_store USING GIN (to_tsvector('chinese', content));

-- 表注释
COMMENT ON TABLE vector_store IS 'RAG 向量存储表 - Spring AI Alibaba PgVectorStore';
COMMENT ON COLUMN vector_store.embedding IS '1536维向量,使用 text-embedding-v2 模型生成';

启动命令:

# 1. 设置环境变量(或创建 .env 文件)
export DB_PASSWORD="your_secure_password"
export DASH_SCOPE_API_KEY="sk-your-api-key"

# 2. 启动服务
docker-compose up -d

# 3. 查看日志
docker-compose logs -f postgres

# 4. 验证数据库
docker exec -it rag-postgres psql -U postgres -d rag_db -c "\dt"
docker exec -it rag-postgres psql -U postgres -d rag_db -c "SELECT * FROM pg_extension WHERE extname = 'vector';"

🖥️ 方案 B:本地 PostgreSQL 安装

Ubuntu24

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

# 安装 pgvector 扩展
sudo apt install postgresql-16-pgvector

# 启动服务
sudo systemctl enable postgresql
sudo systemctl start postgresql

# 执行初始化 SQL
sudo -u postgres psql -f init.sql

macOS (Homebrew)

# 安装
brew install postgresql@16
brew install pgvector

# 启动
brew services start postgresql@16

# 执行初始化 SQL
psql -U postgres -f init.sql

验证 pgvector 安装

-- 检查扩展版本
SELECT * FROM pg_extension WHERE extname = 'vector';

-- 测试向量操作
SELECT '[1,2,3]'::vector <-> '[4,5,6]'::vector AS l2_distance;

六、核心代码解析

📝 RagPgVectorController.java

1️⃣ 依赖注入(构造函数注入)
@RestController
@RequestMapping("/ai/rag")
public class RagPgVectorController {
    
    private final VectorStore vectorStore;     // 向量存储接口(PgVectorStore 实现)
    private final ChatModel chatModel;         // 对话模型(DashScopeChatModel)
    private final RerankModel rerankModel;     // 重排序模型(DashScopeRerankModel)
    private final Resource systemResource;     // 系统提示词模板资源

    // Spring 自动注入(推荐构造函数注入,便于单元测试)
    public RagPgVectorController(
            VectorStore vectorStore,
            ChatModel chatModel,
            RerankModel rerankModel,
            @Value("classpath:prompts/system-qa.st") Resource systemResource) {
        this.vectorStore = vectorStore;
        this.chatModel = chatModel;
        this.rerankModel = rerankModel;
        this.systemResource = systemResource;
    }
}

设计说明

  • VectorStore 是 Spring AI 的抽象接口,底层由 PgVectorStore 实现
  • 使用构造函数注入而非 @Autowired 字段注入,符合 Spring 最佳实践
  • Resource 自动从 classpath 加载提示词模板文件
2️⃣ 文档导入接口(基础版)
@GetMapping("/importDocument")
public ResponseEntity<String> importDocument() {
    try {
        // 1. 读取内置 PDF 资源(classpath 下的示例文档)
        DocumentReader reader = new PagePdfDocumentReader(springAiResource);
        List<Document> documents = reader.get();
        
        log.info("成功读取 PDF,共 {} 页", documents.size());
        
        // 2. 文本分块(关键步骤!)
        // 默认参数:chunkSize=500 tokens, overlap=50 tokens
        // 分块策略:按 Token 边界切分,保持语义连贯性
        List<Document> splitDocuments = new TokenTextSplitter().apply(documents);
        
        log.info("分块完成,生成 {} 个文本片段", splitDocuments.size());
        
        // 3. 向量化并存储(自动调用 DashScope Embedding API)
        // 每个片段会被转换为 1536 维向量,存入 PostgreSQL
        vectorStore.add(splitDocuments);
        
        return ResponseEntity.ok(
            String.format("成功导入 %d 个文本片段", splitDocuments.size())
        );
    } catch (Exception e) {
        log.error("文档导入失败", e);
        return ResponseEntity.internalServerError()
            .body("导入失败: " + e.getMessage());
    }
}

💡 分块策略详解

TokenTextSplitter 的默认参数经过大量实践验证:

  • chunkSize=500:平衡上下文完整性和检索精度。太大(>1000)会降低检索精度,太小(<200)会丢失上下文。
  • overlap=50:相邻块重叠 50 tokens,确保跨边界语义不中断。例如:“今天天气很好,适合出门。我想去公园散步。” 如果在"适合出门"处切分,重叠确保下一块包含"适合出门",保持连贯性。

自定义分块参数

// 适合长文档场景(如技术手册)
TokenTextSplitter splitter = new TokenTextSplitter(800, 100);
List<Document> splitDocs = splitter.apply(documents);
3️⃣ 文本导入接口(直接文本)
@GetMapping("/importText")
public ResponseEntity<String> insertText(@RequestParam("text") String text) {
    // 1. 参数校验(Spring 的 StringUtils 工具)
    if (!StringUtils.hasText(text)) {
        return ResponseEntity.badRequest().body("请输入非空文本");
    }
    
    // 2. 包装为 Document 对象(Spring AI 的标准文档格式)
    // Document 包含 content(文本)和 metadata(元数据)两部分
    Document document = new Document(text);
    
    // 3. 分块(即使是短文本也走统一流程,保持处理一致性)
    List<Document> splitDocuments = new TokenTextSplitter().apply(List.of(document));
    
    // 4. 存储(自动向量化)
    vectorStore.add(splitDocuments);
    
    return ResponseEntity.ok(
        String.format("成功插入 %d 个文本片段", splitDocuments.size())
    );
}
4️⃣ 文件上传接口(多文件支持)
@PostMapping(value = "/importFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> insertFiles(@RequestPart MultipartFile file) {
    // 1. 文件基础校验
    if (file == null || file.isEmpty()) {
        return ResponseEntity.badRequest().body("必须上传非空文件");
    }
    
    // 2. 使用 Tika 解析(支持 PDF、Word、Excel、PPT、TXT、HTML 等 1000+ 格式)
    // TikaDocumentReader 自动检测文件类型并提取文本
    List<Document> docs = new TikaDocumentReader(file.getResource()).get();
    
    log.info("文件解析成功:{},提取 {} 个文档块", file.getOriginalFilename(), docs.size());
    
    // 3. 分块处理
    List<Document> splitDocs = new TokenTextSplitter().apply(docs);
    
    // 4. 存储到向量库
    vectorStore.add(splitDocs);
    
    return ResponseEntity.ok(
        String.format("文件 %s 成功插入 %d 个文本片段", 
            file.getOriginalFilename(), splitDocs.size())
    );
}

⚠️ 大文件上传配置

如果上传文件超过 1MB 报错 413 Payload Too Large,在 application.yml 中添加:

spring:
  servlet:
    multipart:
      max-file-size: 50MB      # 单个文件最大 50MB
      max-request-size: 100MB  # 整个请求最大 100MB
      file-size-threshold: 0   # 超过此大小写入临时文件
5️⃣ 核心 RAG 问答接口(流式输出)
@GetMapping(value = "/rag", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatResponse> generate(@RequestParam String message) throws IOException {
    // 1. 构建检索请求(取前 2 个最相关片段)
    // topK 越小,上下文越聚焦;越大,信息越全面但可能引入噪声
    SearchRequest searchRequest = SearchRequest.builder()
        .topK(2)
        .similarityThreshold(0.1)  // 相似度阈值,低于此值的片段会被过滤
        .build();
    
    // 2. 读取系统提示词模板(从 classpath:prompts/system-qa.st 加载)
    String promptTemplate = systemResource.getContentAsString(StandardCharsets.UTF_8);
    
    // 3. 构建 ChatClient 并配置 RAG 增强(RetrievalRerankAdvisor)
    return ChatClient.builder(chatModel)
        .defaultAdvisors(
            new RetrievalRerankAdvisor(
                vectorStore,                    // 向量存储源
                rerankModel,                    // 重排序模型(精排)
                searchRequest,                  // 检索参数(topK=2)
                new SystemPromptTemplate(promptTemplate),  // 系统提示词模板
                0.1                             // 相似度阈值(与 SearchRequest 一致)
            )
        )
        .build()
        .prompt()
        .user(message)              // 用户问题
        .stream()                   // 流式输出(SSE 格式)
        .chatResponse();            // 返回 ChatResponse 流
}

🧠 RetrievalRerankAdvisor 工作流程详解

Qwen大模型 RerankModel VectorStore RetrievalRerankAdvisor 用户 Qwen大模型 RerankModel VectorStore RetrievalRerankAdvisor 用户 1. 提交问题 "什么是 Spring AI?" 2. 问题向量化(调用 Embedding API) 3. HNSW 相似度搜索(topK=2) 4. 返回候选片段(可能包含噪声) 5. 精排请求(问题 + 候选片段) 6. 返回重排序分数 7. 过滤低相似度片段(<0.1) 8. 构建 Prompt(System + Context + Question) 9. 发送完整 Prompt 10. 流式返回生成结果

系统提示词模板prompts/system-qa.st):

你是一位专业的技术文档助手,请严格根据以下上下文回答问题。

【回答规则】
1. 仅基于提供的上下文回答,不要引入外部知识
2. 如果上下文中没有相关信息,请明确回答"根据现有资料,我无法回答这个问题"
3. 保持回答简洁、准确、有条理
4. 技术术语请保持准确,必要时可引用原文

【上下文】
{documents}

【用户问题】
{question}

请用中文回答:
6️⃣ 带文件 ID 的版本管理接口(V2)

为什么需要 fileId?

在实际业务中,文档会频繁更新。通过 fileId 可以实现:

  • 版本隔离:不同版本的文档互不干扰
  • 精准检索:只搜索特定文件的内容
  • 精确删除:删除文件时清除所有关联向量

导入文件并标记

@PostMapping("/importFileV2")
public ResponseEntity<String> importFileV2(@RequestPart MultipartFile file) {
    // 1-3. 解析 + 分块(同基础版)
    List<Document> docs = new TikaDocumentReader(file.getResource()).get();
    List<Document> splitDocs = new TokenTextSplitter().apply(docs);
    
    // 4. ⭐ 关键:为每个片段添加 fileId 元数据
    // UUID 作为文件唯一标识,同一文件的所有片段共享同一 fileId
    String fileId = UUID.randomUUID().toString();
    for (Document doc : splitDocs) {
        doc.getMetadata().put("fileId", fileId);
        doc.getMetadata().put("fileName", file.getOriginalFilename());
        doc.getMetadata().put("uploadTime", Instant.now().toString());
    }
    
    // 5. 存储(metadata 会存入 PostgreSQL 的 jsonb 字段)
    vectorStore.add(splitDocs);
    
    return ResponseEntity.ok(
        String.format("成功插入 %d 个片段,fileId: %s", splitDocs.size(), fileId)
    );
}

按 fileId 精准检索

@GetMapping(value = "/searchV2", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> search(@RequestParam String message, 
                           @RequestParam String fileId) {
    // 1. 构建过滤表达式:只搜索指定 fileId 的片段
    // FilterExpressionBuilder 提供类型安全的过滤条件构建
    FilterExpressionBuilder builder = new FilterExpressionBuilder();
    Filter.Expression expression = builder.eq("fileId", fileId).build();
    
    // 2. 检索请求(带过滤条件)
    SearchRequest searchRequest = SearchRequest.builder()
        .topK(1)                    // 只取最相关的一个片段
        .filterExpression(expression) // 关键:添加 fileId 过滤
        .similarityThreshold(0.1)
        .build();
    
    // 3. 执行 RAG 流程(同基础版,但检索范围受限)
    return ChatClient.builder(chatModel)
        .defaultAdvisors(new RetrievalRerankAdvisor(
            vectorStore, 
            rerankModel, 
            searchRequest,
            new SystemPromptTemplate(systemResource.getContentAsString(StandardCharsets.UTF_8)),
            0.1
        ))
        .build()
        .prompt()
        .user(message)
        .stream()
        .content();  // 只返回回答内容字符串,不包含完整 ChatResponse 元数据
}

删除指定文件

@PostMapping("/deleteFilesV2")
public ResponseEntity<String> deleteFiles(@RequestParam String fileId) {
    // 1. 构建删除过滤条件
    FilterExpressionBuilder builder = new FilterExpressionBuilder();
    Filter.Expression expression = builder.eq("fileId", fileId).build();
    
    // 2. 删除所有匹配的向量片段
    // 底层执行:DELETE FROM vector_store WHERE metadata->>'fileId' = ?
    vectorStore.delete(expression);
    
    return ResponseEntity.ok(
        String.format("成功删除 fileId=%s 的所有向量片段", fileId)
    );
}

七、完整部署实操指南

🚀 步骤 1:环境准备

# 1. 检查 JDK 版本(需要 JDK 17+)
java -version

# 2. 克隆项目
git clone https://github.com/spring-ai-alibaba/spring-ai-alibaba-examples.git
cd spring-ai-alibaba-examples/spring-ai-alibaba-rag-example/rag-pgvector-example

# 3. 准备环境变量(建议写入 ~/.bashrc 或 ~/.zshrc)
export DASH_SCOPE_API_KEY="sk-your-actual-api-key"
export DB_PASSWORD="your-secure-password"
export DB_HOST="127.0.0.1"
export DB_PORT="5432"
export DB_NAME="rag_db"

# 验证环境变量
echo $DASH_SCOPE_API_KEY

🐳 步骤 2:启动 PostgreSQL + Pgvector

# 使用 Docker Compose(推荐)
docker-compose up -d

# 等待数据库就绪(约 10-15 秒)
sleep 10

# 验证数据库连接
docker exec -it rag-postgres psql -U postgres -d rag_db -c "SELECT version();"

# 验证 pgvector 扩展
docker exec -it rag-postgres psql -U postgres -d rag_db -c "SELECT * FROM pg_extension WHERE extname = 'vector';"

🔧 步骤 3:编译并启动应用

# 1. 清理并编译(跳过测试加速)
mvn clean package -DskipTests

# 2. 启动应用
mvn spring-boot:run

# 或使用 jar 包启动(生产环境推荐)
java -jar target/rag-pgvector-example-*.jar

# 查看启动日志,看到 "Started RagExampleApplication" 表示成功

✅ 步骤 4:验证部署

# 1. 测试健康检查(如果有 actuator)
curl http://localhost:8080/actuator/health

# 2. 测试文档导入(读取内置 PDF)
curl -X GET http://localhost:8080/ai/rag/importDocument

# 3. 测试文本导入
curl -G 'http://localhost:8080/ai/rag/importText' \
  --data-urlencode 'text=Spring AI Alibaba 是一个基于 Spring Boot 的 AI 应用框架,简化大模型应用开发。'

# 4. 测试文件上传(V2 版本,返回 fileId)
curl -X POST http://localhost:8080/ai/rag/importFileV2 \
  -F "file=@/path/to/your/document.pdf"

# 5. 测试基础 RAG 问答(流式输出)
curl -N 'http://localhost:8080/ai/rag?message=什么是 Spring AI Alibaba?'

# 6. 测试带 fileId 的精准检索(使用上一步返回的 fileId)
curl -N 'http://localhost:8080/ai/rag/searchV2?message=文档的核心内容是什么&fileId=xxxxx-xxxxx-xxxxx'

八、常见问题排查

❌ 问题 1:数据库连接失败

症状

Cannot get JDBC Connection: Connection refused

排查步骤

# 1. 检查 PostgreSQL 容器是否运行
docker ps | grep postgres

# 2. 检查端口占用
netstat -tlnp | grep 5432  # Linux
lsof -i :5432              # macOS

# 3. 测试连接
psql -h 127.0.0.1 -U postgres -d rag_db

# 4. 查看数据库日志
docker-compose logs postgres

修复

# 确保数据库已创建
docker exec -it rag-postgres psql -U postgres -c "CREATE DATABASE rag_db;"

# 检查防火墙设置(云服务器需要开放 5432 端口)

❌ 问题 2:pgvector 扩展未安装

症状

ERROR: extension "vector" does not exist

修复

-- 进入数据库执行
CREATE EXTENSION IF NOT EXISTS "vector";

-- 验证安装
SELECT * FROM pg_extension WHERE extname = 'vector';
-- 应显示 vector | 0.7.0 | ...

❌ 问题 3:向量维度不匹配

症状

Vector dimension mismatch: expected 1536, got 1024

原因:Embedding 模型输出维度与 application.yml 配置不一致。

修复

# 确认模型与维度对应关系
# text-embedding-v1: 1536
# text-embedding-v2: 1536(本指南使用)
# text-embedding-v3: 1024(默认)或 768/512
# text-embedding-v4: 1024(默认)或 2048/1536/768/512/256/128/64 

spring:
  ai:
    vectorstore:
      pgvector:
        dimensions: 1536  # 必须与使用的模型匹配

❌ 问题 4:HNSW 索引未生效(检索慢)

症状:查询速度慢,EXPLAIN 显示 Seq Scan 而非 Index Scan

诊断

-- 检查索引是否存在
SELECT indexname, indexdef 
FROM pg_indexes 
WHERE tablename = 'vector_store';

-- 查看执行计划
EXPLAIN ANALYZE 
SELECT * FROM vector_store 
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector 
LIMIT 5;

修复

-- 重建 HNSW 索引
DROP INDEX IF EXISTS vector_store_embedding_idx;
CREATE INDEX vector_store_embedding_idx 
    ON vector_store USING HNSW (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

-- 更新统计信息
ANALYZE vector_store;

-- 设置查询参数(提高召回率)
SET hnsw.ef_search = 100;  -- 默认 40,增大可提高精度 

❌ 问题 5:API Key 无效或额度不足

症状

DashScope API error: 401 Unauthorized 或 Quota exceeded

修复

# 1. 检查 API Key 格式(应以 sk- 开头)
echo $DASH_SCOPE_API_KEY

# 2. 在阿里云百炼控制台检查:
# - API Key 状态是否正常
# - 账户余额或免费额度是否充足
# - 模型调用权限是否开通(text-embedding-v2、qwen-turbo 等)

# 3. 测试 API Key
curl https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding \
  -H "Authorization: Bearer $DASH_SCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "text-embedding-v2", "input": {"texts": ["测试"]}}'

九、替代方案对比

🔄 方案 A:更换向量数据库

方案 优点 缺点 适用场景
Pgvector 与 PostgreSQL 集成、ACID 事务、成熟稳定、无需额外运维 单节点性能瓶颈,>100万向量时检索延迟增加 中小规模(<100万向量)、已有 PostgreSQL 基础设施
Milvus 超大规模(十亿级向量)、分布式架构、专门优化向量检索 部署复杂(需 Etcd、MinIO、Pulsar)、运维成本高 大规模(>1000万向量)、高并发检索场景
Elasticsearch 全文检索 + 向量检索混合、生态完善、Kibana 可视化 内存占用大、向量检索性能不如专用数据库 需要混合检索(关键词 + 语义)的场景
Chroma 轻量级、开发友好、嵌入 Python 应用 生产环境成熟度低、分布式支持弱 原型开发、本地测试、小型应用
Weaviate 多模态支持、GraphQL 查询、模块化设计 学习曲线陡峭、社区相对较小 复杂检索需求、多模态数据(文本+图像)

Milvus 替代配置示例

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-milvus-store</artifactId>
</dependency>
spring:
  ai:
    vectorstore:
      milvus:
        client:
          host: localhost
          port: 19530
        collection-name: rag_collection
        embedding-dimension: 1536
        index-type: IVF_FLAT  # 或 HNSW
        metric-type: COSINE

🔄 方案 B:更换 Embedding 模型

模型 维度 优点 缺点 价格(元/百万 tokens)
DashScope text-embedding-v2 1536 中文效果好、稳定性高、与 Spring AI Alibaba 集成最佳 速度较 v1 稍慢 1.2
DashScope text-embedding-v1 1536 性价比高、速度较快 精度略低于 v2 0.6
DashScope text-embedding-v3 1024(默认)/768/512 支持 50+ 语言、可自定义维度 相对较新 参考官网
DashScope text-embedding-v4 1024(默认) 最新推荐模型、效果最佳 批大小限制 10 参考官网
BGE-M3 1024 开源免费、多语言支持、可本地部署 需要自建推理服务 免费(自建成本)

BGE-M3 本地部署配置

spring:
  ai:
    embedding:
      options:
        model: bge-m3
        base-url: http://localhost:8080  # 本地推理服务地址

🔄 方案 C:简化版(去掉 Rerank)

如果 Rerank 模型不可用或成本敏感,可以简化流程:

@GetMapping("/rag/simple")
public Flux<ChatResponse> generateSimple(@RequestParam String message) {
    SearchRequest searchRequest = SearchRequest.builder()
        .topK(5)  // 增大 topK 补偿精度损失
        .similarityThreshold(0.7)  // 提高阈值过滤低质量片段
        .build();
    
    return ChatClient.builder(chatModel)
        .defaultAdvisors(
            new RetrievalAdvisor(vectorStore, searchRequest)  // 使用基础版 Advisor,去掉 Rerank
        )
        .build()
        .prompt()
        .user(message)
        .stream()
        .chatResponse();
}

性能对比

  • 带 Rerank:检索精度高(NDCG@10 提升 5-10%),但增加一次模型调用延迟(+50-100ms)
  • 不带 Rerank:响应更快,适合对延迟敏感的场景,但需增大 topK 并提高阈值保证质量

十、性能优化建议

🚀 数据库层面

-- 1. 调整 PostgreSQL 配置(postgresql.conf)
shared_buffers = 256MB                    -- 共享内存缓冲区
work_mem = 16MB                           -- 排序/哈希操作内存
maintenance_work_mem = 128MB              -- 维护操作内存(VACUUM、建索引)
effective_cache_size = 1GB                -- 操作系统缓存估计
max_parallel_workers_per_gather = 4         -- 并行查询 worker 数

-- 2. HNSW 索引参数调优(根据数据量选择)
-- 数据量 < 10 万:m=16, ef_construction=64(平衡)
-- 数据量 10-100 万:m=32, ef_construction=128(精度优先)
-- 数据量 > 100 万:m=64, ef_construction=256(大规模)

-- 3. 查询参数调优
SET hnsw.ef_search = 100;  -- 增大搜索候选列表,提高召回率(默认 40)[ ^2^]

-- 4. 定期维护
VACUUM ANALYZE vector_store;  -- 更新统计信息,回收空间
REINDEX INDEX CONCURRENTLY vector_store_embedding_idx;  -- 重建索引(不锁表)

-- 5. 分区表(超大规模,>500万向量)
CREATE TABLE vector_store_2024q1 PARTITION OF vector_store
    FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

🚀 应用层面

// 1. 批量插入(提升 10 倍性能)
// 避免逐条插入,减少网络往返和事务开销
List<Document> batch = new ArrayList<>();
for (Document doc : documents) {
    batch.add(doc);
    if (batch.size() >= 100) {
        vectorStore.add(batch);
        batch.clear();
    }
}
if (!batch.isEmpty()) {
    vectorStore.add(batch);
}

// 2. 异步处理(避免阻塞主线程)
@Async("taskExecutor")
public CompletableFuture<String> importFileAsync(MultipartFile file) {
    // 后台处理大文件导入
    processFile(file);
    return CompletableFuture.completedFuture("处理中");
}

// 3. 缓存检索结果(对热点问题有效)
@Cacheable(value = "rag-results", key = "#message.hashCode() + #fileId")
public Flux<String> search(String message, String fileId) {
    // ...
}

// 4. 连接池优化
@Configuration
public class DataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.hikari")
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }
}

🚀 检索优化

// 1. 动态 topK(根据问题复杂度调整)
int topK = message.length() > 50 ? 5 : 3;  // 长问题可能需要更多上下文

// 2. 混合检索(向量 + 关键词)
SearchRequest searchRequest = SearchRequest.builder()
    .topK(10)
    .similarityThreshold(0.6)
    .includeValues(false)  // 不返回向量,节省带宽
    .build();

// 3. 多路召回(向量检索 + 全文检索合并结果)
// 需要自定义 Advisor 实现

十一、生产环境检查清单

部署到生产环境前,请确认以下事项:

类别 检查项 状态
安全 数据库密码使用环境变量或密钥管理服务(Vault)
安全 API Key 使用密钥管理服务,避免硬编码
安全 配置 HTTPS,启用 TLS 1.2+
安全 配置 CORS 策略,限制跨域来源
安全 添加请求限流(Rate Limiting)防止滥用
安全 实现用户认证授权(JWT/OAuth2)
运维 配置日志级别(INFO 生产环境,DEBUG 开发环境)
运维 集成监控(Micrometer + Prometheus + Grafana)
运维 配置健康检查和就绪探针(/actuator/health)
运维 设置数据库连接池和超时参数
运维 定期备份向量数据(pg_dump 或物理备份)
性能 配置 JVM 参数(-Xmx2g -Xms2g -XX:+UseG1GC)
性能 配置容器资源限制(CPU、内存)
性能 启用 Gzip 压缩响应
文档 补充 API 文档(SpringDoc OpenAPI)
测试 补充单元测试和集成测试

十二、总结

✅ 项目优点

  1. 架构清晰:Controller → Advisor → VectorStore 分层合理,符合 Spring AI 设计哲学
  2. 技术选型成熟:Spring AI Alibaba + Pgvector 是经过验证的生产级组合
  3. 支持版本管理:fileId 机制支持文档版本控制和精准删除
  4. 流式输出:提升用户体验,减少首字等待时间
  5. Rerank 支持:通过二次精排显著提升检索质量

⚠️ 需要改进的地方

优先级 改进项 预计工时 说明
P0 添加全局异常处理 2 小时 使用 @ControllerAdvice 统一处理异常
P0 配置环境变量和密钥管理 1 小时 使用 Spring Cloud Vault 或阿里云 KMS
P1 添加 API 文档 3 小时 集成 SpringDoc OpenAPI,自动生成 Swagger UI
P1 补充集成测试 4 小时 使用 Testcontainers 测试 PostgreSQL 集成
P2 添加监控和告警 6 小时 集成 Micrometer + Prometheus + Grafana
P2 性能压测和优化 8 小时 使用 JMeter 压测,优化 HNSW 参数

🎯 进阶学习

  1. 深入 RAG:学习查询重写(Query Rewriting)、多跳检索(Multi-hop Retrieval)
  2. Agent 集成:结合 Spring AI Alibaba Agent 实现工具调用、任务规划
  3. 多模态扩展:支持图像、音频的向量化检索
  4. 分布式部署:学习 Milvus 或 Weaviate 集群部署,支持十亿级向量
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐