Spring AI 实战系列 | 第 3 篇

VectorStore + RAG:构建私有知识库


目录

  1. 为什么需要 RAG?
  2. RAG 工作原理
  3. 向量数据库选型
  4. Spring AI VectorStore API
  5. 实战:构建文档问答系统
  6. 常见问题
  7. 系列预告

一、为什么需要 RAG?

1.1 AI 的知识局限性

大语言模型的知识有两个致命问题:

问题说明示例
知识截止训练数据有时效性GPT-4 截止 2023 年 9 月
缺乏私有数据无法访问企业内网文档不知道公司内部规定

1.2 解决方案对比

方案原理成本效果
Fine-tuning重新训练模型极高好但昂贵
RAG检索增强生成中等 推荐
Prompt Stuffing塞进上下文受限 token 数

1.3 RAG 核心价值

┌─────────────────────────────────────────────────────────────┐
│                        RAG 核心价值                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   企业私有数据 ──────────────────┐                         │
│  - 产品文档                        │                         │
│  - 客服FAQ                        │  ──→  AI 生成回答        │
│  - 技术手册                        │                         │
│  - 员工手册                        │                         │
│                                     │                         │
│   优点:                                                 │
│  • 无需训练模型                                             │
│  • 数据实时更新                                             │
│  • 保护隐私安全                                             │
│  • 成本可控                                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、RAG 工作原理

2.1 完整流程

┌────────────────────────────────────────────────────────────────────┐
│                        RAG 工作流程                                  │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│  【阶段一:数据准备 - 离线】                                         │
│                                                                    │
│  ┌────────┐    ┌──────────┐    ┌─────────┐    ┌────────────┐     │
│  │ 文档库  │ →  │  文档读取 │ →  │ 文档分割 │ →  │ Embedding │     │
│  │PDF/Word│    │ Reader   │    │ Splitter │    │   转向量   │     │
│  └────────┘    └──────────┘    └─────────┘    └─────┬──────┘     │
│                                                      ↓             │
│                                            ┌────────────┐          │
│                                            │ 向量数据库  │          │
│                                            │  存储向量   │          │
│                                            └────────────┘          │
│                                                                    │
│  【阶段二:查询回答 - 在线】                                          │
│                                                                    │
│  ┌────────┐    ┌──────────┐    ┌─────────┐    ┌────────────┐     │
│  │ 用户   │ →  │ 问题转   │ →  │ 向量    │ →  │  检索相似  │     │
│  │ 问题   │    │ 向量     │    │ 搜索    │    │   文档     │     │
│  └────────┘    └──────────┘    └─────────┘    └─────┬──────┘     │
│                                                      ↓             │
│                                            ┌──────────────────┐      │
│                                            │  构建提示词       │      │
│                                            │  (问题+文档+指令) │      │
│                                            └────────┬─────────┘      │
│                                                     ↓               │
│                                            ┌────────────┐          │
│                                            │   AI 生成   │          │
│                                            │   最终回答   │          │
│                                            └────────────┘          │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

2.2 关键环节

环节作用常见工具
文档读取解析 PDF/Word/HTML/MarkdownPDF Reader, Jsoup
文档分割长文本切分成小片段TokenTextSplitter
Embedding文本转向量OpenAI, DashScope
向量存储高效存储与检索PGVector, Redis, Pinecone
相似度检索找到最相关的文档VectorStore API

三、向量数据库选型

3.1 Spring AI 支持的向量数据库

数据库特点适用场景
PGVector (PostgreSQL)简单易用,生态好中小型项目
Redis性能极高需要快速响应
Pinecone云服务,无需运维云上部署
Milvus功能强大大规模数据
Neo4j图数据库+向量知识图谱
Weaviate开源,云原生现代化架构
Qdrant高性能 Rust 实现高并发场景
Elasticsearch全文搜索+向量已有 ES 集群

3.2 快速选择指南

数据量 < 10万条 → PGVector (PostgreSQL)
需要云服务     → Pinecone / Azure Vector
需要最高性能   → Redis / Qdrant
需要图关系     → Neo4j
已有 ES 集群   → Elasticsearch
需要本地部署   → Ollama + Chroma

