Featured image of post RAG知识库实战_SpringAI_pgvector

RAG知识库实战_SpringAI_pgvector

前言

RAG(检索增强生成)已成为企业级 AI 应用的核心技术栈。通过将外部知识库与大语言模型结合,RAG 能够显著提升回答的准确性和可追溯性。

一、RAG 简介

1.1 什么是 RAG

RAG (Retrieval-Augmented Generation,检索增强生成) 是一种将信息检索技术与生成式大语言模型相结合的框架。核心思想是:在让 LLM 回答问题之前,先从知识库中检索出相关的上下文信息,然后将这些信息与原始问题一并提供给 LLM。

1.2 为什么需要 RAG

问题 RAG 解决方案
知识时效性 预训练 LLM 知识固化,RAG 通过动态检索外部知识源提供"实时"补充
私有数据访问 企业私有数据无法被公开 LLM 访问,RAG 安全连接数据源
模型幻觉 RAG 提供明确参考文本,强制 LLM 基于事实生成回答

1.3 RAG vs 传统搜索

维度 传统搜索 RAG(检索+生成)
用户目标 找到文档/页面 直接得到可读答案
延迟与成本 极低、易扩展 更高(检索+LLM 推理)
可控性 强:给原文链接 弱一些:可能误解
适用场景 编号/标题/关键词检索 客服解答、制度解读、跨文档总结

二、RAG 工作原理

2.1 两大阶段

RAG 分为两个阶段:索引阶段检索阶段

1
2
3
4
5
6
┌─────────────────────────────────────────────────────────────┐
│                    RAG 系统                                  │
├──────────────────────────┬──────────────────────────────────┤
│     📥 索引阶段           │      🔍 检索阶段                  │
│   (离线构建)            │    (在线推理)                    │
└──────────────────────────┴──────────────────────────────────┘

2.2 索引阶段流程

1
文档 → 加载解析 → 文本分割 → 向量化 → 存储到向量数据库
步骤 说明
输入文档 PDF、Word、HTML、数据库记录等
清理文档 去噪处理,移除 HTML 标签、特殊字符
文档拆分 按语义/标题/长度切分为 chunks
向量化 通过 Embedding 模型将文本转为语义向量
存储 向量 + 原始内容 + 元数据存入向量数据库

2.3 检索阶段流程

1
用户查询 → 查询向量化 → 相似度搜索 → 构建上下文 → LLM 生成回答
步骤 说明
接收请求 接收用户自然语言查询
查询向量化 将用户查询转为语义向量
信息检索 在向量数据库中找到 Top-K 相关文档
生成增强 检索片段 + 原始查询 → LLM
输出生成 返回自然语言回复 + 来源引用

三、技术选型

3.1 为什么选择 PostgreSQL + pgvector

PostgreSQL 的可扩展性是其"王牌",通过插件提供多种强大功能:

扩展 功能
pgvector AI 向量检索
pg_bm25 全文搜索
TimescaleDB 时序数据
PostGIS 地理信息

选择 pgvector 的理由

  • 架构简单:不引入额外组件,降低运维复杂度
  • 性能够用:HNSW 索引支持毫秒级检索,百万级文档完全够用
  • 事务一致性:向量数据和业务数据在同一数据库
  • SQL 查询:可结合 WHERE 条件过滤

3.2 向量数据库对比

方案 优点 缺点
PostgreSQL + pgvector 一套数据库,运维简单 向量性能不如专业向量库
PostgreSQL + Milvus 向量检索性能更好 多一个组件
PostgreSQL + Pinecone 云托管,无需运维 成本高,数据在第三方

3.3 HNSW 索引算法

HNSW(Hierarchical Navigable Small World) 是一种多层高速公路网络:

1
2
3
4
5
Layer 2 (稀疏层):  A ◄───────────── 50km ─────────────► B
                         ↘                              ↙
Layer 1 (中等):      A ◄── 10km ──► C ◄── 15km ──► B
                           ↘          ↙          ↙
