跳到主要内容

失败模式

这个项目里最贵的故障不是报错,而是不报错:视觉腿一直 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:

  • 视觉腿:服务没配、没起、模型没加载完、编码超过 2 秒、并发排队超时。API 进程恒不加载 colSmol,所以"API 里视觉腿不可用"是正常状态,不是故障。skipped 会说清是"未配置"、"连不上"还是"模型未就绪"。
  • dense:index_models 记录的模型/维度与当前配置不一致。这不是"随机变差",是两个向量空间里算相似度,排序基础坏了。修法是把配置改回原模型,或重建索引。
  • sparse:查询词全不在词表(OOV),分词后没有可查的 token。这不是错误,是这一腿没有候选。
  • trgm:query 短于 3 字,或语句超过 800ms。超时来自 word_similarity 只能 seq scan;库大时它会拖死 500ms 预算,所以封顶后降级,文本召回仍由 dense/sparse 承担。
  • 精排:模型加载失败或推理失败,保持原顺序,skipped 带原因。

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

摄取失败​

IngestError.retryable 决定去向:

永久错误(第一次就 failed)

  • 格式不支持(应该在网关就 415,但目录导入/聊天导入可能绕过)。
  • blob 缺失:内容寻址存储与 DB 不一致,重试不会让文件回来。
  • 解析为空:不是扫描件、也没有文本层。以前会静默 ready 一个空文档,现在直接失败。
  • 超过 2000 页;扫描件超过 50 页。
  • 页级任务页码越界。
  • LibreOffice 未安装(预览任务)。

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

  • 向量库暂时不可用、网络抖动。
  • Office 转换失败(ConversionError)。
  • 视觉写侧走远端服务时的不可达/503/超时。

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

文档卡在"处理中"​

两种半:

  • 任务失败但文档没翻:ingest 重试耗尽、embed_visual 全部失败。修复前文档永远 pending/parsing。
  • 任务成功但文档没翻:worker 在 finish_job 提交之后、文档结算提交之前被杀。窗口只有几毫秒,但一旦发生就是永久。

现在两半由同一个判定覆盖:每个 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 看起来"有这一条"。

现在防回归的方式是:

  • 三档必须分别判定,workspace 的两个条件在同一个 AND 里;
  • Qdrant 过滤只允许 kb_id,services/ 下任何过滤字典不得用 visibility/workspace_id/source_kind/owner_id 做 key;
  • payload 里的 visibility 是摄取时写死的,改库可见性不回写——一旦有人按它过滤,陈旧值立刻越权;
  • 无权限与不存在同码,阻断存在性探测。

运维依赖​

  • 视觉服务没起:检索 200,视觉腿 skipped。首次加载约 13 秒,dev.sh 等不到它也会正常退出;那段时间视觉腿不可用是正常的。
  • 模型不在本地:HF_HUB_OFFLINE=1 下加载会失败或极慢。换机器先准备模型缓存。
  • 连接池不够:一次 hybrid 最多四条连接,池按并发检索数 × 4 估;不够时请求排队,延迟变差但不会算错。
  • 磁盘写满:上传仍 202,blob 写失败在 handler 里,用户看到的是"文档一直 pending"。巡检的磁盘项是少数能提前发现它的地方。
  • 旧 worker 残留:dev.sh stop 杀不干净 uv run 派生子进程,旧 worker 继续用旧代码领任务,症状是"改了没生效"。重启前先确认没有残留。
  • 备份没演练:只跑 backup.sh 看它成功,证明不了能恢复。必须定期 restore.sh --scratch。

排障顺序​

遇到"搜不到":

  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. 别名模式下要确认当前别名指向哪个物理集合。

已知的静默失败清单​

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

  • kb_visual 恒 0 个点(视觉腿坏了,看起来像"库里没照片")。
  • documents.status 永远停在中间态(任务终态没搬过去)。
  • corpus_stats.n_docs 与 chunk 行数不一致(df 虚高,idf 被压平)。
  • 索引模型与配置不一致(dense 腿静默错排序)。
  • 备份目录存在但恢复不出来(没演练)。
  • 上传返回 202 但 blob 写失败(磁盘满/权限错)。
  • 旧 worker 用旧代码领任务("改了没生效")。
加载评论中...