Java RAG 实战(第 8 篇):Spring Boot RAG 查询 API

系列导航

本篇不会改变 RAG 算法,而是把已经跑通的命令行流程变成一个可被网页、手机端或其他后端调用的 HTTP API。学习重点是分层、对象组装和错误边界。

为什么要学

前面的 RAG 程序只能从命令行运行。每次提问都要执行 Maven 命令,浏览器、前端或其他服务也无法直接复用它。

这一阶段使用 Spring Boot 把 RAG 流程变成一个长期运行的 HTTP 服务:调用方只需要提交问题,服务负责检索知识、调用模型并返回答案和来源。

text 复制代码
浏览器或其他应用
       ↓ HTTP POST /api/rag/ask
Spring Boot Controller
       ↓
RagService
       ├── bge-m3 → Qdrant 检索
       └── 检索结果 → Prompt → qwen3:14b
       ↓
答案和真实来源 JSON

这样做的重点不是换一种启动方式,而是学习一个 AI 功能如何成为可复用、可测试、可配置的后端接口。

05-spring-rag 是独立模块,不会修改 04-qdrant-rag。第 4 个模块继续保留命令行 RAG,第 5 个模块在查询 API 的基础上继续加入动态入库和浏览器工作台。本篇只聚焦最先完成的查询 API;后续能力分别在第 9、10 篇讲解。

本篇目标

完成后,你会:

  • 启动一个 Spring Boot RAG 服务。
  • 用 JSON 调用 POST /api/rag/ask。
  • 理解 Controller、Service 和外部客户端的分工。
  • 理解 answer 和 sources 是怎样生成的。
  • 用配置文件调整模型、Collection、Top-K 和相似度阈值。
  • 识别参数错误、知识不足和外部服务故障这三类结果。

学习前置

  • 已按第 7 篇创建 kubernetes_chunks 并完成入库。
  • 能解释"问题向量化 → Qdrant 检索 → Prompt → qwen3"。
  • Java 17、Maven、Ollama 和 Qdrant 已可用。
  • 知道 HTTP POST 请求会携带 JSON 请求体。

完成进度

  • 1. 确认 Ollama、模型和 Qdrant 正常
  • 2. 认识 api / service / client / config 的分工
  • 3. 启动 Spring Boot 并调用 /api/rag/ask
  • 4. 看懂 answer 与 sources 的不同来源
  • 5. 分别验证 HTTP 200、400 和 503
  • 6. 运行模块测试

第 1 步:确认依赖服务

本模块依赖 Ollama、bge-m3、qwen3:14b 和 Qdrant。先检查 Ollama:

bash 复制代码
ollama list
curl -s http://localhost:11434/api/tags | jq

模型列表中应包含:

text 复制代码
bge-m3
qwen3:14b

再检查 Qdrant:

bash 复制代码
docker ps --filter name=qdrant-study
curl -s http://localhost:6333/collections/kubernetes_chunks | jq '{
  status: .result.status,
  points_count: .result.points_count
}'

预期 Collection 状态为 green,并且 points_count 大于 0。

如果 Qdrant 容器已经创建但没有运行:

bash 复制代码
docker start qdrant-study

如果 Collection 还没有资料,先按照**《第7篇:使用 Qdrant 保存和检索向量》**完成创建和入库:

bash 复制代码
mvn -f 04-qdrant-rag/pom.xml compile exec:java \
  -Dexec.args=--index

第 2 步:认识项目结构

text 复制代码
05-spring-rag
├── pom.xml
└── src
    ├── main
    │   ├── java/com/example/ai/rag
    │   │   ├── SpringRagApplication.java
    │   │   ├── api/       HTTP 接口、请求响应和异常处理
    │   │   ├── service/   RAG 流程和核心业务对象
    │   │   ├── client/    Ollama 与 Qdrant 的调用实现
    │   │   ├── config/    Spring Bean 和类型安全配置
    │   │   └── ingestion/ Markdown 切分、Point 映射和知识入库
    │   └── resources
    │       ├── application.yml
    │       └── static/    HTML、CSS 和 JavaScript 工作台
    └── test               各层自动化测试

主要类的职责:

类 职责
RagController 接收 HTTP 请求并校验 question
RagService 编排检索、Prompt、生成答案和来源列表
QdrantKnowledgeRetriever 将问题向量化并搜索 Qdrant
RagPromptBuilder 把命中的 Chunk 和问题组装成 Prompt
OllamaAnswerGenerator 调用 qwen3:14b 生成答案
RagConfiguration 创建并连接上述对象
RagProperties 读取并校验 application.yml 配置

