背景:
现在用 AI 智能体干活的时候,有个挺现实的瓶颈------模型的上下文窗口就那么大,它又没看过我们公司内部的文档和代码,所以很多内部的东西它根本不知道。结果就是经常答非所问,或者一本正经地胡说八道。
具体表现为两种场景:
一是业务术语它听不懂。
比如"提单人结算"这种内部叫法,或者某个字段长度限制是 200 个字符这种细节,这些信息要么躺在 Word 文档里,要么只存在老员工的脑子里,模型不可能自己悟出来。
二是生成的代码水土不服。
让它写个接口调用,它不知道其他系统提供了什么 API,也不知道团队之前的代码风格和实现套路。写出来的东西语法没问题,但跟现有的系统对不上,没法直接用。
所以现在的做法是,在它回答之前,先把相关的文档片段、接口定义之类的东西从知识库里捞出来,塞进 Prompt 里让它参考。这就是 RAG 在做的事情------说白了就是让模型"带着资料回答问题"。
下来就是我们搭建一个RAG,根据用户提出的问题关联知识库后再统一发送给LLM后回答问题
本地知识库问答系统(RAG)搭建手册
基于 Spring Boot 4.0.5 + Qdrant + 通义千问 Embedding + DeepSeek Chat 的轻量级 RAG 系统
一、项目概述
本项目是一个本地知识库问答系统(RAG, Retrieval-Augmented Generation)。用户上传文档(PDF/Word/Excel 等)后,系统自动完成解析、切片、向量化并存入本地向量数据库 Qdrant。后续用户提问时,通过语义检索召回相关文档片段,拼接 Prompt 后交由 DeepSeek 大模型生成最终答案。
核心能力
| 能力 | 说明 |
|---|---|
| 多格式文档解析 | 支持 PDF、Word、Excel、PPT、HTML、JSON、CSV、Markdown 等 |
| 智能文本切片 | 滑动窗口 + 重叠策略,保持语义连贯 |
| 文本向量化 | 调用阿里百炼 Qwen text-embedding-v3,输出 1024 维向量 |
| 向量存储与检索 | 本地 Qdrant 向量数据库,COSINE 相似度 + 过滤 |
| RAG 增强生成 | 召回 Top-K 文档片段 + Prompt 模板 + DeepSeek Chat |
二、技术栈
| 层次 | 技术 | 用途 |
|---|---|---|
| 框架 | Spring Boot 4.0.5 + Java 17 | 应用基础 |
| 文档解析 | Apache Tika 2.9.1 | 统一解析多种文档格式 |
| 向量数据库 | Qdrant 1.18.3(官方 Java SDK) | 本地向量存储与语义检索 |
| HTTP 客户端 | Spring WebFlux(WebClient) | 异步调用外部 API |
| 默认 Embedding | 阿里百炼 · 通义千问 text-embedding-v3 | 文本向量化 |
| 备选 Embedding | 智谱 AI GLM embedding-2 | 备用向量化 |
| 备选 Embedding | DeepSeek Embedding | 备用向量化 |
| 大模型 | DeepSeek Chat v4-pro | 最终答案生成 |
| 工具 | Lombok、Hutool 5.8.32 | 简化开发 |
三、项目结构
src/main/java/com/example/demo/
├── DemoApplication.java # 启动入口
├── config/
│ ├── AppConfig.java # 业务参数(分片、RAG)
│ ├── DeepSeekConfig.java # DeepSeek WebClient 配置
│ ├── GlmConfig.java # GLM WebClient 配置
│ ├── QdrantConfig.java # Qdrant 客户端 + Collection 初始化
│ └── QwenConfig.java # Qwen WebClient 配置
├── controller/
│ ├── KnowledgeBaseController.java # 知识库管理 API
│ └── QAController.java # 问答 API(非流式 + SSE 流式)
├── exception/
│ ├── GlobalExceptionHandler.java # 全局异常处理
│ └── KnowledgeBaseException.java # 业务异常
└── service/
├── KnowledgeBaseService.java # ★ 知识库聚合服务
├── document/
│ ├── DocumentParseService.java # ★ 文档解析(Tika)
│ └── TextChunkService.java # ★ 文本切片(滑动窗口)
├── embedding/
│ ├── EmbeddingProvider.java # ★ Embedding 策略接口
│ ├── EmbeddingService.java # ★ Embedding 门面
│ ├── QwenEmbeddingProvider.java # ★ 默认实现(@Primary)
│ ├── GlmEmbeddingProvider.java # 备选实现
│ └── DeepSeekEmbeddingProvider.java # 备选实现
├── qdrant/
│ └── QdrantVectorService.java # ★ Qdrant 向量存储
└── rag/
├── RAGService.java # ★ RAG 检索增强生成
└── PromptTemplateService.java # ★ Prompt 模板
Prompt 模板
注:标 ★ 为 RAG 系统核心组件。
四、搭建思路与核心流程
4.1 整体数据流
┌─ 文档入库 ─────────────────────────────────────────────────────────┐
│ │
│ 上传文件 → 文档解析 → 文本切片 → Embedding向量化 → 存入Qdrant │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─ 问答检索 ─────────────────────────────────────────────────────────┐
│ │
│ 用户提问 → Embedding向量化 → Qdrant语义检索 → Prompt拼接 │
│ → DeepSeek生成答案 → 返回用户 │
│ │
└─────────────────────────────────────────────────────────────────────┘
4.2 第一步:文档解析(DocumentParseService)
设计思路:前端上传 MultipartFile,后端统一用 Apache Tika 解析。Tika 内置了 20+ 种格式的解析器,无需手动区分文件类型。
// 核心调用:一行代码完成解析 String content = new Tika().parseToString(file.getInputStream());
支持的格式:PDF、DOCX、XLSX、PPTX、HTML、JSON、CSV、TXT、Markdown 等。
4.3 第二步:文本切片(TextChunkService)
设计思路 :大模型有上下文长度限制(token 数),文档内容需要切成片段。采用滑动窗口重叠策略:
配置项(application.yaml):
chunk.size: 500 # 每个 chunk 最大 500 字符
chunk.overlap: 100 # 相邻 chunk 重叠 100 字符
切分效果示意:
Chunk 1: [████████████████████████████]──────────── 500字符
Chunk 2: ────────────[████████████████████████████] 500字符(前100字符与Chunk1重叠)
为什么要有重叠:避免关键信息恰好落在切片边界上导致语义断裂。比如用户问"什么是向量数据库",如果"向量"在 Chunk1 末尾、"数据库"在 Chunk2 开头,无重叠时两个片段都不完整,有重叠则 Chunk1 和 Chunk2 都可能召回。
4.4 第三步:文本向量化(EmbeddingService)
设计思路 :采用策略模式 解耦 Embedding 提供商。定义了 EmbeddingProvider 接口,三个实现:
public interface EmbeddingProvider { List<Float> embed(String text); // 单条向量化 List<List<Float>> batchEmbed(List<String> texts); // 批量向量化 }
| 实现类 | 优先级 | API 限制 | 向量维度 |
|---|---|---|---|
| QwenEmbeddingProvider | @Primary 默认 | 单次 ≤10 条 | 1024 |
| GlmEmbeddingProvider | 备选 | 单次 ≤8 条 | 1024 |
| DeepSeekEmbeddingProvider | 备选 | 无硬限制 | 1024 |
为什么可以灵活切换 :三个 Provider 都输出 1024 维向量,Qdrant Collection 的维度是固定的。切换 Provider 不需要重建向量库,只需修改 @Primary 注解和对应的 application.yaml 配置即可。
自动分批机制:当批量文本超过 API 单次上限时,自动拆分串行调用,批次间间隔可配置避免限流。
4.5 第四步:向量存储(QdrantVectorService)
设计思路:选用 Qdrant 作为本地向量数据库,原因:
- 本地部署,零网络依赖
- 原生 Java SDK,无需 HTTP 胶水层
- 支持 gRPC 高性能通信(端口 6334)
- 内置 COSINE / DOT / EUCLID 等多种距离算法
Qdrant Collection 配置: collectionName: local_dev_knowledge vectorDimension: 1024 # 与 Embedding Provider 输出维度一致 distanceType: COSINE # 余弦相似度
每次文档入库时,将分片的文本内容 + 向量 + 元数据(文件名、chunk序号)一起存入 Qdrant。
4.6 第五步:RAG 检索增强生成(RAGService + PromptTemplateService)
核心流程:
1. 用户提问 "什么是RAG"
2. EmbeddingService.embed("什么是RAG") → 1024维向量
3. QdrantVectorService.search(向量, topK=5) → 5个最相关文档片段
4. PromptTemplateService.buildPrompt(问题, 召回片段) → 拼接完整Prompt
5. DeepSeek Chat API 调用 → 生成答案
Prompt 模板设计(PromptTemplateService):
你是一个专业的知识助手。请基于以下参考资料回答用户问题。
参考文档:
---
{chunk1}
---
{chunk2}
---
...
用户问题:{question}
要求:
1. 仅基于参考文档回答
2. 如果资料不足以回答,请明确说明
3. 回答要准确、简洁
为什么这样设计 Prompt:
- 明确指令角色:让模型知道自己是"知识助手"
- 限定回答范围:避免模型胡编乱造(幻觉)
- 防止越界回答:资料不足时诚实声明"不知道"
4.7 问答 API 设计(QAController)
提供两种问答方式:
| 接口 | 说明 |
|---|---|
POST /api/qa/ask |
非流式:等待完整答案返回 |
GET /api/qa/ask/stream |
SSE 流式:逐字输出,体验更好 |
SSE 流式输出采用了 SseEmitter 实现,用户提问后答案逐字推送到前端,类似 ChatGPT 的打字效果。
五、关键设计决策
5.1 策略模式解耦 Embedding 提供商
问题 :不同 Embedding API 的请求/响应格式不同(Qwen 请求体用 {"texts": [...]}、GLM 用 {"input": [...]}),如果硬编码在一个类中,切换成本极高。
方案 :定义 EmbeddingProvider 接口,三个实现各自处理自己的 API 差异。EmbeddingService 作为门面,只依赖接口不依赖实现,扩展新提供商只需新增一个类。
5.2 滑动窗口重叠切片
问题:固定长度切片会导致语义断裂,关键信息可能被切到两个 chunk 之间。
方案:chunk_size=500, overlap=100,每个 chunk 与前后各重叠 100 字符,保证语义连续性。
5.3 自动分批 + 限流保护
问题 :批量 Embedding 时,如果一次发 50 条文本,Qwen API 会返回 InvalidParameter: batch size should not be larger than 10。
方案 :每个 Provider 内置自动分批逻辑,超限自动拆分为子批次串行调用,批次间间隔 batch-interval-ms 避免触发限流。
5.4 重试机制 + Retry-After 精确等待
问题:外部 API 可能临时不可用或返回 429(Too Many Requests)。
方案:
- 优先读取 429 响应中的
Retry-After响应头,按服务端指示精确等待 - 无
Retry-After时使用指数退避(1s → 2s → 4s,上限 10s / 30s) - 最大重试次数可配置
5.5 配置驱动,无硬编码
按照编码规范要求,所有 IP、端口、API Key、模型名称等均通过 application.yaml 配置,不写死在代码中。
六、配置参数一览
# DeepSeek Chat 配置(答案生成)
deepseek:
api-key: sk-xxx
chat-model: deepseek-v4-pro
connect-timeout: 30s
# 阿里百炼 Qwen Embedding(默认)
qwen:
api-key: sk-xxx
embed-model: text-embedding-v3
max-batch-size: 10 # API 硬限制 ≤10
batch-interval-ms: 500 # 批次间延迟
retry-max-attempts: 3 # 最大重试次数
# 智谱 AI GLM Embedding(备选)
glm:
api-key: xxx
embed-model: embedding-2
max-batch-size: 8 # API 限制 ≤8
batch-interval-ms: 3000 # GLM 限流严格,间隔放大
retry-max-attempts: 3
# Qdrant 向量数据库
qdrant:
host: 127.0.0.1
port: 6334 # gRPC 端口
collection-name: local_dev_knowledge
vector-dimension: 1024
distance-type: COSINE # 余弦相似度
# 分片参数
chunk:
size: 500 # 每个 chunk 字符数
overlap: 100 # 重叠字符数
# RAG 检索参数
rag:
top-k: 5 # 召回片段数
temperature: 0.1 # 生成温度(低=确定性高)
七、启动与使用
前置条件
- JDK 17+
- Qdrant 服务运行在
127.0.0.1:6334 - 配置好
application.yaml中的 API Key
八、补充
Qdrant的下载地址:
https://github.com/qdrant/qdrant/releases
Qdrant的UI页面下载地址
https://github.com/qdrant/qdrant-web-ui/releases
一次用户提问请求向量数据库的过程

codebuddy配置的mcp

