知识库三项收口:向量-元数据对账 + 导入侧幂等 + 过期行向量清理入口
① 只读对账 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:
@@ -1,4 +1,4 @@
|
||||
"""知识库管理服务(Task 11):上传 / 查询 / 删除文档。
|
||||
"""知识库管理服务(Task 11):上传 / 查询 / 删除文档 / 补投向量清理。
|
||||
|
||||
## 为什么单独一个 Service,而不是塞进 `KnowledgeIngestService`
|
||||
|
||||
@@ -23,9 +23,14 @@
|
||||
且**尽力而为**:归档失败只记日志,不回滚已经提交的删除 —— 否则会出现"库里还是 active、
|
||||
存储里已经归档"的不一致,比"库已 expired、文件还在"危险得多(后者只浪费磁盘)。
|
||||
|
||||
删除只覆盖"**本次调用**造成的 active→expired"。历史上已经过期、但从未投过删除事件的行
|
||||
(早期脚本改库、导入侧幂等上线前的重复副本)需要 `cleanup_vector` 补投一次 —— 为什么不能
|
||||
让 `delete_document` 顺带兼容它们:那会让"重复删除"变成静默成功,调用方再也分不清
|
||||
"这一次真的下线了"和"它早就过期了,我什么都没改"。
|
||||
|
||||
## 权限
|
||||
|
||||
三个端点统一要求 `knowledge:manage`(admin 角色持有;权限码在 `tools/seed_test_rbac.py`
|
||||
四个端点统一要求 `knowledge:manage`(admin 角色持有;权限码在 `tools/seed_test_rbac.py`
|
||||
的 `PERMISSIONS` 里,id 9019)。复用既有 `knowledge:query`(9018)会让**客户角色**看见
|
||||
整库文档清单与正文摘要,知识库的后台维护面不该向客户开放。
|
||||
"""
|
||||
@@ -201,6 +206,55 @@ class KnowledgeManagementService:
|
||||
"vector_delete_event": VECTOR_DELETE_EVENT,
|
||||
}
|
||||
|
||||
async def cleanup_vector(
|
||||
self, context: RequestContext, knowledge_id: int
|
||||
) -> dict[str, Any]:
|
||||
"""给**已经过期**的历史行补投一次向量删除事件(清掉向量侧残留)。
|
||||
|
||||
## 为什么需要它
|
||||
|
||||
`delete_document` 只在"active → expired"这**一次状态转换**上投事件。历史上有一批行
|
||||
是**别的途径**变成 `expired` 的(早期脚本直接改库、重复入库被覆盖、导入侧幂等上线前的
|
||||
重复副本),它们**从来没投过**删除事件,向量还留在 Milvus 里继续参与排序;
|
||||
而 `delete_document` 对这些行**刻意返回 404**("不静默成功"),于是管理端口上
|
||||
根本没有一个能清理它们的动作 —— 这是行为与运维需求之间的缺口,本方法就是补这个缺口。
|
||||
|
||||
## 口径(三条,都是"宁可报错也不假装成功")
|
||||
|
||||
1. id 不存在 → 404(`SESSION_NOT_FOUND` 语义),与删除接口一致;
|
||||
2. 行**仍是 active** → 校验失败(422):在用的文档不该走"只清向量不标状态"这条路,
|
||||
那会让检索侧读不到正文而知识行看起来还好好的,请改用 DELETE(它同时做两件事);
|
||||
3. 成功 → 投出一条**新的** `knowledge.vector_delete_requested`(`event_id` 是新 uuid4,
|
||||
消费侧删除幂等,重复清理同一行只是多删一次空集,不会报错)。
|
||||
|
||||
返回 `status`(当前状态)与 `vector_delete_event`(投出的事件名),
|
||||
调用方据此能确认"事件确实被投出去了",而不是只看一个 200。
|
||||
"""
|
||||
await AuthorizationService.require(context, REQUIRED_PERMISSION)
|
||||
async with self._session_factory() as session, session.begin():
|
||||
row = await session.get(KnowledgeMeta, knowledge_id)
|
||||
if row is None:
|
||||
raise GenericResourceNotFoundError("知识文档不存在")
|
||||
status = str(row.status)
|
||||
if status != EXPIRED_STATUS:
|
||||
raise ValidationAgentError(
|
||||
f"知识文档当前状态为 {status},只有已过期/已下线的文档才需要补投向量清理;"
|
||||
"在用的文档请改用 DELETE 接口(它同时标记过期并投出删除事件)"
|
||||
)
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
self._enqueue_vector_delete(
|
||||
session, knowledge_id, trace_id=context.trace_id, now=now
|
||||
)
|
||||
logger.info(
|
||||
"补投向量清理事件:knowledge_id=%s status=%s trace_id=%s",
|
||||
knowledge_id, status, context.trace_id,
|
||||
)
|
||||
return {
|
||||
"knowledge_id": knowledge_id,
|
||||
"status": status,
|
||||
"vector_delete_event": VECTOR_DELETE_EVENT,
|
||||
}
|
||||
|
||||
# --- 内部 ---------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user