这个项目里最贵的故障不是报错,而是不报错:视觉腿一直 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 会说清是”未配置”、”连不上”还是”模型未就绪”。index_models 记录的模型/维度与当前配置不一致。这不是”随机变差”,是两个向量空间里算相似度,排序基础坏了。修法是把配置改回原模型,或重建索引。word_similarity 只能 seq scan;库大时它会拖死 500ms 预算,所以封顶后降级,文本召回仍由 dense/sparse 承担。skipped 带原因。判断”是没跑还是没命中”只看 skipped:null 是跑了但零命中,字符串是没跑。把两者混在一起,会把”视觉服务没起”读成”库里没有照片”。
IngestError.retryable 决定去向:
永久错误(第一次就 failed)
ready 一个空文档,现在直接失败。瞬时错误(回 pending,最多 3 次)
ConversionError)。一个容易误判的点:Office 预览失败不动文档状态。文档照样能搜、能引用、能下载;把预览失败当成摄取失败,会让人以为文件没导入成功。反过来,带框预览需要坐标,坐标是预览任务补的;预览没跑完时带框预览报 415 并说清去看任务状态,不是 500。
两种半:
ingest 重试耗尽、embed_visual 全部失败。修复前文档永远 pending/parsing。finish_job 提交之后、文档结算提交之前被杀。窗口只有几毫秒,但一旦发生就是永久。现在两半由同一个判定覆盖:每个 ingest/embed_visual 到终态都调 settle_doc_state,成功和失败两条路径都调;启动时 sweep_stuck_docs 补扫”中间态却没有任何待办任务”的文档。
判 failed 的条件是”该有的页级点不够,且没有未完成视觉任务”。thumbnail/pdf_preview 没完成不算——它们不影响内容能不能搜。scan_pages IS NULL 的老文档如果视觉任务全部失败,也判 failed,因为那个 pending 承诺没人能兑现。
三种漂移:
reparse),因为页级向量要重读原件 + colSmol 前向。对账要分腿看。顶层 consistent 是 dense ∪ sparse 的并集口径,”只有一条腿坏了”在顶层看起来一致。视觉腿的期望点来自 documents.scan_pages,不是 is_scanned——后者只说”这份要走视觉路径”,说不出”该有几个点”。
对账不自动修。摄取时 chunk 行先 flush 未提交、点已 upsert,那个窗口里的”孤儿点”其实是正在写的;自动删会留下永久索引空洞。修复走 POST /admin/reindex(文本腿)或 reparse(视觉腿)。
历史上有两次真实越权,形状值得记住:
IN:visibility.in_(("workspace","global")),所有 workspace 库对任何用户可见。or:workspace 分支没有成员资格,或工作区软删的 EXISTS 被写成第四条 or。限定条件完全无效,但 SQL 看起来”有这一条”。现在防回归的方式是:
AND 里;kb_id,services/ 下任何过滤字典不得用 visibility/workspace_id/source_kind/owner_id 做 key;visibility 是摄取时写死的,改库可见性不回写——一旦有人按它过滤,陈旧值立刻越权;skipped。首次加载约 13 秒,dev.sh 等不到它也会正常退出;那段时间视觉腿不可用是正常的。HF_HUB_OFFLINE=1 下加载会失败或极慢。换机器先准备模型缓存。dev.sh stop 杀不干净 uv run 派生子进程,旧 worker 继续用旧代码领任务,症状是”改了没生效”。重启前先确认没有残留。backup.sh 看它成功,证明不了能恢复。必须定期 restore.sh --scratch。遇到”搜不到”:
GET /api/v1/health 看 available_legs、queue、index_models;POST /kb/{id}/search/debug 看每条腿的 n/ms/skipped;skipped,按原因修依赖;如果全跑了但零命中,看 query 与库范围;GET /admin/consistency 分腿看缺/多;GET /jobs?doc_id= 看任务终态,巡检看”卡住的文档”。遇到”看到了不该看的”:
GET /kb 是否列出了它——列表用 visible_kb_clause;kb_id,范围来自 readable_kb_ids;visibility/workspace_id 是否被谁拿去做过滤;EXISTS 是否被写成独立 or。遇到”改了没生效”:
dev.sh restart,有没有残留 worker;reparse;这些不会让任何测试变红,只能靠巡检或对账发现:
kb_visual 恒 0 个点(视觉腿坏了,看起来像”库里没照片”)。documents.status 永远停在中间态(任务终态没搬过去)。corpus_stats.n_docs 与 chunk 行数不一致(df 虚高,idf 被压平)。