从文档上传到智能问答,拆解企业级 RAG 系统的每一步技术实现。
🤖 RAG架构 📊 Elasticsearch 📨 Kafka ⚡ 200ms响应
项目概述 #
智库RAG 是我独立设计并开发的企业级智能知识库管理系统。用户可以上传各种格式的文档(Word、PDF、TXT等),系统会自动解析、向量化并建立索引,然后通过自然语言对话的方式查询知识库,获得基于自身文档的 AI 生成回答。
技术栈 #
| 层级 | 技术选型 |
|---|---|
| 后端框架 | Spring Boot 3.4.2 (Java 17) |
| 搜索引擎 | Elasticsearch 8.10 + IK分词器 |
| 向量模型 | 豆包 Embedding(2048维) |
| 大语言模型 | DeepSeek API / 本地 Ollama |
| 消息队列 | Apache Kafka |
| 缓存 | Redis |
| 文件存储 | MinIO |
| 文档解析 | Apache Tika |
| 安全认证 | Spring Security + JWT |
| 实时通信 | WebSocket |
| 前端 | Vue 3 + TypeScript + Naive UI |
系统架构 #
graph TB
subgraph 用户端
A[Vue3 + Naive UI]
end
subgraph API层
B[Spring Boot REST API]
C[WebSocket Handler]
end
subgraph 异步流水线
D[Kafka Producer]
E[Tika 文档解析]
F[豆包 Embedding 向量化]
G[Elasticsearch 索引]
end
subgraph RAG检索
H[用户提问]
I[Embedding转换]
J[KNN向量召回]
K[BM25关键词匹配]
L[分数融合重排序]
M[增强Prompt构建]
N[DeepSeek生成回答]
end
subgraph 基础设施
O[(MySQL)]
P[(Redis)]
Q[(MinIO)]
R[(Elasticsearch)]
end
A --> B
A --> C
B --> D
D --> E --> F --> G --> R
H --> I --> J
H --> K
J --> L --> M --> N
B --> O
B --> P
B --> Q核心实现 #
一、BitMap 分片状态管理 #
这是我在这个项目中最满意的设计之一。
痛点:大文件上传时,需要记录每个分片的上传状态。如果用 HashMap,1000个分片大约需要 40KB。
方案:使用 Redis BitMap,每个分片只用1个bit表示:
@Service
public class ChunkUploadService {
private static final String UPLOAD_KEY = "upload:";
/**
* 标记分片已上传
* 1000个分片仅需 125 字节
*/
public void markUploaded(String fileId, int chunkIndex) {
String key = UPLOAD_KEY + fileId;
redisTemplate.opsForValue().setBit(key, chunkIndex, true);
}
/**
* 检查分片状态(秒传判断)
*/
public boolean isUploaded(String fileId, int chunkIndex) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.getBit(UPLOAD_KEY + fileId, chunkIndex)
);
}
/**
* 统计已上传分片数
*/
public long countUploaded(String fileId) {
return redisTemplate.execute(
(RedisCallback<Long>) con ->
con.stringCommands()
.bitCount(
(UPLOAD_KEY + fileId).getBytes()
)
);
}
}结合 Redis 缓存 + MinIO 断点续传,1GB 文件上传耗时从 15s 降至 3s。
⚠️ BitMap 的
setBit 操作是 O(1) 的,但 bitCount 是 O(N) 的。对于分片数量在万级以内的场景,性能完全没问题。如果分片数量达到百万级,建议使用 HyperLogLog 做近似统计。
二、双引擎检索:关键词 + 语义融合 #
RAG 系统最核心的部分——如何准确召回相关文档。单纯用关键词搜索,同义词和语义相似的问题召回差;单纯用向量搜索,精确匹配又不如关键词。两者结合效果最好:
graph LR
A[用户提问] --> B[豆包Embedding<br/>转为2048维向量]
A --> C[IK分词器<br/>提取关键词]
B --> D[Elasticsearch KNN<br/>向量语义召回]
C --> E[ES Match Query<br/>关键词匹配]
D --> F[分数融合 + 重排序]
E --> F
F --> G[Top-K 相关片段]SearchRequest request = SearchRequest.of(sr -> sr
.index("knowledge_base")
.size(10)
.query(q -> q.bool(b -> b
// 向量语义搜索
.should(s -> s.knn(k -> k
.field("embedding")
.queryVector(embeddingVector)
.numCandidates(50)
.k(10)
))
// BM25 关键词搜索
.should(s -> s.multiMatch(m -> m
.fields("title", "content")
.query(queryText)
.type(TextQueryType.BestFields)
))
.minimumShouldMatch("1")
))
);
💡 为什么是 2048 维?豆包 Embedding 模型在 2048 维时效果和性能的平衡点最好。维度太低语义信息丢失,太高则检索延迟增加且存储成本翻倍。
三、Kafka 异步文档处理流水线 #
文档上传后不需要同步等待处理完成,通过 Kafka 解耦:
sequenceDiagram
participant 用户
participant API
participant Kafka
participant Consumer
participant ES
用户->>API: 上传文档
API->>Kafka: 发送处理事件
API-->>用户: 200 OK(响应 < 200ms)
Note over Kafka,ES: 异步流水线
Kafka->>Consumer: 消费事件
Consumer->>Consumer: Tika 解析文档
Consumer->>Consumer: 豆包 Embedding 向量化
Consumer->>ES: 写入索引// Producer:上传即返回
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) {
String fileId = generateId();
minioService.upload(file, fileId);
kafkaTemplate.send("doc-process",
new DocProcessEvent(fileId, file.getName()));
return Result.ok(fileId); // < 200ms
}
// Consumer:异步处理
@KafkaListener(topics = "doc-process")
public void process(DocProcessEvent event) {
File file = minioService.download(event.getFileId());
String text = tikaParser.parse(file);
float[] vector = embeddingClient.embed(text);
esService.index(event.getFileId(), text, vector);
}经测试,500MB 文件上传响应仅需 200ms(之前是阻塞等待处理完成的)。
四、多轮对话上下文管理 #
用户和知识库对话时,需要保持上下文连贯性。使用 Redis List 存储对话历史,7天自动过期:
@Service
public class ChatHistoryService {
private static final Duration TTL = Duration.ofDays(7);
public void append(String sessionId, ChatMessage msg) {
String key = "chat:" + sessionId;
redisTemplate.opsForList().rightPush(key, msg);
redisTemplate.expire(key, TTL);
}
public List<ChatMessage> getRecent(String sessionId, int rounds) {
String key = "chat:" + sessionId;
Long size = redisTemplate.opsForList().size(key);
if (size == null || size == 0) return List.of();
int end = size.intValue() - 1;
int start = Math.max(0, end - rounds * 2 + 1);
return redisTemplate.opsForList().range(key, start, end);
}
}性能数据 #
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 1GB文件上传 | 15s | 3s | 5x |
| 500MB上传响应 | 阻塞等待 | 200ms | 异步化 |
| 分片状态存储 | Hash ~40KB | BitMap 125B | 99%↓ |
| 对话上下文 | 每次查库 | Redis缓存 | 实时 |
总结 #
这个项目让我从「会用Spring Boot写接口」跨越到了「能独立设计一个完整系统」。最大的收获不是学会某个具体技术,而是理解了如何用异步、缓存、消息队列这些手段来解决真实问题——BitMap 不是炫技而是真的省了 99% 的存储,Kafka 不是跟风而是真的把响应从秒级压到了毫秒级。