SpringBoot 集成 DeepSeek 实现 RAG 文档问答

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 的 ChatModelChatClient.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;生产环境可替换为 RedisVectorStorePgVectorStore 等,接口一致。
  • 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 文档问答最小可用方案,核心就两个方法:

  1. ingestDocument():文档 → Tika 解析 → Token 切块 → Embedding 向量化 → 存入向量库
  2. query():问题 → 向量检索 topK → 拼接上下文 → 构造 Prompt → DeepSeek 生成回答

整体方案的特点:

  • 零外部 Embedding 成本:用本地 ONNX 模型,不依赖第三方 Embedding API
  • 防幻觉:System Prompt 约束 + 相似度阈值双重控制
  • 多格式文档支持:Tika 天然支持 PDF/Word/TXT 等
  • 代码简洁:Spring AI 封装度高,核心逻辑不到 100 行

可扩展方向:

  • 向量库替换为 PgVector / Redis / Milvus 等持久化方案
  • 加入文档管理接口,支持动态上传入库
  • 接入 Function Calling,让模型能调用外部工具
  • 增加多轮对话上下文记忆
相关推荐
2501_936415692 小时前
可变参数&综合练习&斗地主游戏
java·windows·游戏
极客先躯3 小时前
高级java每日一道面试题-2026年05月03日-实战篇[Docker]-如何实现容器化环境的数据加密?
java·运维·docker·容器·金融·加解密·高级面试
xqqxqxxq3 小时前
Java Socket 多人聊天室(私聊+群聊)技术笔记(V4版本)
java·网络·笔记
ddshub_cc3 小时前
2026 AI API 定价对比:GPT-5.6 vs Claude Fable 5 vs Opus 5,哪款模型最划算?
人工智能·gpt·ai·chatgpt
wang09073 小时前
自己动手写一个spring之aop_1
java·后端·spring
国科安芯4 小时前
FreeRTOS RISC-V 浮点上下文切换移植:在 IAR 工程中完整保存 FPU 寄存器
java·开发语言·单片机·嵌入式硬件·算法·系统架构·risc-v
deng12044 小时前
【Thread类的用法(1)-- Thread类的基本用法】
java·java-ee
suconnect5 小时前
Spring Boot接入企业RAG:文档切分、向量检索、权限过滤和答案溯源
java·spring boot·后端
weixin_BYSJ19875 小时前
springboot校园自习室管理小程序---附源码32142
java·javascript·spring boot·python·django·flask·php