四、Spring AI VectorStore API

4.1 核心接口

public interface VectorStore extends DocumentWriter, VectorStoreRetriever {
    
    // 添加文档
    void add(List<Document> documents);
    
    // 删除文档
    void delete(List<String> idList);
    
    // 相似度搜索
    List<Document> similaritySearch(SearchRequest request);
}

@FunctionalInterface
public interface VectorStoreRetriever {
    List<Document> similaritySearch(SearchRequest request);
}

4.2 Document 结构

public class Document {
    private String id;                    // 唯一ID
    private String content;               // 文本内容
    private Map<String, Object> metadata; // 元数据
}

4.3 SearchRequest 构建

SearchRequest request = SearchRequest.builder()
    .query("用户问题")
    .topK(4)                    // 返回 4 条最相似结果
    .similarityThreshold(0.75)  // 相似度阈值 (0-1)
    .filterExpression(          // 元数据过滤
        FilterExpressionBuilder
            .and(country.eq("US"), year.gte(2020))
    )
    .build();

4.4 依赖配置示例

PGVector (PostgreSQL):

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vectorstore-pgvector</artifactId>
</dependency>
spring.ai.vectorstore.pgvector.enabled=true
spring.datasource.url=jdbc:postgresql://localhost:5432/vectordb

Redis:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vectorstore-redis</artifactId>
</dependency>
spring.ai.vectorstore.redis.enabled=true
spring.data.redis.host=localhost
spring.data.redis.port=6379

五、实战:构建文档问答系统

5.1 项目结构

rag-demo/
├── pom.xml
└── src/main/
    ├── java/com/example/demo/
    │   ├── DemoApplication.java
    │   ├── controller/
    │   │   └── RagController.java
    │   └── service/
    │       ├── DocumentService.java    # 文档加载与存储
    │       └── RagService.java        # RAG 问答核心
    └── resources/
        └── application.properties

5.2 依赖配置 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.0</version>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>rag-demo</artifactId>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.1.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Ollama 本地模型(Chat + Embedding) -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-ollama</artifactId>
        </dependency>
        
        <!-- 向量数据库:Chroma -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-vector-store-chroma</artifactId>
        </dependency>
        
        <!-- 文档读取:PDF -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-pdf-document-reader</artifactId>
        </dependency>
        
        <!-- 文档读取:Markdown -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-markdown-document-reader</artifactId>
        </dependency>
    </dependencies>
</project>

5.3 配置文件 application.properties

# Ollama 本地模型配置
spring.ai.ollama.base-url=
spring.ai.ollama.chat.options.model=qwen2.5:7b
spring.ai.ollama.embedding.options.model=qwen3-embedding:0.6b

# Chroma 向量数据库配置
spring.ai.vectorstore.chroma.client.host=
spring.ai.vectorstore.chroma.client.port=8000
spring.ai.vectorstore.chroma.collection-name=TestCollection
spring.ai.vectorstore.chroma.initialize-schema=true

# 服务端口
server.port=8080

5.4 文档服务 DocumentService.java

package com.example.demo.service;

import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

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

/**
 * 文档加载与向量化存储服务
 */
@Service
public class DocumentService {

    private final VectorStore vectorStore;
    private final ResourceLoader resourceLoader;

    public DocumentService(VectorStore vectorStore, ResourceLoader resourceLoader) {
        this.vectorStore = vectorStore;
        this.resourceLoader = resourceLoader;
    }

    /**
     * 从资源文件加载文档并存储到向量数据库
     */
    public void loadDocument(String resourcePath) throws IOException {
        Resource resource = resourceLoader.getResource(resourcePath);
        
        // 根据文件类型选择 Reader
        DocumentReader reader = getDocumentReader(resource);
        
        // 读取文档
        List<Document> documents = reader.read();
        
        // 存储到向量数据库(自动转为 Embedding)
        vectorStore.add(documents);
        
        System.out.println("已加载 " + documents.size() + " 个文档片段到向量数据库");
    }