Layer 0 (稠密层):   A ◄─ 2km ─► D ◄─ 3km ─► E ◄─ 4km ─► B

HNSW 特点

  • 构建时间较长,但查询速度极快
  • 内存占用较高
  • 支持欧氏距离、余弦距离等多种度量方式

四、环境搭建

4.1 Docker 部署 pgvector

1
2
3
4
5
6
docker run -d \
  --name my_pgvector \
  -p 5432:5432 \
  -e POSTGRES_PASSWORD=123456 \
  -v /Users/guide/docker/postgresql/data:/var/lib/postgresql/data \
  pgvector/pgvector:pg17

4.2 创建数据库

1
2
3
CREATE DATABASE interview_guide;
-- 启用 pgvector 扩展(Spring AI 会自动创建)
CREATE EXTENSION vector;

4.3 Spring AI 配置

1
2
3
4
5
6
7
8
spring:
  ai:
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1024
        initialize-schema: true

五、项目实现

5.1 依赖配置

1
2
3
4
5
6
dependencies {
    // Spring AI OpenAI (兼容 DashScope)
    implementation "org.springframework.ai:spring-ai-starter-model-openai"
    // PostgreSQL Vector Store
    implementation "org.springframework.ai:spring-ai-starter-vector-store-pgvector"
}

5.2 文档上传服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class KnowledgeBaseUploadService {

    private final VectorStore vectorStore;
    private final DocumentParser documentParser;
    private final EmbeddingModel embeddingModel;

    public void uploadDocument(MultipartFile file, String category) {
        // 1. 解析文档
        String content = documentParser.parse(file);

        // 2. 文本分割
        List<TextChunk> chunks = textSplitter.split(content);

        // 3. 构建 Document 对象
        List<Document> documents = chunks.stream()
            .map(chunk -> Document.builder()
                .text(chunk.getContent())
                .metadata(Map.of("category", category))
                .build())
            .toList();

        // 4. 存储到向量数据库
        vectorStore.add(documents);
    }
}

5.3 RAG 问答服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Service
public class KnowledgeBaseQueryService {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;
    private final PromptTemplate ragPromptTemplate;

    public String query(String question) {
        // 1. 查询向量化
        Embedding embedding = embeddingModel.embed(question);

        // 2. 相似度检索
        List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.builder()
                .query(question)
                .topK(5)
                .build()
        );

        // 3. 构建上下文
        String context = relevantDocs.stream()
            .map(doc -> doc.getContent())
            .collect(Collectors.joining("\n\n"));

        // 4. 构建 Prompt
        String systemPrompt = ragPromptTemplate.render(Map.of("context", context));

        // 5. 调用 LLM
        return chatClient.prompt()
            .system(systemPrompt)
            .user(question)
            .call()
            .content();
    }
}

5.4 RAG 提示词模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# RAG System Prompt
你是一个知识库问答助手。请根据以下参考信息回答用户的问题。

## 参考信息
{context}

## 要求
1. 仅基于参考信息回答,不要编造
2. 如果参考信息不足以回答,请说明"根据现有信息无法回答"
3. 回答要条理清晰,尽量引用原文

六、文本分割策略

6.1 分割策略对比

策略 优点 缺点
固定大小分割 简单实现 可能切断语义单元
按段落分割 保留完整语义 段落长度不均
按标题层级分割 结构清晰 需要识别标题

6.2 Overlap 的重要性

相邻 chunk 之间保留重叠,可以防止上下文断裂:

1
2
3
Chunk 1: [段落1 + 段落2 + 段落3]
Chunk 2: [段落3 + 段落4 + 段落5]  ← 段落3重复
Chunk 3: [段落5 + 段落6 + 段落7]

