Java RAG 实战(第 11 篇):RAG 知识工作台网页
2026/8/18 12:48:09 网站建设 项目流程

系列导航

  • 所属专栏:《Java 开发者从零实现 RAG 知识库》
  • 学习位置:第 11 篇 / 共 12 篇
  • 上一篇:《第10篇:知识管理 API,写入、更新与删除》
  • 下一篇:《第12篇:AI 与 RAG 术语索引》

当前进度:已完成。浏览器已经可以完成知识入库、更新、删除和 RAG 问答。

本篇的网页不是新的 RAG 实现。它是前两篇 HTTP API 的可视化入口:左边管理知识,右边提问并查看回答来源。

为什么要学

第 8、9 篇完成了后端 API,但每次操作都要手写curl和 JSON。这样适合验证接口,不适合日常管理知识库,也不容易让其他人体验项目。

这一篇给已有 API 增加一个单页工作台:

浏览器页面 ├── 知识文档表单 ──→ KnowledgeController ──→ Qdrant └── 问题表单 ──→ RagController ──→ Ollama + Qdrant

网页没有替代后端。它只是把用户填写的内容组装成 JSON,通过 HTTP 调用前面已经实现的 Controller。

本篇目标

  • 在 Spring Boot 中直接提供 HTML、CSS 和 JavaScript。
  • 用网页提交或替换 Markdown 文档。
  • 用网页删除指定documentId的知识。
  • 提问并显示模型答案、来源和相似度。
  • 处理等待、成功、失败和无来源状态。
  • 在刷新页面后恢复尚未提交的表单草稿。

完成进度

  • 1. 启动依赖和 Spring Boot 应用
  • 2. 打开工作台,认识左右两条调用链
  • 3. 提交一篇带##二级标题的 Markdown
  • 4. 提问并查看答案、来源、相似度和 Point ID
  • 5. 使用相同documentId更新文档
  • 6. 删除测试文档并确认来源消失
  • 7. 运行自动化测试

第 1 步:运行依赖和应用

确认 Ollama 已安装模型:

ollama list

确认 Qdrant 正在运行:

dockerps--filtername=qdrant-studycurl-shttp://localhost:6333/collections|jq

启动 Spring Boot:

mvn-f05-spring-rag/pom.xml spring-boot:run

浏览器打开:

http://localhost:8080

只需要启动一个 Spring Boot 服务,不需要安装 Node.js,也不需要再启动一个前端开发服务器。

第 2 步:理解静态资源目录

页面文件位于:

05-spring-rag/src/main/resources/static/ ├── index.html 页面结构 ├── app.css 布局、颜色和移动端适配 └── app.js 表单状态和 API 调用

Spring Boot 会自动寻找classpath:/static/index.html,并把它作为/的欢迎页。因此:

GET http://localhost:8080/ ↓ Spring Boot 静态资源处理器 ↓ static/index.html

index.html再加载同源的/app.css/app.js。网页与 API 都使用localhost:8080,所以不需要额外配置 CORS。

第 3 步:页面怎样提交知识

用户填写:

documentId 文档的稳定身份 source 展示给使用者的资料来源 content 完整 Markdown

点击“提交入库”后,app.js执行:

fetch("/api/knowledge/documents",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({documentId:documentId,source:source,content:content})});

完整调用链:

知识文档表单 ↓ submit 事件 saveDocument() ↓ fetch POST /api/knowledge/documents ↓ KnowledgeController.ingest() ↓ KnowledgeIngestionService.ingest() ↓ 切分 Chunk → Embedding → 删除旧 Point → 写入新 Point

使用同一个documentId再次提交就是更新。服务端会先删除这个文档的旧 Chunk,再写入本次内容,避免残留。

第 4 步:页面怎样删除知识

点击“删除文档”后,网页会先显示确认框。确认后调用:

DELETE /api/knowledge/documents/{documentId}

encodeURIComponent(documentId)会把文档 ID 中不适合直接出现在 URL 的字符进行编码。成功响应是204 No Content,表示删除成功但没有 JSON 正文,因此前端不能继续调用response.json()

删除只根据documentId执行。source和 Markdown 输入框不会参与定位。

第 5 步:页面怎样完成 RAG 问答

点击“开始提问”后,网页发送:

fetch("/api/rag/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({question:question})});

完整链路是:

问题输入框 ↓ askQuestion() ↓ POST /api/rag/ask RagController.ask() ↓ RagService.ask() ├── bge-m3:问题向量化 ├── Qdrant:检索相似 Chunk ├── Java:组装 Prompt └── qwen3:14b:根据资料生成回答 ↓ RagResponse { answer, sources } ↓ renderAnswer() 安全渲染答案和来源

页面中的answer来自模型;sources来自 Java 对 Qdrant 真实检索结果的映射。来源包含:

title Chunk 标题 source 原文来源 score 向量相似度 pointId Qdrant Point 身份

前端使用textContent写入这些内容,不把模型回答当 HTML 执行,避免回答中的文本变成页面脚本。

第 6 步:理解页面状态

一次网络请求不是瞬间完成的,网页需要明确区分:

状态页面行为
等待表单可以编辑,顶部显示“等待首次请求”
请求中按钮禁用,显示正在入库、检索或生成
成功显示 Chunk 数量、答案和来源
API 失败优先显示后端返回的message
无来源显示回答,并明确来源数量为 0

requestJson()统一处理所有 HTTP 请求。这样三个操作不用重复解析错误 JSON,也能正确区分204和普通 JSON 响应。

关键代码一:统一处理 HTTP 成功和失败

对应源码:

05-spring-rag/src/main/resources/static/app.js
asyncfunctionrequestJson(path,options){letresponse;try{response=awaitfetch(path,{...options,headers:{"Content-Type":"application/json",...(options.headers||{})}});}catch(cause){thrownewRequestError("无法连接 Spring Boot 服务",cause);}if(response.ok){if(response.status===204){returnnull;}returnresponse.json();}letmessage=`请求失败(HTTP${response.status}`;try{constapiError=awaitresponse.json();if(apiError.message){message=apiError.message;}}catch(_){// 非 JSON 错误响应保留 HTTP 状态提示。}thrownewRequestError(message);}

这个函数把三类结果收口:网络层无法连接、HTTP 成功、HTTP 错误。页面的入库、删除和问答都只需关心各自的业务数据。

关键代码二:答案和来源分开渲染

functionrenderAnswer(result){constsources=Array.isArray(result.sources)?result.sources:[];elements.answerText.textContent=result.answer||"没有返回回答内容。";elements.sourceCount.textContent=`${sources.length}条来源`;elements.sourceList.replaceChildren();if(sources.length===0){constemptyItem=document.createElement("li");emptyItem.textContent="本次回答没有可展示的检索来源";elements.sourceList.append(emptyItem);}else{sources.forEach((source,index)=>{elements.sourceList.append(createSourceItem(source,index));});}}

answersources从 API 响应的两个不同字段读取,不会从模型回答文字中猜来源。所有可变文本使用textContent,不会当作 HTML 执行。

输入草稿保存在浏览器localStorage中。它只是本机浏览器的使用体验功能,不会写入 Qdrant;真正入库仍然要点击“提交入库”。

第 7 步:自己验证完整闭环

先提交一篇独立测试文档:

文档 ID:web-demo 来源:web-demo.md
## Ingress Ingress 用来声明进入集群的 HTTP 和 HTTPS 路由规则。

然后提问:

Ingress 用来做什么?

应当看到:

  • 页面返回 AI 回答。
  • 来源标题中出现Ingress
  • 来源文件是web-demo.md
  • 页面显示相似度和 Point ID。

最后填写web-demo,点击“删除文档”。再次提问时,这篇文档不应再出现在来源列表中。

第 8 步:运行测试

mvn-f05-spring-rag/pom.xmltest

SpringRagApplicationTest会启动随机端口,通过真实 HTTP 请求验证:

  • /返回 HTTP 200。
  • 响应类型兼容text/html
  • 页面包含工作台标题。

API Controller、入库服务、Qdrant 映射和 Ollama 客户端仍由原有测试覆盖。

常见问题

打开根地址仍然是 404

网页文件只有在 Spring Boot 重新启动后才会进入运行时 classpath。先停止旧进程,再重新执行:

mvn-f05-spring-rag/pom.xml spring-boot:run

检查首页响应:

curl-Ihttp://localhost:8080/

应返回 HTTP200Content-Type: text/html

页面显示“外部服务暂时不可用”

这对应后端 HTTP503。依次检查:

curlhttp://localhost:11434/api/tagsdockerps--filtername=qdrant-studycurlhttp://localhost:6333/collections/kubernetes_chunks

Ollama 和 Qdrant 是独立进程,Spring Boot 启动成功不代表它们一定可用。

提交文档时提示没有可入库章节

当前拆分规则只索引##二级标题。下面的内容不能入库:

# 只有一级标题 这段正文没有二级标题。

至少增加一个二级标题:

## 可检索章节 这段内容会生成 Chunk。

入库成功但回答资料不足

先确认问题与文档内容确实相关,再检查:

  • 页面是否显示写入了至少 1 个 Chunk。
  • documentId是否误删或被其他内容替换。
  • rag.search.minimum-score是否设置过高。
  • 回答的sources是否为空。

阈值过滤发生在模型调用之前。没有 Chunk 达到阈值时,程序会直接返回“根据现有资料无法确定”。

端口 8080 已被占用

查找占用进程:

lsof-nP-iTCP:8080-sTCP:LISTEN

也可以临时改用 8081:

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

浏览器地址随之改为http://localhost:8081

刷新后仍然出现以前填写的内容

这是草稿恢复功能,不代表内容又被写入 Qdrant。清空输入框后会同步清空对应的浏览器草稿。

删除按钮无法使用

先填写要删除的documentId。删除接口只通过文档 ID 定位知识,不使用source或 Markdown 内容定位。

本篇完成检查

  • 可以打开http://localhost:8080
  • 可以提交 Markdown 并看到 Chunk 数量
  • 可以更新相同documentId的文档
  • 可以提问并看到答案和来源
  • 可以删除测试文档
  • 刷新页面后表单草稿仍在
  • mvn -f 05-spring-rag/pom.xml test全部通过

完成这一篇后,项目已经从命令行练习发展成可直接体验的本地 RAG 应用。下一阶段可以学习 Docker 镜像、配置外置和 Kubernetes 部署,不必继续增加前端框架复杂度。

概念混淆时可以回到**《第12篇:AI 与 RAG 术语索引》**,按照“模型调用、检索、完整 RAG、动态入库”四条主线复习。

本篇自测

  1. 网页会自己调用 Qdrant SDK 吗?
  2. 为什么前端要区分普通 JSON 响应和 HTTP 204?
  3. 为什么模型回答要用textContent渲染,不直接使用innerHTML
  4. localStorage中有 Markdown 是否代表 Qdrant 中已经有这篇文档?
  5. 判断一次 RAG 验证是否可信,为什么要同时看回答和sources

参考答案:网页只调用 Spring Boot HTTP API,Qdrant SDK 在 Java 后端;204 没有 JSON 正文,继续解析会失败;textContent不会把模型文本当成 HTML 执行;localStorage只是浏览器草稿;回答可能看起来合理,但sources才能证明本次确实检索到了哪些资料。


本篇小结

  1. Spring Boot 直接托管static/下的 HTML、CSS 和 JavaScript,不需要 Node.js,也不需要额外的前端开发服务器。
  2. 网页没有替代后端,它只是把表单内容组装成 JSON,调用已经实现好的KnowledgeControllerRagController
  3. 页面要显式区分等待、请求中、成功、API 失败和无来源五种状态;requestJson()统一处理响应,并正确区分204与普通 JSON。
  4. 答案和来源使用textContent渲染,不把模型输出当 HTML 执行,避免回答内容变成页面脚本。
  5. localStorage只保存表单草稿,草稿恢复不等于内容已经写入 Qdrant。

下一篇

👉 本专栏下一篇:《第12篇:AI 与 RAG 术语索引》

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

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

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

  • 代码与教程同步更新,对照每一篇动手实践效果最好。
  • 如果这份教程帮到了你,点个Star就是对我最大的支持,也方便你之后找回最新版本。
  • 遇到问题或发现错漏,欢迎在仓库提 Issue / PR。项目采用 MIT 协议,可自由学习与二次创作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询