SpringAI实用-RAG

RAG(Retrieval Augmented Generation),检索增强生成,有助于克服大语言模型在处理长篇内容、事实准确性和上下文感知方面的局限性。本质就是用向量数据库解决大模型知识盲区

Spring AI 中的 RAG 是解决大模型"一本正经胡说八道"(幻觉)和"知识滞后"问题的核心技术。

简单来说,RAG 就是给大模型配了一个**"外挂大脑"**(私有知识库)。当用户提问时,系统先去知识库里"翻书"(检索),把找到的相关内容作为"参考资料"(上下文)塞给大模型,让它基于这些资料回答问题。

核心公式: 最终答案 = 大模型能力 + 用户问题 + 检索到的私有知识。

1、核心原理:RAG 是怎么工作的?

在 Spring AI 中,RAG 的流程通常分为两个阶段:数据入库(ETL)问答检索

数据入库 (Indexing)

  1. 加载:读取 PDF、Word、TXT 等文档。
  2. 切分:将长文档切成小块(Chunk),因为大模型一次处理不了整本书。
  3. 向量化:使用 Embedding 模型将文字块转化为计算机能理解的"向量"(一串数字)。
  4. 存储:存入向量数据库(如 Redis, Milvus, PGVector)。

问答检索 (Retrieval & Generation)

  1. 向量化:把用户的问题也转化为向量。
  2. 检索:在向量数据库中搜索与问题向量最相似的文档块。
  3. 增强:将搜到的文档块拼接到 Prompt 中(例如:"参考资料:... 问题:...")。
  4. 生成:大模型基于参考资料生成答案。

核心组件

  • VectorStore:负责存和取(向量数据库)。
  • RetrievalAugmentationAdvisor:负责"拦截-检索-增强"的自动化流程。

2、使用

Spring AI 提供了非常优雅的封装,主要有两种使用方式。

2.1、开箱即用(推荐,最简单)

Spring AI 提供了 RetrievalAugmentationAdvisor,这是一个"顾问"组件。只需要把它加到 ChatClient 里,它就会自动拦截你的提问,先去查库,再发给大模型。

示例代码:

java 复制代码
@RestController
public class RagController {

    @Autowired
    private ChatClient.Builder chatClientBuilder;

    @Autowired
    private VectorStore vectorStore; // 注入你的向量数据库(如 Redis/Milvus)

    /**
     * 定义一个 RAG 顾问
     */
    @Bean
    public RetrievalAugmentationAdvisor ragAdvisor() {
        return RetrievalAugmentationAdvisor.builder()
                .vectorStore(vectorStore) // 指定向量库
                .topK(3)                  // 每次检索最相关的 3 个文档片段
                .similarityThreshold(0.7) // 相似度阈值,低于 0.7 的不要
                .build();
    }

    /**
     * 问答接口
     */
    public String chat(String question) {
        // 1. 构建客户端,并挂载 RAG 顾问
        ChatClient client = chatClientBuilder
                .defaultAdvisors(ragAdvisor()) // 关键:开启 RAG
                .build();

        // 2. 发送问题,底层会自动完成:检索 -> 拼接 -> 生成
        return client.prompt(question)
                .call()
                .content();
    }
}

2.2、手动控制(适合进阶)

如果想完全掌控流程(比如先检索,再人工过滤,最后再调用模型),可以手动调用 VectorStore

java 复制代码
// 1. 手动检索
List<Document> docs = vectorStore.similaritySearch("Spring AI 是什么?");

// 2. 手动拼接上下文
String context = docs.stream()
        .map(Document::getText)
        .collect(Collectors.joining("\n"));

// 3. 构造 Prompt 并调用模型
String prompt = "基于以下资料回答问题:\n" + context + "\n问题:Spring AI 是什么?";
String answer = chatClient.prompt(prompt).call().content();

3、从零构建 RAG 系统

实现一个**"企业文档问答助手"**,支持上传 PDF 并问答。

3.1、引入依赖

你需要向量库的依赖(以 Redis 为例)和文档读取依赖。

XML 复制代码
<dependencies>
    <dependency>
		<groupId>org.springframework.ai</groupId>
	    <artifactId>spring-ai-rag</artifactId>
	</dependency>
    <!-- 向量库支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-redis-store-spring-boot-starter</artifactId>
    </dependency>
    <!-- 文档读取 (支持 PDF/Word 等) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pdf-document-reader</artifactId>
    </dependency>
</dependencies>

3.2、数据入库(ETL)

**Extract-Transform-Load(抽取、转换、加载)**‌

创建一个接口,用于上传文件并写入向量库。

java 复制代码
@RestController
@RequestMapping("/rag")
public class RagIngestionController {

    @Autowired
    private VectorStore vectorStore;

    @PostMapping("/upload")
    public String uploadFile(MultipartFile file) throws IOException {
        // 1. 读取文档 (Tika 支持多种格式)
        TikaDocumentReader reader = new TikaDocumentReader(file.getResource());
        List<Document> documents = reader.read();

        // 2. (可选) 切分文档 - 如果文档很长,建议使用 Splitter 切分
        // List<Document> chunks = new TokenTextSplitter().apply(documents);

        // 3. 存入向量库 (自动向量化)
        vectorStore.add(documents);
        
        return "上传成功,共入库 " + documents.size() + " 个文档片段";
    }
}

