《Spring AI 实战系列 入门篇》第 3 篇

Spring AI 实战系列 | 第 3 篇

VectorStore + RAG:构建私有知识库

系列说明:本文为《Spring AI 实战系列 入门篇》第 3 篇

前置知识:完成第 1-2 篇

预计阅读时间:15 分钟


📖 目录

  1. [为什么需要 RAG?](#为什么需要 RAG? "#%E4%B8%80-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-rag")
  2. [RAG 工作原理](#RAG 工作原理 "#%E4%BA%8C-rag-%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86")
  3. 向量数据库选型
  4. [Spring AI VectorStore API](#Spring AI VectorStore API "#%E5%9B%9B-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 完整流程

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

2.2 关键环节

环节 作用 常见工具
文档读取 解析 PDF/Word/HTML/Markdown PDF 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 快速选择指南

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

四、Spring AI VectorStore API

4.1 核心接口

java 复制代码
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 结构

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

4.3 SearchRequest 构建

java 复制代码
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):

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

Redis:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vectorstore-redis</artifactId>
</dependency>
properties 复制代码
spring.ai.vectorstore.redis.enabled=true
spring.data.redis.host=localhost
spring.data.redis.port=6379

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

5.1 项目结构

bash 复制代码
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 复制代码
<?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

properties 复制代码
# Ollama 本地模型配置
spring.ai.ollama.base-url=http://localhost:11434
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=http://localhost
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

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

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("\n\n"));

        // 第三步:构建提示词,包含上下文和问题
        String prompt = String.format(
            "你是一个问答助手。请根据以下上下文来回答用户的问题。\n\n" +
            "上下文信息:\n%s\n\n" +
            "用户问题:%s\n\n" +
            "请基于上下文信息来回答问题。如果上下文中没有相关信息,请说明。",
            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("\n\n", docs);
        String prompt = """
            请根据以下上下文来回答问题。如果上下文中没有相关信息,请如实说明。
            
            上下文:
            %s
            
            问题:%s
            """.formatted(context, question);
        
        // 3. 调用 AI
        return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
    }
}

5.6 控制器 RagController.java

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 测试

bash 复制代码
# 1. 健康检查
curl "http://localhost:8080/rag/health"

# 2. 添加文档
curl -X POST http://localhost:8080/rag/add-doc \
  -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 分割:

java 复制代码
List<Document> chunks = new TokenTextSplitter()
    .apply(documents);

Q2: 向量检索效果不好?

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

Q3: 首次运行报错 schema?

确保设置:

properties 复制代码
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 文档

  2. Spring AI ETL Pipeline 文档

  3. Spring AI RAG 文档


📌 引用说明 :本文核心概念与技术描述参考自 Spring AI 官方文档(docs.spring.io/spring-ai/r... 与 ETL Pipeline 内容来自官方对应章节。


关注公众号「AI日撰」,点击菜单「获取源码」获取完整代码(Gitee 仓库)。


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

相关推荐
Memory_荒年2 小时前
Netty:从“网络搬砖”到“流水线大师”的奇幻之旅
java·后端
ChaseDreamRunner2 小时前
如何用 NSSM 把 Jar 做成 Windows 服务
java·windows·jar
神の愛2 小时前
java的Aop
java·开发语言
左左右右左右摇晃2 小时前
ConcurrentHashMap ——put + get
java·开发语言·笔记
啥咕啦呛2 小时前
java打卡学习4:HashMap底层结构、扩容机制
java·学习·哈希算法
qq_297574673 小时前
K8s系列第十四篇:K8s 故障排查实战:常见故障定位与解决方法
java·docker·kubernetes
Flittly3 小时前
【SpringAIAlibaba新手村系列】(3)ChatModel 与 ChatClient 的深度对比
java·人工智能·spring boot·spring
2401_835792543 小时前
Java复习上
java·开发语言·python
小昭在路上……3 小时前
编译与链接的本质:段(Section)的生成与定位
java·linux·开发语言