knowledge-hub

失败模式

这个项目里最贵的故障不是报错,而是不报错:视觉腿一直 0 个点、文档永远停在”处理中”、df 虚高让 idf 变负、改库可见性后旧 payload 仍然被召回。它们都不会让测试变红,也不会让接口 500,只会让某一天的用户觉得”搜不到东西”。所以排障顺序应该先问”哪条链路在静默失败”,而不是先看日志里有没有 ERROR。

一张分类表

摄取失败的永久/瞬时分流

类别 典型表现 会不会报错 先看哪里
检索腿降级 结果变少、某类题总是搜不到 不报错,skipped 有原因 /search/debug 的 legs
摄取失败 文档 failed、状态消息 任务 failed,文档可能没翻 /jobs、documents.status_msg
状态卡住 一直”处理中” 不报错 巡检的”卡住的文档”、worker 启动补扫
索引漂移 删了还能搜到、点数对不上 不报错 /admin/consistency 分腿
权限/越权 看到了不该看的 不报错 三档判据、payload 的旧 visibility
运维依赖 视觉服务没起、模型没加载 检索 200 但视觉 skipped /health、/tmp/pkh_visual.log

检索腿降级

四种腿各有各的跳过条件,且都不该让检索 500:

判断”是没跑还是没命中”只看 skipped:null 是跑了但零命中,字符串是没跑。把两者混在一起,会把”视觉服务没起”读成”库里没有照片”。

摄取失败

IngestError.retryable 决定去向:

永久错误(第一次就 failed)

瞬时错误(回 pending,最多 3 次)

一个容易误判的点:Office 预览失败不动文档状态。文档照样能搜、能引用、能下载;把预览失败当成摄取失败,会让人以为文件没导入成功。反过来,带框预览需要坐标,坐标是预览任务补的;预览没跑完时带框预览报 415 并说清去看任务状态,不是 500。

文档卡在”处理中”

两种半:

现在两半由同一个判定覆盖:每个 ingest/embed_visual 到终态都调 settle_doc_state,成功和失败两条路径都调;启动时 sweep_stuck_docs 补扫”中间态却没有任何待办任务”的文档。

判 failed 的条件是”该有的页级点不够,且没有未完成视觉任务”。thumbnail/pdf_preview 没完成不算——它们不影响内容能不能搜。scan_pages IS NULL 的老文档如果视觉任务全部失败,也判 failed,因为那个 pending 承诺没人能兑现。

索引漂移

三种漂移:

  1. 孤儿点:Qdrant 有、PG 里连 kb_id 都没有。常见于硬删用户/库(测试清理、级联删除),点没人清。重建时按三条腿清掉。
  2. 非当前点:同名版本被顶替或文档软删,旧点还在。宽限 5 分钟后由 worker 扫掉;不扫的话会占召回名额,让对账在 30 天里一直黄。
  3. 缺索引:PG 有 chunk、某条腿没有点。文本腿可以重建;视觉腿只能重新摄取(reparse),因为页级向量要重读原件 + colSmol 前向。

对账要分腿看。顶层 consistent 是 dense ∪ sparse 的并集口径,”只有一条腿坏了”在顶层看起来一致。视觉腿的期望点来自 documents.scan_pages,不是 is_scanned——后者只说”这份要走视觉路径”,说不出”该有几个点”。

对账不自动修。摄取时 chunk 行先 flush 未提交、点已 upsert,那个窗口里的”孤儿点”其实是正在写的;自动删会留下永久索引空洞。修复走 POST /admin/reindex(文本腿)或 reparse(视觉腿)。

权限与越权的形状

历史上有两次真实越权,形状值得记住:

  1. 把三档可见性合成一个 IN:visibility.in_(("workspace","global")),所有 workspace 库对任何用户可见。
  2. 把限定条件追加成另一条 or:workspace 分支没有成员资格,或工作区软删的 EXISTS 被写成第四条 or。限定条件完全无效,但 SQL 看起来”有这一条”。

现在防回归的方式是:

运维依赖

排障顺序

遇到”搜不到”:

  1. GET /api/v1/health 看 available_legs、queue、index_models;
  2. POST /kb/{id}/search/debug 看每条腿的 n/ms/skipped;
  3. 如果某腿 skipped,按原因修依赖;如果全跑了但零命中,看 query 与库范围;
  4. 如果点数与文档数对不上,GET /admin/consistency 分腿看缺/多;
  5. 如果文档一直处理中,GET /jobs?doc_id= 看任务终态,巡检看”卡住的文档”。

遇到”看到了不该看的”:

  1. 先确认 GET /kb 是否列出了它——列表用 visible_kb_clause;
  2. 再确认检索结果——Qdrant 过滤只按 kb_id,范围来自 readable_kb_ids;
  3. 检查 payload 里的 visibility/workspace_id 是否被谁拿去做过滤;
  4. 检查工作区软删的 EXISTS 是否被写成独立 or。

遇到”改了没生效”:

  1. 后端代码改动是否 dev.sh restart,有没有残留 worker;
  2. 切分策略/PII 开关只对之后摄取的文档生效,旧文档要 reparse;
  3. 新增 payload 字段有重摄取欠账,旧点没有该字段;
  4. 别名模式下要确认当前别名指向哪个物理集合。

已知的静默失败清单

这些不会让任何测试变红,只能靠巡检或对账发现: