# AI 话术生成 — 设计文档(v1 设计稿)

> **状态**:🎨 设计稿(抛开现状重设计,待评审后落地)
> **定位**:PAC 五大算法之"⑤ AI 话术生成"的重写设计。上游消费 fact.content / persona / plan_reasons,产出客服通话脚本。
> **背景**:现状是"技能组合 + 单次生成"(skill-composer + 1 次 AiCall → 4 段)。本文重新设计为**分档(投入档)+ 脊柱-策略分离**架构。
>
> **关联**:[`pac-algorithms-overview.md`](./pac-algorithms-overview.md) §五 · [`canonical-fact-layer.md`](./canonical-fact-layer.md)(接地纪律)· [`potential-treatment-recall-flow.md`](./potential-treatment-recall-flow.md)(上游召回)

---

## 一、话术要达成什么(目标函数)

一通召回电话的脚本,本质给客服 4 件事:

```
开场 → 切入(把"该治没治"自然带出)→ 异议(患者推脱怎么接)→ close(约下一步)
```

**硬约束**:
- **接地**:只用真事实,不编(医生没说的不准说)。
- **不承诺**:不保证疗效、不报价、不写死时间(诊所无排班接口,时间只能"示例")。
- **贴这个患者**:病种/牙位/年龄(儿童对家长)/新老客。
- **失败兜底**:任何环节挂 → 退回安全模板,客服永远有东西可用。

---

## 二、核心架构:一个旋钮,三样齐动;安全地板恒定

用户(客服)只感知**一个"投入档"旋钮**(稳健 / 标准 / 深度),它同时拉动三样;而**安全/接地地板对所有档恒定**。

```
投入档 ──┬─→ ① 输入 context:  base → base+ → base++   (扩展块增长)
         ├─→ ② 输出 rail:     4段模板 → 4段自由 → 多段自由(段数模型自定)
         └─→ ③ 步数:          1 → 1 → 3(深度加 计划/写/自检)

恒定地板(所有档):聚焦接地(只 canonical,不喂 raw EMR dump)+ 机器安全闸 + 兜底 + 落账
```

**关键自洽**:深度档拿到更多料(base++),正好配它多一步的**自检** —— 料越多越易跑题/编,自检来兜底。**富输入 ↔ 强校验,成对出现**。

### 2.1 档位定义(输出自由度 + 投入阶梯)

| 档 | 标签(客服看到) | 输入 context | 输出 rail | 步数 | 本质 |
|---|---|---|---|---|---|
| 1 | **稳健** | base | **4 段 + 模板骨架**(标题固定、部分句式固定,填空+润色) | 1 | 紧 rail,低方差,可预测 |
| 2 | **标准**(默认) | base + 小扩展 | **4 段,标题/结构自由**(去模板) | 1 | 松 rail,贴人 |
| 3 | **深度** | base + 大扩展 | **段数不限、多段**(模型自定结构) | 3(计划→写→自检) | 无 rail,质量最高 |

- **档位选择**:系统按患者价值/复杂度给**智能默认**,客服在"重新生成"处下拉**覆盖**(与已有"模型选择 Qwen/Gemini"并列两个旋钮)。3 档封顶,避免决策疲劳。
- **"组装提示词"是三档共用底座,不分档** —— 按 患者/病种/人群 选块拼指令,稳健也做,只是拼得更死。档位差在**输出 rail / 输入丰富度 / 步数**,不在"是否组装"。

### 2.2 步骤:控制流定死,内容开放

| 层 | 放不放开 |
|---|---|
| **pipeline 编排步骤**(跑几步/什么顺序) | ❌ 代码定死(可预测/可重放/自检保证跑) |
| **话术内容的段数/多步**(深度档多段) | ✅ 模型自定(输出 rail 放开) |
| **上下文丰富度**(露出多少事实) | ✅ 按档放开(扩展块,确定性预选) |
| **步骤自主**(模型自己决定 pipeline 跑几轮) | ❌ 不放开 |

> 深度档若缺料,升级路径 = 加一个**有界事实拉取工具**(模型可就某牙再要一段 canonical 事实,调用次数/范围代码卡死),**而非**开放步骤自主。

---

## 三、脊柱(固定)+ 策略(可插)

三档共用一根**脊柱**,只换中间一段**策略插件**(同 PlanScenarioPlugin / assembler 范式)。

```
        ┌───────────────── 固定脊柱(所有档共用)─────────────────┐
输入 →  [1 buildContext(reason, tier)] → [ ScriptStrategy(可插) ] → [3 机器安全闸] → [4 兜底] → [5 落账] → 输出
        └────────────────────────────┘      ↑换这里      └────────────────────────────────────────┘
```

```ts
// 脊柱契约
interface ScriptStrategy {
  tier: 'stable' | 'standard' | 'deep';
  generate(ctx: ScriptContext): Promise<ScriptDraft>;   // 内部步骤自己定死
}
// 脊柱控制流(定死):
//   buildContext(focusedReason, tier) → strategy.generate(ctx)
//     → machineSafetyGate(draft) → (fail ? templateFallback) → audit(agent_invocations)
```

加一档 = 实现一个 `ScriptStrategy`,脊柱一行不动。

### 脊柱六件(共性,恒定地板)

| # | 件 | 说明 |
|---|---|---|
| 1 | 输入契约 `ScriptContext` | 聚焦切片 + 薄画像 + 分档扩展(§四) |
| 2 | 输出契约 `ScriptDraft` | 统一结构,UI/存储不关心哪档产的(§五) |
| 3 | 机器安全闸 | 禁词/不承诺/不报价/不写死时间 —— **每档都过,不可谈判** |
| 4 | 兜底 | 任何失败 → 模板兜底(`source='template_fallback'`) |
| 5 | 落账 | `agent_invocations`:档位 / 模型 / token / cost / 用了哪些块或步骤 |
| 6 | provider 抽象 | 走 AiCall,DeepSeek / Gemini 可切 |

---

## 四、ScriptContext — 输入契约(地基)

**原则**:**聚焦切片 + 薄画像 > 全历史**。召回只为一件事(focusedReason),喂全历史 → 跑题、编造、token 爆。画像字段只**定语气**不进内容。

### base(三档一致,接地地板)

```
召回主体(聚焦,核心)——「说什么」
  scenario / sub_key、诊断码+名、牙位、拖了多久(daysSince)、目标治疗(expectedCategories)、
  做该诊断的医生(focusedReason 证据 fact 的 doctor_name)、
  医生医嘱原话(emr.doctor_advice / treat_plan.message,**仅引用、限本次诊断相关**)

薄画像(只定语气,不是内容)——「怎么对他说」
  称呼、年龄→人群(儿童对家长 / 老人慢)、新老客、价值档(persona.value)、
  流失风险(persona.recall_risk)、距上次就诊、(儿童/老人)监护人

合规态(已硬过滤,带过即可)
  doNotContact / deceased 已在召回层排除;触达对象 = 本人 or 监护人(儿童/老人)
```

> ⚠️ **"诊断了未启动"不需要治疗链** —— focusedReason 存在本身就代表"诊断未启动治疗",不读 chain。

### 扩展块(随档增长,仍全是 canonical 切片,非 raw dump)

| 档 | 在 base 上加 |
|---|---|
| 稳健 | 无(就 base) |
| 标准 + | 同区域相关诊断/缺口(可顺带提)、最近 1-2 次就诊**做过什么治疗**(treatment_record:类目/牙位/时间)、**上次回访结果**(returnVisits) |
| 深度 ++ | **历史召回 outcome**(recallHistory:上次为啥没成/被暂缓)、可一并邀约的其他牙问题、家庭/联系人(家庭决策)、投诉历史(避雷)、消费结构(VIP 维系话术)、相关 EMR 自由文本(**仅引用,不演绎**) |

- 扩展由脊柱 `buildContext(reason, tier)` **确定性抽取**(按 focusedReason 选块),不是把全表丢进去、更不是让模型自己捞。

---

## 五、ScriptDraft — 输出契约

```
status: ready | failed
source: agent | template_fallback
tier:   stable | standard | deep
sections: [{ id, title, markdown }]      // 稳健=固定4段;标准=4段自由标题;深度=多段
metadata: { model, promptVersion, blocksUsed[], stepsRun[], tokens, costCents }
```

- 落 `plan_scripts.content`(Markdown 多子段),`source` / `agentInvocationId` 同现状 schema。
- **UI/存储与档位解耦** —— 不管哪档,都是 `sections[]`。

---

## 六、安全 / 接地(地板,所有档)

| 闸 | 机制 |
|---|---|
| **接地** | 只喂 `ScriptContext`(canonical 切片);**严禁拼 raw EMR 全文 dump**;EMR 自由文本只作"引用",指令"只能引用医生原话,不可演绎" |
| **机器安全闸** | 正则/规则:禁词、无"保证治好"式承诺、无报价、close 段无加粗写死时间。**每档输出必过** |
| **深度档额外**:独立 LLM 校验 | 逐句核"每个说法能追到 ScriptContext 里的 fact" + 复查安全 → 不过则 repair / 兜底(呼应召回 oracle 的对抗哲学) |
| **兜底** | 任何失败 → 模板话术,`source='template_fallback'` |

---

## 七、患者信息清单 —— 详情页能给的 vs 能进大模型

> 调研自 `/plans/:id/full`(plan-aggregate)实际 payload + 本地全量 fact content 字段。

### ✅ 可进(结构化 canonical,安全)

| 来源 | 字段 | 用途 |
|---|---|---|
| diagnosis_record | code / name_zh / tooth_position / onset_date / severity | 召回主体 |
| treatment_record | category / subtype / tooth_position / status / started_at·completed_at | **"做过什么治疗"**(替代治疗链) |
| recommendation_record | code / tooth_position / window_days / confidence | 医生建议信号 |
| appointment_record | scheduled_at / status / complaint_category | 是否已有预约 / 就诊意向 |
| payment·recharge·refund | amount_cents | 价值/消费(薄画像) |
| returnVisits | type / status / treatmentItems / **result** | 上次回访怎么说的 |
| recallHistory | outcome / notes / scheduledNextAt | 上次召回结果(深度) |
| persona | **value / recall_risk**(只这两个,定语气) | 价值/流失 |
| patient/profile | 称呼 / 年龄→人群 / 新老客 / 距上次就诊 / 监护人 / dedicatedCs | 薄画像 |
| plan_reasons | scenario / sub_key / signals(牙位·daysSince·expectedCategories) | 召回主体 |

### ⚠️ 慎入(自由文本 → 编造风险;仅"引用"、限相关、深度档为主)

| 来源 | 字段 |
|---|---|
| emr_record | **doctor_advice / treatment_plan**(医嘱原话,base 可引用本次相关那条)<br/>illness_desc / pre_illness / exam_findings / past_history / disposal(深度档,仅引用) |
| image_record | finding(影像所见,仅引用) |

### ❌ 不进

| 排除项 | 原因 |
|---|---|
| **chains[](治疗链 5 阶段 / chain-composer)** | **后续废弃,链相关一律不进**;需要"做过什么"用 treatment_record |
| **persona.treatment_chain_status** | 名为"链状态",概念与废弃的治疗链重叠 → 不用;"诊断未启动"由 focusedReason 自带 |
| raw EMR 全文 dump | 接地纪律:不整段塞自由文本(只受控引用) |
| 身份证 / 详细地址 / 婚育等 | 召回不需要 + 合规(PAC 主档本就不存) |
| 完整消费流水 / 无关旧诊断 | 聚焦原则:让话术跑题 + 涨 token |

---

## 八、智能默认(档位路由,草案)

```
默认档 = f(患者价值, 复杂度):
  低值 / 单病种单牙           → 稳健
  常规                       → 标准(默认)
  高值(VIP/钻金)/ 多病种多牙 → 深度
客服可在"重新生成"下拉覆盖(档位 + 模型 两旋钮)
```
(阈值待业务方拍;真理源放配置,可调。)

