# PAC 三大算法 · 通俗说明

> **谁该读**:产品 / 运营 / 诊所管理者 / 客服主管 —— 不需要懂技术。
> **本文目的**:用大白话讲清楚 PAC 到底在算什么、为什么这么算。
> **读法**:正文是大白话;每个关键论断下面带一个 `📐 证据` 块(简短公式 / 数值 / 判断条件),
> 专业人员看证据即可核对,非技术读者可跳过。完整实现见 `potential-treatment-recall-flow.md`。

---

## 〇、PAC 是干什么的

一句话:**PAC 帮诊所找出"该回来但还没回来"的患者,告诉客服先打给谁、怎么打。**

诊所每天产生大量就诊记录(诊断、治疗、收费、预约……),但里面藏着很多"漏掉的机会":
医生说过"这颗牙该补",患者却一直没来补;种植做完该年度复查,患者忘了（将来实现）……

PAC 做三件事,像一条流水线:

```
   全院患者                                            客服每天的电话清单
      │                                                      ▲
      │  ① 召回算法                ② 画像算法        ③ 优先级算法 │
      └──「谁该被请回来?」──→「这个人是谁?」──→「先打给谁?」──┘
          (筛出候选)            (了解背景)          (排好顺序)
```

- **① 召回算法**:从几万患者里,筛出"该回来做点什么、但还没做"的人。
- **② 画像算法**:给每个筛出来的患者贴标签(价值高不高、容易流失吗、能不能打扰)。
- **③ 优先级算法**:把这些人排个先后,客服时间有限,先打最该打的。

下面逐个讲。

---

## 一、召回算法 —— "谁该被请回来?"

### 它在找什么

**核心判断只有一句**:
> 医生发现/建议了某个治疗,但患者一直没去做 —— 这个人就该被召回。

举例:
- 医生诊断"36 号牙缺失,建议种植",可患者半年都没来种 → **该召回**
- 医生说"有牙周炎,要做基础治疗",患者没做 → **该召回**
- 医生发现龋齿要补,患者没补 → **该召回**

> 📐 **证据 · 触发条件**(两个信号源,任一命中):
> ```
> 有一条 active 的「诊断记录」或「医生建议」  (diagnosis_record / recommendation_record)
>   且其 code ∈ 本子场景的诊断码/建议码集合
>   且【不存在】诊断之后启动的同类治疗(见第 3 道过滤网)
> ```
> 诊断 confidence=1.0;AI 从文本抽的建议 confidence=0.8(影响优先级,见算法三⑥)。

### 覆盖哪些情况

按牙科疾病大类(ICD-10 的 K00–K09),PAC 覆盖 **10 种**"发现了没去做"的情况。
表里的 **基线分** 就是这种病的"重要程度起步分"(越值钱/越严重越高,直接进优先级算法):

| 诊断码 | 情况 | 大白话 | 基线分 | 冷静期(天) |
|---|---|---|---|---|
| K08 | 缺失牙未修复 | 缺了牙没去种/没镶 | **60** | 30 |
| K07 | 错颌畸形未正畸 | 牙不齐没去矫正 | 55 | 30 |
| K04 | 牙髓炎未做根管 | 牙神经发炎没去做根管 | 52 | 14 |
| K05 | 牙周炎未做基础治疗 | 牙周病没去洗治 | 50 | 30 |
| K09 | 颌骨囊肿未处理 | 颌骨囊肿没去摘 | 50 | 14 |
| K02 | 龋齿未充填 | 蛀牙没去补 | 45 | 14 |
| K03 | 牙体损伤未修复 | 牙磨损/缺损没去修 | 35 | 14 |
| K06 | 牙龈牙槽问题未处置 | 牙龈问题没去看 | 35 | 14 |
| K01 | 阻生牙未拔除 | 智齿该拔没拔 | 30 | 14 |
| K00 | 牙发育/萌出异常未处置 | 儿童换牙/长牙问题没去看 | 25 | 30 |

> 📐 **证据**:基线分 = scenario 配置 `SUB_SCENARIOS[*].base`;冷静期 = `DiagnosisTreatmentMap[K??].cooldownDays`。
> 排序体现临床价值:种植(¥1.5–3 万)> 正畸 > 根管 > 牙周 > 龋齿 > 智齿。**不漏码**:出现的 K-code 都进池,低基线的靠分数自然沉底。

### 三道"过滤网"(避免打错电话)

筛选时层层过滤,确保不打扰不该打的人:

**第 1 道 · 合规红线**(硬规矩,一票否决):
- 已故患者 → 不打
- 标了"勿打扰"的 → 不打
- 没有电话的 → 提示客服补,但不算合规拒绝(数据问题 ≠ 不让打)

> 📐 **证据**:`p.active = true AND pp.do_not_contact = false AND pp.deceased = false`(SQL 三条 WHERE,任一不满足直接踢)。

**第 2 道 · 时间窗**(不能太早,但不嫌晚):
- **太早不打**:医生刚诊断完(比如缺牙 30 天内),患者还在考虑,这时候打像催债。每种病有自己的"冷静期"(见上表)。
- **越晚越该打**:缺牙拖了一年,比拖三个月更该召回。所以**没有"太晚就不打"这一说**——拖得久的不会被踢掉,只是在排队时往后稍微挪(交给优先级算法的时间窗因子衰减)。

