系列导航
- 所属专栏:《Java 开发者从零实现 RAG 知识库》
- 学习位置:第 8 篇 / 共 12 篇
- 上一篇:《第7篇:使用 Qdrant 保存和检索向量》
- 下一篇:《第9篇:知识入库基础,切分、向量化与 Point》
本篇不会改变 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 步:理解请求链路
一次正常请求经过以下步骤:
RagController从 JSON 中取得question。- Bean Validation 检查问题非空且不超过 2000 个字符。
RagService调用KnowledgeRetriever。OllamaEmbeddingClient使用bge-m3把问题变成 1024 维向量。QdrantKnowledgeRetriever用问题向量执行 Top-K 搜索和阈值过滤。RagPromptBuilder把命中的 Chunk 与原始问题组装成 Prompt。OllamaAnswerGenerator调用qwen3:14b。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》**。
代码阅读顺序
- 从
RagController.ask()开始,看 HTTP JSON 如何变成request.question()。 - 进入
RagService.ask(),把方法拆成检索、无资料短路、Prompt、生成和来源五段。 - 顺着接口找实现:
KnowledgeRetriever对应QdrantKnowledgeRetriever,AnswerGenerator对应OllamaAnswerGenerator。 - 最后阅读
RagConfiguration,看 Spring 如何把这些对象组装成 Bean。
本篇自测
- Controller 可以直接调用 Qdrant 吗?为什么项目不这样做?
answer和sources分别由谁生成?- 没有资料时为什么仍然返回 HTTP 200?
- Spring Bean 解决了什么问题?
- HTTP 400 和 503 分别表示谁需要修正问题?
参考答案:技术上可以,但会把 HTTP、业务流程和外部 SDK 耦合在一起;answer 来自 qwen3,sources 由 Java 根据 Qdrant 结果生成;"没有资料"是一次正常完成的查询结果;Bean 让 Spring 统一创建和注入对象,便于配置和测试;400 表示调用方请求有误,503 表示服务依赖不可用。
本篇小结
- Spring Boot 把命令行 RAG 变成长期运行的 HTTP 服务,调用方只需
POST /api/rag/ask提交问题。 - Controller、Service 与外部客户端分层清晰,
RagProperties负责绑定并校验application.yml配置。 answer由qwen3:14b生成,sources由 Java 根据 Qdrant 的真实检索结果生成,两者来源完全不同。- 三类结果不要混淆:
200(正常回答或知识不足)、400(参数错误)、503(Ollama 或 Qdrant 故障)。 - 模型名、Collection、Top-K 与相似度阈值都能通过配置调整,改完需要重启服务才会生效。
下一篇
👉 本专栏下一篇:《第9篇:知识入库基础,切分、向量化与 Point》
完整代码都在 GitHub(欢迎 Star ⭐)
本专栏的全部示例代码都已开源,包含 5 个可独立运行的 Maven 模块、自动化测试和完整分篇教程。建议 Fork / Clone 下来,边读边跑:
🔗 https://github.com/bysbsh/ai-rag-learning-guide
- 代码与教程同步更新,对照每一篇动手实践效果最好。
- 如果这份教程帮到了你,点个 Star 就是对我最大的支持,也方便你之后找回最新版本。
- 遇到问题或发现错漏,欢迎在仓库提 Issue / PR。项目采用 MIT 协议,可自由学习与二次创作。