---

## 八bis、现状落地(诚实记录)

> 抛开任务清单的"声称",**实际在跑的稳健档** = 以下三件,别的(病种 SKILL.md / 异议库 / 安全规则包)**没建**。

| 件 | 现状 |
|---|---|
| system 提示词 | `base-system.md`(铁律)+ **人群包 `population/adult|child/SKILL.md`**(4段模板骨架)|
| user 提示词 | `script-facts.ts` **确定性前置算好**(称呼/智能日期/漏诊项/风险/优势/复查时长)注入,LLM 只润色 |
| 病种差异化 | **不在 SKILL.md**,而在 `script-facts.ts` 字典(`MISSED_DIAGNOSIS_KEY_POINTS` / `SUBKEY_TO_MISSED` / `TREATMENT_DURATION`)插值 |

**已知 bug**:`jaw_cyst` 的 key `颌骨囊肿` 在 `MISSED_DIAGNOSIS_KEY_POINTS` 里只有 `囊肿` → 精确查表 miss → 颌骨囊肿召回无风险/优势要点(双跳 + exact/includes 混用的脆弱性,§九 P0 修)。

---

## 八ter、⭐ 知识 vs 模板 分离(核心架构,多档共用的关键)

> **洞察**:现在的"人群包"和"病种字典"都是**稳健档的模板**(固定结构 + 插值)。其他档接入时,**模板不能共用** —— 要把"知识/规则"从"模板"里抽出来:**知识 tier-agnostic 共用,模板只属稳健**。

### 两类内容,必须切开

| | 知识 / 规则(tier-agnostic,单一源)| 模板(稳健档专用)|
|---|---|---|
| **病种** | 风险(不处理后果)/ 趁早理由 / 复查周期 / 临床要点 —— **规则事实** | "4段·小节1现状/小节2风险…"脚手架 + 挑 1-2 条插槽 |
| **人群** | 儿童对家长 / 老人慢+复述 / 新老客语气 —— **沟通规则** | 成人 4 句 / 儿童 5 句的固定句位 |

### 三档怎么消费同一份知识

```
              ┌─ 病种知识(canonical-keyed)─┐   ┌─ 人群知识(child/adolescent/adult/elderly)─┐
              └──────────── 单一源 知识库(tier-agnostic)────────────────────────┘
                                          │
   稳健 ───────────────────────────────────┼─ 程序挑选(1-2 条)→ 填 4段模板槽 → LLM 只润色
   标准 ───────────────────────────────────┼─ 规则全量 → 放进 prompt 作"事实+规则" → LLM 自由组织 4 段
   深度 ───────────────────────────────────┴─ 同标准 + 多段 + 计划/写/自检
```

- **稳健 = 知识 + 模板**(强约束,LLM 填空)。
- **标准/深度 = 知识(只给规则,不给模板)**,LLM 自己组织 → 模板不复用。

### 文件落位(收口)

> ⭐ **核心**:`base` 和 `人群` **各自都拆成"共性部分(三档共用)+ 稳健模板部分(独占)"** —— 不是整块归稳健。
> 病种的**口语文案**(risks/advantages/duration)是**稳健模板**(为填槽而写),不是共性知识;
> 共性的病种只有**抽象临床要点**(规则形态),且其他档多半用 LLM 自带常识即可,未必需要。

```
calls/script/
  shared/                           ← 真·tier-agnostic(三档共用)
    script-context (input.types)    ← 患者事实切片(✅ 已有)
    safety-rules.ts                 ← 禁词/承诺/报价/拍片 **单一源**(机器闸 + 各档 base 同引)
    base-common.md                  ← base 的**共性**:角色定位(医疗关怀非销售)/语调/接地铁律/不承诺
    population-common.md            ← 人群的**共性沟通知识**:儿童→家长 / 老人→慢+复述 / 新老客语气
    clinical-points.ts (可选)        ← 抽象病种临床要点(规则,非口语);标准/深度可引用,未必需要
  tiers/
    stable/                         ← 稳健独占(全是"模板/格式")
      base-format.md                ← base 的**稳健部分**:4段结构 + 占位符约定({}/【】)+ 顺序固定
      population-template/{adult,child}.md  ← 4段·句位脚手架(成人4句/儿童5句)
      phrasing.ts                   ← 口语文案库:risks/advantages/duration(现 disease-knowledge 内容,落位纠正到此)
      stable.call.ts
    standard/ deep/ (future)        ← 各自 base-format;只吃 shared(context+safety+base-common+population-common),
                                       临床措辞 LLM 自供
```

