从零构建企业级 RAG 系统:Spring AI + Qdrant + 智谱AI 实战
本文将带你从零开始构建一个完整的 RAG (Retrieval-Augmented Generation) 智能问答系统,涵盖架构设计、核心实现、性能优化和工程实践。适合希望深入了解 RAG 技术栈的开发者。
一、什么是 RAG?
1.1 RAG 的核心价值
在大模型时代,我们面临一个核心问题:如何让 AI 回答基于特定领域的知识?
传统的大模型训练数据是通用的,对于企业私有文档、最新资料往往无能为力。RAG (检索增强生成) 技术通过"先检索,后生成"的方式解决了这个问题:
用户提问 → 向量检索 → 获取相关文档 → 构建上下文 → LLM生成答案
1.2 为什么需要 RAG?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 直接问 LLM | 简单快速 | 无法回答私有知识 |
| 微调模型 | 专业性强 | 成本高、更新慢 |
| RAG | 实时更新、成本低、可解释 | 需要维护向量库 |
1.3 RAG 的典型应用场景
- 企业知识库问答: 基于内部文档的智能客服
- 技术文档助手: 快速查询 API 文档、技术规范
- 论文研读助手: 基于论文库的专业问答
- 法律法规查询: 基于法律条文的精准回答
二、技术选型与架构设计
2.1 技术栈全景
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (Spring Boot) │
├─────────────────────────────────────────────────────────────┤
│ Spring AI 1.0.0-M6 │ 统一抽象 Embedding + Chat 接口 │
├─────────────────────────────────────────────────────────────┤
│ Qdrant 1.9.1 │ 高性能向量数据库 │
├─────────────────────────────────────────────────────────────┤
│ Caffeine Cache │ 本地 Embedding 缓存 │
├─────────────────────────────────────────────────────────────┤
│ 智谱AI │ Embedding-2 + GLM-4-Flash │
└─────────────────────────────────────────────────────────────┘
2.2 为什么选择这些技术?
Spring AI vs LangChain4j
- Spring AI 与 Spring Boot 生态深度整合
- 配置驱动,无需大量样板代码
- 统一的抽象接口,便于切换底层模型
Qdrant vs Milvus vs Weaviate
- 部署简单,单节点即可运行
- 性能优秀,支持亿级向量
- 客户端成熟,Java SDK 完善
智谱AI vs OpenAI
- 国内访问稳定,无需代理
- 价格优势明显
- Embedding-2 模型效果优秀
2.3 系统架构图
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 文档导入 │────►│ 文本分块 │────►│ Embedding │
│ (PDF/TXT) │ │ (1000字符) │ │ 向量化 │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户提问 │────►│ 缓存检查 │────►│ Qdrant │
│ │ │ (Caffeine) │ │ 向量检索 │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ 构建Prompt │
│ (模板+上下文)│
└──────┬──────┘
│
▼
┌─────────────┐
│ 智谱AI │
│ GLM-4-Flash│
└──────┬──────┘
│
▼
┌─────────────┐
│ 返回答案 │
└─────────────┘
三、核心实现详解
3.1 Embedding 缓存设计
为什么需要缓存?
在生产环境中,Embedding API 调用是主要成本之一。对于高频查询,缓存可以显著降低成本:
@Component
public class EmbeddingCache {
private final EmbeddingModel embeddingModel;
private final Cache<String, List<Float>> cache;
public EmbeddingCache(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
this.cache = Caffeine.newBuilder()
.maximumSize(1000) // 最多缓存1000个查询
.expireAfterWrite(Duration.ofMinutes(10)) // 10分钟过期
.recordStats() // 记录统计信息
.build();
}
public List<Float> get(String text) {
return cache.get(text, this::doEmbed);
}
private List<Float> doEmbed(String text) {
// 实际调用 Embedding API
EmbeddingResponse response = embeddingModel.embedForResponse(List.of(text));
return toList(response.getResults().get(0).getOutput());
}
}
缓存效果评估:
- 假设平均每次 Embedding 调用耗时 200ms,费用 0.001元
- 缓存命中率 80%,1000次查询可节省:
- 时间:1000 × 200ms × 80% = 160秒
- 费用:1000 × 0.001元 × 80% = 0.8元
3.2 双模式 RAG 服务设计
我们实现了两种 RAG 查询模式,适应不同场景:
模式一:高性能模式 (ask)
public String ask(String question, int topK) {
// 1. 从缓存获取向量
List<Float> embedding = embeddingCache.get(question);
// 2. 使用原生 QdrantClient 直接搜索
List<Document> docs = searchWithVector(embedding, topK);
// 3. 构建 Prompt 并调用 LLM
String prompt = buildPrompt(question, docs);
return chatClient.prompt().user(prompt).call().content();
}
模式二:标准模式 (ask2)
public String ask2(String question, int topK) {
// 使用 Spring AI 标准接口
SearchRequest request = SearchRequest.builder()
.query(question)
.topK(topK)
.build();
List<Document> docs = vectorStore.similaritySearch(request);
// ... 后续相同
}
性能对比:
| 指标 | ask() 高性能模式 | ask2() 标准模式 |
|---|---|---|
| Embedding 计算 | 缓存优先 | 每次重新计算 |
| 向量搜索 | 原生 Client | Spring AI 封装 |
| 适用场景 | 生产环境高频查询 | 开发测试 |
| 灵活性 | 需手动管理 | 配置驱动 |
3.3 可配置 Prompt 模板
设计思路:
硬编码的 Prompt 难以维护,我们采用文件化配置:
# src/main/resources/prompts/rag-system.txt
你是一位专业的技术文档助手。请基于提供的上下文回答用户问题。
回答规则:
1. 如果上下文中没有相关信息,请明确说明"根据现有资料无法回答"
2. 回答应简洁准确,重点突出
3. 技术术语保留英文原文
4. 不确定的内容标注[待确认]
上下文信息:
{context}
用户问题:
{question}
请用中文回答:
实现代码:
@Service
public class RagService {
private String systemPromptTemplate;
public RagService(ResourceLoader resourceLoader) {
loadSystemPrompt(resourceLoader);
}
private void loadSystemPrompt(ResourceLoader resourceLoader) {
try {
Resource resource = resourceLoader.getResource(
"classpath:prompts/rag-system.txt"
);
this.systemPromptTemplate = FileCopyUtils.copyToString(
new InputStreamReader(resource.getInputStream(), UTF_8)
);
} catch (IOException e) {
// 降级:使用默认模板
this.systemPromptTemplate = DEFAULT_PROMPT;
}
}
private String buildPrompt(String question, String context) {
return systemPromptTemplate
.replace("{context}", context)
.replace("{question}", question);
}
}
优势:
- 无需重启即可调整 Prompt(每次请求重新读取)
- 支持多环境配置(开发/测试/生产不同模板)
- 业务人员可独立调整,无需修改代码
3.4 性能监控与优化
性能计时实现:
public String ask(String question, int topK) {
Stopwatch stopwatch = Stopwatch.createStarted();
try {
// 1. Embedding 缓存查询
List<Float> embedding = embeddingCache.get(question);
// 2. 向量搜索
List<Document> docs = searchWithVector(embedding, topK);
// 3. LLM 调用
String answer = chatClient.prompt()
.user(buildPrompt(question, docs))
.call()
.content();
return answer;
} finally {
stopwatch.stop();
System.out.println("RAG 请求耗时: " + stopwatch.elapsed());
}
}
实际性能数据(本地测试):
| 阶段 | 耗时 | 优化手段 |
|---|---|---|
| Embedding | 0-200ms | Caffeine 缓存 |
| 向量检索 | 10-50ms | Qdrant 内存索引 |
| LLM 生成 | 500-2000ms | 流式响应(待实现) |
| 总计 | 500-2500ms | - |
四、工程实践与最佳实践
4.1 文本分块策略
为什么需要分块?
大模型有上下文长度限制,且过长的文本会降低检索精度。
当前实现(简单策略):
private List<String> splitText(String text, int maxLength) {
List<String> chunks = new ArrayList<>();
StringBuilder current = new StringBuilder();
for (String line : text.split("\n")) {
if (current.length() + line.length() > maxLength
&& current.length() > 0) {
chunks.add(current.toString());
current = new StringBuilder();
}
current.append(line).append("\n");
}
if (current.length() > 0) {
chunks.add(current.toString());
}
return chunks;
}
进阶策略(待实现):
- 语义分块: 基于句子边界,保持语义完整
- 重叠分块: 相邻块保留 20% 重叠,避免信息截断
- 层次分块: 父文档-子块关系,支持上下文回溯
4.2 错误处理与降级
多层降级策略:
public String ask(String question, int topK) {
try {
// 正常流程
return doRag(question, topK);
} catch (EmbeddingException e) {
// 降级1:Embedding失败,返回相似文档
return "【系统提示】Embedding服务暂时不可用,为您找到相关文档:\n"
+ searchDocuments(question);
} catch (QdrantException e) {
// 降级2:向量库失败,直接问LLM
return chatClient.prompt()
.user(question)
.call()
.content();
} catch (Exception e) {
// 最终降级
return "【系统繁忙】请稍后重试";
}
}
4.3 配置管理最佳实践
application.yaml 分层配置:
spring:
ai:
# 向量存储配置
vectorstore:
qdrant:
host: ${QDRANT_HOST:localhost}
port: ${QDRANT_PORT:6334}
collection-name: ${QDRANT_COLLECTION:my_collection}
initialize-schema: true
# OpenAI 兼容配置(智谱AI)
openai:
api-key: ${ZHIPU_API_KEY}
base-url: ${ZHIPU_BASE_URL:https://open.bigmodel.cn/api/paas/v4}
embedding:
options:
model: ${EMBEDDING_MODEL:embedding-2}
chat:
options:
model: ${CHAT_MODEL:glm-4-flash}
temperature: ${CHAT_TEMPERATURE:0.7}
max-tokens: ${CHAT_MAX_TOKENS:2000}
环境变量优先级:
- 环境变量(生产环境)
- application-{profile}.yaml(测试环境)
- application.yaml(默认配置)
五、快速开始
5.1 环境准备
# 1. 安装 JDK 17
java -version
# 2. 安装 Maven
mvn -version
# 3. 启动 Qdrant (Docker)
docker run -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant
5.2 获取代码并运行
# 克隆代码
git clone <repository-url>
cd demoAI
# 配置 API Key
export ZHIPU_API_KEY=your-api-key
# 编译运行
mvn clean install
mvn spring-boot:run
5.3 使用示例
导入文档:
curl -X POST "http://localhost:8080/api/import/data"
智能问答:
curl -X POST "http://localhost:8080/api/rag/ask" \
-H "Content-Type: application/json" \
-d '{
"question": "什么是 Spring Boot 的自动配置?",
"topK": 5
}'
带引用的问答:
curl -X POST "http://localhost:8080/api/rag/ask-with-sources" \
-H "Content-Type: application/json" \
-d '{
"question": "如何优化 JVM 性能?",
"topK": 3
}'
六、性能优化建议
6.1 短期优化(已实现)
- ✅ Embedding 缓存(Caffeine)
- ✅ 原生 QdrantClient 搜索
- ✅ 性能计时监控
6.2 中期优化(建议)
- 异步处理: 使用 CompletableFuture 并行处理多个查询
- 连接池: QdrantClient 连接池化
- 批量导入: 支持批量文档导入,减少网络往返
6.3 长期优化(规划)
- 混合检索: 向量检索 + 关键词检索(BM25)
- 重排序: 使用 Cross-Encoder 对检索结果重排序
- 多路召回: 同时查询多个向量索引,合并结果
七、常见问题
Q1: 为什么使用智谱AI而不是 OpenAI?
A: 主要考虑因素:
- 稳定性: 国内直接访问,无需代理
- 成本: Embedding 和 Chat 价格更低
- 合规: 数据不出境,符合企业要求
Q2: 如何支持 PDF 文件?
A: 当前版本仅支持文本文件。PDF 支持需要:
- 添加依赖:
org.apache.pdfbox:pdfbox - 实现 PDFTextStripper 提取文本
- 处理表格、图片等非文本内容
Q3: 缓存数据会丢失吗?
A: Caffeine 是本地内存缓存,应用重启会丢失。如需持久化:
- 使用 Redis 作为分布式缓存
- 或实现磁盘持久化(Caffeine 支持)
Q4: 如何扩展到多节点?
A: 当前是单节点架构,多节点需要:
- 共享向量数据库(Qdrant 集群)
- 分布式缓存(Redis)
- 负载均衡(Nginx/网关)
八、总结与展望
8.1 项目亮点
- 完整的 RAG 链路: 从文档导入到智能问答的全流程
- 性能优化: Embedding 缓存、原生 Client、性能监控
- 工程化: 配置驱动、降级策略、可维护的代码结构
- 国产化: 基于智谱AI,适合国内企业使用
8.2 后续规划
- 支持 PDF、Word、Markdown 等格式
- 添加 Web UI 界面
- 实现多轮对话和上下文管理
- 支持多租户和权限控制
- 接入更多大模型(文心一言、通义千问)
8.3 学习资源
九、参考代码
完整代码已开源,欢迎 Star 和 PR:
git clone https://github.com/yourusername/demoAI.git
许可证
MIT License
如果本文对你有帮助,欢迎点赞和分享!有问题请在评论区交流。