知识库三项收口:向量-元数据对账 + 导入侧幂等 + 过期行向量清理入口

① 只读对账 tools/reconcile_knowledge_vectors.py
   按集合列出:孤儿向量 / 死向量 / 缺向量 / 重复正文 / 低信息量碎片 / 纯标题。
   关键口径:非数字 id(FAQ-0013 这类语义 id)是灌库脚本有意写进 Milvus 的,
   单独归类、不建议删;向量数取自 query 实际行数,不用 get_collection_stats
   (后者含已软删未 compaction 的行)。

② 导入侧幂等:同 source_file + 集合重传 = 覆盖上一版
   app/service/knowledge_ingest_service.py 新增 _supersede_previous_version:
   把上一版 active 行置为 expired,并逐行投 knowledge.vector_delete_requested
   (与本次入库同事务)。写入侧只认 active 而检索侧不看 status,旧向量不清掉
   会继续参与排序、和同题活块抢答。
   顺带修掉一个真 bug:改为先判 chunks 非空再下线 —— 否则传一份解析出 0 块的
   文档会把上一版下架、新版一行没写,这份文档在检索侧凭空消失。

③ 清理入口:POST /api/v1/knowledge/{knowledge_id}/vector-cleanups
   给历史上"被别的途径置为 expired、从未投过删除事件"的行补投向量清理。
   DELETE 对已过期行返回 404 的口径保持不变(重复删除静默成功会让调用方
   分不清"这次真下线了"和"早就过期了"),因此新开一个语义明确的端点:
   不存在 404 / 仍是 active 422(请改用 DELETE)/ 已 expired 200 并回传事件名。
   配套 tools/purge_expired_knowledge_vectors.py(默认 dry-run)批量驱动该端点。

文档:docs/演示用/知识库向量对账与清理-2026-09-15.md(含真机验证输出),
并对 docs/演示用/知识库问答诊断-2026-09-14.md 做两处更正 —— 实测孤儿向量 0 条、
那 175 行历史副本从来没有向量(不参与排序),当时的差额来自 get_collection_stats
把已软删行算进去。

新发现(未修,需业务拍板):661 条向量里 451 条正文不到 40 字,是灌库时把
markdown 表格/标题切碎产生的碎片。「风险评估问卷怎么评分」实测前 4 名是 4 条
一模一样的 19 字碎片(gap 0.0024),真正 2828 字的答案排第 5 → 客服必然转人工。
属灌库切分缺陷,补内容救不了,也不应靠放宽 MIN_GAP 解决。

验证:pytest tests/unit tests/contract → 1500 passed, 2 skipped, 0 failed;
mypy app → 3 个错全在组员文件中(与本次改动无关);ruff 本次改动文件 0 错。
真机端到端:重传 → 旧行 expired + 删除事件 published + 旧向量已从 Milvus 删除;
两个问句回归仍正常回答(r1到r5 gap 0.0766;申购确认 0.8453)。
This commit is contained in:
2026-09-15 09:06:57 +08:00
parent 64ad12e44a
commit 58b73ff594
11 changed files with 2022 additions and 12 deletions
+24 -2
View File
@@ -1,13 +1,16 @@
"""知识库管理接口(Task 11):上传 / 查询 / 删除文档。
"""知识库管理接口(Task 11):上传 / 查询 / 删除文档(+ 补投向量清理)。
老师《需求文档-修改版》Phase 1 验收第 7 条要求"知识库管理接口可正常上传/查询/删除文档",
F1.2 点名了这三个端点。既有 `app/api/controllers/knowledge.py` 只有 `/api/v1/knowledge-references`
(引用解析,K001),与此处是**两个不同的资源面**,因此单独一个模块、一个 router:
不动既有路由,也不把"引用解析"和"文档管理"混在同一个 prefix 下。
第四个端点 `POST /{knowledge_id}/vector-cleanups` 是**运维补口**(不在老师点名的三个之内):
只给已经过期的历史行补投向量删除事件,理由见 Service 的 `cleanup_vector`。
## 鉴权(硬约束,不得绕过)
三个端点都声明 `Depends(build_request_context)`(并且 router 级叠了 `enforce_rate_limit`,
全部端点都声明 `Depends(build_request_context)`(并且 router 级叠了 `enforce_rate_limit`,
它自身又依赖 `build_request_context`,所以认证一定先于限流)。底座**没有**匿名路径,
`context.user_id` 是入库 `created_by` 的唯一来源。
@@ -120,3 +123,22 @@ async def delete_document(
不存在的 id 返回 404(`SESSION_NOT_FOUND` 语义),**不静默成功**。
"""
return await knowledge_management_service().delete_document(context, knowledge_id)
@router.post("/{knowledge_id}/vector-cleanups")
async def cleanup_document_vectors(
knowledge_id: int = Path(gt=0),
context: RequestContext = Depends(build_request_context), # noqa: B008
) -> dict[str, Any]:
"""补投一次向量清理:给**已经过期**的历史行补发 `knowledge.vector_delete_requested`。
与 `DELETE` 的分工:`DELETE` 是"下线一份在用文档"(标记 + 投事件,一步到位),
本端点**不改状态**,只把"这条已下线的知识,向量可能还在 Milvus 里"这件事补投出去。
没有它,历史上那些"被别的途径置为 expired、从未投过删除事件"的行在管理端口上无路可走
(`DELETE` 对它们一律 404,见 Service docstring)。
- id 不存在 → 404;
- 行仍是 `active` → 422(请改用 `DELETE`,否则会出现"行是 active、向量已删"的不一致);
- 成功 → 200 + `vector_delete_event`,调用方能确认事件真的投了出去。
"""
return await knowledge_management_service().cleanup_vector(context, knowledge_id)