SpringBoot 集成 DeepSeek 实现 RAG 文档问答
一、概述
1.1 什么是 RAG
RAG(Retrieval-Augmented Generation,检索增强生成) 是一种将「信息检索」与「大语言模型」相结合的技术方案。
大模型本身存在两个痛点:
- 知识截止:训练数据有截止日期,无法回答最新信息;
- 幻觉问题:对不熟悉的内容容易"一本正经地胡说八道"。
RAG 的核心思路是:先从外部知识库(文档)中检索出与用户问题最相关的片段,再把这些片段连同问题一起喂给大模型,让模型"看着资料答题",从而既准确又可控。
1.2 整体流程
一个完整的 RAG 流程分为两个阶段:

1.3 技术选型
| 组件 | 选型 | 说明 |
|---|---|---|
| 框架 | Spring Boot 3.5.16 + Spring AI 1.0.1 | Spring 官方 AI 集成框架 |
| JDK | Java 21 | |
| 大模型 | DeepSeek(deepseek-chat) | 通过 Spring AI 的 DeepSeek Starter 接入 |
| Embedding 模型 | all-MiniLM-L6-v2(本地 ONNX) | 无需调用外部 API,本地推理,零成本 |
| 向量存储 | SimpleVectorStore(内存版) | 轻量级,适合 demo 与学习 |
| 文档解析 | Apache Tika(spring-ai-tika-document-reader) | 支持 PDF / Word / TXT 等多种格式 |
为什么用本地 ONNX Embedding? 因为 DeepSeek 官方 API 目前没有开放的 Embedding 接口,所以我们用 Spring AI 的
TransformersEmbeddingModel加载本地 ONNX 模型来完成向量化,完全不依赖第三方 API Key。
二、pom 文件引入依赖
核心依赖如下(仅展示 AI 相关部分,完整 pom 见项目源码):
xml
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ① DeepSeek 大模型接入 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
<!-- ② 本地 ONNX Embedding 模型(无需 API Key) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-transformers</artifactId>
</dependency>
<!-- ③ 向量存储(包含 SimpleVectorStore 内存版) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store</artifactId>
</dependency>
<!-- ④ 文档解析(支持 PDF/Word/TXT 等) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
</dependencies>
<!-- Spring AI 版本统一管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
四个依赖各司其职:
| 依赖 | 作用 |
|---|---|
spring-ai-starter-model-deepseek |
自动配置 DeepSeek 的 ChatModel 与 ChatClient.Builder |
spring-ai-starter-model-transformers |
提供 TransformersEmbeddingModel,加载本地 ONNX 模型做向量化 |
spring-ai-vector-store |
提供 SimpleVectorStore(内存向量库)及 SearchRequest 检索 API |
spring-ai-tika-document-reader |
提供 TikaDocumentReader,解析多种格式文档为 Document 对象 |
三、配置文件
3.1 application.yml
yaml
server:
port: 8080
spring:
profiles:
active: dev
application:
name: demo
servlet:
context-path: /
3.2 application-dev.yml
DeepSeek 的 API Key 与模型参数配置在这里:
yaml
spring:
ai:
deepseek:
api-key: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 替换为你的 DeepSeek API Key
chat:
options:
model: deepseek-chat # 可选: deepseek-chat / deepseek-reasoner
temperature: 0.8
获取 API Key :前往 DeepSeek 开放平台 注册后在「API Keys」页面创建。
3.3 本地 ONNX 模型文件
Embedding 模型文件放在 src/main/resources/onnx/all-MiniLM-L6-v2/ 目录下:
src/main/resources/
└── onnx/
└── all-MiniLM-L6-v2/
├── model.onnx # 模型文件
└── tokenizer.json # 分词器文件
all-MiniLM-L6-v2是一个轻量级句子向量模型(384 维),可从 HuggingFace 下载,放到上述目录即可。
四、实现代码
4.1 AI 配置类:RagConfig
手动注册 Embedding 模型、向量库和 ChatClient 三个 Bean:
java
package com.zhh.web_demo_ai.config;
import com.zhh.web_demo_ai.service.OrderAssistantService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.transformers.TransformersEmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
/**
* AI 配置类:Embedding 模型、向量存储、ChatClient
*/
@Configuration
public class RagConfig {
/**
* 本地 ONNX Embedding 模型,使用已下载的模型文件,不走 HuggingFace 网络下载
*/
@Bean
public TransformersEmbeddingModel embeddingModel() {
var model = new TransformersEmbeddingModel();
model.setModelResource(new ClassPathResource("onnx/all-MiniLM-L6-v2/model.onnx"));
model.setTokenizerResource(new ClassPathResource("onnx/all-MiniLM-L6-v2/tokenizer.json"));
return model;
}
/**
* 内存版向量存储,依赖上面的 EmbeddingModel
*/
@Bean
public SimpleVectorStore simpleVectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
/**
* ChatClient
*/
@Bean
public ChatClient chatClient(ChatClient.Builder builder, OrderAssistantService orderAssistantService) {
return builder
//.defaultTools(orderAssistantService) // 如需 Function Calling 可放开
.build();
}
}
要点说明:
TransformersEmbeddingModel指定本地 ONNX 文件路径,启动时加载模型,之后向量化全在本地完成。SimpleVectorStore把向量存在内存里,适合 demo;生产环境可替换为RedisVectorStore、PgVectorStore等,接口一致。ChatClient由 DeepSeek Starter 自动注入ChatClient.Builder,直接build()即可使用。
4.2 RAG 核心服务:YourRagService
这是整个 RAG 的核心,包含入库 和问答两个方法:
java
package com.zhh.web_demo_ai.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* RAG 文档问答服务
*/
@Slf4j
@Service
public class YourRagService {
@Autowired
private SimpleVectorStore vectorStore;
@Autowired
private ChatClient chatClient;
/**
* 文档入库:读取 → 切块 → 向量化 → 存入向量库
*/
public void ingestDocument(Resource documentResource) {
// 1. 读取文档(Tika 自动识别 PDF/Word/TXT 等格式)
TikaDocumentReader reader = new TikaDocumentReader(documentResource);
List<Document> documents = reader.read();
log.info("读取到 {} 个原始文档段落", documents.size());
// 2. 文本切块(800 token/块)
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(800)
.withMinChunkSizeChars(50)
.build();
List<Document> chunks = splitter.apply(documents);
log.info("切分后得到 {} 个文本块", chunks.size());
// 3. 存入向量库(add 时自动调用 EmbeddingModel 向量化)
vectorStore.add(chunks);
log.info("已将所有文本块存入向量库");
}
/**
* 文档问答:检索相关块 → 拼成 Prompt → 发给 DeepSeek
*/
public String query(String question) {
// 1. 从向量库检索最相关的 4 个文本块
List<Document> relevantChunks = vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(4)
.similarityThreshold(0.0)
.build()
);
if (relevantChunks.isEmpty()) {
return "未在文档中找到相关内容。";
}
log.info("检索到 {} 个相关文本块", relevantChunks.size());
// 2. 将检索到的文本块拼成上下文
String context = relevantChunks.stream()
.map(Document::getText)
.collect(Collectors.joining("\n\n---\n\n"));
// 3. 构建 Prompt(System 约束 + User 拼上下文)
var systemMessage = new SystemMessage(
"你是一个文档助手。请严格根据下面提供的文档内容回答问题。" +
"如果文档中没有相关信息,请直接说「文档中未找到相关信息」,不要编造答案。"
);
var userMessage = new UserMessage(
"文档内容:\n\n" + context + "\n\n问题:" + question
);
// 4. 调用 DeepSeek 生成回答
var prompt = new Prompt(List.of(systemMessage, userMessage));
String answer = chatClient.prompt(prompt).call().content();
log.info("问题:{}", question);
log.info("回答:{}", answer);
return answer;
}
}
核心逻辑拆解:
ingestDocument() --- 入库三步走:
| 步骤 | 组件 | 作用 |
|---|---|---|
| 读取 | TikaDocumentReader |
把 PDF/Word/TXT 等解析为 Document 列表 |
| 切块 | TokenTextSplitter |
按 token 数(800/块)切分,避免单块过长超出模型上下文 |
| 存储 | vectorStore.add() |
存入时自动调用 Embedding 模型将文本转为向量 |
query() --- 问答四步走:
| 步骤 | 说明 |
|---|---|
| 检索 | similaritySearch 用问题的向量去向量库里找最相似的 topK=4 个文本块 |
| 拼接 | 把检索到的文本块用分隔符拼成一段上下文 |
| 构造 Prompt | System Message 约束模型"只根据文档回答、不许编造";User Message 把上下文和问题一起给模型 |
| 生成 | 调用 DeepSeek ChatClient 生成最终回答 |
防幻觉关键 :
SystemMessage中的约束语是控制幻觉的核心------明确告诉模型"找不到就说找不到,不要编"。配合similarityThreshold过滤低相关度结果,双重保险。
4.3 测试入口:RagChatTest
java
package com.zhh.web_demo_ai.ai;
import com.zhh.web_demo_ai.service.YourRagService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
/**
* 简单RAG 文档问答demo:基于入库的文档回答问题
*/
@Slf4j
@SpringBootTest
public class RagChatTest {
@Autowired
private ChatClient chatClient;
@Autowired
private YourRagService yourRagService;
/**
* 简单问答(冒烟测试,验证 DeepSeek 连通性)
*/
@Test
public void show() {
String msg = chatClient.prompt().user("你好").call().content();
System.out.println(msg);
}
/**
* RAG 文档问答:基于入库的文档回答问题
*/
@Test
public void testRagQuery() {
// 先入库
var resource = new ClassPathResource("test-doc.txt");
yourRagService.ingestDocument(resource);
// 问一个文档中有明确答案的问题
String question = "云上贵州的核心产品是什么?公司有多少人?";
String answer = yourRagService.query(question);
System.out.println("问题:" + question);
System.out.println("回答:" + answer);
System.out.println("---");
// 问一个文档中没有的问题,验证不会胡编
String question2 = "云上贵州2025年的营收是多少?";
String answer2 = yourRagService.query(question2);
System.out.println("问题:" + question2);
System.out.println("回答:" + answer2);
}
}
测试用的文档 test-doc.txt(放在 src/test/resources/ 下)内容是一段关于"云上贵州"公司介绍的文本。测试里故意问了两个问题:
- 文档里有的:核心产品是什么、多少人 → 验证能准确回答
- 文档里没有的:2025年营收 → 验证不会编造
五、运行效果
执行 testRagQuery() 测试,预期输出类似:

第一个问题从文档中精准提取了答案;第二个问题文档里没有,模型遵守了 System Prompt 的约束,没有编造。
六、总结
本文实现了一个基于 Spring Boot + Spring AI + DeepSeek 的 RAG 文档问答最小可用方案,核心就两个方法:
ingestDocument():文档 → Tika 解析 → Token 切块 → Embedding 向量化 → 存入向量库query():问题 → 向量检索 topK → 拼接上下文 → 构造 Prompt → DeepSeek 生成回答
整体方案的特点:
- ✅ 零外部 Embedding 成本:用本地 ONNX 模型,不依赖第三方 Embedding API
- ✅ 防幻觉:System Prompt 约束 + 相似度阈值双重控制
- ✅ 多格式文档支持:Tika 天然支持 PDF/Word/TXT 等
- ✅ 代码简洁:Spring AI 封装度高,核心逻辑不到 100 行
可扩展方向:
- 向量库替换为
PgVector/Redis/Milvus等持久化方案 - 加入文档管理接口,支持动态上传入库
- 接入 Function Calling,让模型能调用外部工具
- 增加多轮对话上下文记忆