为什么这些对象要做成 Spring Bean

RagService 需要 KnowledgeRetriever、AnswerGenerator 和 RagPromptBuilder,Controller 又需要 RagService。如果每个类都在内部自己 new 下游对象,配置会四处散落,测试时也很难替换真实 Ollama 和 Qdrant。

Spring Bean 可以先简单理解为:由 Spring 创建、保存和注入的 Java 对象。

text 复制代码
RagConfiguration 创建各个 Bean
              ↓
Spring 根据构造方法参数建立依赖关系
              ↓
RagController 得到 RagService
              ↓
RagService 得到 Retriever / AnswerGenerator / PromptBuilder

所以你可能没有在 main() 中看到 new RagService(...),但它仍然会被创建和调用。真正的 HTTP 入口是 RagController.ask(),它会调用 Spring 注入的 RagService。

第 3 步:理解请求链路

一次正常请求经过以下步骤:

  1. RagController 从 JSON 中取得 question。
  2. Bean Validation 检查问题非空且不超过 2000 个字符。
  3. RagService 调用 KnowledgeRetriever。
  4. OllamaEmbeddingClient 使用 bge-m3 把问题变成 1024 维向量。
  5. QdrantKnowledgeRetriever 用问题向量执行 Top-K 搜索和阈值过滤。
  6. RagPromptBuilder 把命中的 Chunk 与原始问题组装成 Prompt。
  7. OllamaAnswerGenerator 调用 qwen3:14b。
  8. RagService 用同一批检索结果生成 sources,与答案一起返回。

这里有两个重要边界:

  • Qdrant 没有返回达到阈值的 Chunk 时,不调用聊天模型,直接回答"根据现有资料无法确定"。
  • sources 不是大模型编写的,而是 Java 根据 Qdrant 的真实结果生成的。

关键代码一:Controller 只处理 HTTP 边界

对应源码:

text 复制代码
05-spring-rag/src/main/java/com/example/ai/rag/api/RagController.java
java 复制代码
@RestController
@RequestMapping("/api/rag")
public final class RagController {
    private final RagService ragService;

    public RagController(RagService ragService) {
        this.ragService = ragService;
    }

    @PostMapping("/ask")
    public RagResponse ask(@Valid @RequestBody RagRequest request) {
        return ragService.ask(request.question());
    }
}
  • @RequestMapping("/api/rag") 定义公共路径。
  • @PostMapping("/ask") 定义 POST 子路径。
  • @RequestBody 把 JSON 转成 RagRequest。
  • @Valid 在进入 Service 前执行参数校验。

Controller 不知道 Embedding 维度,也不直接调用 Ollama 或 Qdrant。它只把已校验的问题交给 RagService。

关键代码二:RagService 编排完整 RAG

对应源码:

text 复制代码
05-spring-rag/src/main/java/com/example/ai/rag/service/RagService.java
java 复制代码
public RagResponse ask(String question) {
    List<RetrievedChunk> chunks;
    try {
        chunks = retriever.retrieve(question);
    } catch (Exception exception) {
        throw new ExternalServiceException("知识检索服务调用失败", exception);
    }

    if (chunks.isEmpty()) {
        return new RagResponse(NO_KNOWLEDGE_ANSWER, List.of());
    }

    RagPromptBuilder.Prompt prompt = promptBuilder.build(question, chunks);

    String answer;
    try {
        answer = answerGenerator.generate(prompt);
    } catch (Exception exception) {
        throw new ExternalServiceException("聊天模型调用失败", exception);
    }

    List<RagSource> sources = chunks.stream()
            .map(chunk -> new RagSource(
                    chunk.pointId(),
                    chunk.source(),
                    chunk.title(),
                    chunk.score()
            ))
            .toList();

    return new RagResponse(answer, sources);
}

这段源码直接解释了 API 的两个字段:answer 使用 answerGenerator 调用 qwen3 得到,sources 使用同一批 chunks 由 Java 映射得到。

第 4 步:查看配置

配置文件是 05-spring-rag/src/main/resources/application.yml:

yaml 复制代码
server:
  port: 8080

