如何用RAG解决AI智能体的知识盲区?

背景:

现在用 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              # 生成温度(低=确定性高)

七、启动与使用

前置条件

  1. JDK 17+
  2. Qdrant 服务运行在 127.0.0.1:6334
  3. 配置好 application.yaml 中的 API Key

八、补充

Qdrant的下载地址:

https://github.com/qdrant/qdrant/releases

Qdrant的UI页面下载地址

https://github.com/qdrant/qdrant-web-ui/releases

一次用户提问请求向量数据库的过程

codebuddy配置的mcp

相关推荐
吹什么轩1 小时前
c++复习:模拟实现list来更好的理解和应用list
开发语言·c++
小白学大数据1 小时前
Python 爬虫实战:抓取汽车之家二手车成交价格与里程数据
开发语言·爬虫·python·汽车
深盾科技_Virbox1 小时前
软件加密工具怎么选:试用验证、采购成本与授权范围
开发语言·安全·软件需求
A_cainiao_A2 小时前
C++20:std::stop_source让线程说停就听
开发语言·c++20
Nemo_XP2 小时前
C# gridlookupedit选中内容重复还原操作
服务器·前端·c#
fīɡЙtīиɡ ℡2 小时前
深入学习JAVA并发编程(上)
java·开发语言·学习
zzzll11112 小时前
HashMap、HashTable、ConcurrentHashMap 详细区别与深度解析
开发语言·python
老师我太想进步了20263 小时前
C语言版讲解函数递归
c语言·开发语言
猿长大人3 小时前
C# | 函数式编程入门
开发语言·c#·.net