    private DocumentReader getDocumentReader(Resource resource) {
        String filename = resource.getFilename();
        
        if (filename == null) {
            throw new IllegalArgumentException("文件名不能为空");
        }
        
        if (filename.endsWith(".pdf")) {
            return new PagePdfDocumentReader(resource);
        } else if (filename.endsWith(".md")) {
            return new MarkdownDocumentReader(resource,
                MarkdownDocumentReaderConfig.builder().build());
        } else {
            throw new IllegalArgumentException("不支持的文件类型: " + filename);
        }
    }

    /**
     * 添加单个文档到向量数据库
     */
    public void addDocument(String content) {
        Document document = new Document(content);
        vectorStore.add(List.of(document));
    }

    /**
     * 添加单个文档到向量数据库(带元数据)
     */
    public void addDocument(String content, java.util.Map<String, Object> metadata) {
        Document document = new Document(content, metadata);
        vectorStore.add(List.of(document));
    }
}

5.5 RAG 服务 RagService.java

package com.example.demo.service;

import org.springframework.ai.chat.client.ChatClient;
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.List;
import java.util.stream.Collectors;

/**
 * RAG 问答服务
 */
@Service
public class RagService {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    public RagService(ChatClient.Builder builder, VectorStore vectorStore) {
        this.chatClient = builder.build();
        this.vectorStore = vectorStore;
    }

    /**
     * 基于知识库回答问题(使用 RAG Advisor)
     */
    public String answer(String question) {
        // 第一步:在向量数据库中搜索相似文档
        List<Document> similarDocuments = vectorStore.similaritySearch(
            SearchRequest.builder()
                .query(question)
                .topK(3)  // 返回最相似的 3 个文档
                .build()
        );

        // 第二步:提取文档内容作为上下文
        String context = similarDocuments.stream()
            .map(Document::getText)  // 使用 getText() 获取文档内容
            .collect(Collectors.joining("nn"));

        // 第三步:构建提示词,包含上下文和问题
        String prompt = String.format(
            "你是一个问答助手。请根据以下上下文来回答用户的问题。nn" +
            "上下文信息:n%snn" +
            "用户问题:%snn" +
            "请基于上下文信息来回答问题。如果上下文中没有相关信息,请说明。",
            context,
            question
        );

        // 第四步:调用 AI 模型获取回答
        return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
    }

    /**
     * 手动实现 RAG(更灵活的控制)
     */
    public String answerManual(String question) {
        // 1. 检索相似文档
        List<String> docs = vectorStore.similaritySearch(question).stream()
            .map(d -> d.getText())
            .toList();
        
        // 2. 构建提示词
        String context = String.join("nn", docs);
        String prompt = """
            请根据以下上下文来回答问题。如果上下文中没有相关信息,请如实说明。
            
            上下文:
            %s
            
            问题:%s
            """.formatted(context, question);
        
        // 3. 调用 AI
        return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
    }
}

5.6 控制器 RagController.java

package com.example.demo.controller;

import com.example.demo.service.DocumentService;
import com.example.demo.service.RagService;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * RAG 问答控制器
 */
@RestController
@RequestMapping("/rag")
public class RagController {

    private final RagService ragService;
    private final DocumentService documentService;

    public RagController(RagService ragService, DocumentService documentService) {
        this.ragService = ragService;
        this.documentService = documentService;
    }

    /**
     * 基于知识库回答问题
     * GET /rag/ask?question=什么是Spring AI?
     */
    @GetMapping("/ask")
    public Map<String, Object> ask(@RequestParam String question) {
        String answer = ragService.answer(question);
        return Map.of(
            "question", question,
            "answer", answer,
            "timestamp", System.currentTimeMillis()
        );
    }

    /**
     * 手动实现 RAG(更灵活的控制)
     * GET /rag/ask-manual?question=什么是Spring AI?
     */
    @GetMapping("/ask-manual")
    public Map<String, Object> askManual(@RequestParam String question) {
        String answer = ragService.answerManual(question);
        return Map.of(
            "question", question,
            "answer", answer,
            "timestamp", System.currentTimeMillis()
        );
    }

