前言
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 知识库的核心方案:
- 索引阶段:文档解析 → 文本分割 → 向量化 → 存储
- 检索阶段:查询向量化 → 相似度搜索 → 上下文增强 → LLM 生成
- 技术选型:PostgreSQL + pgvector 一站式方案,运维简单
- 优化策略:混合检索、Rerank、语义缓存、异步处理