rag:
  ollama:
    base-url: http://localhost:11434
    embedding-model: bge-m3
    chat-model: qwen3:14b
    connect-timeout: 5s
    request-timeout: 2m

  qdrant:
    host: localhost
    port: 6334
    use-tls: false
    collection: kubernetes_chunks
    request-timeout: 10s

  search:
    top-k: 2
    minimum-score: 0.60

关键配置含义:

  • server.port:Spring Boot 对外监听端口。
  • embedding-model:把问题转换成向量的模型。
  • chat-model:根据知识资料生成答案的大模型。
  • qdrant.port:Java SDK 使用的 gRPC 端口 6334。
  • collection:要搜索的 Qdrant Collection。
  • top-k:最多取回几个知识片段。
  • minimum-score:允许进入 Prompt 的最低相似度。

第 5 步:启动服务

在仓库根目录运行:

bash 复制代码
mvn -f 05-spring-rag/pom.xml spring-boot:run

看到类似下面的日志表示启动成功:

text 复制代码
Started SpringRagApplication

服务地址是:

text 复制代码
http://localhost:8080

直接用浏览器打开这个地址会进入 RAG 知识工作台。工作台和本篇的查询接口属于同一个 Spring Boot 应用;如果只想验证 API,可以继续使用下一步的 curl 命令。

第 6 步:提出知识库内的问题

另开一个终端执行:

bash 复制代码
curl -sS -X POST http://localhost:8080/api/rag/ask \
  -H 'Content-Type: application/json' \
  -d '{"question":"密码、令牌和证书应该保存在哪里?"}' | jq

示例响应:

json 复制代码
{
  "answer": "密码、令牌和证书应该保存在 Kubernetes 的 Secret 中。",
  "sources": [
    {
      "pointId": "4",
      "source": "kubernetes.md",
      "title": "Secret",
      "score": 0.6504381895065308
    }
  ]
}

字段含义:

字段 来源
answer qwen3:14b 根据 Prompt 生成
sources Java 根据 Qdrant 的真实检索结果生成
pointId Qdrant Point ID,API 统一使用字符串以兼容数字 ID 和 UUID
source 入库时保存在 payload 中的文件名
title 入库时保存在 payload 中的 Chunk 标题
score Qdrant 返回的向量相似度

第 7 步:测试知识库没有覆盖的问题

bash 复制代码
curl -sS -X POST http://localhost:8080/api/rag/ask \
  -H 'Content-Type: application/json' \
  -d '{"question":"Ingress 应该怎样配置 TLS 证书?"}' | jq

如果没有 Chunk 达到 0.60,返回:

json 复制代码
{
  "answer": "根据现有资料无法确定。",
  "sources": []
}

HTTP 状态仍然是 200,因为系统正常完成了检索,只是知识库没有足够资料。这不是服务器错误。

第 8 步:观察参数校验

提交空问题:

bash 复制代码
curl -sS -X POST http://localhost:8080/api/rag/ask \
  -H 'Content-Type: application/json' \
  -d '{"question":""}' | jq

返回 HTTP 400:

json 复制代码
{
  "code": "VALIDATION_ERROR",
  "message": "question 不能为空"
}

问题超过 2000 个字符时也返回 400,并且不会调用 Ollama 或 Qdrant。

第 9 步:理解外部服务错误

如果 Ollama 或 Qdrant 无法访问,接口返回 HTTP 503:

json 复制代码
{
  "code": "EXTERNAL_SERVICE_UNAVAILABLE",
  "message": "外部服务暂时不可用"
}

三种结果不要混淆:

情况 HTTP 状态 含义
正常回答 200 找到资料并生成答案
没有相关资料 200 服务正常,但知识不足
请求参数错误 400 调用方需要修改问题
Ollama 或 Qdrant 故障 503 外部依赖暂时不可用

第 10 步:运行测试

bash 复制代码
mvn -f 05-spring-rag/pom.xml test

测试覆盖:

  • RAG 编排、无资料短路和来源生成。
  • 请求 JSON、参数校验与错误响应。
  • Ollama 请求格式和响应解析。
  • Qdrant 搜索参数和 payload 映射。
  • Spring 配置绑定和完整 Bean 创建。
  • 根地址能够通过真实 HTTP 请求返回工作台 HTML。

客户端测试会启动临时本地 HTTP 服务模拟 Ollama,不要求真正调用模型。这样测试执行快,并且结果稳定。真实服务仍需使用 curl 再验证一次。

常见问题

端口 8080 已被占用

临时换一个端口:

