---
title: 召回正确性验证
description: 用独立扫描器对全量数据做牙位级交叉验证(差分测试),量化召回的 FP / FN。
icon: ShieldCheck
---

> **真理源**:扫描器 `apps/pac-service/sql/verify-recall.sql`;被验对象 `treatment-initiation-recall.scenario.ts`(召回本体)。每次摄入 / 改算法后跑一遍当回归。

---

## 方法:差分测试(独立重述,交叉比对)

扫描器**独立于 scenario 代码**,自己重述一份「码 → 排除口径(resolver / cooldown / 牙位剔除…)」配置,跟代码各写一份 —— **两边不一致即暴露**(差分测试,防口径漂移)。它读 `plan_reasons.signals` 反查 `patient_facts`,逐颗(患者 × 牙)摊开做 **2×2 牙位级交叉**:

| | 已召回 | 未召回 |
|---|---|---|
| **未治(有缺口)** | ✓ 该召 | FN 候选 |
| **已治(resolved)** | ✗ FP(已治却召) | ✓ 该排 |

两条判定纪律(纯 SQL 不建模会虚报,必须做对):

- **FN 必须逐层剥合法排除**才看"真漏召":未来预约 / 近期到诊(14d）/ 废用牙 · 乳牙 / §E 剔除 / 同牙更晚诊断取代…—— 剥完剩下的才是真漏。
- **FP 用「最新诊断」作时间基准**(非最早):否则"同牙复发(旧诊断治了、新诊断未治)"会被误判 FP。

## 性能:必须集合式

把治疗 / 召回 / 诊断牙位各 `unnest` 一次进**索引临时表 + ANALYZE**,再 EXISTS 索引查找。逐行相关子查询(`EXISTS + CROSS JOIN LATERAL unnest`)在 13 万患者上是 O(n²)(实测 1h21min 没跑完);集合式 **~39 秒**。

## 结论

最近一次全量参考运行(13 万患者):**FP 极低(约 0.02%)、真漏召接近 0**;FP 残量多为"继发龋 / 不良充填物复诊"(旧填料坏了又诊断龋)—— 其实是**对**的召回,被扫描器按"已治"误判,真 FP 更少。具体数字随数据 / 算法迭代变化,以当次运行输出为准。

> **口径**:上游 garbage-in(影像 AI 牙位错位 / name_map 误映射)的错误归"影像 / 摄入层"修,**不在召回逻辑里加猜对齐的启发式**(否则越堆越多 magic 还掩盖真 bug)。一线反馈核出的误召场景(固定桥盲点 / 间隙关闭 / 患者拒绝 / 外院已治疗 / ICD 错标纠偏)已收进召回算法的[牙位级判定](/docs/algorithms/recall)。

## 跑法

```bash
docker exec -i pac-postgres psql -U pac -d pac -f - < apps/pac-service/sql/verify-recall.sql
```

章节:§A 规模 / §B FP 硬闸(应 0)/ §C 牙位交叉 + FP / §C3–C4 FN 逐层解释 / §D 全口复发 / §E K08 正畸语境。
