Java RAG 实战(第 10 篇):知识管理 API,写入、更新与删除

发布时间:2026/8/18 12:48:47
Java RAG 实战(第 10 篇):知识管理 API,写入、更新与删除 系列导航所属专栏《Java 开发者从零实现 RAG 知识库》学习位置第 10 篇 / 共 12 篇上一篇《第9篇知识入库基础切分、向量化与 Point》下一篇《第11篇RAG 知识工作台网页》上一篇已经准备好 Point。本篇把它们写入 Qdrant编排覆盖更新流程再通过 Spring Boot HTTP 接口提供新增、更新和删除能力。完成进度1. 理解KnowledgeStore与 Qdrant Gateway 的边界2. 使用 upsert 写入 Point3. 按documentId删除整篇旧文档4. 理解embed → delete → upsert顺序5. 使用 POST 接口新增或替换文档6. 使用 DELETE 接口删除文档7. 验证 200、204、400 和 503 响应为什么要学准备好 Point 还不等于完成入库。真实应用需要处理数据库写入、旧知识清理、失败边界和 HTTP 调用入口才能让网页或其他服务稳定管理知识库。第一阶段Qdrant 写入与入库服务第 1 步设计 Qdrant 写入网关程序需要两种存储操作upsert(points) 批量写入新的知识 Point deleteByDocumentId(documentId) 删除某篇文档的全部旧 Point代码分成三层KnowledgeStore ↓ Qdrant 实现 QdrantKnowledgeStore ↓ 外部调用边界 QdrantWriteGateway ↓ QdrantClient官方 SDKKnowledgeStore是入库流程依赖的接口publicinterfaceKnowledgeStore{voidupsert(ListPointStructpoints)throwsException;voiddeleteByDocumentId(StringdocumentId)throwsException;}上层不需要了解 Qdrant SDK、超时或 Filter 的组装方式。Gateway 隔离真实网络调用使单元测试不必启动 Qdrant。第 2 步批量 upsertQdrantKnowledgeStore.upsert(points)会把完整列表一次交给网关ListPointStruct ↓ QdrantClient.upsertAsync(collectionName, points, timeout)upsert表示“存在就更新不存在就新增”。Point ID 是稳定 UUID因此同一个documentId chunkIndex再次写入时会覆盖原 Point。空列表会被拒绝避免发送没有内容的写请求。第 3 步按 documentId 删除一篇文档通常被拆成多个 Point所以删除不能只传一个 Point ID。程序会生成 payload Filtermust: documentId kubernetes-guideJava SDK 组装方式FilterfilterFilter.newBuilder().addMust(matchKeyword(documentId,documentId)).build();documentId读取 Point payload 中的同名字段。matchKeyword执行完整字符串匹配不是向量搜索。must表示只有满足条件的 Point 才能删除。空白文档 ID 会在调用网关前被拒绝避免含义不清楚的删除条件。第 4 步连接真实 QdrantRagConfiguration为网关提供真实 SDK 调用qdrantClient.upsertAsync(collectionName,points,requestTimeout).get();qdrantClient.deleteAsync(collectionName,filter,requestTimeout).get();当前学习项目使用.get()等待操作结束让 HTTP 接口在返回成功前确认 Qdrant 已接受操作。两种调用共用配置rag:qdrant:collection:kubernetes_chunksrequest-timeout:10s第 5 步编排完整入库流程KnowledgeIngestionService把已有组件组成一个完整用例MarkdownChunker.chunk(...) ↓ EmbeddingClient.embedAll(...) ↓ QdrantPointMapper.toPoint(...) ↓ KnowledgeStore.deleteByDocumentId(...) ↓ KnowledgeStore.upsert(...)先完成不会修改 Qdrant 的准备阶段切分 → 批量 Embedding → 组装全部 Point全部成功后才进入替换阶段删除旧 Point → 写入新 Point因此 Ollama 失败、向量数量错误或 Point 组装失败时旧知识不会被提前删除。测试要求事件顺序是embed → delete → upsert需要注意删除和写入是两个独立 Qdrant 请求并非数据库事务。删除成功但写入失败时文档会暂时没有数据调用方应使用相同文档重试。关键代码KnowledgeIngestionService 怎样编排入库对应源码05-spring-rag/src/main/java/com/example/ai/rag/ingestion/KnowledgeIngestionService.java下面是ingest()的核心部分ListMarkdownChunker.Chunkchunkschunker.chunk(documentId,source,markdown);if(chunks.isEmpty()){thrownewIllegalArgumentException(Markdown 中没有可入库的二级章节);}Listdouble[]embeddingsembeddingClient.embedAll(chunks.stream().map(MarkdownChunker.Chunk::content).toList());if(embeddings.size()!chunks.size()){thrownewIllegalStateException(Embedding 数量与 Chunk 数量不一致);}ListPointStructpointsnewArrayList(chunks.size());for(intindex0;indexchunks.size();index){points.add(pointMapper.toPoint(chunks.get(index),embeddings.get(index)));}knowledgeStore.deleteByDocumentId(documentId);knowledgeStore.upsert(List.copyOf(points));为了突出业务顺序上面片段省略了源码中的异常包装完整实现请在 GitHub 查看对应类。读代码时要抓住两条边界deleteByDocumentId()之前都属于准备阶段不修改 Qdrant。Chunk 和 Embedding 严格按相同index组装所以数量不一致必须立即拒绝。第 6 步保证 Chunk 与 Embedding 配对批量 Embedding 的返回顺序与输入相同chunks[0].content → embeddings[0] chunks[1].content → embeddings[1]服务在映射前检查embeddings.size()chunks.size()数量不同会抛出IllegalStateException并且不会删除或写入数据。这项保护不能只依赖当前 Ollama 客户端因为以后可能替换 Embedding 实现。第 7 步拒绝空文档更新当前规则只索引##二级章节。下面的文档会得到 0 个 Chunk# 只有一级标题 没有二级章节。如果继续更新程序可能删除旧文档却没有新 Point 可写。因此服务会在调用 Embedding 和 Qdrant 前拒绝这种输入。第 8 步理解入库结果和 Spring Bean成功后返回publicrecordKnowledgeIngestionResult(StringdocumentId,Stringsource,intchunkCount){}RagConfiguration创建MarkdownChunker PointIdGenerator QdrantPointMapper KnowledgeIngestionService成为 Bean 只表示这些对象由 Spring 管理并可被注入本文第二阶段会加入真正的 HTTP 调用入口。测试验证什么mvn-f05-spring-rag/pom.xmltest相关测试验证upsert 使用正确 Collection 并保留全部 Point。删除使用documentId精确匹配。所有 Chunk 只执行一次批量 Embedding。全部 Point 准备完成后才删除和写入。空文档和向量数量错误不会修改原知识。返回正确的文档 ID、来源和 Chunk 数量。完成检查理解KnowledgeStore、实现类和 Gateway 的边界。理解 upsert 与按文档删除的区别。能解释为什么先准备全部 Point 再删除旧知识。能说明删除和写入为什么不是真正事务。05-spring-rag测试全部通过。第二阶段知识管理 HTTP 接口第 9 篇完成了切分、向量化和 Point 映射本文第一阶段完成了 Qdrant 写入和KnowledgeIngestionService。但 Service 仍只是一个等待调用的 Spring Bean真正的应用还需要稳定的 HTTP 入口让网页、脚本或其他服务都能管理知识。本篇完成下面这条链路HTTP 请求 ↓ KnowledgeController ↓ KnowledgeIngestionService ↓ MarkdownChunker → Ollama → Qdrant前置条件已完成本篇前面的切分、向量化和 Qdrant 写入部分。Ollama 已包含bge-m3。Qdrant 容器和kubernetes_chunksCollection 正常。Java 17 和 Maven 3.9 可用。第 1 步理解 Controller 怎样调用 BeanKnowledgeController是知识管理的 HTTP 入口。Controller 构造方法中的参数由 Spring 自动注入publicKnowledgeController(KnowledgeIngestionServiceingestionService){this.ingestionServiceingestionService;}对应源码05-spring-rag/src/main/java/com/example/ai/rag/api/KnowledgeController.javaController 对外暴露两个操作PostMappingpublicKnowledgeIngestionResultingest(ValidRequestBodyKnowledgeDocumentRequestrequest){returningestionService.ingest(request.documentId(),request.source(),request.content());}DeleteMapping(/{documentId})ResponseStatus(HttpStatus.NO_CONTENT)publicvoiddelete(PathVariableStringdocumentId){ingestionService.deleteDocument(documentId);}PostMapping没有额外路径因此对应类上的/api/knowledge/documents。DeleteMapping(/{documentId})则在后面增加文档 ID删除成功时通过ResponseStatus返回 HTTP 204。请求到达后的调用关系POST /api/knowledge/documents ↓ KnowledgeController.ingest(...) ↓ KnowledgeIngestionService.ingest(...) ↓ MarkdownChunker → Ollama → Qdrant这就是knowledgeIngestionServiceBean 真正被业务代码使用的位置。Controller 只转换 HTTP 数据不自己切分 Markdown也不直接操作 Qdrant。第 2 步启动应用先确认依赖ollama listdockerps--filternameqdrant-study再从仓库根目录启动mvn-f05-spring-rag/pom.xml spring-boot:run应用默认监听http://localhost:8080第 3 步提交或替换文档在另一个终端执行curl-sS-XPOST http://localhost:8080/api/knowledge/documents\-HContent-Type: application/json\-d{ documentId: http-learning-demo, source: http-learning-demo.md, content: # Kubernetes\n\n## Service\n\nService 为 Pod 提供稳定访问地址。 }|jq成功响应{documentId:http-learning-demo,source:http-learning-demo.md,chunkCount:1}再次提交相同documentId会替换旧文档而不是创建第二篇同名文档。示例使用独立 ID避免覆盖第 9 篇写入的原始知识。第 4 步验证新知识可以查询curl-sS-XPOST http://localhost:8080/api/rag/ask\-HContent-Type: application/json\-d{question:什么为 Pod 提供稳定访问地址}|jq返回的sources中应包含source http-learning-demo.md title Service这说明入库和查询使用的是同一个 Qdrant Collection新知识不需要重启 Spring Boot 就能被检索。第 5 步删除测试文档curl-i-XDELETE\http://localhost:8080/api/knowledge/documents/http-learning-demo成功时返回HTTP/1.1 204 No Content204表示删除成功但响应没有 JSON 正文。DELETE 只按documentId删除旧 Point不调用 EmbeddingPOST 切分、向量化并替换整篇文档 DELETE 只按 documentId 删除旧 Point第 6 步理解请求校验和错误响应KnowledgeDocumentRequest的三个字段都使用NotBlankdocumentId 不能为空 source 不能为空 content 不能为空无效字段或没有##二级章节时返回HTTP/1.1 400 Bad Request{code:VALIDATION_ERROR,message:Markdown 中没有可入库的二级章节}Ollama 或 Qdrant 调用失败时服务层使用ExternalServiceException隐藏底层连接细节统一返回HTTP/1.1 503 Service Unavailable{code:EXTERNAL_SERVICE_UNAVAILABLE,message:外部服务暂时不可用}第 7 步理解真实生命周期验证项目开发时使用独立文档完成了以下验证1. 入库“令牌默认有效期为 37 分钟” 2. Qdrant 文档数量增加 1 3. 问答接口返回“37 分钟” 4. 使用相同 documentId 更新为“52 分钟” 5. 文档数量不变Point UUID 保持不变 6. 问答接口返回新答案“52 分钟” 7. DELETE 返回 204Collection 恢复原数量这证明相同documentId chunkIndex会生成稳定 UUID更新不会累积重复 Point。查询读取的是 Qdrant 中更新后的内容不会继续回答旧知识。删除整篇文档后对应 Chunk 不再参与检索。Qdrant Point ID 支持数字和 UUID 两种形式数字 ID2 UUID6d490220-6a6e-3d69-87d7-3f2f9e527376RetrievedChunk和RagSource使用字符串保存 ID并根据 Qdrant 实际设置的类型读取数字 2 → 2 UUID → 6d490220-6a6e-3d69-87d7-3f2f9e527376这样不会把 UUID 错误地读取成 protobuf 默认数字0。常见问题POST 返回 400确认三个字段都不是空字符串并且 Markdown 至少包含一个##二级标题。POST 返回 503依次检查curlhttp://localhost:11434/api/tagsdockerps--filternameqdrant-studycurlhttp://localhost:6333/collections/kubernetes_chunks更新后出现重复知识必须使用与旧文档相同的documentId。source只是展示来源不负责定位需要替换的文档。DELETE 后仍然看到旧答案先确认documentId与入库时完全一致再查看回答的sources。模型可能使用自身知识生成相似文本但被删除的文档不应再出现在真实来源中。完成检查POST 返回文档 ID、来源和 Chunk 数量。新知识可以通过/api/rag/ask查询。相同documentId可以替换旧知识。DELETE 返回 HTTP 204。参数错误返回 HTTP 400。外部服务故障返回 HTTP 503。测试文档已经删除没有污染示例知识库。本篇自测为什么更新文档前不能只对新 Chunk 执行 upsert为什么先准备全部 Point再删除旧知识删除旧 Point 成功、写入新 Point 失败时当前流程是事务性的吗KnowledgeIngestionService在哪里被真正调用DELETE 为什么不需要调用bge-m3参考答案新版可能减少 Chunk只 upsert 会残留旧 Point避免切分或 Embedding 提前失败时破坏旧知识两次远程请求不构成数据库事务由KnowledgeController调用删除根据 payload 中的documentId精确过滤。本篇小结KnowledgeStore隔离业务编排与 Qdrant SDKGateway 负责真实远程调用。更新流程先准备全部 Point再删除旧 Point 并 upsert 新 Point减少提前破坏旧知识的风险。删除和写入仍是两个独立请求不是数据库事务失败后应使用相同文档重试。POST、DELETE 和已有查询接口组成知识新增、更新、查询、删除的完整生命周期。下一篇 本专栏下一篇《第11篇RAG 知识工作台网页》完整代码都在 GitHub欢迎 Star ⭐本专栏的全部示例代码都已开源包含 5 个可独立运行的 Maven 模块、自动化测试和完整分篇教程。建议Fork / Clone下来边读边跑 https://github.com/bysbsh/ai-rag-learning-guide代码与教程同步更新对照每一篇动手实践效果最好。如果这份教程帮到了你点个Star就是对我最大的支持也方便你之后找回最新版本。遇到问题或发现错漏欢迎在仓库提 Issue / PR。项目采用 MIT 协议可自由学习与二次创作。