RAG(Retrieval Augmented Generation),检索增强生成,有助于克服大语言模型在处理长篇内容、事实准确性和上下文感知方面的局限性。本质就是用向量数据库解决大模型知识盲区。
Spring AI 中的 RAG 是解决大模型"一本正经胡说八道"(幻觉)和"知识滞后"问题的核心技术。
简单来说,RAG 就是给大模型配了一个**"外挂大脑"**(私有知识库)。当用户提问时,系统先去知识库里"翻书"(检索),把找到的相关内容作为"参考资料"(上下文)塞给大模型,让它基于这些资料回答问题。
核心公式: 最终答案 = 大模型能力 + 用户问题 + 检索到的私有知识。
1、核心原理:RAG 是怎么工作的?
在 Spring AI 中,RAG 的流程通常分为两个阶段:数据入库(ETL) 和 问答检索。
数据入库 (Indexing):
- 加载:读取 PDF、Word、TXT 等文档。
- 切分:将长文档切成小块(Chunk),因为大模型一次处理不了整本书。
- 向量化:使用 Embedding 模型将文字块转化为计算机能理解的"向量"(一串数字)。
- 存储:存入向量数据库(如 Redis, Milvus, PGVector)。
问答检索 (Retrieval & Generation):
- 向量化:把用户的问题也转化为向量。
- 检索:在向量数据库中搜索与问题向量最相似的文档块。
- 增强:将搜到的文档块拼接到 Prompt 中(例如:"参考资料:... 问题:...")。
- 生成:大模型基于参考资料生成答案。
核心组件:
- 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 实现权限控制的核心在于:
- 元数据设计 :在入库时,将
department等权限信息存入metadata字段。 - 混合查询 :利用 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();