企业级 RAG 实现

Retrieval Augmented Generation:让 AI 基于私有知识库回答问题


RAG 工作流程

1
2
3
文档 → 分块 (Chunking) → 向量化 (Embedding) → 存储 (Vector DB)

用户问题 → 向量化 → 相似度搜索 → 检索上下文 → 注入 Prompt → LLM → 回答

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
src/main/java/com/example/rag/
├── RAGApplication.java
├── controller/
│ └── RagController.java
├── service/
│ ├── DocumentService.java # 文档加载与分块
│ ├── EmbeddingService.java # 向量化
│ └── RetrievalService.java # 检索
├── repository/
│ └── VectorStoreConfig.java # 向量数据库配置
└── model/
└── DocumentChunk.java # 分块实体

Step 1: 文档加载与分块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class DocumentService {

public List<DocumentChunk> loadAndChunk(String filePath) {
// 使用 Spring AI 的 DocumentReader
Document document = new ClassPathResourceDocumentReader(filePath)
.read();

// 按段落分块
TextSplitter splitter = new TextSplitter(500, 100);
List<Document> chunks = splitter.split(document);

return chunks.stream()
.map(doc -> new DocumentChunk(doc.getContent(), doc.getMetadata()))
.toList();
}
}

Step 2: 向量化并存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class EmbeddingService {

@Autowired
private EmbeddingModel embeddingModel;

@Autowired
private VectorStore vectorStore; // InMemoryVectorStore / PGVector / Milvus...

public void indexDocuments(List<DocumentChunk> chunks) {
for (DocumentChunk chunk : chunks) {
// 向量化
Embedding embedding = embeddingModel.embed(chunk.getContent());

// 存储到向量数据库
vectorStore.add(chunk.getContent(), embedding, chunk.getMetadata());
}
}
}

Step 3: 检索并增强生成

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
36
37
38
39
@Service
public class RetrievalService {

private final ChatClient chatClient;
private final VectorStore vectorStore;

public String answer(String question) {
// 1. 向量化问题
Embedding questionEmbedding = embeddingModel.embed(question);

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

// 3. 构建 Prompt(注入检索到的上下文)
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n"));

String prompt = """
基于以下参考资料回答问题。如果资料中没有答案,请如实说明。

参考资料:
%s

问题:%s
""".formatted(context, question);

// 4. 调用 LLM
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}

向量数据库选型

数据库特点适用场景
InMemoryVectorStore无需安装,测试用开发调试
PostgreSQL + pgvector企业级,支持 SQL 联合查询生产环境
Milvus海量向量,高性能大规模 RAG
Qdrant轻量,支持云原生微服务架构

常见优化

  • 分块策略: overlap=50~100 字符,减少边界丢失
  • 元数据过滤: 按时间、来源、标签预过滤
  • 重排序 (Rerank): 用 BGE-Reranker 二次排序