# PAC 三层模型 — schema 落地版

> Patient Facts(事实)→ Persona(画像)→ Follow-up Plan(召回计划)
> 单向数据流 + 版本化推断 + 横切审计账本

> **⚠️ 代码漂移修订(2026-06-16,以代码为准):** 图中 `LLM Provider` 注释"v1 = DeepSeek 直连"已扩展为 **AiCall 框架多 provider 路由**(deepseek/gemini/qwen,摘要类走 Qwen),无 Dify;`plan_summaries` 由 3 type 增至 **4 type**(新增 `recall_history`);隔离基线字段为 **`hostId`**(非 `platformId`)。

---

## 一张图

```mermaid
flowchart TB

%% =========== 外部数据源 ===========
subgraph Sources["🌐 宿主数据源(异构)"]
  direction LR
  H1["5i5ya"]
  H2["Friday"]
  H3["400 中心"]
  H4["DW 数仓"]
end

%% =========== 接入层 ===========
subgraph Ingest["📥 接入层(adapter)"]
  direction LR
  PULL["Pull adapter<br/>━━━━━━━━━━━<br/>周期拉资源<br/>比对前后态合成 transaction"]
  PUSH["Push adapter<br/>━━━━━━━━━━━<br/>实时收事件<br/>HMAC 验签"]
end

%% =========== Layer 1 ===========
subgraph L1["📘 第一层 · 患者事实"]
  direction TB
  PT["<b>patient_transactions</b><br/>━━━━━━━━━━━━━━━━━━<br/>操作账本(append-only)<br/>source_event_id 幂等键<br/>event_seq BigInt 水位<br/>action(14)× subject_type(10)<br/>raw_payload 原文留底"]
  PARSER{{"Parser<br/>解析 + canonical + 衍生"}}
  PF["<b>patient_facts</b><br/>━━━━━━━━━━━━━━━━━━<br/>事实单元(版本流)<br/>subject_id × version<br/>status: active/superseded/<br/>cancelled/fulfilled/expired/invalidated<br/>type: actual 12 + planned 4<br/>transaction_ids 证据链"]
  PAT["<b>patients</b><br/>━━━━━━━━━━<br/>主档(身份)<br/>tenant 级唯一<br/>mergedInto 自引用<br/>不物删,只软删 / 匿名"]
  PT -->|"1 transaction<br/>→ 0/1/N fact 版本"| PARSER
  PARSER --> PF
end

%% =========== Layer 2 ===========
subgraph L2["🟠 第二层 · Persona 画像"]
  direction TB
  PER["<b>personas</b><br/>━━━━━━━━━━━━━<br/>版本流 supersededAt<br/>eventWatermark 水位<br/>(基于 facts active 算)"]
  PFEAT["<b>persona_features</b><br/>━━━━━━━━━━━━━<br/>key + description<br/>+ score? + evidence{factIds...}<br/>13 个候选 key"]
  PER --> PFEAT
end

%% =========== Layer 3 ===========
subgraph L3["🟣 第三层 · 召回计划"]
  direction TB
  FP["<b>followup_plans</b><br/>━━━━━━━━━━━━━━━━━━<br/>版本流 + target_clinic_id<br/>status: active/assigned/<br/>completed/abandoned/superseded<br/>partial UNIQUE(单一活跃)"]
  PS["<b>plan_scripts</b><br/>━━━━━━━━━━━━<br/>per-plan 单行<br/>Markdown 多子段<br/>status: pending/ready/failed<br/>source: agent/template_fallback"]
  PSU["<b>plan_summaries</b><br/>━━━━━━━━━━━━<br/>per-plan × 3 type<br/>(one_page / medical_record<br/>/ treatment_chain)"]
  PEX["<b>plan_executions</b><br/>━━━━━━━━━━━━<br/>executor_clinic_id<br/>outcome 强枚举驱动状态机"]
  FP --> PS
  FP --> PSU
  FP -->|"客服执行"| PEX
end

%% =========== AI 调用通路 ===========
subgraph AICall["🤖 AI 调用通路 · AiCall 框架"]
  direction LR
  AIC["AiCall 框架<br/>━━━━━━━━━━━━<br/>PAC 内部(packages/ai-core)<br/>统一鉴权 / 限流 / 计费 / 缓存<br/>每次调用强制落 agent_invocations"]
  LLM["LLM Provider<br/>━━━━━━━━━━<br/>v1 = DeepSeek V4 Pro 直连<br/>(国内合规)<br/>未来可切 Qwen / GPT 等"]
  AIC --> LLM
end

%% =========== 横切 ===========
subgraph Cross["🔧 横切支撑(账本)"]
  direction LR
  AI["agent_invocations<br/>━━━━━━━━━━━━<br/>AI 调用审计账本<br/>每次调用强制落一条<br/>token / cost / replay<br/>modelProvider / modelName / callKey"]
  SL["sync_logs<br/>━━━━━━━━━━<br/>同步运行账本<br/>cursor / reconcile"]
  PRL["persona_recompute_logs<br/>━━━━━━━━━━━━━<br/>画像重算账本<br/>rule/agent/fallback 计数"]
  PGL["plan_generation_logs<br/>━━━━━━━━━━━━<br/>召回生成账本<br/>scenarioRun / assets / agent"]
end

%% =========== 主数据流(实线) ===========
Sources --> Ingest
PULL --> PT
PUSH --> PT
PF -->|"读 active 版本<br/>跑 features 提取器"| PER
PF -->|"主输入"| FP
PER -.->|"可选增强<br/>(enhanced 模式)"| FP

%% =========== 反馈流(虚线,只回到 Plan 层) ===========
PEX -.->|"状态机反馈<br/><b>v1 不回写 facts</b>"| FP

%% =========== AI 调用链(异步 BullMQ → AiCall → DeepSeek) ===========
PS -.->|"draft_plan_script<br/>(异步 BullMQ)"| AIC
PSU -.->|"draft_plan_summary<br/>(一次产 3 type)"| AIC
AIC -.->|"调用落账"| AI

%% =========== 编排账本关联(虚线) ===========
Ingest -.-> SL
PER -.-> PRL
FP -.-> PGL

%% =========== 颜色 ===========
style Sources fill:#fff3e0,stroke:#f57c00
style Ingest  fill:#e0f7fa,stroke:#00838f
style L1 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style L2 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style L3 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
style AICall fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
style Cross fill:#f5f5f5,stroke:#616161
style PT fill:#bbdefb
style PF fill:#bbdefb
style PAT fill:#bbdefb
style PER fill:#ffe0b2
style PFEAT fill:#ffe0b2
style FP fill:#e1bee7
style PS fill:#e1bee7
style PSU fill:#e1bee7
style PEX fill:#e1bee7
style AIC fill:#c8e6c9
style LLM fill:#c8e6c9
```