**两条拆分线**:
- **base** = `base-common.md`(定位/语调/安全/接地,共用)+ `tiers/*/base-format.md`(输出格式,每档不同)。
- **人群** = `population-common.md`(沟通知识,共用)+ `tiers/stable/population-template`(固定句位,稳健独占)。
- **病种** = (可选)`clinical-points` 抽象要点(共用)+ `tiers/stable/phrasing.ts` 口语文案(稳健独占)。
- 现 `disease-knowledge.ts`(口语文案)落位纠正:它是**稳健 phrasing**,reorg 时移入 `tiers/stable/phrasing.ts`,不在 shared。

---

## 八quater、单一源收口清单(整理 drift)

| 东西 | 现状(多处/漂移)| 收口 |
|---|---|---|
| 病种知识 | 3 字典双跳 + exact/includes 混用(颌骨囊肿 漏)| **1 个 disease-knowledge(subKey-keyed)** |
| 人群 | SKILL.md 把"知识 + 稳健模板"混在一起 | 拆:population-knowledge(共用)+ stable/template.md(独占)|
| 禁词 | `base-system.md` 一份 + `call.ts FORBIDDEN_PHRASES` 一份(**不一致**,base 多了 免费/赠送)| **1 个 safety-rules.ts**;机器闸用它,prompt 从它生成 |
| 4 段结构规则 | `schema.describe` + `base-system.md` + `population SKILL.md` **三处复述** | base 管铁律 / 稳健模板管段内 / schema 管字段,**不互相复读** |
| ≤18 禁拍片 | 仅 prompt,机器闸不查(无 age)| safety context 加 age + 机器规则,**双保险** |

---

## 九、待定 / 下一步

| 项 | 状态 |
|---|---|
| **P0 病种知识单 config 化**(3 字典双跳 → 1 个 subKey-keyed,修 颌骨囊肿)| 🎯 先做(修 bug + 收口 + 为多档铺路)|
| **P0 ScriptContext base 已落地**(剔治疗链 + 正名,本轮完成)| ✅ |
| **P1 人群 SKILL.md 拆分**(知识 → population-knowledge 共用;模板 → stable/template.md 独占)| 🎯 标准档接入时 |
| **P1 禁词单一源**(base-system.md + call.ts 合并 → safety-rules.ts)| 🎯 |
| **P1 ≤18 禁拍片机器闸**(safety context 加 age)| 🎯 |
| **P1 4段规则去三处复述**(base/schema/模板 职责分清)| 🟡 |
| 稳健档 4 段模板 + 标准档 4 段自由 prompt 草稿 | 🎯 起草对比手感 |
| 深度档 3 步(planner schema / writer / verifier 判则)细化 | 🎯 |
| 老人(elderly)人群知识补全 | 🟡 1.5w 老人,语气慢/复述/家属 |
| 档位智能默认阈值 | 🟡 待业务方 |
| 有界事实拉取工具(深度档可选升级) | 🔵 按需 |
| 成功通话 RAG 喂养(W7+ 有 outcome 数据后) | 🔵 增强层 |

---

## ▎一句话归纳

> **一个"投入档"旋钮联动(输入 base→base++ / 输出 4段模板→多段自由 / 步数 1→3),安全接地地板恒定;脊柱固定、策略可插(每档一个 ScriptStrategy);上下文聚焦切片 + 薄画像,治疗链不进(用 treatment_record 看"做过什么");pipeline 步骤定死、话术段数模型自定。**
