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 和外部客户端的分工。
  • 理解 answersources 是怎样生成的。
  • 用配置文件调整模型、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. 看懂 answersources 的不同来源
  • 5. 分别验证 HTTP 200、400 和 503
  • 6. 运行模块测试

第 1 步:确认依赖服务

本模块依赖 Ollama、bge-m3qwen3: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 需要 KnowledgeRetrieverAnswerGeneratorRagPromptBuilder,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 对应 QdrantKnowledgeRetrieverAnswerGenerator 对应 OllamaAnswerGenerator
  4. 最后阅读 RagConfiguration,看 Spring 如何把这些对象组装成 Bean。

本篇自测

  1. Controller 可以直接调用 Qdrant 吗?为什么项目不这样做?
  2. answersources 分别由谁生成?
  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. answerqwen3: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 协议,可自由学习与二次创作。
相关推荐
FlyWIHTSKY1 小时前
idea中集成claude功能
java·ide·人工智能·intellij-idea·cloudera
Java成神之路-1 小时前
Spring AI 统一结构化返回:ChatModel / ChatClient 两种实现方式
java·springaialibaba
Mr. zhihao1 小时前
深度解析:为什么Java序列化需要搭配ByteArrayOutputStream?IO装饰器模式的精妙设计
java·开发语言·装饰器模式
vx-程序开发1 小时前
springboot旅游推介平台---附源码24175
java·spring boot·python·spring cloud·eclipse·django·idea
瑞码空间2 小时前
贪心算法详情分析
贪心算法·算法设计·算法分析
艾莉丝努力练剑2 小时前
【QT:解决问题】Qt5Core.dll:无法定位程序输入点
java·开发语言·qt·学习·面试
小蒜学长2 小时前
“喵汪联盟”宠物领养系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·宠物
程序员黑豆2 小时前
Java字符串常量池完全指南:原理、intern()方法与性能优化最佳实践
java·前端·ai编程
AI人工智能+电脑小能手2 小时前
大白话说Java设计模式-14-适配器模式(业务实战篇)
java·设计模式·适配器模式·系统兼容·多渠道对接