6.3 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public List<TextChunk> splitWithOverlap(String content, int chunkSize, int overlapSize) {
    List<TextChunk> chunks = new ArrayList<>();
    int start = 0;

    while (start < content.length()) {
        int end = Math.min(start + chunkSize, content.length());
        String chunk = content.substring(start, end);
        chunks.add(new TextChunk(chunk, start));

        start = end - overlapSize;  // 减去重叠部分
        if (start >= content.length()) break;
    }

    return chunks;
}

七、向量检索优化

7.1 混合检索

为什么需要混合检索?

  • 向量检索:捕捉语义相似性
  • BM25 关键词检索:捕捉精确关键词匹配

实现方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 1. 向量检索
List<Document> vectorResults = vectorStore.similaritySearch(
    SearchRequest.builder().query(question).topK(10).build()
);

// 2. BM25 关键词检索
List<Document> keywordResults = bm25Indexer.search(question, 10);

// 3. 结果融合
List<Document> fusedResults = fusion.merge(vectorResults, keywordResults);

7.2 Rerank 精排

Cross-Encoder 精排

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public List<Document> rerank(List<Document> candidates, String query) {
    // 对顶部候选进行更精细的相似度计算
    return candidates.stream()
        .sorted((doc1, doc2) -> {
            double score1 = crossEncoder.score(doc1.getContent(), query);
            double score2 = crossEncoder.score(doc2.getContent(), query);
            return Double.compare(score2, score1);
        })
        .limit(5)
        .toList();
}

7.3 元数据过滤

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 按分类过滤检索
List<Document> results = vectorStore.similaritySearch(
    SearchRequest.builder()
        .query(question)
        .topK(10)
        .filterExpression(
            MetadataFilterExpression.builder()
                .eq("category", "Java")
                .build()
        )
        .build()
);

八、生产环境优化

8.1 异步向量化

大文档向量化耗时长,应异步处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 发送消息到 Redis Stream
@Async
public void vectorizeAsync(Long documentId, String content) {
    vectorizeProducer.send(documentId, content);
}

// Stream Consumer 处理
@StreamListener
public void handleVectorization(VectorMessage message) {
    List<Document> chunks = splitDocument(message.getContent());
    vectorStore.add(chunks);
    updateDocumentStatus(message.getDocumentId(), COMPLETED);
}

8.2 语义缓存

高频相似问题直接命中缓存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public String queryWithCache(String question) {
    // 1. 计算问题向量
    String questionHash = hash(question);

    // 2. 检查缓存
    String cachedAnswer = redis.get("rag:cache:" + questionHash);
    if (cachedAnswer != null) {
        return cachedAnswer;
    }

    // 3. 执行 RAG 查询
    String answer = ragQuery(question);

    // 4. 写入缓存
    redis.setex("rag:cache:" + questionHash, 3600, answer);

    return answer;
}

8.3 监控指标

指标 说明 工具
检索延迟 Top-K 检索耗时 Micrometer
召回率 检索结果相关性 人工评估
Token 消耗 Prompt/Completion Tokens Prometheus
缓存命中率 语义缓存命中比例 Redis Info

九、常见问题与解决方案

问题 原因 解决方案
检索效果差 chunk 大小不合理 调整 chunk size 和 overlap
幻觉严重 检索片段不相关 增加 Rerank 或过滤低分结果
响应慢 向量检索慢 优化索引参数或换 HNSW
上下文截断 chunk 太多超出窗口 限制 Top-K 或做二次检索
关键词搜不到 依赖向量检索 增加 BM25 混合检索

十、总结

RAG 是企业级 AI 知识库的核心方案:

  1. 索引阶段:文档解析 → 文本分割 → 向量化 → 存储
  2. 检索阶段:查询向量化 → 相似度搜索 → 上下文增强 → LLM 生成
  3. 技术选型:PostgreSQL + pgvector 一站式方案,运维简单
  4. 优化策略:混合检索、Rerank、语义缓存、异步处理
使用 Hugo 构建
主题 StackJimmy 设计

发布了 27 篇文章 | 共 60988 字