**图例**:实线 = 主数据流(单向);虚线 = 反馈 / 异步调用 / 编排关联

---

## 单向数据流纪律

```
宿主 → transactions → facts → persona → plan → assets/execution
                       ↑                  ↑
                       └─不可回写─┘
```

- **数据流严格单向**:facts → persona → plan,从不回头写
- **plan_executions** 反馈只回 Plan 状态机(active → assigned → completed / abandoned),**v1 不回写 facts / transactions**
- **AI 解析结果**(如影像 AI)归 `agent_invocations.output`,**不进 fact 层**
- **Merge 穿透**:status='merged' 的旧档,历史 facts/transactions **不迁移**,应用层用递归 CTE 穿透读 root + mergedFrom 子树

---

## 各层生成时机 & 策略

### 第一层 · 患者事实

| 子表 | 写入模式 | 触发 |
|---|---|---|
| `patient_transactions` | append-only | Pull cron(周期)/ Push 实时 / Manual 重放 |
| `patient_facts` | 版本流(append + supersede) | transaction 落库后立即调 parser 衍生 |
| `patients` | upsert | 主档拉取 / Push 首见 stub 创建 |

**关键纪律**:
- `source_event_id` 幂等(MIGRATION partial UNIQUE)
- `raw_payload` 永久原文,fieldMapping 改了能重跑
- 并发写 fact 用 BullMQ `jobName=subject_id` 串行化

### 第二层 · Persona 画像

```
事件驱动 ┐
定时兜底 ┼→ PersonaService.recompute(patientId)
人工救火 ┘     (核心函数,共享)
                 ↓
        队列收敛 (BullMQ jobName=patient_id)
                 ↓
     水位幂等:if MAX(event_seq) ≤ persona.eventWatermark → no-op
                 ↓
     读 patient_facts active → 跑 rule + agent extractors
                 ↓
     写新 persona + features(supersede 旧)
```

| 触发源 | triggeredBy | 时机 | 价值 |
|---|---|---|---|
| txn 事件驱动 | `txn:<id>` | 新 transaction 落库即触发 | ⭐⭐⭐ 主路径,秒级 |
| 手工 | `manual:<userId>` | admin / API 调试 | ⭐⭐ 运维必需 |
| 定时兜底 | `scheduled:<cronId>` | 每日扫漏 | ⭐⭐ 可靠性 |

### 第三层 · 召回计划

