从零构建企业级 RAG 系统:Spring AI + Qdrant + 智谱AI 实战

从零构建企业级 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;
}

进阶策略(待实现):

  1. 语义分块: 基于句子边界,保持语义完整
  2. 重叠分块: 相邻块保留 20% 重叠,避免信息截断
  3. 层次分块: 父文档-子块关系,支持上下文回溯

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}

环境变量优先级:

  1. 环境变量(生产环境)
  2. application-{profile}.yaml(测试环境)
  3. 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 支持需要:

  1. 添加依赖:org.apache.pdfbox:pdfbox
  2. 实现 PDFTextStripper 提取文本
  3. 处理表格、图片等非文本内容

Q3: 缓存数据会丢失吗?

A: Caffeine 是本地内存缓存,应用重启会丢失。如需持久化:

  • 使用 Redis 作为分布式缓存
  • 或实现磁盘持久化(Caffeine 支持)

Q4: 如何扩展到多节点?

A: 当前是单节点架构,多节点需要:

  • 共享向量数据库(Qdrant 集群)
  • 分布式缓存(Redis)
  • 负载均衡(Nginx/网关)

八、总结与展望

8.1 项目亮点

  1. 完整的 RAG 链路: 从文档导入到智能问答的全流程
  2. 性能优化: Embedding 缓存、原生 Client、性能监控
  3. 工程化: 配置驱动、降级策略、可维护的代码结构
  4. 国产化: 基于智谱AI,适合国内企业使用

8.2 后续规划

  • 支持 PDF、Word、Markdown 等格式
  • 添加 Web UI 界面
  • 实现多轮对话和上下文管理
  • 支持多租户和权限控制
  • 接入更多大模型(文心一言、通义千问)

8.3 学习资源

九、参考代码

完整代码已开源,欢迎 Star 和 PR:

git clone https://github.com/yourusername/demoAI.git

许可证

MIT License


如果本文对你有帮助,欢迎点赞和分享!有问题请在评论区交流。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容