3.3、智能问答

使用前面提到的 RetrievalAugmentationAdvisor。

java 复制代码
// 配置 Advisor
@Configuration
public class RagConfig {
    @Autowired private VectorStore vectorStore;

    @Bean
    public RetrievalAugmentationAdvisor ragAdvisor() {
        return RetrievalAugmentationAdvisor.builder()
                .vectorStore(vectorStore)
                .topK(5) // 找 5 个相关片段
                .build();
    }
}

// 控制器
@RestController
public class ChatController {
    @Autowired private ChatClient.Builder builder;
    @Autowired private RetrievalAugmentationAdvisor ragAdvisor;

    @GetMapping("/chat")
    public String chat(String msg) {
        return builder
                .defaultSystem("你是一个助手,请严格基于提供的上下文回答问题。如果不知道,就说不知道。")
                .defaultAdvisors(ragAdvisor) // 注入 RAG 能力
                .build()
                .prompt(msg)
                .call()
                .content();
    }
}

4、向量数据库

数据库 核心定位 优点 缺点 适用场景
Milvus 企业级重型坦克 功能最全、支持亿级数据、GPU加速、高可用 架构复杂(依赖Etcd/MinIO等)、运维门槛高 超大规模数据(亿级+)、私有化部署、大厂
Pinecone 云端托管标杆 零运维、Serverless弹性、SLA高、上手极快 闭源、长期成本高、数据在第三方(合规风险) 初创团队、无运维能力、追求快速上线
Qdrant 性能与性价比之王 Rust编写(极速/省内存)、部署简单、过滤强 超大规模(十亿级)经验略少于Milvus 中大规模生产环境、追求高性能与低成本的平衡
Chroma 开发者轻量首选 纯Python、零配置、本地运行、LangChain集成好 不适合大规模并发、功能单一、无分布式 本地开发、原型验证、个人项目
PGVector 传统数据库扩展 与PostgreSQL无缝集成、支持ACID事务、SQL混合查 性能不如专用库、大规模数据调优难 已有PG架构、中小规模数据、不想引入新组件
Weaviate 混合搜索专家 原生混合搜索(向量+关键词)、模块化、GraphQL 资源占用较高、学习曲线略陡 需要高精度语义搜索、多模态场景

5、带数据权限的企业问答系统

用PGVector实现一个企业文档问答系统,支持按部门权限过滤检索结果。

这是一个非常实用的企业级 RAG 场景。使用 PGVector 的优势在于,我们可以利用 PostgreSQL 强大的关系型数据库特性(如 JSONB、SQL 过滤)来实现复杂的权限控制,而不仅仅是简单的向量相似度搜索。

为了实现权限过滤,我们的数据模型需要包含**"谁可以访问"**的元数据。

数据模型设计:我们需要一张表,既存向量(Embedding),也存业务元数据(部门、密级)。

检索策略预过滤 :在向量搜索之前,先通过 SQL WHERE 子句锁定当前用户有权限的数据范围(例如 department = 'IT')。

技术栈 :数据库:PostgreSQL + pgvector 插件。框架:Spring AI (Java) ------ 因为它对 PGVector 的元数据过滤支持非常成熟。

用 PGVector 实现权限控制的核心在于:

  1. 元数据设计 :在入库时,将 department 等权限信息存入 metadata 字段。
  2. 混合查询 :利用 Spring AI 的 FilterExpressionBuilder 构建 SQL 过滤条件,在向量相似度计算之前,先通过关系型数据库的过滤能力圈定数据范围。

5.1、数据库准备 (SQL)

在 PostgreSQL 中启用 vector 插件,并创建包含权限字段的表。

sql 复制代码
-- 1. 启用 pgvector 插件
CREATE EXTENSION IF NOT EXISTS vector;

-- 2. 创建文档表
-- 注意:这里多了 department 和 security_level 字段,用于权限控制
CREATE TABLE document_store (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    content TEXT NOT NULL,           -- 文档片段内容
    embedding vector(1536),          -- 向量数据 (假设使用 OpenAI 1536 维)
    metadata JSONB NOT NULL,         -- 元数据 (存部门、文件名等)
    created_at TIMESTAMP DEFAULT NOW()
);

-- 3. 创建索引 (HNSW 适合高性能搜索)
-- 注意:索引只建立在 embedding 列上
CREATE INDEX document_embedding_idx 
ON document_store 
USING hnsw (embedding vector_cosine_ops) 
WITH (m = 16, ef_construction = 64);

5.2、数据入库 (带权限元数据)

在将文档存入向量库时,必须把"权限信息"一起存进去。

假设我们有一个 HR 的文档,只有"HR"部门能看;还有一个技术文档,"IT"部门能看。

java 复制代码
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

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

@Service
public class KnowledgeBaseService {

    private final VectorStore vectorStore;