```
v1 主路径 = 每日 cron 全扫
  ├─ 命中 scenario 规则 + 无 active plan → 生成新 plan
  ├─ 命中 + 已有 active plan (差异小) → skip
  ├─ 命中 + 已有 assigned plan → 不动(避免抢正在跟进的客服)
  └─ 生成完触发 ScriptAgent / SummaryAgent (异步)

manual API = POST /pac/v1/plans/recompute (单 patient)
  ├─ refreshFacts=true → 先 pull 该 patient 数据再算
  └─ scenarios? = 指定 scenario / 全部

W5+ 加 event-driven (txn 触发 / persona 触发)
```

**Plan 字段产出来源**:
| 字段 | 产出方式 |
|---|---|
| `priorityScore` | 纯规则(加权打分) |
| `recommendedAt` | 纯规则(时间窗) |
| `recommendedRole` / `recommendedChannel` | 纯规则(scenario 配置 + patient preferences) |
| `inclusionReason` | 起步纯规则模板(初期不引入 agent) |

### Plan Assets(scripts / summaries)

```
Plan 写入 → 同步插占位行 (status='pending', content=null)
             → enqueue plan-asset-generate (BullMQ)
                  ↓ 异步消化
             worker → AiCall.run(draft_plan_script/summary) → DeepSeek API
                  ↓
             成功 → status='ready', source='agent', content=...
                    + 落一条 agent_invocations(token / cost / output)
             失败 → 模板兜底 → status='ready', source='template_fallback'
             兜底也挂 → status='failed'(罕见)
```

**两个产物 token 优化**:
- PlanScript = **一次 AiCall** → 1 行,Markdown 含开场 / 跟进 / 异议处理多子段
- PlanSummary = **一次 AiCall** → 3 行(one_page / medical_record / treatment_chain),共享同一 agentInvocationId

**AI 调用链**(v1 = DeepSeek 直连):
- **PAC → AiCall 框架 → LLM provider**:PAC 通过 AiCall 框架(packages/ai-core)统一调用 LLM
- AiCall 框架职责:prompt 装配 / zod output schema / token 计量 / cost 计算 / prompt cache / 失败重试
- v1 LLM provider = DeepSeek V4 Pro(国内合规直连);未来可切 Qwen / GPT 等 — provider 切换不动 AiCall 调用方
- 每次调用强制落 `agent_invocations`(`modelProvider` + `modelName` + `callKey` + token / cost),作为 audit / replay / 成本核算唯一真理源
- 失败 / 限流 / 超时由 BullMQ 重试 + 退避;耗尽 → template_fallback

### PlanExecution 回写

```
客服工作台 → POST /pac/v1/plans/{id}/executions (单次提交)
   body: { outcome, notes, abandonReasons?, scheduledNextAt? }
                 ↓
   写一行 plan_executions
                 ↓
   按 outcome 驱动 Plan.status:
     success_appointed → completed
     abandoned         → abandoned
     no_answer         → 维持 active 等下次
     scheduled_next    → 维持 assigned
                 ↓
   触发熔断检查(累积达 maxContactAttempts → 自动 abandoned)
```

---

## 横切支撑

| 表 | 职责 | 平行关系 |
|---|---|---|
| `agent_invocations` | 每次 AI 调用落账,token / cost / replay / 影像 AI 结果落点 | 跨所有层共享 |
| `sync_logs` | Pull / Push 运行账本,cursor 断点续传 / 对账 trigger | 接入层 |
| `persona_recompute_logs` | 重算执行账本,rule/agent/fallback 计数 → harness 健康度 | Layer 2 |
| `plan_generation_logs` | 召回生成账本,scenarioRun / assetsCreated / agent 计数 | Layer 3 |

三张 orchestration log **平行不混用**,每个 domain 一张账本,字段贴近自身语义。

---

## 七层防线(系统间通信可靠性)

| # | 防线 | schema 落地 |
|---|---|---|
| 1 | 幂等 | `patient_transactions.source_event_id` partial UNIQUE |
| 2 | 水位续传 | `sync_logs.cursor_before/after` |
| 3 | 原始报文留底 | `patient_transactions.raw_payload` |
| 4 | Reconcile 对账 | `sync_logs.triggered_by='reconcile:<date>'` |
| 5 | Quarantine | (v1 暂缓,SyncLog.failed + errorMessage 兜底) |
| 6 | Drift 监控 | 应用层 metric |
| 7 | 推断 stale | `personas.event_watermark` |

---

## 一句话归纳

**三层模型 = 把"宿主散乱事件" 升级成"集团统一画像" 再升级成"客服可执行的召回计划"**。
每层各自承担(账本留底 / 综合评价 / 行动指令),数据流单向 + 证据可追溯 + 版本化推断。