> 📐 **证据**:只设下界 `诊断日 <= now − cooldownDays`,**不设上界**。
> (旧版曾有上界 = 黄金窗末,导致缺牙 >180 天被错误踢出池;W3 末已废上界。)

**第 3 道 · 已经做了就别打**(关键):
- 患者**已经做了**对应治疗 → 不召回(已经补过这颗牙了,别再喊人来补)
- 精确到**牙位**判断:36 号牙补过了,但 46 号牙还没补 → 只召回 46 号那件事
- 已经把这颗牙**拔了** → 这件事终结,不召回
- 已经**约了**未来的号 → 人在来的路上了,不打扰

> 📐 **证据**:`NOT EXISTS(诊断之后启动的同类 actual 治疗)`,且:
> ```
> 双方都有牙位 → 牙位集合有交集才算"已做"(36 做了不挡 46)
> 信号无牙位(如牙周全口) → 按类别(category)算
> 治疗无牙位(如全口洁治)  → 视为覆盖该类全部牙位
> 时间方向:治疗.occurred_at >= 诊断.occurred_at(只算诊断后启动的,历史旧治疗不算)
> ```

### 一个聪明的合并

同一个患者,可能同时有好几颗牙、好几个问题。PAC 会**智能合并**:
- 同一类问题、相邻或重叠的牙位 → 合成一条(比如"18、28、38、48 四颗智齿都要拔"合成一件事)
- 不同牙位的不同问题 → 分开列(36 要补 + 全口要做牙周 → 两件事)

这样客服看到的是**整理好的几件事**,而不是一堆零散记录。

> 📐 **证据 · 合并规则**(按牙位重叠做并查集 union-find):
> ```
> 同患者同病种的多条诊断:牙位集合有交集 → 并入同一组
> 每组产出一条理由,标识 = 病种@牙位:
>   caries_no_filling@36          (单颗)
>   impacted_tooth@18;28;38;48    (多颗合并)
>   perio_no_srp@whole            (全口诊断无具体牙位)
> 组内取最早诊断日(最有召回价值);两种信号源都有 → 标"(诊断+医生建议)"
> ```

> **本质**:召回算法不"猜",它只认医生白纸黑字写下的诊断和建议。医生没说的,它绝不编。

---

## 二、画像算法 —— "这个患者是谁?"

筛出候选后,光知道"该召回"还不够,客服打电话前想知道:这人值不值得花时间?会不会已经不想来了?能不能打?

画像算法给每个患者打 **4 个标签**:

### 标签 1 · 患者价值
**这个患者对诊所贡献多大?** 按累计净消费分 5 档。

> 📐 **证据**:`累计净额 = Σ收款 + Σ充值 − Σ退费`,分档:
> ```
> ≥ ¥30,000 → 钻卡 (score 4)    ≥ ¥10,000 → 金卡 (3)
> ≥ ¥3,000  → 银卡 (2)          ≥ ¥500   → 普通付费 (1)
> < ¥500    → 新客/未消费 (0)
> ```
> 这个 score 直接喂优先级算法的"价值加分"(见算法三③)。

### 标签 2 · 流失风险
**这个患者是不是快"跑"了?** 看多久没来 + 治疗链有没有断口。

> 📐 **证据**(距上次临床事件天数 + 链缺口):
> ```
> ≥540 天 且 链有缺口 → high   (score 3)
> ≥360 天 或 链有缺口 → medium (2)
> ≥180 天             → low    (1)
> 否则                 → none   (0)
> ```
> 风险越低 → 越可能接电话愿意来 → 优先级"转化加分"越高(见算法三④,有点反直觉:不是越高风险越优先,而是越可能转化越优先)。

### 标签 3 · 治疗链状态
**这个患者的治疗进行到哪一步了?** 从"发现"到"闭环"分几个状态。

> 📐 **证据**(读独立的诊断/治疗记录判断):
> ```
> 有 actual 治疗在管(active)        → in_progress(进行中)
> 所有诊断都有对应已完成治疗          → closed(已闭环)
> 有诊断但缺对应治疗                  → gap(有缺口,= 召回切入点)
> ```

### 标签 4 · 不打扰状态
**这个患者能不能联系?** 标记勿打扰、已故、投诉等合规状态。

> 📐 **证据**(硬命中任一即标 DNC):
> ```
> profile.do_not_contact = true  或  profile.deceased = true  或  有投诉记录
> ⚠️ phone 缺失【不算】DNC(那是数据问题,只提示客服补,不拦召回)
> ```
> 跟召回算法第 1 道合规红线呼应,双保险。

### 两个特点

1. **全靠规则,不靠 AI 猜**:每个标签怎么算都是写死的明确规则(比如"累计消费 > X 算高价值"),**可解释、可追溯**——主管问"为什么标他高价值",能立刻答出来。
2. **自动更新**:患者有新动作(新就诊、新消费)就自动重算标签,不用人工维护。

