Skip to content

Latest commit

 

History

History
352 lines (258 loc) · 9.92 KB

File metadata and controls

352 lines (258 loc) · 9.92 KB

第 8 篇:Spring Boot RAG API

为什么要学

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

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

浏览器或其他应用
       ↓ 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 和相似度阈值。
  • 识别参数错误、知识不足和外部服务故障这三类结果。

第 1 步:确认依赖服务

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

ollama list
curl -s http://localhost:11434/api/tags | jq

模型列表中应包含:

bge-m3
qwen3:14b

再检查 Qdrant:

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 容器已经创建但没有运行:

docker start qdrant-study

如果 Collection 还没有资料,先按照第 7 篇完成创建和入库:

mvn -f 04-qdrant-rag/pom.xml compile exec:java \
  -Dexec.args=--index

第 2 步:认识项目结构

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 配置

第 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 的真实结果生成的。

第 4 步:查看配置

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

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 步:启动服务

在仓库根目录运行:

mvn -f 05-spring-rag/pom.xml spring-boot:run

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

Started SpringRagApplication

服务地址是:

http://localhost:8080

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

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

另开一个终端执行:

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

示例响应:

{
  "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 步:测试知识库没有覆盖的问题

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

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

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

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

第 8 步:观察参数校验

提交空问题:

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

返回 HTTP 400:

{
  "code": "VALIDATION_ERROR",
  "message": "question 不能为空"
}

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

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

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

{
  "code": "EXTERNAL_SERVICE_UNAVAILABLE",
  "message": "外部服务暂时不可用"
}

三种结果不要混淆:

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

第 10 步:运行测试

mvn -f 05-spring-rag/pom.xml test

测试覆盖:

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

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

常见问题

端口 8080 已被占用

临时换一个端口:

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

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

返回 HTTP 503

依次检查:

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 的终端按:

Control + C

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

成功标准

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

下一步

下一阶段为 Spring Boot 增加动态知识入库能力,让 Markdown 在切分、向量化并写入 Qdrant 后立即可以查询。继续阅读第 9 篇(上):知识库动态入库核心流程。