    /**
     * 添加文档到向量数据库
     * POST /rag/add-doc
     * Body: {"content": "Spring AI 是...", "metadata": {"source": "docs"}}
     */
    @PostMapping("/add-doc")
    public Map<String, String> addDocument(@RequestBody AddDocRequest request) {
        try {
            if (request.metadata() != null && !request.metadata().isEmpty()) {
                documentService.addDocument(request.content(), request.metadata());
            } else {
                documentService.addDocument(request.content());
            }
            return Map.of("status", "success", "message", "文档已添加到向量数据库");
        } catch (Exception e) {
            return Map.of("status", "error", "message", "添加文档失败: " + e.getMessage());
        }
    }

    /**
     * 从资源文件加载文档
     * POST /rag/load-docs
     * Body: {"resourcePath": "classpath:docs/faq.md"}
     */
    @PostMapping("/load-docs")
    public Map<String, String> loadDocuments(@RequestBody LoadDocsRequest request) {
        try {
            documentService.loadDocument(request.resourcePath());
            return Map.of("status", "success", "message", "文档加载完成");
        } catch (Exception e) {
            return Map.of("status", "error", "message", "加载文档失败: " + e.getMessage());
        }
    }

    /**
     * 获取知识库统计信息
     * GET /rag/stats
     */
    @GetMapping("/stats")
    public Map<String, Object> getStats() {
        return Map.of(
            "documentCount", ragService.getDocumentCount(),
            "timestamp", System.currentTimeMillis()
        );
    }

    /**
     * 健康检查
     * GET /rag/health
     */
    @GetMapping("/health")
    public Map<String, String> health() {
        try {
            ragService.answer("test");
            return Map.of("status", " RAG 服务正常运行");
        } catch (Exception e) {
            return Map.of("status", " RAG 服务异常: " + e.getMessage());
        }
    }

    public record AddDocRequest(String content, java.util.Map<String, Object> metadata) {}
    public record LoadDocsRequest(String resourcePath) {}
}

5.7 测试

# 1. 健康检查
curl "http://localhost:8080/rag/health"

# 2. 添加文档
curl -X POST  
  -H "Content-Type: application/json" 
  -d '{"content": "Spring AI 是 Spring 官方推出的 AI 集成框架。", "metadata": {"source": "docs"}}'

# 3. 提问
curl "http://localhost:8080/rag/ask?question=什么是Spring AI?"

# 4. 手动 RAG
curl "http://localhost:8080/rag/ask-manual?question=Spring AI支持哪些模型?"

# 5. 获取统计
curl "http://localhost:8080/rag/stats"

六、常见问题

Q1: 文档太长怎么办?

使用 TokenTextSplitter 分割:

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

Q2: 向量检索效果不好?

  1. 调整 topK 数量
  2. 调整 similarityThreshold 阈值
  3. 优化文档分割策略

Q3: 首次运行报错 schema?

确保设置:

spring.ai.vectorstore.chroma.initialize-schema=true

七、系列预告

本篇小结

  • 理解了 RAG 的价值与原理
  • 掌握了 VectorStore API
  • 完成了文档问答系统实战

完整系列

篇目内容状态
第 1 篇核心概念 + 快速上手
第 2 篇Tool Calling
第 3 篇VectorStore + RAG 本文
第 4 篇结构化输出 下篇
第 5 篇Advisors 中间件
第 6 篇国产模型集成

下篇预告

第 4 篇:结构化输出 - AI 结果映射为 POJO

  • BeanOutputConverter 用法
  • Native Structured Output
  • 泛型集合处理

参考资料

  1. Spring AI VectorStore 文档

    • docs.spring.io/spring-ai/r…
  2. Spring AI ETL Pipeline 文档

    • docs.spring.io/spring-ai/r…
  3. Spring AI RAG 文档

    • docs.spring.io/spring-ai/r…



系列:《Spring AI 实战系列 入门篇》第 3 篇(共 6 篇)

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com