> **本质**:画像算法把散乱的就诊记录,浓缩成客服一眼能读懂的几个标签。

---

## 三、召回优先级算法 —— "先打给谁?"

客服一天能打的电话有限。同样是"该召回",500 个候选里先打谁?优先级算法给每个人打一个 **0–100 的分数**,分越高越优先。

### 分数怎么来(6 个因素)

把它想成**给每个召回机会打分,像考试加权**:

| 因素 | 通俗解释 | 📐 取值证据 |
|---|---|---|
| **① 临床基线** | 这种病本身多重要 | 病种基线分(K08=60 / K07=55 / K04=52 / K05=50 / K02=45 / … / K00=25)|
| **② 时间窗形状** | 现在是不是召回最佳时机 | 黄金窗内 ×1.0;过早 0.6→1.0 线性;过晚 1.0→0.4 衰减(2× 窗末降到底) |
| **③ 患者价值加分** | 高价值患者加分 | 钻卡 +20 / 金卡 +15 / 银卡 +10 / 普通 +5 / 新客 +0 |
| **④ 转化可能加分** | 接电话后真来的可能性 | `(3−风险档)×2`(0–6)+ 近期回访成功 +4,上限 +10 |
| **⑤ 临床紧迫加分** | 再拖会出大问题 | 诊断天数 > 该病种紧迫临界 → +5(如缺牙 >120 天) |
| **⑥ 信号可信度** | 召回理由多靠谱 | 最低 confidence ≥0.9→×1.0;≥0.7→×0.9;否则 ×0.75 |

**完整公式**(代码 `priority-scorer.ts` 1:1):
```
raw   = (base × timeWindowFactor + valueBonus + likelihoodBonus + urgencyBonus) × confidenceFactor
score = clamp(round(raw), 0, 100)
```

> 📐 阈值都集中在 `priority-scorer.ts` 的 `computeXxx()` 函数,业务方改分档改这一处即可。

### 真实例子:卜晓平,87 分

| 因素 | 他的情况 | 贡献 | 对应公式 |
|---|---|---|---|
| 临床基线 | 缺失牙 K08 | 60 | base |
| 时间窗 | 缺了 151 天 ∈ [30,180] 黄金窗 | ×1.0 | timeWindowFactor |
| 患者价值 | 钻石卡,累计 5.7 万+(≥¥3 万)| +20 | valueBonus |
| 转化可能 | 流失风险=中(链有缺口)→ (3−2)×2 | +2 | likelihoodBonus |
| 临床紧迫 | 151 > 紧迫临界 120(邻牙倾斜)| +5 | urgencyBonus |
| 信号可信度 | 医生明确诊断 confidence=1.0 | ×1.0 | confidenceFactor |
| **最终** | (60×1.0 + 20 + 2 + 5) × 1.0 | **= 87 分** | clamp(round(raw),0,100) |

### 为什么要"可解释"

每个分数都附带**完整的计算明细**(上面那张表),客服详情页能直接看到。
主管问"为什么他排第一",不是"算法说的",而是**6 个因素一条条摆出来**——这是 PAC 跟黑盒推荐系统的根本区别:**可解释 = 可信任**。

> **本质**:优先级算法把"该召回的一堆人"排成"今天先打这几个"的有序清单,且每个排序理由透明。

---

## 四、三者怎么配合(端到端)

用卜晓平走一遍完整流程:

```
① 召回算法
   扫全院 → 发现卜晓平 151 天前诊断"36 缺失",至今没种植,且没拔没约
   → 通过合规红线(在世/可联系)→ 进候选池,理由="缺失牙未启动修复"

② 画像算法
   给卜晓平贴标签:价值=钻石卡高价值 / 流失风险=低 / 治疗链=种植已发现未启动 / 可联系
   → 客服打电话前就知道"这是个重要客户,值得好好聊"

③ 优先级算法
   综合 6 因素算出 87 分 → 在今天的电话清单里排很靠前
   → 客服优先打给他,且看得到"为什么是 87 分"

(再往后)
   客服点"生成话术" → AI 根据卜晓平的年龄/缺牙情况/老客身份,生成定制开场白和应答
```

**一句话总括**:
> **召回算法**筛出"该回来的人",**画像算法**告诉客服"他是谁",**优先级算法**排好"先打谁"——三者串成一条流水线,把诊所散乱的就诊数据,变成客服每天一份**排好序、带背景、可解释**的电话清单。

---

## 附:常见疑问

**Q:算法会不会乱召回、打扰患者?**
A:不会。三道过滤网(合规红线 + 时间冷静期 + 已做就不打)层层把关;且只认医生写下的诊断,不自己编理由。

**Q:为什么有的患者分高有的分低?**
A:6 个因素综合算,每个患者的明细都能在详情页看到,完全透明。

**Q:画像标签准吗?会不会 AI 算错?**
A:画像目前全是明确规则算的(不是 AI 猜),比如"消费多少算高价值"都是写死的,可解释可追溯。

**Q:换一家诊所/集团,算法还能用吗?**
A:能。算法逻辑跟具体诊所无关,新诊所只需要把自己的数据按格式对接进来,算法直接复用。