    public KnowledgeBaseService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    /**
     * 添加文档到向量库
     * @param content 文本内容
     * @param allowedDepartment 允许访问的部门
     */
    public void addDocument(String content, String allowedDepartment) {
        // 1. 构建元数据
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("department", allowedDepartment); // 关键字段:部门
        metadata.put("security_level", "INTERNAL");    // 关键字段:密级

        // 2. 构建文档对象
        Document doc = new Document(content, metadata);

        // 3. 存入 PGVector (Spring AI 会自动处理向量化和入库)
        vectorStore.add(List.of(doc));
        
        System.out.println("文档已入库,仅限部门: " + allowedDepartment + " 访问");
    }
}

5.2、实现带权限的检索 (核心逻辑)

这是最关键的一步。我们需要在搜索时,根据当前登录用户的身份,动态构建过滤条件。

Spring AI 的 FilterExpressionBuilder 可以帮我们将权限条件转换为 PGVector 能理解的 SQL 过滤表达式。

java 复制代码
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class RagSearchService {

    private final VectorStore vectorStore;

    public RagSearchService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    /**
     * 带权限的搜索
     * @param query 用户问题
     * @param userDepartment 当前用户的部门 (从登录信息中获取)
     */
    public List<Document> searchWithPermission(String query, String userDepartment) {
        
        // 1. 构建过滤表达式
        // 逻辑:只搜索 department = userDepartment 的文档
        FilterExpressionBuilder builder = new FilterExpressionBuilder();
        
        // 构建 "department == 'IT'" 这样的条件
        var expression = builder.eq("department", userDepartment).build();

        // 2. 构建搜索请求
        SearchRequest request = SearchRequest.query(query)
                .withTopK(5)                  // 返回最相似的 5 条
                .withFilterExpression(expression); // 【关键】注入权限过滤器

        // 3. 执行搜索
        // 底层会生成类似: SELECT * FROM document_store WHERE metadata->>'department' = 'IT' ORDER BY embedding <-> ...
        return vectorStore.similaritySearch(request);
    }
}

5.4、调用验证

模拟两个不同部门的员工提问,结果会隔离。

java 复制代码
@RestController
@RequestMapping("/api/qa")
public class QaController {

    @Autowired
    private KnowledgeBaseService kbService;
    
    @Autowired
    private RagSearchService searchService;

    // 模拟初始化数据
    @PostMapping("/init")
    public String initData() {
        kbService.addDocument("公司Java开发规范:所有接口必须使用RestController。", "IT");
        kbService.addDocument("公司薪酬制度:P7职级基本工资范围是20k-30k。", "HR");
        kbService.addDocument("公司考勤制度:早上9点打卡。", "ALL"); // 全员可见
        return "数据初始化完成";
    }

    // 模拟IT员工提问
    @GetMapping("/ask-it")
    public String askIT() {
        // 假设当前登录用户是 IT 部门的
        List<Document> results = searchService.searchWithPermission("开发规范是什么?", "IT");
        return "IT员工搜到了 " + results.size() + " 条结果: \n" + 
               results.stream().map(Document::getText).toList();
    }

    // 模拟HR员工提问
    @GetMapping("/ask-hr")
    public String askHR() {
        // 假设当前登录用户是 HR 部门的
        List<Document> results = searchService.searchWithPermission("开发规范是什么?", "HR");
        return "HR员工搜到了 " + results.size() + " 条结果: \n" + 
               results.stream().map(Document::getText).toList();
    }
}

结果

IT员工提问 :会搜到"公司Java开发规范...",也会搜到"公司考勤制度..."(因为部门是 ALL),不会搜到"薪酬制度..."。

HR员工提问 :会搜到"公司考勤制度..."。不会搜到"Java开发规范..."(权限隔离成功)

5.5、全员可见处理

如果文档标记为 department = 'ALL',让所有人都能搜到。我们需要修改过滤表达式的逻辑,使用 OR 条件:

java 复制代码
// 修改后的过滤逻辑
var expression = builder.or(
    builder.eq("department", userDepartment), // 匹配用户部门
    builder.eq("department", "ALL")           // 或者 匹配全员标记
).build();
相关推荐
LienJack1 小时前
AI 架构设计有点菜,我写了个 Skill 给它补课
人工智能·架构
人工智能AI技术2 小时前
循环结构基础:for、while、递归性能对比
人工智能
盼小辉丶2 小时前
PyTorch强化学习实战(5)——PyTorch Ignite 事件驱动机制与实践
人工智能·pytorch·python·强化学习
eastyuxiao10 小时前
思维导图拆解项目范围 3 个真实落地案例
大数据·运维·人工智能·流程图
风落无尘10 小时前
《智能重生:从垃圾堆到AI工程师》——第五章 代码与灵魂
服务器·网络·人工智能
冬奇Lab10 小时前
RAG 系列(八):RAG 评估体系——用数据说话
人工智能·llm
landyjzlai11 小时前
蓝迪哥玩转Ai(8)---端侧AI:RK3588 端侧大语言模型(LLM)开发实战指南
人工智能·python