bash 复制代码
mvn -f 05-spring-rag/pom.xml spring-boot:run \
  -Dspring-boot.run.arguments=--server.port=8081

调用地址也要改成 http://localhost:8081。

返回 HTTP 503

依次检查:

bash 复制代码
curl -s http://localhost:11434/api/tags | jq
docker ps --filter name=qdrant-study
curl -s http://localhost:6333/collections/kubernetes_chunks | jq

一直回答资料不足

检查 Collection 是否已经入库,并观察 points_count。如果有数据,再检查 minimum-score 是否过高,以及查询内容是否确实在示例知识库中。

修改配置后没有生效

停止并重新启动 Spring Boot。开发模式下,配置不会因为保存文件而自动重载。

停止服务

在运行 Spring Boot 的终端按:

text 复制代码
Control + C

这只停止 Java API。Ollama 和 Qdrant 是独立服务,不会一起停止。

成功标准

  • Spring Boot 能在 http://localhost:8080 启动。
  • 知识库内问题返回模型答案和非空 sources。
  • 知识库外问题返回固定答案和空 sources。
  • 空问题返回 HTTP 400。
  • mvn -f 05-spring-rag/pom.xml test 全部通过。

下一步

下一阶段先学习 Markdown 怎样切分、向量化并组装成 Qdrant Point。继续阅读**《第9篇:知识入库基础,切分、向量化与 Point》**。

代码阅读顺序

  1. 从 RagController.ask() 开始,看 HTTP JSON 如何变成 request.question()。
  2. 进入 RagService.ask(),把方法拆成检索、无资料短路、Prompt、生成和来源五段。
  3. 顺着接口找实现:KnowledgeRetriever 对应 QdrantKnowledgeRetriever,AnswerGenerator 对应 OllamaAnswerGenerator。
  4. 最后阅读 RagConfiguration,看 Spring 如何把这些对象组装成 Bean。

本篇自测

  1. Controller 可以直接调用 Qdrant 吗?为什么项目不这样做?
  2. answer 和 sources 分别由谁生成?
  3. 没有资料时为什么仍然返回 HTTP 200?
  4. Spring Bean 解决了什么问题?
  5. HTTP 400 和 503 分别表示谁需要修正问题?

参考答案:技术上可以,但会把 HTTP、业务流程和外部 SDK 耦合在一起;answer 来自 qwen3,sources 由 Java 根据 Qdrant 结果生成;"没有资料"是一次正常完成的查询结果;Bean 让 Spring 统一创建和注入对象,便于配置和测试;400 表示调用方请求有误,503 表示服务依赖不可用。


本篇小结

  1. Spring Boot 把命令行 RAG 变成长期运行的 HTTP 服务,调用方只需 POST /api/rag/ask 提交问题。
  2. Controller、Service 与外部客户端分层清晰,RagProperties 负责绑定并校验 application.yml 配置。
  3. answer 由 qwen3:14b 生成,sources 由 Java 根据 Qdrant 的真实检索结果生成,两者来源完全不同。
  4. 三类结果不要混淆:200(正常回答或知识不足)、400(参数错误)、503(Ollama 或 Qdrant 故障)。
  5. 模型名、Collection、Top-K 与相似度阈值都能通过配置调整,改完需要重启服务才会生效。

下一篇

👉 本专栏下一篇:《第9篇:知识入库基础,切分、向量化与 Point》

完整代码都在 GitHub(欢迎 Star ⭐)

本专栏的全部示例代码都已开源,包含 5 个可独立运行的 Maven 模块、自动化测试和完整分篇教程。建议 Fork / Clone 下来,边读边跑:

🔗 https://github.com/bysbsh/ai-rag-learning-guide

  • 代码与教程同步更新,对照每一篇动手实践效果最好。
  • 如果这份教程帮到了你,点个 Star 就是对我最大的支持,也方便你之后找回最新版本。
  • 遇到问题或发现错漏,欢迎在仓库提 Issue / PR。项目采用 MIT 协议,可自由学习与二次创作。
相关推荐
小羊没烦恼!2 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
俊昭喜喜里2 天前
java中的继承和多态的区别
java
小羊没烦恼!2 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕2 天前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码2 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖2 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时2 天前
01-06-A-JVM排查实战详解
java·jvm
vipxieliang3 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
ba_pi3 天前
mysql 查询所有表名并授权
java
ProcessOn官方账号3 天前
java函数式编程--入门基础
java·编程·函数式编程